Skip to main content

rocketmq_runtime/
service_context.rs

1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::future::Future;
16use std::sync::Arc;
17
18use crate::blocking::BlockingExecutor;
19use crate::blocking::BlockingLane;
20use crate::blocking::BlockingLanePolicies;
21use crate::blocking::GlobalBlockingBudget;
22use crate::critical::CriticalFailureState;
23use crate::diagnostics::RuntimeDiagnostics;
24use crate::diagnostics::RuntimeDiagnosticsSnapshot;
25use crate::error::RuntimeContractViolation;
26use crate::error::RuntimeResult;
27use crate::handle::RuntimeHandle;
28use crate::resource_budget::ResourceBudget;
29use crate::resources::RuntimeResources;
30use crate::scheduled::ScheduledTaskGroup;
31use crate::shutdown_report::ShutdownReport;
32use crate::task_group::TaskGroup;
33use crate::task_group::TaskId;
34use crate::task_group::TaskKind;
35use crate::task_spawner::TaskSpawner;
36use crate::RuntimeContractPolicy;
37
38/// A validated, named position in the service ownership tree.
39#[derive(Debug, Clone, PartialEq, Eq, Hash)]
40pub struct ScopeId(Arc<str>);
41
42impl ScopeId {
43    /// Creates a scope identifier.
44    ///
45    /// # Errors
46    ///
47    /// Returns a deterministic contract violation when `name` is empty or
48    /// only contains whitespace.
49    pub fn try_new(name: impl Into<Arc<str>>) -> Result<Self, RuntimeContractViolation> {
50        let name = name.into();
51        if name.trim().is_empty() {
52            return Err(RuntimeContractViolation::InvalidConfiguration {
53                policy: RuntimeContractPolicy::ServiceContextScopeNotBlank,
54            });
55        }
56        Ok(Self(name))
57    }
58
59    /// Creates a statically named scope.
60    ///
61    /// # Panics
62    ///
63    /// Panics when `name` is empty or only contains whitespace. Static scope
64    /// names are programmer-owned lifecycle invariants.
65    pub fn from_static(name: &'static str) -> Self {
66        Self::try_new(Arc::<str>::from(name)).expect("static service context scope name must be valid")
67    }
68
69    /// Borrows this value as str.
70    pub fn as_str(&self) -> &str {
71        &self.0
72    }
73
74    fn into_inner(self) -> Arc<str> {
75        self.0
76    }
77}
78
79impl From<&'static str> for ScopeId {
80    fn from(value: &'static str) -> Self {
81        Self::from_static(value)
82    }
83}
84
85impl TryFrom<String> for ScopeId {
86    type Error = RuntimeContractViolation;
87
88    fn try_from(value: String) -> Result<Self, Self::Error> {
89        Self::try_new(Arc::<str>::from(value))
90    }
91}
92
93impl TryFrom<Arc<str>> for ScopeId {
94    type Error = RuntimeContractViolation;
95
96    fn try_from(value: Arc<str>) -> Result<Self, Self::Error> {
97        Self::try_new(value)
98    }
99}
100
101#[cfg(test)]
102mod scope_id_tests {
103    use super::*;
104
105    #[test]
106    fn dynamic_scope_ids_return_contract_violations() {
107        assert_eq!(
108            ScopeId::try_new(Arc::<str>::from("  ")),
109            Err(RuntimeContractViolation::InvalidConfiguration {
110                policy: RuntimeContractPolicy::ServiceContextScopeNotBlank,
111            })
112        );
113        assert!(ScopeId::try_from(String::new()).is_err());
114    }
115}
116
117#[derive(Debug, Clone)]
118struct BlockingLanes {
119    storage_io: BlockingExecutor,
120    metadata_io: BlockingExecutor,
121    cpu_crypto: BlockingExecutor,
122}
123
124impl BlockingLanes {
125    fn new(policies: BlockingLanePolicies, global_capacity: usize, runtime: RuntimeHandle, scope: &TaskGroup) -> Self {
126        let budget = GlobalBlockingBudget::managed(global_capacity, &policies);
127        Self {
128            storage_io: BlockingExecutor::new_managed(
129                policies.storage_io,
130                BlockingLane::StorageIo,
131                budget.clone(),
132                runtime.clone(),
133            )
134            .expect("RuntimeConfig validates storage blocking policy before root context construction")
135            .scoped_to(scope.clone()),
136            metadata_io: BlockingExecutor::new_managed(
137                policies.metadata_io,
138                BlockingLane::MetadataIo,
139                budget.clone(),
140                runtime.clone(),
141            )
142            .expect("RuntimeConfig validates metadata blocking policy before root context construction")
143            .scoped_to(scope.clone()),
144            cpu_crypto: BlockingExecutor::new_managed(policies.cpu_crypto, BlockingLane::CpuCrypto, budget, runtime)
145                .expect("RuntimeConfig validates CPU blocking policy before root context construction")
146                .scoped_to(scope.clone()),
147        }
148    }
149
150    fn scoped_to(&self, scope: &TaskGroup) -> Self {
151        Self {
152            storage_io: self.storage_io.scoped_to(scope.clone()),
153            metadata_io: self.metadata_io.scoped_to(scope.clone()),
154            cpu_crypto: self.cpu_crypto.scoped_to(scope.clone()),
155        }
156    }
157
158    fn get(&self, lane: BlockingLane) -> &BlockingExecutor {
159        match lane {
160            BlockingLane::StorageIo => &self.storage_io,
161            BlockingLane::MetadataIo => &self.metadata_io,
162            BlockingLane::CpuCrypto => &self.cpu_crypto,
163        }
164    }
165
166    fn snapshots(&self) -> Vec<crate::blocking::BlockingExecutorSnapshot> {
167        vec![
168            self.storage_io.snapshot(),
169            self.metadata_io.snapshot(),
170            self.cpu_crypto.snapshot(),
171        ]
172    }
173
174    fn aggregates(&self) -> Vec<crate::blocking::BlockingExecutorAggregate> {
175        vec![
176            self.storage_io.aggregate(),
177            self.metadata_io.aggregate(),
178            self.cpu_crypto.aggregate(),
179        ]
180    }
181}
182
183/// The unique, non-cloneable lifecycle root owned by [`crate::RuntimeOwner`].
184///
185/// The type has no public constructor and cannot be promoted from a child
186/// context. Composition roots only use it to derive the first named child.
187#[derive(Debug)]
188pub struct RootServiceContext {
189    name: Arc<str>,
190    task_group: TaskGroup,
191    blocking_lanes: BlockingLanes,
192    diagnostics: RuntimeDiagnostics,
193    resources: RuntimeResources,
194    _sealed: RootContextSeal,
195}
196
197#[derive(Debug)]
198struct RootContextSeal;
199
200/// Permit to create a root [`TaskGroup`].
201///
202/// The private field keeps construction inside this module, so
203/// [`RootServiceContext::new`] is the only code that can create a root group.
204pub(crate) struct RootGroupPermit(());
205
206impl RootServiceContext {
207    /// Creates a root context and the root task group it owns.
208    pub(crate) fn new(
209        name: Arc<str>,
210        runtime: RuntimeHandle,
211        blocking_policies: BlockingLanePolicies,
212        global_blocking_capacity: usize,
213        diagnostics: RuntimeDiagnostics,
214        resources: RuntimeResources,
215    ) -> Self {
216        let task_group = TaskGroup::root(Arc::clone(&name), runtime.clone(), RootGroupPermit(()));
217        let blocking_lanes = BlockingLanes::new(blocking_policies, global_blocking_capacity, runtime, &task_group);
218        Self {
219            name,
220            task_group,
221            blocking_lanes,
222            diagnostics,
223            resources,
224            _sealed: RootContextSeal,
225        }
226    }
227
228    /// Returns the name.
229    pub fn name(&self) -> &str {
230        &self.name
231    }
232
233    /// Creates a long-lived component context owned by this process root.
234    ///
235    /// Dynamically supplied names must be validated with [`ScopeId::try_new`]
236    /// before they reach this boundary. Static literals may use the constrained
237    /// `From<&'static str>` conversion.
238    pub fn component(&self, scope: impl Into<ScopeId>) -> ChildServiceContext {
239        ChildServiceContext::new(
240            scope.into(),
241            &self.task_group,
242            self.blocking_lanes.clone(),
243            self.diagnostics.clone(),
244            self.resources.clone(),
245        )
246    }
247
248    /// Returns the shared process resource-budget root.
249    pub fn process_budget(&self) -> ResourceBudget {
250        self.resources.process_budget()
251    }
252
253    /// Returns the process-wide runtime resource capabilities.
254    pub fn resources(&self) -> &RuntimeResources {
255        &self.resources
256    }
257
258    /// Returns the diagnostics snapshot.
259    pub fn diagnostics_snapshot(&self) -> RuntimeDiagnosticsSnapshot {
260        self.diagnostics
261            .snapshot(&self.task_group, self.blocking_lanes.snapshots())
262    }
263
264    /// Returns a bounded, sanitized diagnostics view for this service runtime.
265    pub fn diagnostics_view_v1(
266        &self,
267        component: crate::diagnostics::RuntimeComponent,
268    ) -> crate::diagnostics::RuntimeDiagnosticsViewV1 {
269        self.diagnostics.view_v1_with_aggregates(
270            component,
271            &self.task_group,
272            self.blocking_lanes.aggregates(),
273            Default::default(),
274        )
275    }
276
277    pub(crate) fn task_group(&self) -> &TaskGroup {
278        &self.task_group
279    }
280
281    pub(crate) fn blocking(&self, lane: BlockingLane) -> &BlockingExecutor {
282        self.blocking_lanes.get(lane)
283    }
284
285    pub(crate) fn diagnostics(&self) -> &RuntimeDiagnostics {
286        &self.diagnostics
287    }
288
289    /// Adds the blocking work of the managed lanes and of the isolated
290    /// executors bound to this tree to `report`.
291    pub(crate) fn merge_blocking_into(&self, report: &mut ShutdownReport) {
292        for snapshot in self.blocking_lanes.snapshots() {
293            report.merge_blocking(snapshot);
294        }
295        let (still_running, tasks) = self.task_group.isolated_blocking_report();
296        report.merge_blocking_tasks(still_running, tasks);
297    }
298}
299
300/// A sealed, cloneable descendant of a [`RootServiceContext`].
301///
302/// Libraries receive this type or a capability derived from it. They cannot
303/// construct it from a Tokio handle, create a root task group, or promote it
304/// back to a root.
305#[derive(Debug, Clone)]
306pub struct ChildServiceContext {
307    name: Arc<str>,
308    task_group: TaskGroup,
309    blocking_lanes: BlockingLanes,
310    diagnostics: RuntimeDiagnostics,
311    resources: RuntimeResources,
312    _sealed: Arc<ChildContextSeal>,
313}
314
315#[derive(Debug)]
316struct ChildContextSeal;
317
318impl ChildServiceContext {
319    fn new(
320        scope: ScopeId,
321        parent_group: &TaskGroup,
322        blocking_lanes: BlockingLanes,
323        diagnostics: RuntimeDiagnostics,
324        resources: RuntimeResources,
325    ) -> Self {
326        let name = scope.into_inner();
327        let task_group = parent_group.component(name.clone());
328        Self {
329            name: name.clone(),
330            task_group: task_group.clone(),
331            blocking_lanes: blocking_lanes.scoped_to(&task_group),
332            diagnostics,
333            resources,
334            _sealed: Arc::new(ChildContextSeal),
335        }
336    }
337
338    fn try_new(
339        scope: ScopeId,
340        parent_group: &TaskGroup,
341        blocking_lanes: BlockingLanes,
342        diagnostics: RuntimeDiagnostics,
343        resources: RuntimeResources,
344    ) -> RuntimeResult<Self> {
345        let name = scope.into_inner();
346        let task_group = parent_group.try_child(name.clone())?;
347        Ok(Self {
348            name: name.clone(),
349            task_group: task_group.clone(),
350            blocking_lanes: blocking_lanes.scoped_to(&task_group),
351            diagnostics,
352            resources,
353            _sealed: Arc::new(ChildContextSeal),
354        })
355    }
356
357    /// Returns the name.
358    pub fn name(&self) -> &str {
359        &self.name
360    }
361
362    /// Returns the task spawner.
363    pub fn task_spawner(&self) -> TaskSpawner {
364        TaskSpawner::new(self.task_group.clone())
365    }
366
367    /// Returns the task group.
368    pub fn task_group(&self) -> &TaskGroup {
369        &self.task_group
370    }
371
372    /// Returns the blocking.
373    pub fn blocking(&self, lane: BlockingLane) -> &BlockingExecutor {
374        self.blocking_lanes.get(lane)
375    }
376
377    /// Returns the storage io.
378    pub fn storage_io(&self) -> &BlockingExecutor {
379        self.blocking(BlockingLane::StorageIo)
380    }
381
382    /// Returns the metadata io.
383    pub fn metadata_io(&self) -> &BlockingExecutor {
384        self.blocking(BlockingLane::MetadataIo)
385    }
386
387    /// Returns the cpu crypto.
388    pub fn cpu_crypto(&self) -> &BlockingExecutor {
389        self.blocking(BlockingLane::CpuCrypto)
390    }
391
392    /// Returns the diagnostics.
393    pub fn diagnostics(&self) -> &RuntimeDiagnostics {
394        &self.diagnostics
395    }
396
397    /// Returns the shared process resource-budget root.
398    pub fn process_budget(&self) -> ResourceBudget {
399        self.resources.process_budget()
400    }
401
402    /// Returns the process-wide runtime resource capabilities.
403    pub fn resources(&self) -> &RuntimeResources {
404        &self.resources
405    }
406
407    /// Returns the diagnostics snapshot.
408    pub fn diagnostics_snapshot(&self) -> RuntimeDiagnosticsSnapshot {
409        self.diagnostics
410            .snapshot(&self.task_group, self.blocking_lanes.snapshots())
411    }
412
413    /// Returns a bounded, sanitized diagnostics view for this service subtree.
414    pub fn diagnostics_view_v1(
415        &self,
416        component: crate::diagnostics::RuntimeComponent,
417    ) -> crate::diagnostics::RuntimeDiagnosticsViewV1 {
418        self.diagnostics.view_v1_with_aggregates(
419            component,
420            &self.task_group,
421            self.blocking_lanes.aggregates(),
422            Default::default(),
423        )
424    }
425
426    /// Returns a bounded, explicitly scoped diagnostics view for this subtree.
427    ///
428    /// The runtime context does not own scheduled groups or a metadata actor, so
429    /// the caller supplies the snapshots it holds; a section without an input is
430    /// reported as absent rather than as an empty value.
431    pub fn diagnostics_view_v2(
432        &self,
433        component: crate::diagnostics::RuntimeComponent,
434        inputs: crate::diagnostics::RuntimeDiagnosticsInputs,
435    ) -> crate::diagnostics::RuntimeDiagnosticsViewV2 {
436        self.diagnostics_view_v2_with_options(
437            component,
438            inputs,
439            crate::diagnostics::RuntimeDiagnosticsViewOptionsV2::default(),
440        )
441    }
442
443    /// Returns a bounded, explicitly scoped diagnostics view with explicit
444    /// budget limits, including the scan and output budgets of an on-demand task
445    /// detail list.
446    pub fn diagnostics_view_v2_with_options(
447        &self,
448        component: crate::diagnostics::RuntimeComponent,
449        inputs: crate::diagnostics::RuntimeDiagnosticsInputs,
450        options: crate::diagnostics::RuntimeDiagnosticsViewOptionsV2,
451    ) -> crate::diagnostics::RuntimeDiagnosticsViewV2 {
452        self.diagnostics.view_v2_with_aggregates(
453            component,
454            &self.task_group,
455            self.blocking_lanes.aggregates(),
456            inputs,
457            options,
458        )
459    }
460
461    /// Creates a long-lived component context owned by this service.
462    ///
463    /// Dynamically supplied names must be validated with [`ScopeId::try_new`]
464    /// before they reach this boundary. Static literals may use the constrained
465    /// `From<&'static str>` conversion.
466    pub fn component(&self, scope: impl Into<ScopeId>) -> Self {
467        Self::new(
468            scope.into(),
469            &self.task_group,
470            self.blocking_lanes.clone(),
471            self.diagnostics.clone(),
472            self.resources.clone(),
473        )
474    }
475
476    /// Creates an independently cancellable child component owned by this service.
477    ///
478    /// Parent cancellation propagates to the returned component. Cancelling the
479    /// child does not cancel this service or sibling components.
480    ///
481    /// # Errors
482    ///
483    /// Returns an unavailable runtime failure after this service starts
484    /// shutting down or becomes poisoned. Dynamically supplied names must be
485    /// validated with [`ScopeId::try_new`] before this call.
486    pub fn try_component(&self, scope: impl Into<ScopeId>) -> RuntimeResult<Self> {
487        Self::try_new(
488            scope.into(),
489            &self.task_group,
490            self.blocking_lanes.clone(),
491            self.diagnostics.clone(),
492            self.resources.clone(),
493        )
494    }
495
496    /// Returns the scheduled tasks.
497    pub fn scheduled_tasks(&self, scope: impl Into<ScopeId>) -> ScheduledTaskGroup {
498        let component = self.component(scope);
499        ScheduledTaskGroup::new(component.task_group)
500    }
501
502    /// Spawns the supplied task.
503    pub fn spawn<F>(&self, name: impl Into<crate::TaskName>, kind: TaskKind, future: F) -> RuntimeResult<TaskId>
504    where
505        F: Future<Output = ()> + Send + 'static,
506    {
507        self.task_group.spawn(name, kind, future)
508    }
509
510    /// Spawns a service that owns its shutdown protocol.
511    ///
512    /// Use [`Self::spawn_cancellable_service`] when owner cancellation may
513    /// safely drop the service future immediately.
514    pub fn spawn_service<F>(&self, name: impl Into<crate::TaskName>, future: F) -> RuntimeResult<TaskId>
515    where
516        F: Future<Output = ()> + Send + 'static,
517    {
518        self.task_group.spawn_service(name, future)
519    }
520
521    /// Spawns a service that exits when either its future completes or this context is cancelled.
522    ///
523    /// # Errors
524    ///
525    /// Returns an error when this service context is shutting down or closed.
526    pub fn spawn_cancellable_service<F>(&self, name: impl Into<crate::TaskName>, future: F) -> RuntimeResult<TaskId>
527    where
528        F: Future<Output = ()> + Send + 'static,
529    {
530        self.task_group.spawn_cancellable_service(name, future)
531    }
532
533    /// Spawns work whose panic is recorded as a critical failure.
534    ///
535    /// # Errors
536    ///
537    /// Returns an error when this service context is shutting down or closed.
538    pub fn spawn_critical<F>(
539        &self,
540        name: impl Into<crate::TaskName>,
541        kind: TaskKind,
542        failures: CriticalFailureState,
543        future: F,
544    ) -> RuntimeResult<TaskId>
545    where
546        F: Future<Output = ()> + Send + 'static,
547    {
548        self.task_group.spawn_critical(name, kind, failures, future)
549    }
550
551    /// Spawns a critical service that must run until this context is cancelled.
552    ///
553    /// # Errors
554    ///
555    /// Returns an error when this service context is shutting down or closed.
556    pub fn spawn_critical_service<F>(
557        &self,
558        name: impl Into<crate::TaskName>,
559        failures: CriticalFailureState,
560        future: F,
561    ) -> RuntimeResult<TaskId>
562    where
563        F: Future<Output = ()> + Send + 'static,
564    {
565        self.task_group.spawn_critical_service(name, failures, future)
566    }
567}