Concurrent processing using N independent long-poll loops (for Lambda managed-concurrency).
(
service: S,
config: Arc<Config>,
client: Arc<ApiClient>,
concurrency_limit: u32,
)
| 199 | |
| 200 | /// Concurrent processing using N independent long-poll loops (for Lambda managed-concurrency). |
| 201 | async fn run_concurrent_inner( |
| 202 | service: S, |
| 203 | config: Arc<Config>, |
| 204 | client: Arc<ApiClient>, |
| 205 | concurrency_limit: u32, |
| 206 | ) -> Result<(), BoxError> { |
| 207 | let limit = concurrency_limit as usize; |
| 208 | |
| 209 | // Use FuturesUnordered so we can observe worker exits as they happen, |
| 210 | // rather than waiting for all workers to finish (join_all). |
| 211 | let mut workers: FuturesUnordered<tokio::task::JoinHandle<(tokio::task::Id, Result<(), BoxError>)>> = |
| 212 | FuturesUnordered::new(); |
| 213 | let spawn_worker = |service: S, config: Arc<Config>, client: Arc<ApiClient>| { |
| 214 | tokio::spawn(async move { |
| 215 | let task_id = tokio::task::id(); |
| 216 | let result = concurrent_worker_loop(service, config, client).await; |
| 217 | (task_id, result) |
| 218 | }) |
| 219 | }; |
| 220 | // Spawn one worker per concurrency slot; the last uses the owned service to avoid an extra clone. |
| 221 | for _ in 1..limit { |
| 222 | workers.push(spawn_worker(service.clone(), config.clone(), client.clone())); |
| 223 | } |
| 224 | workers.push(spawn_worker(service, config, client)); |
| 225 | |
| 226 | // Track worker exits across tasks. A single worker failing should not |
| 227 | // terminate the whole runtime (LMI keeps running with the remaining |
| 228 | // healthy workers). We only return an error once there are no workers |
| 229 | // left (i.e., we cannot keep at least 1 worker alive). |
| 230 | // |
| 231 | // Note: Handler errors (Err returned from user code) do NOT trigger this. |
| 232 | // They are reported to Lambda via /invocation/{id}/error and the worker |
| 233 | // continues. This only captures unrecoverable runtime failures like |
| 234 | // API client failures, runtime panics, etc. |
| 235 | let mut errors: Vec<WorkerError> = Vec::new(); |
| 236 | let mut remaining_workers = limit; |
| 237 | while let Some(result) = futures::StreamExt::next(&mut workers).await { |
| 238 | remaining_workers = remaining_workers.saturating_sub(1); |
| 239 | match result { |
| 240 | Ok((task_id, Ok(()))) => { |
| 241 | // `concurrent_worker_loop` runs indefinitely, so an Ok return indicates |
| 242 | // an unexpected worker exit; we still decrement because the task is gone. |
| 243 | error!( |
| 244 | task_id = %task_id, |
| 245 | remaining_workers, |
| 246 | "Concurrent worker exited cleanly (unexpected - loop should run forever)" |
| 247 | ); |
| 248 | errors.push(WorkerError::CleanExit(task_id)); |
| 249 | } |
| 250 | Ok((task_id, Err(err))) => { |
| 251 | error!( |
| 252 | task_id = %task_id, |
| 253 | error = %err, |
| 254 | remaining_workers, |
| 255 | "Concurrent worker exited with error" |
| 256 | ); |
| 257 | errors.push(WorkerError::Failure(task_id, err)); |
| 258 | } |
nothing calls this directly
no test coverage detected