1use 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#[derive(Clone, Debug)]
53pub struct ClientRuntimeConfig {
54 pub shutdown_timeout: Duration,
56 pub process_memory_limit_bytes: u64,
59 pub managed_memory_numerator: u64,
61 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
76pub 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 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 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 pub fn pool(&self) -> &ClientPool {
140 &self.pool
141 }
142
143 pub fn service_context(&self) -> &ChildServiceContext {
145 &self.service_context
146 }
147
148 pub fn telemetry_handle(&self) -> &TelemetryHandle {
150 &self.telemetry_handle
151 }
152
153 pub fn client_metrics(&self) -> &ClientMetrics {
155 &self.client_metrics
156 }
157
158 pub fn resource_budget(&self) -> ResourceBudget {
161 self.resource_budget.clone()
162 }
163
164 pub fn component(&self, scope: impl Into<rocketmq_runtime::ScopeId>) -> ChildServiceContext {
166 self.service_context.component(scope)
167 }
168
169 pub async fn shutdown(&self) -> ShutdownReport {
171 self.shutdown_until(ShutdownDeadline::after(self.config.shutdown_timeout))
172 .await
173 }
174
175 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 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
313pub(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 const REGISTRY_SETTLE_TIMEOUT: Duration = Duration::from_secs(5);
666
667 const REGISTRY_SETTLE_POLL_INTERVAL: Duration = Duration::from_millis(5);
669
670 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}