1use std::collections::{BTreeMap, BTreeSet, HashMap};
4use std::sync::Arc;
5
6use aion_core::{ActivityError, ActivityId, Payload, WorkflowId};
7use async_trait::async_trait;
8use futures::StreamExt;
9use futures::future;
10use tokio::sync::{Semaphore, mpsc};
11use tracing::{debug, info};
12
13use crate::config::WorkerConfig;
14use crate::context::{ActivityContext, HeartbeatRequest};
15use crate::error::WorkerError;
16use crate::protocol::reconnect::UnackedResultTracker;
17use crate::protocol::{
18 ActivityExecutionKey, ActivityTask, HeartbeatBookkeeper, WorkerSession, WorkerSessionEvent,
19};
20use crate::runtime::report::{
21 DispatchFinished, InFlightActivity, RuntimeChannels, drain_remaining, record_first_error,
22 report_finished,
23};
24
25#[async_trait]
27pub trait ActivityDispatcher: Send + Sync + 'static {
28 async fn dispatch(
30 &self,
31 task: ActivityTask,
32 context: ActivityContext,
33 ) -> Result<DispatchOutcome, WorkerError>;
34
35 fn activity_types(&self) -> BTreeSet<String>;
37}
38
39#[derive(Clone, Debug, PartialEq, Eq)]
41pub enum DispatchOutcome {
42 Completed {
44 output: Payload,
46 },
47 Failed {
49 failure: ActivityError,
51 },
52}
53
54pub type NoShutdown = future::Pending<()>;
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum ServeEnd {
60 Shutdown,
62 StreamClosed,
66 Drained,
70}
71
72#[derive(Debug, Default)]
75pub struct SessionHealth {
76 pub tasks_reported: usize,
78 pub stream_ended_at: Option<tokio::time::Instant>,
82 pub drain_received: bool,
88}
89
90pub async fn serve_activity_tasks<S, D>(
111 config: &WorkerConfig,
112 session: &mut S,
113 dispatcher: Arc<D>,
114 tracker: &mut UnackedResultTracker,
115) -> Result<ServeEnd, WorkerError>
116where
117 S: WorkerSession,
118 D: ActivityDispatcher,
119{
120 let mut health = SessionHealth::default();
121 serve_activity_tasks_until(
122 config,
123 session,
124 dispatcher,
125 tracker,
126 &mut health,
127 future::pending(),
128 )
129 .await
130}
131
132pub async fn serve_activity_tasks_until<S, D, Shutdown>(
173 config: &WorkerConfig,
174 session: &mut S,
175 dispatcher: Arc<D>,
176 tracker: &mut UnackedResultTracker,
177 health: &mut SessionHealth,
178 shutdown: Shutdown,
179) -> Result<ServeEnd, WorkerError>
180where
181 S: WorkerSession,
182 D: ActivityDispatcher,
183 Shutdown: Future<Output = ()> + Send,
184{
185 ensure_max_concurrency(config)?;
186 let semaphore = Arc::new(Semaphore::new(config.max_concurrency));
187 let (result_sender, heartbeat_sender, mut channels) = runtime_channels();
188 let heartbeat_bookkeeper = HeartbeatBookkeeper::default();
189 let mut liveness_pump = liveness_pump_for(session);
190 let mut stream = session.receive_tasks();
191 let mut in_flight = HashMap::<ActivityExecutionKey, InFlightActivity>::new();
192 let mut pending_error = None;
193 let mut end = ServeEnd::StreamClosed;
196 tokio::pin!(shutdown);
197
198 while pending_error.is_none() {
201 tokio::select! {
202 biased;
203 () = &mut shutdown => {
204 cancel_all_in_flight(&in_flight);
205 end = ServeEnd::Shutdown;
206 break;
207 }
208 finished = channels.results.recv() => {
214 consume_finished(
215 session,
216 &heartbeat_bookkeeper,
217 finished,
218 &mut in_flight,
219 tracker,
220 health,
221 &mut pending_error,
222 )
223 .await;
224 }
225 request = channels.heartbeats.recv() => {
229 forward_heartbeat(session, &heartbeat_bookkeeper, request, &mut pending_error)
230 .await;
231 }
232 () = tick_liveness_pump(&mut liveness_pump) => {
236 pump_liveness(session, &heartbeat_bookkeeper, &in_flight, &mut pending_error)
237 .await;
238 }
239 event = stream.next() => {
240 let Some(event) = event else { break; };
241 match event {
242 Ok(WorkerSessionEvent::Cancel { workflow_id, activity_id }) => {
243 deliver_cancellation(workflow_id, &activity_id, &in_flight);
244 }
245 Ok(WorkerSessionEvent::ResultAck { workflow_id, activity_id }) => {
248 acknowledge_result(&workflow_id, &activity_id, tracker);
249 }
250 Ok(WorkerSessionEvent::Drain) => {
251 info!("server drain received; finishing in-flight work before reconnect");
252 health.drain_received = true;
253 end = ServeEnd::Drained;
254 break;
255 }
256 Err(error) => {
257 pending_error = Some(error);
258 break;
259 }
260 Ok(WorkerSessionEvent::Task(proto_task)) => {
261 let Some(permit) =
262 acquire_permit_or_shutdown(shutdown.as_mut(), &semaphore).await?
263 else {
264 cancel_all_in_flight(&in_flight);
265 end = ServeEnd::Shutdown;
266 break;
267 };
268 if !handle_task(
269 *proto_task,
270 SessionEventContext {
271 permit,
272 dispatcher: Arc::clone(&dispatcher),
273 result_sender: &result_sender,
274 heartbeat_sender: &heartbeat_sender,
275 heartbeat_bookkeeper: &heartbeat_bookkeeper,
276 in_flight: &mut in_flight,
277 pending_error: &mut pending_error,
278 },
279 )? {
280 break;
281 }
282 }
283 }
284 }
285 }
286 }
287
288 health.stream_ended_at = Some(tokio::time::Instant::now());
292
293 drop((result_sender, heartbeat_sender));
294 drain_remaining(
295 session,
296 &heartbeat_bookkeeper,
297 &mut channels,
298 &mut in_flight,
299 tracker,
300 &mut health.tasks_reported,
301 &mut pending_error,
302 )
303 .await;
304
305 pending_error.map_or(Ok(end), Err)
306}
307
308fn runtime_channels() -> (
310 mpsc::UnboundedSender<DispatchFinished>,
311 mpsc::UnboundedSender<HeartbeatRequest>,
312 RuntimeChannels,
313) {
314 let (result_sender, result_receiver) = mpsc::unbounded_channel();
315 let (heartbeat_sender, heartbeat_receiver) = mpsc::unbounded_channel();
316 let channels = RuntimeChannels {
317 heartbeats: heartbeat_receiver,
318 results: result_receiver,
319 };
320 (result_sender, heartbeat_sender, channels)
321}
322
323struct SessionEventContext<'a, D> {
324 permit: tokio::sync::OwnedSemaphorePermit,
325 dispatcher: Arc<D>,
326 result_sender: &'a mpsc::UnboundedSender<DispatchFinished>,
327 heartbeat_sender: &'a mpsc::UnboundedSender<HeartbeatRequest>,
328 heartbeat_bookkeeper: &'a HeartbeatBookkeeper,
329 in_flight: &'a mut HashMap<ActivityExecutionKey, InFlightActivity>,
330 pending_error: &'a mut Option<WorkerError>,
331}
332
333fn handle_task<D>(
334 proto_task: aion_proto::ProtoActivityTask,
335 ctx: SessionEventContext<'_, D>,
336) -> Result<bool, WorkerError>
337where
338 D: ActivityDispatcher,
339{
340 let task = match ActivityTask::try_from(proto_task) {
341 Ok(task) => task,
342 Err(error) => {
343 drop(ctx.permit);
344 *ctx.pending_error = Some(error);
345 return Ok(false);
346 }
347 };
348 spawn_activity(
349 task,
350 ctx.permit,
351 ctx.dispatcher,
352 ctx.result_sender.clone(),
353 ctx.heartbeat_sender.clone(),
354 ctx.heartbeat_bookkeeper,
355 ctx.in_flight,
356 )?;
357 Ok(true)
358}
359
360fn ensure_max_concurrency(config: &WorkerConfig) -> Result<(), WorkerError> {
362 if config.max_concurrency == 0 {
363 return Err(WorkerError::registration(InvalidMaxConcurrency));
364 }
365 Ok(())
366}
367
368async fn acquire_permit_or_shutdown<F>(
371 shutdown: std::pin::Pin<&mut F>,
372 semaphore: &Arc<Semaphore>,
373) -> Result<Option<tokio::sync::OwnedSemaphorePermit>, WorkerError>
374where
375 F: Future<Output = ()> + Send,
376{
377 tokio::select! {
378 biased;
379 () = shutdown => Ok(None),
380 permit = Arc::clone(semaphore).acquire_owned() => {
381 permit.map(Some).map_err(WorkerError::registration)
382 }
383 }
384}
385
386fn liveness_pump_for<S>(session: &S) -> Option<tokio::time::Interval>
393where
394 S: WorkerSession,
395{
396 session.heartbeat_window().map(|window| {
397 let mut ticks = tokio::time::interval(liveness_pump_interval(window));
398 ticks.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
399 ticks
400 })
401}
402
403async fn consume_finished<S>(
407 session: &mut S,
408 heartbeat_bookkeeper: &HeartbeatBookkeeper,
409 finished: Option<DispatchFinished>,
410 in_flight: &mut HashMap<ActivityExecutionKey, InFlightActivity>,
411 tracker: &mut UnackedResultTracker,
412 health: &mut SessionHealth,
413 pending_error: &mut Option<WorkerError>,
414) where
415 S: WorkerSession,
416{
417 if let Some(finished) = finished {
418 report_finished(
419 session,
420 heartbeat_bookkeeper,
421 finished,
422 in_flight,
423 tracker,
424 &mut health.tasks_reported,
425 pending_error,
426 )
427 .await;
428 }
429}
430
431#[must_use]
443pub(crate) fn liveness_pump_interval(heartbeat_window: std::time::Duration) -> std::time::Duration {
444 (heartbeat_window / 4).max(std::time::Duration::from_millis(1))
445}
446
447async fn tick_liveness_pump(pump: &mut Option<tokio::time::Interval>) {
451 match pump {
452 Some(ticks) => {
453 ticks.tick().await;
454 }
455 None => future::pending().await,
456 }
457}
458
459async fn pump_liveness<S>(
462 session: &mut S,
463 heartbeat_bookkeeper: &HeartbeatBookkeeper,
464 in_flight: &HashMap<ActivityExecutionKey, InFlightActivity>,
465 pending_error: &mut Option<WorkerError>,
466) where
467 S: WorkerSession,
468{
469 record_first_error(pending_error, session.send_connection_heartbeat().await);
470 if pending_error.is_some() {
471 return;
472 }
473 for key in in_flight.keys() {
474 record_first_error(
475 pending_error,
476 crate::protocol::send_heartbeat(
477 session,
478 heartbeat_bookkeeper,
479 HeartbeatRequest {
480 workflow_id: key.workflow_id.clone(),
481 activity_id: key.activity_id.clone(),
482 detail: None,
483 },
484 )
485 .await,
486 );
487 if pending_error.is_some() {
488 return;
491 }
492 }
493}
494
495async fn forward_heartbeat<S>(
498 session: &mut S,
499 heartbeat_bookkeeper: &HeartbeatBookkeeper,
500 request: Option<HeartbeatRequest>,
501 pending_error: &mut Option<WorkerError>,
502) where
503 S: WorkerSession,
504{
505 if let Some(request) = request {
506 record_first_error(
507 pending_error,
508 crate::protocol::send_heartbeat(session, heartbeat_bookkeeper, request).await,
509 );
510 }
511}
512
513fn acknowledge_result(
516 workflow_id: &WorkflowId,
517 activity_id: &ActivityId,
518 tracker: &mut UnackedResultTracker,
519) {
520 if tracker.acknowledge(workflow_id, activity_id).is_some() {
521 debug!(
522 workflow_id = %workflow_id,
523 activity_id = activity_id.sequence_position(),
524 "server acknowledged activity result; tracker entry cleared"
525 );
526 } else {
527 debug!(
528 workflow_id = %workflow_id,
529 activity_id = activity_id.sequence_position(),
530 "result ack for unknown tracker entry ignored"
531 );
532 }
533}
534
535fn render_labels(labels: &BTreeMap<String, String>) -> String {
539 labels
540 .iter()
541 .map(|(key, value)| format!("{key}={value}"))
542 .collect::<Vec<_>>()
543 .join(" ")
544}
545
546fn spawn_activity<D>(
547 task: ActivityTask,
548 permit: tokio::sync::OwnedSemaphorePermit,
549 dispatcher: Arc<D>,
550 result_sender: mpsc::UnboundedSender<DispatchFinished>,
551 heartbeat_sender: mpsc::UnboundedSender<HeartbeatRequest>,
552 heartbeat_bookkeeper: &HeartbeatBookkeeper,
553 in_flight: &mut HashMap<ActivityExecutionKey, InFlightActivity>,
554) -> Result<(), WorkerError>
555where
556 D: ActivityDispatcher,
557{
558 info!(
559 activity_type = %task.activity_type,
560 activity_id = task.activity_id.sequence_position(),
561 workflow_id = %task.workflow_id,
562 attempt = task.attempt,
563 labels = %render_labels(&task.labels),
564 "received activity task"
565 );
566 let key = ActivityExecutionKey::new(task.workflow_id.clone(), task.activity_id.clone());
567 heartbeat_bookkeeper.register(key.clone())?;
568 let (context, cancellation_handle) = ActivityContext::for_task(
569 task.workflow_id.clone(),
570 task.activity_id.clone(),
571 task.attempt,
572 task.idempotency_key.clone(),
573 Some(heartbeat_sender),
574 );
575 let finished_key = key.clone();
576 let finished_run_id = task.run_id.clone();
577 let finished_completion_token = task.completion_token.clone();
578 let join_handle = tokio::spawn(async move {
579 let outcome = dispatcher.dispatch(task, context).await;
580 if result_sender
581 .send(DispatchFinished {
582 key: finished_key,
583 run_id: finished_run_id,
584 completion_token: finished_completion_token,
585 outcome,
586 })
587 .is_err()
588 {
589 debug!("worker loop stopped before dispatch outcome could be delivered");
590 }
591 drop(permit);
592 });
593 in_flight.insert(
594 key,
595 InFlightActivity {
596 cancellation_handle,
597 join_handle,
598 },
599 );
600 Ok(())
601}
602
603fn deliver_cancellation(
604 workflow_id: WorkflowId,
605 activity_id: &ActivityId,
606 in_flight: &HashMap<ActivityExecutionKey, InFlightActivity>,
607) {
608 let key = ActivityExecutionKey::new(workflow_id, activity_id.clone());
609 if let Some(in_flight_activity) = in_flight.get(&key) {
610 in_flight_activity.cancellation_handle.cancel();
611 info!(
612 activity_id = activity_id.sequence_position(),
613 "delivered cooperative activity cancellation"
614 );
615 }
616}
617
618fn cancel_all_in_flight(in_flight: &HashMap<ActivityExecutionKey, InFlightActivity>) {
619 for (key, in_flight_activity) in in_flight {
620 in_flight_activity.cancellation_handle.cancel();
621 info!(
622 activity_id = key.activity_id.sequence_position(),
623 workflow_id = %key.workflow_id,
624 "delivered cooperative activity cancellation during worker shutdown"
625 );
626 }
627}
628
629#[derive(Debug, thiserror::Error)]
630#[error("worker max_concurrency must be greater than zero")]
631struct InvalidMaxConcurrency;
632
633#[cfg(test)]
634#[path = "loop_tests.rs"]
635mod tests;