aster_forge_tasks/
component.rs

1//! Runtime component integration for background task collections.
2//!
3//! Products still decide which workers to spawn and which task descriptors they
4//! expose to operators. Forge owns the common lifecycle mechanics for the
5//! spawned handle collection: registering the `background_tasks` component,
6//! attaching static task metadata, and shutting down all workers exactly once
7//! during dependency-aware runtime shutdown.
8
9use aster_forge_runtime::{
10    AsterRuntimeBuilder, AsterRuntimeComponent, RuntimeComponentBundle,
11    RuntimeComponentBundleRegistration, RuntimeComponentKind, RuntimeComponentRegistry,
12    runtime_component,
13};
14use tokio_util::sync::CancellationToken;
15
16use crate::{BACKGROUND_TASKS_COMPONENT, BackgroundTasks, RuntimeTaskDefinition};
17
18/// Stable shutdown phase name for background task workers.
19pub const BACKGROUND_TASKS_SHUTDOWN_PHASE: &str = "background_tasks";
20
21/// Runtime component that owns spawned background task handles.
22pub struct BackgroundTaskRuntimeComponent {
23    background_tasks: BackgroundTasks,
24}
25
26/// Runtime component that owns spawned task handles and registers task definitions.
27pub struct BackgroundTaskRuntimeDefinitionsComponent<Kind: 'static, PresentationCode: 'static> {
28    background_tasks: BackgroundTasks,
29    definitions: &'static [RuntimeTaskDefinition<Kind, PresentationCode>],
30}
31
32/// Runtime builder component that spawns background tasks from the shared shutdown token.
33pub struct BackgroundTaskRuntimeComponentFromShutdown<F> {
34    spawn: F,
35}
36
37/// Runtime builder component that spawns background tasks and registers task definitions.
38pub struct BackgroundTaskRuntimeDefinitionsComponentFromShutdown<
39    Kind: 'static,
40    PresentationCode: 'static,
41    F,
42> {
43    definitions: &'static [RuntimeTaskDefinition<Kind, PresentationCode>],
44    spawn: F,
45}
46
47impl BackgroundTaskRuntimeComponent {
48    /// Creates a background task runtime component from spawned task handles.
49    #[must_use]
50    pub const fn new(background_tasks: BackgroundTasks) -> Self {
51        Self { background_tasks }
52    }
53}
54
55impl<F> BackgroundTaskRuntimeComponentFromShutdown<F> {
56    /// Creates a component from a worker-spawning function.
57    pub const fn new(spawn: F) -> Self {
58        Self { spawn }
59    }
60}
61
62impl<Kind: 'static, PresentationCode: 'static>
63    BackgroundTaskRuntimeDefinitionsComponent<Kind, PresentationCode>
64{
65    /// Creates a background task runtime component with product-owned task definitions.
66    pub const fn new(
67        background_tasks: BackgroundTasks,
68        definitions: &'static [RuntimeTaskDefinition<Kind, PresentationCode>],
69    ) -> Self {
70        Self {
71            background_tasks,
72            definitions,
73        }
74    }
75}
76
77impl<Kind: 'static, PresentationCode: 'static, F>
78    BackgroundTaskRuntimeDefinitionsComponentFromShutdown<Kind, PresentationCode, F>
79{
80    /// Creates a component from static task definitions and a worker-spawning function.
81    pub const fn new(
82        definitions: &'static [RuntimeTaskDefinition<Kind, PresentationCode>],
83        spawn: F,
84    ) -> Self {
85        Self { definitions, spawn }
86    }
87}
88
89impl RuntimeComponentBundle for BackgroundTaskRuntimeComponent {
90    fn register(self, registry: &mut RuntimeComponentRegistry) {
91        register_background_tasks_shutdown(registry, self.background_tasks);
92    }
93}
94
95impl<Kind: 'static, PresentationCode: 'static> RuntimeComponentBundle
96    for BackgroundTaskRuntimeDefinitionsComponent<Kind, PresentationCode>
97{
98    fn register(self, registry: &mut RuntimeComponentRegistry) {
99        register_background_task_definitions(registry, self.definitions);
100        register_background_tasks_shutdown(registry, self.background_tasks);
101    }
102}
103
104impl<S, F> AsterRuntimeComponent<S> for BackgroundTaskRuntimeComponentFromShutdown<F>
105where
106    F: FnOnce(CancellationToken) -> BackgroundTasks,
107{
108    type Output = AsterRuntimeBuilder<S>;
109
110    fn apply(self, builder: AsterRuntimeBuilder<S>) -> Self::Output {
111        let background_tasks = (self.spawn)(builder.shutdown_token().clone());
112        background_task_component(background_tasks).apply(builder)
113    }
114}
115
116impl<S, Kind, PresentationCode, F> AsterRuntimeComponent<S>
117    for BackgroundTaskRuntimeDefinitionsComponentFromShutdown<Kind, PresentationCode, F>
118where
119    Kind: Sync + 'static,
120    PresentationCode: Sync + 'static,
121    F: FnOnce(CancellationToken) -> BackgroundTasks,
122{
123    type Output = AsterRuntimeBuilder<S>;
124
125    fn apply(self, builder: AsterRuntimeBuilder<S>) -> Self::Output {
126        let background_tasks = (self.spawn)(builder.shutdown_token().clone());
127        background_task_component_with_definitions(background_tasks, self.definitions)
128            .apply(builder)
129    }
130}
131
132/// Creates the background task runtime component used by product entrypoints.
133#[must_use]
134pub fn background_task_component(
135    background_tasks: BackgroundTasks,
136) -> RuntimeComponentBundleRegistration<BackgroundTaskRuntimeComponent> {
137    runtime_component(BackgroundTaskRuntimeComponent::new(background_tasks))
138}
139
140/// Creates a runtime component that spawns background tasks from the shared shutdown token.
141///
142/// Use this from product entrypoints when worker creation needs the same shutdown token owned by
143/// `AsterRuntime`. Forge handles the runtime-component adapter; product code only supplies the
144/// worker spawning function.
145pub fn background_task_component_from_shutdown<F>(
146    spawn: F,
147) -> BackgroundTaskRuntimeComponentFromShutdown<F>
148where
149    F: FnOnce(CancellationToken) -> BackgroundTasks,
150{
151    BackgroundTaskRuntimeComponentFromShutdown::new(spawn)
152}
153
154/// Creates the background task runtime component with product task definitions.
155///
156/// This is the preferred companion for task catalogs generated by
157/// [`crate::runtime_task_registry!`]. Products keep their enum and presentation
158/// code, while Forge registers the runtime task descriptor fields from the
159/// shared definition list.
160pub fn background_task_component_with_definitions<Kind, PresentationCode>(
161    background_tasks: BackgroundTasks,
162    definitions: &'static [RuntimeTaskDefinition<Kind, PresentationCode>],
163) -> RuntimeComponentBundleRegistration<
164    BackgroundTaskRuntimeDefinitionsComponent<Kind, PresentationCode>,
165>
166where
167    Kind: Sync + 'static,
168    PresentationCode: Sync + 'static,
169{
170    runtime_component(BackgroundTaskRuntimeDefinitionsComponent::new(
171        background_tasks,
172        definitions,
173    ))
174}
175
176/// Creates a task-definition component that spawns workers from the shared shutdown token.
177///
178/// This is the high-level component factory for Aster products that use
179/// `AsterRuntime::builder().component(...)`: product code passes its static task definitions and
180/// one worker-spawning function, while Forge owns the adapter between the runtime shutdown token and
181/// the background-task shutdown component.
182pub fn background_task_component_with_definitions_from_shutdown<Kind, PresentationCode, F>(
183    definitions: &'static [RuntimeTaskDefinition<Kind, PresentationCode>],
184    spawn: F,
185) -> BackgroundTaskRuntimeDefinitionsComponentFromShutdown<Kind, PresentationCode, F>
186where
187    Kind: Sync + 'static,
188    PresentationCode: Sync + 'static,
189    F: FnOnce(CancellationToken) -> BackgroundTasks,
190{
191    BackgroundTaskRuntimeDefinitionsComponentFromShutdown::new(definitions, spawn)
192}
193
194/// Registers graceful shutdown for all spawned runtime background tasks.
195fn register_background_tasks_shutdown(
196    registry: &mut RuntimeComponentRegistry,
197    background_tasks: BackgroundTasks,
198) {
199    registry.component_shutdown_once(
200        BACKGROUND_TASKS_COMPONENT,
201        RuntimeComponentKind::Tasks,
202        BACKGROUND_TASKS_SHUTDOWN_PHASE,
203        None,
204        background_tasks,
205        |background_tasks| async move {
206            background_tasks.shutdown().await;
207            Ok(())
208        },
209    );
210}
211
212/// Registers static metadata from product runtime task definitions.
213fn register_background_task_definitions<Kind, PresentationCode>(
214    registry: &mut RuntimeComponentRegistry,
215    definitions: &'static [RuntimeTaskDefinition<Kind, PresentationCode>],
216) {
217    for task in definitions {
218        registry.component_task(
219            BACKGROUND_TASKS_COMPONENT,
220            RuntimeComponentKind::Tasks,
221            task.wire_value,
222            task.display_name,
223        );
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use aster_forge_runtime::{RuntimeComponentBundle, RuntimeComponentKind};
230    use tokio_util::sync::CancellationToken;
231
232    use super::{
233        BACKGROUND_TASKS_COMPONENT, BACKGROUND_TASKS_SHUTDOWN_PHASE,
234        background_task_component_from_shutdown, background_task_component_with_definitions,
235    };
236    use crate::{BackgroundTasks, RuntimeTaskDefinition};
237
238    const TEST_DEFINITIONS: &[RuntimeTaskDefinition<TestRuntimeTask, TestPresentationCode>] = &[
239        RuntimeTaskDefinition {
240            kind: TestRuntimeTask::Cleanup,
241            wire_value: "cleanup",
242            display_name: "Cleanup",
243            presentation_code: TestPresentationCode::Cleanup,
244        },
245        RuntimeTaskDefinition {
246            kind: TestRuntimeTask::Dispatch,
247            wire_value: "dispatch",
248            display_name: "Dispatch",
249            presentation_code: TestPresentationCode::Dispatch,
250        },
251    ];
252
253    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
254    enum TestRuntimeTask {
255        Cleanup,
256        Dispatch,
257    }
258
259    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
260    enum TestPresentationCode {
261        Cleanup,
262        Dispatch,
263    }
264
265    #[test]
266    fn background_task_component_registers_definitions_and_shutdown() {
267        let registry = aster_forge_runtime::RuntimeComponentRegistry::configured(|registry| {
268            background_task_component_with_definitions(BackgroundTasks::new(), TEST_DEFINITIONS)
269                .register(registry);
270        });
271
272        let descriptor = registry
273            .descriptor(BACKGROUND_TASKS_COMPONENT)
274            .expect("background task component should be registered");
275        assert_eq!(descriptor.kind, RuntimeComponentKind::Tasks);
276        assert_eq!(
277            descriptor
278                .shutdown
279                .first()
280                .expect("background task shutdown should be registered")
281                .phase_name,
282            BACKGROUND_TASKS_SHUTDOWN_PHASE
283        );
284        assert_eq!(
285            descriptor
286                .tasks
287                .iter()
288                .map(|task| (task.task_name, task.display_name))
289                .collect::<Vec<_>>(),
290            vec![("cleanup", "Cleanup"), ("dispatch", "Dispatch")]
291        );
292    }
293
294    #[tokio::test]
295    async fn background_task_component_from_shutdown_uses_runtime_shutdown_token() {
296        let observed = std::sync::Arc::new(std::sync::Mutex::new(false));
297        let observed_spawn = observed.clone();
298        let runtime = aster_forge_runtime::AsterRuntime::builder()
299            .component(aster_forge_runtime::RuntimeServiceComponent::new(
300                "test_service",
301                RuntimeComponentKind::Core,
302                async {},
303                CancellationToken::default(),
304                || async {},
305            ))
306            .component(background_task_component_from_shutdown(move |shutdown| {
307                let mut tasks = BackgroundTasks::with_shutdown_token(shutdown.clone());
308                let observed_task = observed_spawn.clone();
309                tasks.push(async move {
310                    shutdown.cancelled().await;
311                    match observed_task.lock() {
312                        Ok(mut value) => *value = true,
313                        Err(poisoned) => *poisoned.into_inner() = true,
314                    }
315                });
316                tasks
317            }))
318            .build()
319            .expect("runtime should build with spawned background task component");
320
321        runtime
322            .run()
323            .await
324            .expect("runtime should shut down cleanly");
325        assert!(
326            *observed
327                .lock()
328                .expect("observed mutex should not be poisoned")
329        );
330    }
331}