Skip to main content

rocketmq_client_rust/
runtime.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::io;
17use std::pin::Pin;
18use std::sync::atomic::AtomicBool;
19use std::sync::atomic::Ordering;
20use std::sync::mpsc as std_mpsc;
21use std::sync::Arc;
22use std::time::Duration;
23use std::time::Instant;
24
25use crate::ClientError;
26use crate::ClientResult;
27use parking_lot::Mutex;
28pub use rocketmq_observability::metrics::client::ClientMetrics;
29pub use rocketmq_observability::TelemetryHandle;
30use rocketmq_protocol::protocol::remoting_command_defaults::RemotingCommandFactory;
31use rocketmq_protocol::protocol::remoting_command_facade::initialize_remoting_command_factory;
32use rocketmq_runtime::BudgetCapacity;
33use rocketmq_runtime::BudgetLimit;
34use rocketmq_runtime::ChildServiceContext;
35use rocketmq_runtime::FullPolicy;
36use rocketmq_runtime::ResourceBudget;
37use rocketmq_runtime::ResourceBudgetTree;
38use rocketmq_runtime::ScheduledExecutionPolicy;
39use rocketmq_runtime::ScheduledTaskConfig;
40use rocketmq_runtime::ScheduledTaskControl;
41use rocketmq_runtime::ScheduledTaskGroup;
42use rocketmq_runtime::ScheduledTaskSnapshot;
43use rocketmq_runtime::ShutdownDeadline;
44use rocketmq_runtime::ShutdownReport;
45use rocketmq_runtime::TaskGroup;
46use rocketmq_runtime::TaskGroupLifecycleState;
47use rocketmq_runtime::TaskId;
48
49use crate::implementation::mq_client_manager::ClientPool;
50
51/// Configuration for one explicitly owned RocketMQ client runtime.
52#[derive(Clone, Debug)]
53pub struct ClientRuntimeConfig {
54    /// Maximum duration used by [`ClientRuntime::shutdown`].
55    pub shutdown_timeout: Duration,
56    /// Optional cap below the injected process memory limit. Zero uses the
57    /// complete process budget supplied by the runtime owner.
58    pub process_memory_limit_bytes: u64,
59    /// Share of the process hard limit managed by client resource budgets.
60    pub managed_memory_numerator: u64,
61    /// Denominator for [`Self::managed_memory_numerator`].
62    pub managed_memory_denominator: u64,
63}
64
65impl Default for ClientRuntimeConfig {
66    fn default() -> Self {
67        Self {
68            shutdown_timeout: Duration::from_secs(30),
69            process_memory_limit_bytes: 0,
70            managed_memory_numerator: 1,
71            managed_memory_denominator: 4,
72        }
73    }
74}
75
76/// Explicit lifecycle owner for all RocketMQ clients in one application domain.
77///
78/// The caller must derive the supplied context from its application-owned
79/// [`rocketmq_runtime::RuntimeOwner`]. The client never discovers the current
80/// Tokio runtime and never creates a fallback runtime.
81pub struct ClientRuntime {
82    service_context: ChildServiceContext,
83    telemetry_handle: TelemetryHandle,
84    client_metrics: ClientMetrics,
85    pool: ClientPool,
86    resource_budget: ResourceBudget,
87    config: ClientRuntimeConfig,
88    shutdown: AtomicBool,
89}
90
91impl ClientRuntime {
92    /// Creates a client runtime with one shared process-derived resource root.
93    pub fn try_new(
94        service_context: ChildServiceContext,
95        config: ClientRuntimeConfig,
96        telemetry_handle: TelemetryHandle,
97    ) -> ClientResult<Arc<Self>> {
98        let remoting_command_factory =
99            initialize_remoting_command_factory(rocketmq_model::version::CURRENT_VERSION as i32)
100                .map_err(|error| crate::ClientError::config_parse_source("remoting.command.defaults", error))?;
101        Self::try_new_with_remoting_command_factory(service_context, config, telemetry_handle, remoting_command_factory)
102    }
103
104    /// Creates a client runtime with explicit immutable remoting wire defaults.
105    ///
106    /// This entry point does not read process configuration and permits
107    /// independent embedded client runtimes to use different wire defaults.
108    ///
109    /// # Errors
110    ///
111    /// Returns an error when the configured client resource budget is invalid.
112    pub fn try_new_with_remoting_command_factory(
113        service_context: ChildServiceContext,
114        config: ClientRuntimeConfig,
115        telemetry_handle: TelemetryHandle,
116        remoting_command_factory: RemotingCommandFactory,
117    ) -> ClientResult<Arc<Self>> {
118        let resource_budget = build_client_resource_budget(&config, &service_context.process_budget())?;
119        let client_metrics = ClientMetrics::from_handle(&telemetry_handle);
120        let pool = ClientPool::new(
121            service_context.component("pool"),
122            resource_budget.clone(),
123            telemetry_handle.clone(),
124            client_metrics.clone(),
125            remoting_command_factory,
126        );
127        Ok(Arc::new(Self {
128            service_context,
129            telemetry_handle,
130            client_metrics,
131            pool,
132            resource_budget,
133            config,
134            shutdown: AtomicBool::new(false),
135        }))
136    }
137
138    /// Returns this runtime's isolated client-instance pool.
139    pub fn pool(&self) -> &ClientPool {
140        &self.pool
141    }
142
143    /// Returns the root client scope injected by the application.
144    pub fn service_context(&self) -> &ChildServiceContext {
145        &self.service_context
146    }
147
148    /// Returns the cloneable telemetry capability injected by the application.
149    pub fn telemetry_handle(&self) -> &TelemetryHandle {
150        &self.telemetry_handle
151    }
152
153    /// Returns this runtime's typed, lifecycle-gated client metric recorder.
154    pub fn client_metrics(&self) -> &ClientMetrics {
155        &self.client_metrics
156    }
157
158    /// Returns the shared parent budget for every client component owned by
159    /// this runtime.
160    pub fn resource_budget(&self) -> ResourceBudget {
161        self.resource_budget.clone()
162    }
163
164    /// Creates a named component scope owned by this client runtime.
165    pub fn component(&self, scope: impl Into<rocketmq_runtime::ScopeId>) -> ChildServiceContext {
166        self.service_context.component(scope)
167    }
168
169    /// Stops all pooled clients and joins every task owned by this runtime.
170    pub async fn shutdown(&self) -> ShutdownReport {
171        self.shutdown_until(ShutdownDeadline::after(self.config.shutdown_timeout))
172            .await
173    }
174
175    /// Stops all pooled clients using the caller's absolute shutdown deadline.
176    pub async fn shutdown_until(&self, deadline: ShutdownDeadline) -> ShutdownReport {
177        if !self.shutdown.swap(true, Ordering::AcqRel) {
178            self.pool.shutdown_until(deadline).await;
179        }
180        self.service_context.task_group().shutdown_until(deadline).await
181    }
182
183    /// Returns whether shutdown admission has been closed.
184    pub fn is_shutdown(&self) -> bool {
185        self.shutdown.load(Ordering::Acquire)
186    }
187}
188
189pub(crate) fn build_client_resource_budget(
190    config: &ClientRuntimeConfig,
191    process_budget: &ResourceBudget,
192) -> ClientResult<ResourceBudget> {
193    const CLIENT_ITEM_LIMIT: usize = 262_144;
194    const CONTROL_RESERVE_COUNT: usize = 1_024;
195
196    let process_bytes = process_budget.limit().capacity.bytes;
197    let configured_bytes = if config.process_memory_limit_bytes == 0 {
198        process_bytes
199    } else {
200        usize::try_from(config.process_memory_limit_bytes)
201            .unwrap_or(usize::MAX)
202            .min(process_bytes)
203    };
204    if config.managed_memory_numerator == 0
205        || config.managed_memory_denominator == 0
206        || config.managed_memory_numerator > config.managed_memory_denominator
207    {
208        return Err(ClientError::config_invalid(
209            "client.runtime.managedMemoryFraction",
210            format!(
211                "{}/{}",
212                config.managed_memory_numerator, config.managed_memory_denominator
213            ),
214            "numerator and denominator must be positive and numerator must not exceed denominator",
215        ));
216    }
217    let managed_bytes = ((configured_bytes as u128 * u128::from(config.managed_memory_numerator))
218        / u128::from(config.managed_memory_denominator)) as usize;
219    let managed_bytes = managed_bytes.max(1);
220    let control_bytes = (managed_bytes / 16).max(1);
221    process_budget
222        .child(
223            "client",
224            BudgetLimit::new(CLIENT_ITEM_LIMIT, managed_bytes, FullPolicy::Reject)
225                .with_control_reserve(BudgetCapacity::new(CONTROL_RESERVE_COUNT, control_bytes)),
226        )
227        .map_err(|error| ClientError::config_invalid_source("client.runtime.resourceBudget", true, error))
228}
229
230pub(crate) fn standalone_client_resource_budget() -> ClientResult<ResourceBudget> {
231    const STANDALONE_TEST_BYTES: usize = 256 * 1024 * 1024;
232    let process_budget = ResourceBudgetTree::new(
233        "standalone-client-process",
234        BudgetLimit::new(usize::MAX, STANDALONE_TEST_BYTES, FullPolicy::Reject),
235    )
236    .map_err(|error| ClientError::config_invalid_source("client.runtime.standaloneResourceBudget", true, error))?
237    .root();
238    build_client_resource_budget(&ClientRuntimeConfig::default(), &process_budget)
239}
240
241#[cfg(test)]
242static TEST_RUNTIME_OWNER: std::sync::LazyLock<rocketmq_runtime::RuntimeOwner> = std::sync::LazyLock::new(|| {
243    rocketmq_runtime::RuntimeOwner::plan(rocketmq_runtime::RuntimeConfig {
244        thread_name: "rocketmq-client-unit-test".to_string(),
245        ..Default::default()
246    })
247    .expect("runtime configuration is valid")
248    .build()
249    .expect("client unit-test runtime owner should start")
250});
251
252#[cfg(test)]
253pub(crate) fn test_client_runtime(scope: &'static str) -> Arc<ClientRuntime> {
254    ClientRuntime::try_new(
255        TEST_RUNTIME_OWNER.root_context().component(scope),
256        ClientRuntimeConfig::default(),
257        TelemetryHandle::noop(),
258    )
259    .expect("client unit-test runtime should be valid")
260}
261
262#[cfg(test)]
263pub(crate) fn test_service_context(scope: &'static str) -> ChildServiceContext {
264    TEST_RUNTIME_OWNER.root_context().component(scope)
265}
266
267pub(crate) struct ClientRuntimeTaskHandle {
268    task_group: TaskGroup,
269    task_id: TaskId,
270    completion_rx: Mutex<Option<std_mpsc::Receiver<()>>>,
271}
272
273impl ClientRuntimeTaskHandle {
274    pub(crate) fn task_id(&self) -> TaskId {
275        self.task_id
276    }
277
278    pub(crate) fn task_count(&self) -> usize {
279        usize::from(self.task_group.contains_task(self.task_id))
280    }
281
282    pub(crate) fn is_finished(&self) -> bool {
283        let mut completion_rx = self.completion_rx.lock();
284        let Some(receiver) = completion_rx.as_ref() else {
285            return true;
286        };
287
288        match receiver.try_recv() {
289            Ok(()) | Err(std_mpsc::TryRecvError::Disconnected) => {
290                completion_rx.take();
291                true
292            }
293            Err(std_mpsc::TryRecvError::Empty) => false,
294        }
295    }
296
297    pub(crate) fn wait_finished(&self, timeout: Duration) -> bool {
298        let mut completion_rx = self.completion_rx.lock();
299        let Some(receiver) = completion_rx.take() else {
300            return true;
301        };
302
303        match receiver.recv_timeout(timeout) {
304            Ok(()) | Err(std_mpsc::RecvTimeoutError::Disconnected) => true,
305            Err(std_mpsc::RecvTimeoutError::Timeout) => {
306                *completion_rx = Some(receiver);
307                false
308            }
309        }
310    }
311}
312
313/// Heap-erased future submitted through the client task ownership boundary.
314///
315/// Callers allocate the concrete future before it enters the generic task-group
316/// spawn chain so large async state does not exhaust default Windows stacks.
317pub(crate) type ClientTaskFuture = Pin<Box<dyn Future<Output = ()> + Send + 'static>>;
318
319pub(crate) fn spawn_client_task_with_context(
320    context: &ChildServiceContext,
321    task_name: &'static str,
322    task: ClientTaskFuture,
323) -> io::Result<ClientRuntimeTaskHandle> {
324    let task_group = context.task_group().clone();
325    let (completion_tx, completion_rx) = std_mpsc::channel();
326    let task_id = task_group
327        .spawn_service(task_name, async move {
328            let _completion = ClientTrackedTaskCompletion::new(completion_tx);
329            task.await;
330        })
331        .map_err(io::Error::other)?;
332
333    Ok(ClientRuntimeTaskHandle {
334        task_group,
335        task_id,
336        completion_rx: Mutex::new(Some(completion_rx)),
337    })
338}
339
340pub(crate) fn spawn_background_client_task_with_context(
341    context: &ChildServiceContext,
342    task_name: &'static str,
343    task: ClientTaskFuture,
344) -> io::Result<()> {
345    drop(spawn_client_task_with_context(context, task_name, task)?);
346    Ok(())
347}
348
349pub(crate) struct ClientTrackedTaskHandle {
350    task_group: TaskGroup,
351    task_id: TaskId,
352    task_name: &'static str,
353    completion_rx: Mutex<Option<std_mpsc::Receiver<()>>>,
354}
355
356impl ClientTrackedTaskHandle {
357    pub(crate) fn task_count(&self) -> usize {
358        usize::from(self.task_group.contains_task(self.task_id))
359    }
360
361    pub(crate) fn is_finished(&self) -> bool {
362        let mut completion_rx = self.completion_rx.lock();
363        let Some(receiver) = completion_rx.as_ref() else {
364            return true;
365        };
366
367        match receiver.try_recv() {
368            Ok(()) | Err(std_mpsc::TryRecvError::Disconnected) => {
369                completion_rx.take();
370                true
371            }
372            Err(std_mpsc::TryRecvError::Empty) => false,
373        }
374    }
375
376    pub(crate) async fn shutdown(self, timeout: Duration) -> ShutdownReport {
377        let started_at = Instant::now();
378        if self.task_group.wait_task(self.task_id, timeout).await {
379            return completed_task_report(self.task_name, started_at.elapsed());
380        }
381
382        let aborted = self.task_group.abort_task_and_wait(self.task_id, timeout).await;
383        timed_out_task_report(self.task_name, started_at.elapsed(), aborted)
384    }
385
386    pub(crate) async fn abort_and_wait(self, timeout: Duration) -> bool {
387        if !self.task_group.contains_task(self.task_id) {
388            return true;
389        }
390        self.task_group.abort_task_and_wait(self.task_id, timeout).await || !self.task_group.contains_task(self.task_id)
391    }
392
393    pub(crate) fn shutdown_blocking(self, timeout: Duration) -> (ShutdownReport, bool) {
394        let started_at = Instant::now();
395        let completed = match self.completion_rx.into_inner() {
396            Some(completion_rx) => match completion_rx.recv_timeout(timeout) {
397                Ok(()) | Err(std_mpsc::RecvTimeoutError::Disconnected) => true,
398                Err(std_mpsc::RecvTimeoutError::Timeout) => false,
399            },
400            None => true,
401        };
402
403        let report = if completed {
404            completed_task_report(self.task_name, started_at.elapsed())
405        } else {
406            let aborted = self.task_group.abort_task(self.task_id);
407            timed_out_task_report(self.task_name, started_at.elapsed(), aborted)
408        };
409        (report, completed)
410    }
411
412    pub(crate) fn shutdown_now(self) -> ShutdownReport {
413        let mut report = ShutdownReport::new(self.task_name, Duration::ZERO);
414        if self.task_group.abort_task(self.task_id) {
415            report.aborted = 1;
416        } else {
417            report.completed = 1;
418        }
419        report
420    }
421}
422
423fn completed_task_report(task_name: &'static str, elapsed: Duration) -> ShutdownReport {
424    let mut report = ShutdownReport::new(task_name, elapsed);
425    report.completed = 1;
426    report
427}
428
429fn timed_out_task_report(task_name: &'static str, elapsed: Duration, aborted: bool) -> ShutdownReport {
430    let mut report = ShutdownReport::new(task_name, elapsed);
431    report.timed_out = 1;
432    report.aborted = usize::from(aborted);
433    report
434}
435
436pub(crate) fn spawn_client_tracked_task_with_context(
437    context: &ChildServiceContext,
438    task_name: &'static str,
439    task: ClientTaskFuture,
440) -> io::Result<ClientTrackedTaskHandle> {
441    let task_group = context.task_group().clone();
442    let (completion_tx, completion_rx) = std_mpsc::channel();
443    let task_id = task_group
444        .spawn_service(task_name, async move {
445            let _completion = ClientTrackedTaskCompletion::new(completion_tx);
446            task.await;
447        })
448        .map_err(io::Error::other)?;
449
450    Ok(ClientTrackedTaskHandle {
451        task_group,
452        task_id,
453        task_name,
454        completion_rx: Mutex::new(Some(completion_rx)),
455    })
456}
457
458pub(crate) enum ClientAdaptiveTaskControl {
459    ContinueAfter(Duration),
460    Stop,
461}
462
463pub(crate) struct ClientAdaptiveTaskHandle {
464    task_group: TaskGroup,
465}
466
467impl ClientAdaptiveTaskHandle {
468    pub(crate) fn task_count(&self) -> usize {
469        self.task_group.task_count()
470    }
471
472    pub(crate) async fn shutdown(&self, timeout: Duration) -> ShutdownReport {
473        self.task_group.shutdown(timeout).await
474    }
475}
476
477pub(crate) fn spawn_client_adaptive_task_with_context<F, Fut>(
478    context: &ChildServiceContext,
479    task_name: &'static str,
480    initial_delay: Duration,
481    mut task: F,
482) -> io::Result<ClientAdaptiveTaskHandle>
483where
484    F: FnMut() -> Fut + Send + 'static,
485    Fut: Future<Output = ClientAdaptiveTaskControl> + Send + 'static,
486{
487    let component = context.try_component(task_name).map_err(io::Error::other)?;
488    let task_group = component.task_group().clone();
489    let cancellation = task_group.cancellation_token();
490    task_group
491        .spawn_service(task_name, async move {
492            let mut delay = initial_delay;
493            loop {
494                tokio::select! {
495                    biased;
496                    _ = cancellation.cancelled() => break,
497                    _ = tokio::time::sleep(delay) => {}
498                }
499                let control = tokio::select! {
500                    biased;
501                    _ = cancellation.cancelled() => break,
502                    control = task() => control,
503                };
504                match control {
505                    ClientAdaptiveTaskControl::ContinueAfter(next_delay) => delay = next_delay,
506                    ClientAdaptiveTaskControl::Stop => break,
507                }
508            }
509        })
510        .map_err(io::Error::other)?;
511    Ok(ClientAdaptiveTaskHandle { task_group })
512}
513
514struct ClientTrackedTaskCompletion {
515    completion_tx: Option<std_mpsc::Sender<()>>,
516}
517
518impl ClientTrackedTaskCompletion {
519    fn new(completion_tx: std_mpsc::Sender<()>) -> Self {
520        Self {
521            completion_tx: Some(completion_tx),
522        }
523    }
524}
525
526impl Drop for ClientTrackedTaskCompletion {
527    fn drop(&mut self) {
528        if let Some(completion_tx) = self.completion_tx.take() {
529            let _ = completion_tx.send(());
530        }
531    }
532}
533
534pub(crate) struct ClientScheduledTaskHandle {
535    task_group: TaskGroup,
536    scheduled_tasks: ScheduledTaskGroup,
537}
538
539impl ClientScheduledTaskHandle {
540    pub(crate) fn is_running(&self) -> bool {
541        self.task_group.lifecycle_state() == TaskGroupLifecycleState::Open && self.task_count() > 0
542    }
543
544    pub(crate) fn task_count(&self) -> usize {
545        self.task_group.task_count()
546    }
547
548    pub(crate) fn schedule_snapshot(&self) -> Vec<ScheduledTaskSnapshot> {
549        self.scheduled_tasks.snapshot()
550    }
551
552    pub(crate) async fn shutdown(self, timeout: Duration) -> ShutdownReport {
553        self.task_group.shutdown(timeout).await
554    }
555
556    pub(crate) fn shutdown_now(self) -> ShutdownReport {
557        self.task_group.shutdown_now()
558    }
559}
560
561pub(crate) fn schedule_client_fixed_delay_task_with_context<F, Fut>(
562    context: &ChildServiceContext,
563    task_name: &'static str,
564    initial_delay: Duration,
565    period: Duration,
566    task: F,
567) -> io::Result<ClientScheduledTaskHandle>
568where
569    F: FnMut() -> Fut + Send + 'static,
570    Fut: Future<Output = ()> + Send + 'static,
571{
572    let task_group = context.component(task_name).task_group().clone();
573    let scheduled_tasks = ScheduledTaskGroup::new(task_group.clone());
574    let mut config = ScheduledTaskConfig::fixed_delay(task_name, period);
575    config.initial_delay = initial_delay;
576    scheduled_tasks
577        .schedule(config, ScheduledExecutionPolicy::default(), task)
578        .map_err(io::Error::other)?;
579
580    Ok(ClientScheduledTaskHandle {
581        task_group,
582        scheduled_tasks,
583    })
584}
585
586pub(crate) fn schedule_client_fixed_delay_controlled_task_with_context<F, Fut>(
587    context: &ChildServiceContext,
588    task_name: &'static str,
589    initial_delay: Duration,
590    period: Duration,
591    task: F,
592) -> io::Result<ClientScheduledTaskHandle>
593where
594    F: FnMut() -> Fut + Send + 'static,
595    Fut: Future<Output = ScheduledTaskControl> + Send + 'static,
596{
597    let task_group = context.component(task_name).task_group().clone();
598    let scheduled_tasks = ScheduledTaskGroup::new(task_group.clone());
599    let mut config = ScheduledTaskConfig::fixed_delay(task_name, period);
600    config.initial_delay = initial_delay;
601    scheduled_tasks
602        .schedule_controlled(config, task)
603        .map_err(io::Error::other)?;
604
605    Ok(ClientScheduledTaskHandle {
606        task_group,
607        scheduled_tasks,
608    })
609}
610
611pub(crate) async fn spawn_client_blocking_io_with_context<F, R>(
612    context: &ChildServiceContext,
613    task_name: &'static str,
614    task: F,
615) -> io::Result<R>
616where
617    F: FnOnce() -> R + Send + 'static,
618    R: Send + 'static,
619{
620    context
621        .metadata_io()
622        .spawn_io(task_name, task)
623        .await
624        .map_err(io::Error::other)
625}
626
627pub(crate) fn spawn_delayed_client_action_with_context<F>(
628    context: &ChildServiceContext,
629    task_name: &'static str,
630    delay: Duration,
631    action: F,
632) where
633    F: FnOnce() + Send + 'static,
634{
635    if delay.is_zero() {
636        action();
637        return;
638    }
639
640    if let Err(error) = spawn_background_client_task_with_context(
641        context,
642        task_name,
643        Box::pin(async move {
644            tokio::time::sleep(delay).await;
645            action();
646        }),
647    ) {
648        tracing::error!(%error, task_name, "failed to spawn delayed client action");
649    }
650}
651
652#[cfg(test)]
653mod tests {
654    use std::sync::atomic::AtomicUsize;
655
656    use rocketmq_protocol::protocol::remoting_command_defaults::RemotingCommandDefaults;
657    use rocketmq_protocol::protocol::SerializeType;
658
659    use super::*;
660
661    /// Upper bound for a registry-settling wait.
662    ///
663    /// The wait covers bookkeeping only, never task execution, so a generous bound costs nothing on
664    /// the happy path while keeping a genuine regression failing instead of hanging.
665    const REGISTRY_SETTLE_TIMEOUT: Duration = Duration::from_secs(5);
666
667    /// How often [`wait_for_registry_to_settle`] re-reads the registry.
668    const REGISTRY_SETTLE_POLL_INTERVAL: Duration = Duration::from_millis(5);
669
670    /// Waits for a task group's registry records to drain, returning how many remain.
671    ///
672    /// The runtime publishes a task's completion signal before it removes that task's registry
673    /// record: `TaskGroupInner::finish_task` runs only after the user future, and therefore the
674    /// completion handle that future owns, has been dropped. A group can consequently still report
675    /// a nonzero `task_count` after every `wait_finished` call has returned true, so a test that
676    /// asserts on the group's own counters must let the registry settle first.
677    async fn wait_for_registry_to_settle(task_group: &TaskGroup, timeout: Duration) -> usize {
678        let deadline = Instant::now() + timeout;
679        loop {
680            let remaining = task_group.task_count();
681            if remaining == 0 || Instant::now() >= deadline {
682                return remaining;
683            }
684            tokio::time::sleep(REGISTRY_SETTLE_POLL_INTERVAL).await;
685        }
686    }
687
688    #[test]
689    fn client_runtimes_keep_independent_remoting_command_factories() {
690        let json_factory = RemotingCommandFactory::new(RemotingCommandDefaults::new(301, SerializeType::JSON));
691        let binary_factory = RemotingCommandFactory::new(RemotingCommandDefaults::new(302, SerializeType::ROCKETMQ));
692        let json_runtime = ClientRuntime::try_new_with_remoting_command_factory(
693            TEST_RUNTIME_OWNER.root_context().component("client-json-command-owner"),
694            ClientRuntimeConfig::default(),
695            TelemetryHandle::noop(),
696            json_factory,
697        )
698        .expect("JSON client runtime");
699        let binary_runtime = ClientRuntime::try_new_with_remoting_command_factory(
700            TEST_RUNTIME_OWNER
701                .root_context()
702                .component("client-binary-command-owner"),
703            ClientRuntimeConfig::default(),
704            TelemetryHandle::noop(),
705            binary_factory,
706        )
707        .expect("ROCKETMQ client runtime");
708        let mut json_config = crate::base::client_config::ClientConfig::default();
709        json_config.set_instance_name("client-json-command-owner".into());
710        let mut binary_config = crate::base::client_config::ClientConfig::default();
711        binary_config.set_instance_name("client-binary-command-owner".into());
712
713        let json_client = json_runtime
714            .pool()
715            .get_or_create(json_config, None)
716            .expect("JSON client owner");
717        let binary_client = binary_runtime
718            .pool()
719            .get_or_create(binary_config, None)
720            .expect("ROCKETMQ client owner");
721
722        assert_eq!(json_client.instance().remoting_command_factory(), json_factory);
723        assert_eq!(binary_client.instance().remoting_command_factory(), binary_factory);
724        assert_eq!(
725            json_client
726                .instance()
727                .get_mq_client_api_impl()
728                .expect("JSON API owner")
729                .remoting_command_factory(),
730            json_factory
731        );
732        assert_eq!(
733            binary_client
734                .instance()
735                .get_mq_client_api_impl()
736                .expect("ROCKETMQ API owner")
737                .remoting_command_factory(),
738            binary_factory
739        );
740    }
741
742    #[test]
743    fn sibling_component_budgets_share_the_client_runtime_parent_limit() {
744        let runtime = ClientRuntime::try_new(
745            TEST_RUNTIME_OWNER.root_context().component("client-budget-parent-test"),
746            ClientRuntimeConfig {
747                process_memory_limit_bytes: 4_096,
748                managed_memory_numerator: 1,
749                managed_memory_denominator: 1,
750                ..ClientRuntimeConfig::default()
751            },
752            TelemetryHandle::noop(),
753        )
754        .expect("client runtime budget");
755        let parent = runtime.resource_budget();
756        let first = parent
757            .child("first-component", BudgetLimit::new(10, 4_096, FullPolicy::Reject))
758            .expect("first child");
759        let second = parent
760            .child("second-component", BudgetLimit::new(10, 4_096, FullPolicy::Reject))
761            .expect("second child");
762
763        let _first_permit = first.try_acquire_data(3_000).expect("first reservation");
764
765        assert!(second.try_acquire_data(1_000).is_err());
766        assert_eq!(parent.snapshot().current_bytes, 3_000);
767    }
768
769    #[tokio::test]
770    async fn explicit_context_owns_spawned_task() {
771        let service_context = test_service_context("client-explicit-runtime-test");
772        let completed = Arc::new(AtomicUsize::new(0));
773        let completed_in_task = Arc::clone(&completed);
774
775        let handle = spawn_client_task_with_context(
776            &service_context,
777            "owned-task",
778            Box::pin(async move {
779                completed_in_task.fetch_add(1, Ordering::Release);
780            }),
781        )
782        .expect("task should spawn");
783
784        assert!(handle.wait_finished(Duration::from_secs(1)));
785        assert_eq!(completed.load(Ordering::Acquire), 1);
786        assert!(handle.is_finished());
787        assert!(
788            service_context
789                .task_group()
790                .wait_task(handle.task_id(), Duration::from_secs(1))
791                .await
792        );
793        assert_eq!(handle.task_count(), 0);
794        let report = service_context.task_group().shutdown(Duration::from_secs(1)).await;
795        assert!(report.is_healthy(), "{report:?}");
796    }
797
798    #[tokio::test]
799    async fn transient_tasks_share_component_owner() {
800        const TASKS: usize = 1_024;
801
802        let service_context = test_service_context("client-transient-owner-test");
803        let baseline_components = service_context.task_group().component_count();
804        let mut handles = Vec::with_capacity(TASKS);
805
806        for _ in 0..TASKS {
807            handles.push(
808                spawn_client_task_with_context(&service_context, "transient-task", Box::pin(async {}))
809                    .expect("transient task should spawn"),
810            );
811        }
812
813        for handle in &handles {
814            assert!(handle.wait_finished(Duration::from_secs(1)));
815        }
816
817        assert_eq!(
818            wait_for_registry_to_settle(service_context.task_group(), REGISTRY_SETTLE_TIMEOUT).await,
819            0,
820            "finished tasks must leave the group registry without a shutdown"
821        );
822        assert_eq!(
823            service_context.task_group().component_count(),
824            baseline_components,
825            "completed task handles must not retain per-task component groups"
826        );
827        drop(handles);
828
829        let report = service_context.task_group().shutdown(Duration::from_secs(1)).await;
830        assert!(report.is_healthy(), "{}", report.to_json());
831    }
832
833    #[tokio::test]
834    async fn tracked_task_shutdown_does_not_close_component_owner() {
835        let service_context = test_service_context("client-tracked-task-isolation-test");
836        let first = spawn_client_tracked_task_with_context(
837            &service_context,
838            "first-tracked-task",
839            Box::pin(std::future::pending()),
840        )
841        .expect("first task should spawn");
842        let (second_shutdown_tx, second_shutdown_rx) = tokio::sync::oneshot::channel();
843        let second = spawn_client_tracked_task_with_context(
844            &service_context,
845            "second-tracked-task",
846            Box::pin(async move {
847                let _ = second_shutdown_rx.await;
848            }),
849        )
850        .expect("second task should spawn");
851
852        let report = first.shutdown_now();
853        assert_eq!(report.aborted, 1, "{}", report.to_json());
854        assert_eq!(
855            service_context.task_group().lifecycle_state(),
856            TaskGroupLifecycleState::Open
857        );
858        assert_eq!(second.task_count(), 1);
859
860        second_shutdown_tx
861            .send(())
862            .expect("second task should still be running");
863        let report = second.shutdown(Duration::from_secs(1)).await;
864        assert!(report.is_healthy(), "{}", report.to_json());
865        assert_eq!(report.completed, 1);
866
867        let report = service_context.task_group().shutdown(Duration::from_secs(1)).await;
868        assert!(report.is_healthy(), "{}", report.to_json());
869    }
870
871    #[tokio::test]
872    async fn tracked_task_abort_waits_for_registry_removal() {
873        let service_context = test_service_context("client-tracked-task-abort-wait-test");
874        let handle = spawn_client_tracked_task_with_context(
875            &service_context,
876            "abort-and-wait-task",
877            Box::pin(std::future::pending()),
878        )
879        .expect("tracked task should spawn");
880
881        assert!(handle.abort_and_wait(Duration::from_secs(1)).await);
882        assert_eq!(service_context.task_group().task_count(), 0);
883
884        let report = service_context.task_group().shutdown(Duration::from_secs(1)).await;
885        assert!(report.is_healthy(), "{}", report.to_json());
886    }
887}