Skip to main content

rocketmq_runtime/
owner.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;
16
17use crate::config::RuntimeConfig;
18use crate::diagnostics::RuntimeDiagnostics;
19use crate::error::RuntimeError;
20use crate::error::RuntimeResult;
21use crate::handle::RuntimeHandle;
22use crate::resource_budget::ManagedMemoryBudget;
23use crate::resource_budget::ManagedMemoryPolicy;
24use crate::resource_budget::ProcessMemoryLimit;
25use crate::resources::RuntimeResources;
26use crate::service_context::RootServiceContext;
27use crate::shutdown_deadline::ShutdownDeadline;
28use crate::shutdown_report::ShutdownReport;
29use crate::task_group::TaskGroupLifecycleState;
30
31/// Owns one Tokio runtime and its root service context.
32///
33/// Dropping this value without an explicit shutdown starts Tokio background
34/// shutdown and logs any still-tracked work.
35pub struct RuntimeOwner {
36    config: RuntimeConfig,
37    runtime: Option<tokio::runtime::Runtime>,
38    resources: RuntimeResources,
39    root_context: RootServiceContext,
40}
41
42/// A runtime configuration that has passed deterministic validation.
43///
44/// Construct this value with [`RuntimeOwner::plan`], then invoke
45/// [`Self::build`] at the operational startup boundary.
46#[derive(Debug)]
47pub struct RuntimeOwnerPlan {
48    config: RuntimeConfig,
49    memory_limit: Option<ProcessMemoryLimit>,
50    memory_policy: ManagedMemoryPolicy,
51    metadata_target_capacity: std::num::NonZeroUsize,
52}
53
54impl RuntimeOwnerPlan {
55    /// Sets the maximum retained metadata target histories for this runtime.
56    ///
57    /// Idle and fenced targets consume capacity until explicitly and safely
58    /// retired. The default is 4,096; this is not an in-flight operation limit.
59    /// Each actor also retains at most this many resource histories, including
60    /// tombstones retired by another actor. Replace an actor to release those
61    /// tombstones; removing them while it is open could revive stale authority.
62    #[must_use]
63    pub fn with_metadata_target_capacity(mut self, capacity: std::num::NonZeroUsize) -> Self {
64        self.metadata_target_capacity = capacity;
65        self
66    }
67    /// Supplies a container-provided memory limit for this runtime.
68    #[must_use]
69    pub fn with_memory_limit(mut self, memory_limit: ProcessMemoryLimit) -> Self {
70        self.memory_limit = Some(memory_limit);
71        self
72    }
73
74    /// Derives the chargeable managed budget from the effective memory limit.
75    ///
76    /// The default policy charges the whole effective limit, which is the
77    /// behavior that existed before the policy existed. A ratio or headroom is
78    /// adopted explicitly after measuring the service.
79    #[must_use]
80    pub fn with_memory_policy(mut self, memory_policy: ManagedMemoryPolicy) -> Self {
81        self.memory_policy = memory_policy;
82        self
83    }
84
85    /// Builds the validated Tokio runtime owner.
86    ///
87    /// # Errors
88    ///
89    /// Returns an operational error when process-memory discovery or Tokio
90    /// runtime construction fails, and a contract violation when the policy
91    /// cannot resolve a positive budget from the effective limit.
92    pub fn build(self) -> RuntimeResult<RuntimeOwner> {
93        let Self {
94            config,
95            memory_limit,
96            memory_policy,
97            metadata_target_capacity,
98        } = self;
99        RuntimeOwner::build_validated(config, metadata_target_capacity, move || {
100            let memory_limit = memory_limit.map_or_else(ProcessMemoryLimit::detect, Ok)?;
101            ManagedMemoryBudget::resolve(memory_limit, None, memory_policy).map_err(|violation| {
102                RuntimeError::configuration_failure(crate::RuntimeOperation::ResolveManagedMemoryBudget, violation)
103            })
104        })
105    }
106}
107
108impl RuntimeOwner {
109    /// Validates runtime configuration before runtime construction.
110    ///
111    /// # Errors
112    ///
113    /// Returns a deterministic contract violation without discovering system
114    /// resources or starting a Tokio runtime.
115    pub fn plan(config: RuntimeConfig) -> Result<RuntimeOwnerPlan, crate::RuntimeContractViolation> {
116        config.validate()?;
117        Ok(RuntimeOwnerPlan {
118            config,
119            memory_limit: None,
120            memory_policy: ManagedMemoryPolicy::default(),
121            metadata_target_capacity: std::num::NonZeroUsize::new(crate::metadata_target::DEFAULT_MAX_METADATA_TARGETS)
122                .expect("the static default metadata target capacity is positive"),
123        })
124    }
125
126    /// Builds an owner from the internally validated default profile.
127    ///
128    /// # Errors
129    ///
130    /// Returns an operational error when process-memory discovery or runtime
131    /// construction fails. Use [`Self::plan`] when the caller supplies any
132    /// runtime configuration or memory-limit value.
133    pub fn new() -> RuntimeResult<Self> {
134        Self::plan(RuntimeConfig::default())
135            .expect("RuntimeConfig::default always satisfies its validation contract")
136            .build()
137    }
138
139    fn build_validated<F>(
140        config: RuntimeConfig,
141        metadata_target_capacity: std::num::NonZeroUsize,
142        detector: F,
143    ) -> RuntimeResult<Self>
144    where
145        F: FnOnce() -> RuntimeResult<ManagedMemoryBudget>,
146    {
147        let memory_budget = detector()?;
148        let resources =
149            RuntimeResources::from_memory_budget_with_metadata_capacity(memory_budget, metadata_target_capacity.get());
150
151        let mut builder = tokio::runtime::Builder::new_multi_thread();
152        builder
153            .worker_threads(config.worker_threads)
154            .max_blocking_threads(config.tokio_blocking_threads())
155            .thread_name(config.thread_name.clone())
156            .thread_keep_alive(config.thread_keep_alive);
157        if let Some(thread_stack_size) = config.thread_stack_size {
158            builder.thread_stack_size(thread_stack_size);
159        }
160        if config.enable_io {
161            builder.enable_io();
162        }
163        if config.enable_time {
164            builder.enable_time();
165        }
166
167        let runtime = build_tokio_runtime(&mut builder)?;
168        let runtime_handle = RuntimeHandle::new(runtime.handle().clone());
169        let diagnostics = RuntimeDiagnostics::new();
170        let root_context = RootServiceContext::new(
171            config.thread_name.clone().into(),
172            runtime_handle,
173            config.blocking_lane_policies.clone(),
174            config.max_blocking_threads,
175            diagnostics,
176            resources.clone(),
177        );
178        Ok(Self {
179            config,
180            runtime: Some(runtime),
181            resources,
182            root_context,
183        })
184    }
185
186    /// Returns the root context used to derive owned component scopes.
187    pub fn root_context(&self) -> &RootServiceContext {
188        &self.root_context
189    }
190
191    /// Returns the validated runtime configuration.
192    pub fn config(&self) -> &RuntimeConfig {
193        &self.config
194    }
195
196    /// Returns the process-wide resource capabilities.
197    pub fn resources(&self) -> &RuntimeResources {
198        &self.resources
199    }
200
201    /// Blocks the current thread until `future` completes.
202    ///
203    /// A future above the runtime's inline boundary is polled from the heap,
204    /// so a process entrypoint does not need to box its root future.
205    pub fn block_on<F>(&self, future: F) -> F::Output
206    where
207        F: Future,
208    {
209        let runtime = self.runtime.as_ref().expect("runtime owner must still own the runtime");
210        if std::mem::size_of::<F>() > crate::stack::MAX_INLINE_SIZE {
211            runtime.block_on(Box::pin(future))
212        } else {
213            runtime.block_on(future)
214        }
215    }
216
217    /// Cancels and awaits tracked tasks until the configured deadline.
218    pub async fn shutdown_tasks(&self) -> ShutdownReport {
219        self.shutdown_tasks_until(ShutdownDeadline::after(self.config.shutdown_timeout))
220            .await
221    }
222
223    /// Blocks while shutting down tracked work and the Tokio runtime.
224    ///
225    /// # Errors
226    ///
227    /// Returns an error when invoked from inside a Tokio runtime.
228    pub fn shutdown_runtime_blocking(self) -> RuntimeResult<ShutdownReport> {
229        let timeout = self.config.shutdown_timeout;
230        self.shutdown_runtime_blocking_with_timeout(timeout)
231    }
232
233    /// Cancels tracked work and asks Tokio to finish runtime shutdown in the background.
234    ///
235    /// The returned report captures only the immediate task-group shutdown
236    /// state; background Tokio work is not awaited by this method.
237    pub fn shutdown_background(mut self) -> ShutdownReport {
238        let report = self.shutdown_tasks_now();
239        report.log_if_unhealthy();
240        if let Some(runtime) = self.runtime.take() {
241            runtime.shutdown_background();
242        }
243        report
244    }
245
246    /// Blocks while shutting down tracked work within `timeout`.
247    ///
248    /// # Errors
249    ///
250    /// Returns an error when invoked from inside a Tokio runtime.
251    pub fn shutdown_runtime_blocking_with_timeout(self, timeout: std::time::Duration) -> RuntimeResult<ShutdownReport> {
252        self.shutdown_runtime_blocking_until(ShutdownDeadline::after(timeout))
253    }
254
255    /// Shuts down tracked work and the Tokio runtime using an existing absolute deadline.
256    ///
257    /// This is the process-entrypoint boundary for a deadline already frozen by
258    /// [`crate::ServiceLifecycle`]. It never grants a new timeout to the runtime layer.
259    ///
260    /// # Errors
261    ///
262    /// Returns an unsupported-operation runtime failure when called from an
263    /// asynchronous Tokio context.
264    pub fn shutdown_runtime_blocking_until(mut self, deadline: ShutdownDeadline) -> RuntimeResult<ShutdownReport> {
265        if tokio::runtime::Handle::try_current().is_ok() {
266            return Err(RuntimeError::unsupported(
267                crate::RuntimeOperation::ShutdownRuntimeBlocking,
268            ));
269        }
270
271        let runtime = self.runtime.take().expect("runtime owner must still own the runtime");
272        let report = runtime.block_on(self.shutdown_tasks_until(deadline));
273        report.log_if_unhealthy();
274        runtime.shutdown_timeout(deadline.remaining());
275        Ok(report)
276    }
277}
278
279fn build_tokio_runtime(builder: &mut tokio::runtime::Builder) -> RuntimeResult<tokio::runtime::Runtime> {
280    builder
281        .build()
282        .map_err(|source| RuntimeError::build(crate::RuntimeOperation::BuildTokioRuntime, source))
283}
284
285impl Drop for RuntimeOwner {
286    fn drop(&mut self) {
287        if let Some(runtime) = self.runtime.take() {
288            if self.root_context.task_group().lifecycle_state() != TaskGroupLifecycleState::ShutdownCompleted {
289                let report = self.shutdown_tasks_now();
290                tracing::warn!(
291                    report = %report.to_json(),
292                    "RuntimeOwner dropped before root TaskGroup shutdown completed"
293                );
294            }
295            runtime.shutdown_background();
296        }
297    }
298}
299
300impl RuntimeOwner {
301    /// Cancels and joins all owned tasks using an existing absolute deadline.
302    ///
303    /// This keeps nested lifecycle owners on the caller's single shutdown
304    /// budget without consuming or destroying the owned Tokio runtime.
305    pub async fn shutdown_tasks_until(&self, deadline: ShutdownDeadline) -> ShutdownReport {
306        let mut report = self.root_context.task_group().shutdown_until(deadline).await;
307        self.root_context.merge_blocking_into(&mut report);
308        report
309    }
310
311    fn shutdown_tasks_now(&self) -> ShutdownReport {
312        let mut report = self.root_context.task_group().shutdown_now();
313        self.root_context.merge_blocking_into(&mut report);
314        report
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321
322    #[test]
323    fn plan_rejects_invalid_configuration_before_resource_discovery() {
324        let invalid = RuntimeConfig {
325            worker_threads: 0,
326            ..RuntimeConfig::default()
327        };
328
329        assert!(matches!(
330            RuntimeOwner::plan(invalid),
331            Err(crate::RuntimeContractViolation::InvalidConfiguration {
332                policy: crate::RuntimeContractPolicy::WorkerThreadsPositive,
333            })
334        ));
335    }
336
337    #[test]
338    fn validated_plan_builds_with_a_supplied_memory_limit() {
339        let owner = RuntimeOwner::plan(RuntimeConfig::default())
340            .expect("default runtime configuration is valid")
341            .with_memory_limit(ProcessMemoryLimit::configured(4 * 1024 * 1024).expect("test limit is valid"))
342            .build()
343            .expect("runtime owner");
344
345        assert_eq!(owner.resources().memory_limit().bytes(), 4 * 1024 * 1024);
346    }
347
348    #[test]
349    fn tokio_builder_uses_the_production_build_helper() {
350        let mut builder = tokio::runtime::Builder::new_multi_thread();
351        builder.worker_threads(1).max_blocking_threads(1).enable_all();
352
353        let runtime = build_tokio_runtime(&mut builder).expect("valid Tokio builder should start");
354
355        runtime.block_on(async {});
356    }
357
358    #[test]
359    fn a_memory_policy_bounds_the_process_budget() {
360        let owner = RuntimeOwner::plan(RuntimeConfig::default())
361            .expect("default config is valid")
362            .with_memory_limit(ProcessMemoryLimit::configured(4 * 1024 * 1024).expect("test limit is valid"))
363            .with_memory_policy(ManagedMemoryPolicy::fraction(1, 2).expect("a half is bounded"))
364            .build()
365            .expect("the owner should start");
366
367        // The detected limit stays reportable while the ledger charges the
368        // derived budget.
369        assert_eq!(owner.resources().memory_limit().bytes(), 4 * 1024 * 1024);
370        assert_eq!(owner.resources().memory_budget().managed_bytes(), 2 * 1024 * 1024);
371
372        let budget = owner.resources().process_budget();
373        assert_eq!(budget.limit().capacity.bytes, 2 * 1024 * 1024);
374        assert!(budget.try_acquire_data(2 * 1024 * 1024 + 1).is_err());
375        assert!(budget.try_acquire_data(2 * 1024 * 1024).is_ok());
376    }
377
378    #[test]
379    fn an_unresolvable_memory_policy_fails_the_owner_build() {
380        let error = RuntimeOwner::plan(RuntimeConfig::default())
381            .expect("default config is valid")
382            .with_memory_limit(ProcessMemoryLimit::configured(4 * 1024 * 1024).expect("test limit is valid"))
383            .with_memory_policy(ManagedMemoryPolicy::whole_limit().with_headroom(8 * 1024 * 1024))
384            .build()
385            .err()
386            .expect("headroom above the effective limit cannot resolve a budget");
387
388        assert_eq!(error.operation(), crate::RuntimeOperation::ResolveManagedMemoryBudget);
389    }
390}