Skip to main content

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