meerkat_mobkit/unified_runtime/
mob_ops.rs1use meerkat_mob::{MobError, MobHandle, SpawnMemberSpec, SpawnResult};
10use std::future::Future;
11
12use crate::mob_handle_runtime::MobRuntimeError;
13
14use super::UnifiedRuntime;
15
16const MAX_CONCURRENT_SPAWN_MANY: usize = 1;
20
21pub(crate) const DEFAULT_WAIT_READY_TIMEOUT: std::time::Duration =
32 std::time::Duration::from_mins(10);
33
34pub(crate) fn is_ready_wait_timeout(err: &MobError) -> bool {
44 matches!(err, MobError::ReadyWaitTimedOut { .. })
45}
46
47impl UnifiedRuntime {
48 pub fn mob_handle(&self) -> MobHandle {
49 self.mob_runtime.handle()
50 }
51
52 pub fn mob_runtime(&self) -> &crate::mob_handle_runtime::MobRuntime {
54 &self.mob_runtime
55 }
56
57 pub async fn spawn(&self, mut spec: SpawnMemberSpec) -> Result<SpawnResult, MobRuntimeError> {
65 let member_id = spec.identity.to_string();
66 let profile = spec.role_name.to_string();
67 spec.identity = crate::member_comms_id::mob_member_id(member_id.as_str());
68 match Box::pin(self.mob_handle().spawn_spec(spec)).await {
69 Ok(result) => {
70 if let Some(hook) = &self.post_spawn_hook {
71 hook(vec![member_id]).await;
72 }
73 Ok(result)
74 }
75 Err(err) => {
76 self.fire_error(super::types::ErrorEvent::SpawnFailure {
77 member_id,
78 profile,
79 error: format!("{err}"),
80 });
81 Err(err.into())
82 }
83 }
84 }
85
86 pub async fn spawn_many(
88 &self,
89 mut specs: Vec<SpawnMemberSpec>,
90 ) -> Result<Vec<SpawnResult>, MobRuntimeError> {
91 let member_ids: Vec<String> = specs.iter().map(|s| s.identity.to_string()).collect();
92 for spec in &mut specs {
94 spec.identity = crate::member_comms_id::mob_member_id(spec.identity.as_str());
95 }
96 let handle = self.mob_handle();
97 let refs = try_join_in_batches(specs, MAX_CONCURRENT_SPAWN_MANY, |spec| {
98 let handle = handle.clone();
99 async move { Box::pin(handle.spawn_spec(spec)).await }
100 })
101 .await
102 .map_err(MobRuntimeError::from)?;
103 if !member_ids.is_empty()
104 && let Some(hook) = &self.post_spawn_hook
105 {
106 hook(member_ids).await;
107 }
108 Ok(refs)
109 }
110}
111
112async fn try_join_in_batches<I, F, T, E, Build>(
113 items: Vec<I>,
114 batch_size: usize,
115 mut build: Build,
116) -> Result<Vec<T>, E>
117where
118 F: Future<Output = Result<T, E>>,
119 Build: FnMut(I) -> F,
120{
121 let batch_size = batch_size.max(1);
122 let mut results = Vec::with_capacity(items.len());
123 let mut iter = items.into_iter();
124
125 loop {
126 let batch: Vec<I> = iter.by_ref().take(batch_size).collect();
127 if batch.is_empty() {
128 break;
129 }
130
131 let futures = batch.into_iter().map(&mut build);
132 let mut batch_results = futures::future::try_join_all(futures).await?;
133 results.append(&mut batch_results);
134 tokio::task::yield_now().await;
135 }
136
137 Ok(results)
138}
139
140#[cfg(test)]
141mod tests {
142 use std::sync::{
143 Arc,
144 atomic::{AtomicUsize, Ordering},
145 };
146
147 use super::{is_ready_wait_timeout, try_join_in_batches};
148 use meerkat_mob::MobError;
149
150 #[tokio::test]
151 async fn spawn_many_batch_size_stays_serial_until_upstream_backpressure_exists() {
152 assert_eq!(super::MAX_CONCURRENT_SPAWN_MANY, 1);
153 }
154
155 #[test]
156 fn ready_wait_timeout_is_classified_as_envelope_not_error() {
157 let timed_out = MobError::ReadyWaitTimedOut {
163 pending_member_ids: vec![],
164 };
165 assert!(is_ready_wait_timeout(&timed_out));
166
167 let display = timed_out.to_string().to_lowercase();
169 assert!(
170 !display.contains("timeout"),
171 "old substring check would have missed this timeout"
172 );
173 assert!(display.contains("timed out"));
174
175 assert!(!is_ready_wait_timeout(&MobError::KickoffWaitTimedOut {
178 pending_member_ids: vec![],
179 }));
180 }
181
182 #[tokio::test]
183 async fn try_join_in_batches_can_run_serially() {
184 let active = Arc::new(AtomicUsize::new(0));
185 let max_active = Arc::new(AtomicUsize::new(0));
186 let items: Vec<usize> = (0..25).collect();
187
188 let results = try_join_in_batches(items.clone(), 1, |item| {
189 let active = active.clone();
190 let max_active = max_active.clone();
191 async move {
192 let current = active.fetch_add(1, Ordering::SeqCst) + 1;
193 max_active.fetch_max(current, Ordering::SeqCst);
194 tokio::task::yield_now().await;
195 active.fetch_sub(1, Ordering::SeqCst);
196 Ok::<_, ()>(item)
197 }
198 })
199 .await;
200
201 assert_eq!(results, Ok(items));
202 assert_eq!(max_active.load(Ordering::SeqCst), 1);
203 }
204
205 #[tokio::test]
206 async fn try_join_in_batches_limits_concurrent_work_and_preserves_order() {
207 let active = Arc::new(AtomicUsize::new(0));
208 let max_active = Arc::new(AtomicUsize::new(0));
209 let items: Vec<usize> = (0..75).collect();
210
211 let results = try_join_in_batches(items.clone(), 16, |item| {
212 let active = active.clone();
213 let max_active = max_active.clone();
214 async move {
215 let current = active.fetch_add(1, Ordering::SeqCst) + 1;
216 max_active.fetch_max(current, Ordering::SeqCst);
217 tokio::task::yield_now().await;
218 active.fetch_sub(1, Ordering::SeqCst);
219 Ok::<_, ()>(item)
220 }
221 })
222 .await;
223
224 assert_eq!(results, Ok(items));
225 assert!(max_active.load(Ordering::SeqCst) <= 16);
226 }
227
228 #[tokio::test]
229 async fn try_join_in_batches_stops_before_starting_later_batches_after_error() {
230 let started = Arc::new(AtomicUsize::new(0));
231 let items: Vec<usize> = (0..40).collect();
232
233 let result = try_join_in_batches(items, 16, |item| {
234 let started = started.clone();
235 async move {
236 started.fetch_add(1, Ordering::SeqCst);
237 tokio::task::yield_now().await;
238 if item == 20 { Err(item) } else { Ok(item) }
239 }
240 })
241 .await;
242
243 assert_eq!(result, Err(20));
244 assert_eq!(started.load(Ordering::SeqCst), 32);
245 }
246}