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), if !in_flight.is_empty() => {
238 pump_liveness(session, &heartbeat_bookkeeper, &in_flight, &mut pending_error)
239 .await;
240 }
241 event = stream.next() => {
242 let Some(event) = event else { break; };
243 match event {
244 Ok(WorkerSessionEvent::Cancel { workflow_id, activity_id }) => {
245 deliver_cancellation(workflow_id, &activity_id, &in_flight);
246 }
247 Ok(WorkerSessionEvent::ResultAck { workflow_id, activity_id }) => {
250 acknowledge_result(&workflow_id, &activity_id, tracker);
251 }
252 Ok(WorkerSessionEvent::Drain) => {
253 info!("server drain received; finishing in-flight work before reconnect");
254 health.drain_received = true;
255 end = ServeEnd::Drained;
256 break;
257 }
258 Err(error) => {
259 pending_error = Some(error);
260 break;
261 }
262 Ok(WorkerSessionEvent::Task(proto_task)) => {
263 let Some(permit) =
264 acquire_permit_or_shutdown(shutdown.as_mut(), &semaphore).await?
265 else {
266 cancel_all_in_flight(&in_flight);
267 end = ServeEnd::Shutdown;
268 break;
269 };
270 if !handle_task(
271 proto_task,
272 SessionEventContext {
273 permit,
274 dispatcher: Arc::clone(&dispatcher),
275 result_sender: &result_sender,
276 heartbeat_sender: &heartbeat_sender,
277 heartbeat_bookkeeper: &heartbeat_bookkeeper,
278 in_flight: &mut in_flight,
279 pending_error: &mut pending_error,
280 },
281 )? {
282 break;
283 }
284 }
285 }
286 }
287 }
288 }
289
290 health.stream_ended_at = Some(tokio::time::Instant::now());
294
295 drop((result_sender, heartbeat_sender));
296 drain_remaining(
297 session,
298 &heartbeat_bookkeeper,
299 &mut channels,
300 &mut in_flight,
301 tracker,
302 &mut health.tasks_reported,
303 &mut pending_error,
304 )
305 .await;
306
307 pending_error.map_or(Ok(end), Err)
308}
309
310fn runtime_channels() -> (
312 mpsc::UnboundedSender<DispatchFinished>,
313 mpsc::UnboundedSender<HeartbeatRequest>,
314 RuntimeChannels,
315) {
316 let (result_sender, result_receiver) = mpsc::unbounded_channel();
317 let (heartbeat_sender, heartbeat_receiver) = mpsc::unbounded_channel();
318 let channels = RuntimeChannels {
319 heartbeats: heartbeat_receiver,
320 results: result_receiver,
321 };
322 (result_sender, heartbeat_sender, channels)
323}
324
325struct SessionEventContext<'a, D> {
326 permit: tokio::sync::OwnedSemaphorePermit,
327 dispatcher: Arc<D>,
328 result_sender: &'a mpsc::UnboundedSender<DispatchFinished>,
329 heartbeat_sender: &'a mpsc::UnboundedSender<HeartbeatRequest>,
330 heartbeat_bookkeeper: &'a HeartbeatBookkeeper,
331 in_flight: &'a mut HashMap<ActivityExecutionKey, InFlightActivity>,
332 pending_error: &'a mut Option<WorkerError>,
333}
334
335fn handle_task<D>(
336 proto_task: aion_proto::ProtoActivityTask,
337 ctx: SessionEventContext<'_, D>,
338) -> Result<bool, WorkerError>
339where
340 D: ActivityDispatcher,
341{
342 let task = match ActivityTask::try_from(proto_task) {
343 Ok(task) => task,
344 Err(error) => {
345 drop(ctx.permit);
346 *ctx.pending_error = Some(error);
347 return Ok(false);
348 }
349 };
350 spawn_activity(
351 task,
352 ctx.permit,
353 ctx.dispatcher,
354 ctx.result_sender.clone(),
355 ctx.heartbeat_sender.clone(),
356 ctx.heartbeat_bookkeeper,
357 ctx.in_flight,
358 )?;
359 Ok(true)
360}
361
362fn ensure_max_concurrency(config: &WorkerConfig) -> Result<(), WorkerError> {
364 if config.max_concurrency == 0 {
365 return Err(WorkerError::registration(InvalidMaxConcurrency));
366 }
367 Ok(())
368}
369
370async fn acquire_permit_or_shutdown<F>(
373 shutdown: std::pin::Pin<&mut F>,
374 semaphore: &Arc<Semaphore>,
375) -> Result<Option<tokio::sync::OwnedSemaphorePermit>, WorkerError>
376where
377 F: Future<Output = ()> + Send,
378{
379 tokio::select! {
380 biased;
381 () = shutdown => Ok(None),
382 permit = Arc::clone(semaphore).acquire_owned() => {
383 permit.map(Some).map_err(WorkerError::registration)
384 }
385 }
386}
387
388fn liveness_pump_for<S>(session: &S) -> Option<tokio::time::Interval>
395where
396 S: WorkerSession,
397{
398 session.heartbeat_window().map(|window| {
399 let mut ticks = tokio::time::interval(liveness_pump_interval(window));
400 ticks.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
401 ticks
402 })
403}
404
405async fn consume_finished<S>(
409 session: &mut S,
410 heartbeat_bookkeeper: &HeartbeatBookkeeper,
411 finished: Option<DispatchFinished>,
412 in_flight: &mut HashMap<ActivityExecutionKey, InFlightActivity>,
413 tracker: &mut UnackedResultTracker,
414 health: &mut SessionHealth,
415 pending_error: &mut Option<WorkerError>,
416) where
417 S: WorkerSession,
418{
419 if let Some(finished) = finished {
420 report_finished(
421 session,
422 heartbeat_bookkeeper,
423 finished,
424 in_flight,
425 tracker,
426 &mut health.tasks_reported,
427 pending_error,
428 )
429 .await;
430 }
431}
432
433#[must_use]
445pub(crate) fn liveness_pump_interval(heartbeat_window: std::time::Duration) -> std::time::Duration {
446 (heartbeat_window / 4).max(std::time::Duration::from_millis(1))
447}
448
449async fn tick_liveness_pump(pump: &mut Option<tokio::time::Interval>) {
453 match pump {
454 Some(ticks) => {
455 ticks.tick().await;
456 }
457 None => future::pending().await,
458 }
459}
460
461async fn pump_liveness<S>(
465 session: &mut S,
466 heartbeat_bookkeeper: &HeartbeatBookkeeper,
467 in_flight: &HashMap<ActivityExecutionKey, InFlightActivity>,
468 pending_error: &mut Option<WorkerError>,
469) where
470 S: WorkerSession,
471{
472 for key in in_flight.keys() {
473 record_first_error(
474 pending_error,
475 crate::protocol::send_heartbeat(
476 session,
477 heartbeat_bookkeeper,
478 HeartbeatRequest {
479 workflow_id: key.workflow_id.clone(),
480 activity_id: key.activity_id.clone(),
481 detail: None,
482 },
483 )
484 .await,
485 );
486 if pending_error.is_some() {
487 return;
490 }
491 }
492}
493
494async fn forward_heartbeat<S>(
497 session: &mut S,
498 heartbeat_bookkeeper: &HeartbeatBookkeeper,
499 request: Option<HeartbeatRequest>,
500 pending_error: &mut Option<WorkerError>,
501) where
502 S: WorkerSession,
503{
504 if let Some(request) = request {
505 record_first_error(
506 pending_error,
507 crate::protocol::send_heartbeat(session, heartbeat_bookkeeper, request).await,
508 );
509 }
510}
511
512fn acknowledge_result(
515 workflow_id: &WorkflowId,
516 activity_id: &ActivityId,
517 tracker: &mut UnackedResultTracker,
518) {
519 if tracker.acknowledge(workflow_id, activity_id).is_some() {
520 debug!(
521 workflow_id = %workflow_id,
522 activity_id = activity_id.sequence_position(),
523 "server acknowledged activity result; tracker entry cleared"
524 );
525 } else {
526 debug!(
527 workflow_id = %workflow_id,
528 activity_id = activity_id.sequence_position(),
529 "result ack for unknown tracker entry ignored"
530 );
531 }
532}
533
534fn render_labels(labels: &BTreeMap<String, String>) -> String {
538 labels
539 .iter()
540 .map(|(key, value)| format!("{key}={value}"))
541 .collect::<Vec<_>>()
542 .join(" ")
543}
544
545fn spawn_activity<D>(
546 task: ActivityTask,
547 permit: tokio::sync::OwnedSemaphorePermit,
548 dispatcher: Arc<D>,
549 result_sender: mpsc::UnboundedSender<DispatchFinished>,
550 heartbeat_sender: mpsc::UnboundedSender<HeartbeatRequest>,
551 heartbeat_bookkeeper: &HeartbeatBookkeeper,
552 in_flight: &mut HashMap<ActivityExecutionKey, InFlightActivity>,
553) -> Result<(), WorkerError>
554where
555 D: ActivityDispatcher,
556{
557 info!(
558 activity_type = %task.activity_type,
559 activity_id = task.activity_id.sequence_position(),
560 workflow_id = %task.workflow_id,
561 attempt = task.attempt,
562 labels = %render_labels(&task.labels),
563 "received activity task"
564 );
565 let key = ActivityExecutionKey::new(task.workflow_id.clone(), task.activity_id.clone());
566 heartbeat_bookkeeper.register(key.clone())?;
567 let (context, cancellation_handle) = ActivityContext::for_workflow(
568 Some(task.workflow_id.clone()),
569 task.activity_id.clone(),
570 task.attempt,
571 Some(heartbeat_sender),
572 );
573 let finished_key = key.clone();
574 let finished_run_id = task.run_id.clone();
575 let join_handle = tokio::spawn(async move {
576 let outcome = dispatcher.dispatch(task, context).await;
577 if result_sender
578 .send(DispatchFinished {
579 key: finished_key,
580 run_id: finished_run_id,
581 outcome,
582 })
583 .is_err()
584 {
585 debug!("worker loop stopped before dispatch outcome could be delivered");
586 }
587 drop(permit);
588 });
589 in_flight.insert(
590 key,
591 InFlightActivity {
592 cancellation_handle,
593 join_handle,
594 },
595 );
596 Ok(())
597}
598
599fn deliver_cancellation(
600 workflow_id: WorkflowId,
601 activity_id: &ActivityId,
602 in_flight: &HashMap<ActivityExecutionKey, InFlightActivity>,
603) {
604 let key = ActivityExecutionKey::new(workflow_id, activity_id.clone());
605 if let Some(in_flight_activity) = in_flight.get(&key) {
606 in_flight_activity.cancellation_handle.cancel();
607 info!(
608 activity_id = activity_id.sequence_position(),
609 "delivered cooperative activity cancellation"
610 );
611 }
612}
613
614fn cancel_all_in_flight(in_flight: &HashMap<ActivityExecutionKey, InFlightActivity>) {
615 for (key, in_flight_activity) in in_flight {
616 in_flight_activity.cancellation_handle.cancel();
617 info!(
618 activity_id = key.activity_id.sequence_position(),
619 workflow_id = %key.workflow_id,
620 "delivered cooperative activity cancellation during worker shutdown"
621 );
622 }
623}
624
625#[derive(Debug, thiserror::Error)]
626#[error("worker max_concurrency must be greater than zero")]
627struct InvalidMaxConcurrency;
628
629#[cfg(test)]
630#[path = "loop_tests.rs"]
631mod tests;