use std::collections::BTreeSet;
use std::sync::Arc;
use std::time::Duration;
use aion_core::{ActivityId, ContentType, Payload, RunId, WorkflowId};
use aion_proto::{ProtoActivityId, ProtoActivityTask, ProtoPayload, ProtoWorkflowId};
use async_trait::async_trait;
use tokio::sync::{Mutex, Notify, mpsc};
use super::{ActivityDispatcher, DispatchOutcome, ServeEnd, serve_activity_tasks};
use crate::context::ActivityContext;
use crate::error::WorkerError;
use crate::protocol::{
ActivityTask, UnackedResultTracker, WorkerSession, WorkerSessionEvent, WorkerTaskStream,
validate_activity_handlers,
};
use crate::{ReconnectConfig, WorkerConfig};
type TestError = Box<dyn std::error::Error>;
const ACTIVITY_TYPE: &str = "charge-card";
const OBSERVATION: Duration = Duration::from_millis(300);
struct AnswerRecordingSession {
receiver: Option<mpsc::Receiver<Result<WorkerSessionEvent, WorkerError>>>,
answered: Arc<Mutex<Vec<u64>>>,
}
#[async_trait]
impl WorkerSession for AnswerRecordingSession {
async fn handshake(&mut self, config: &WorkerConfig) -> Result<(), WorkerError> {
drop(config.clone());
Ok(())
}
async fn register(
&mut self,
activity_types: Vec<String>,
available_handlers: &BTreeSet<String>,
) -> Result<(), WorkerError> {
validate_activity_handlers(&activity_types, available_handlers)
}
fn receive_tasks(&mut self) -> WorkerTaskStream {
match self.receiver.take() {
Some(receiver) => Box::pin(tokio_stream::wrappers::ReceiverStream::new(receiver)),
None => Box::pin(futures::stream::empty()),
}
}
async fn report_result(
&mut self,
workflow_id: WorkflowId,
activity_id: ActivityId,
run_id: Option<RunId>,
completion_token: String,
result: Payload,
) -> Result<(), WorkerError> {
drop((workflow_id, activity_id, run_id, completion_token, result));
Ok(())
}
async fn report_failure(
&mut self,
workflow_id: WorkflowId,
activity_id: ActivityId,
run_id: Option<RunId>,
completion_token: String,
failure: aion_core::ActivityError,
) -> Result<(), WorkerError> {
drop((workflow_id, activity_id, run_id, completion_token, failure));
Ok(())
}
async fn send_heartbeat(
&mut self,
workflow_id: WorkflowId,
activity_id: ActivityId,
progress: Option<Payload>,
) -> Result<(), WorkerError> {
drop((workflow_id, activity_id, progress));
Ok(())
}
async fn answer_liveness_ping(&mut self, sequence: u64) -> Result<(), WorkerError> {
self.answered.lock().await.push(sequence);
Ok(())
}
}
struct HeldDispatcher {
release: Arc<Notify>,
}
#[async_trait]
impl ActivityDispatcher for HeldDispatcher {
async fn dispatch(
&self,
task: ActivityTask,
context: ActivityContext,
) -> Result<DispatchOutcome, WorkerError> {
drop((task, context));
self.release.notified().await;
Ok(DispatchOutcome::Completed {
output: Payload::new(ContentType::Json, b"{}".to_vec()),
})
}
fn activity_types(&self) -> BTreeSet<String> {
[String::from(ACTIVITY_TYPE)].into_iter().collect()
}
}
fn config(max_concurrency: usize) -> WorkerConfig {
WorkerConfig::new(
"http://127.0.0.1:50051",
"payments",
"worker-a",
max_concurrency,
ReconnectConfig::new(Duration::from_millis(5), Duration::from_millis(20), 3),
None,
)
}
fn task_event() -> WorkerSessionEvent {
WorkerSessionEvent::Task(Box::new(ProtoActivityTask {
workflow_id: Some(ProtoWorkflowId::from(WorkflowId::new_v4())),
activity_id: Some(ProtoActivityId::from(ActivityId::from_sequence_position(1))),
activity_type: String::from(ACTIVITY_TYPE),
input: Some(ProtoPayload::from(Payload::new(
ContentType::Json,
b"{}".to_vec(),
))),
attempt: 1,
labels: std::collections::HashMap::new(),
run_id: Some(aion_proto::ProtoRunId::from(RunId::new_v4())),
completion_token: String::from("generation-1"),
idempotency_key: String::from("effect-key"),
}))
}
const fn ping_event(sequence: u64) -> WorkerSessionEvent {
WorkerSessionEvent::LivenessPing {
sequence,
silence_window: Duration::from_secs(4),
}
}
struct Fixture {
events: mpsc::Sender<Result<WorkerSessionEvent, WorkerError>>,
answered: Arc<Mutex<Vec<u64>>>,
release: Arc<Notify>,
serve: tokio::task::JoinHandle<Result<ServeEnd, WorkerError>>,
}
impl Fixture {
fn start(max_concurrency: usize) -> Self {
let (events, receiver) = mpsc::channel(8);
let answered = Arc::new(Mutex::new(Vec::new()));
let release = Arc::new(Notify::new());
let mut session = AnswerRecordingSession {
receiver: Some(receiver),
answered: Arc::clone(&answered),
};
let dispatcher = Arc::new(HeldDispatcher {
release: Arc::clone(&release),
});
let config = config(max_concurrency);
let serve = tokio::spawn(async move {
let mut tracker = UnackedResultTracker::default();
serve_activity_tasks(&config, &mut session, dispatcher, &mut tracker).await
});
Self {
events,
answered,
release,
serve,
}
}
async fn answered(&self) -> Vec<u64> {
self.answered.lock().await.clone()
}
async fn finish(self) -> Result<(), TestError> {
self.release.notify_waiters();
drop(self.events);
self.serve.await??;
Ok(())
}
}
#[tokio::test]
async fn a_ping_behind_a_running_activity_is_answered_while_it_runs() -> Result<(), TestError> {
let fixture = Fixture::start(2);
fixture.events.send(Ok(task_event())).await?;
fixture.events.send(Ok(ping_event(7))).await?;
tokio::time::sleep(OBSERVATION).await;
assert_eq!(
fixture.answered().await,
vec![7],
"the ping must be answered while the delivery is still in flight; a runtime that answered \
only after the activity finished would report every busy worker as unreachable"
);
fixture.finish().await
}
#[tokio::test]
async fn a_ping_behind_a_task_that_cannot_get_a_permit_waits_for_that_permit()
-> Result<(), TestError> {
let fixture = Fixture::start(1);
fixture.events.send(Ok(task_event())).await?;
fixture.events.send(Ok(task_event())).await?;
fixture.events.send(Ok(ping_event(9))).await?;
tokio::time::sleep(OBSERVATION).await;
assert!(
fixture.answered().await.is_empty(),
"MEASURED HAZARD: the loop is parked acquiring a permit for the queued task and has not \
read the ping behind it. This is the head-of-line block a saturated worker suffers"
);
fixture.release.notify_waiters();
tokio::time::sleep(OBSERVATION).await;
assert_eq!(
fixture.answered().await,
vec![9],
"once a permit frees, the queued ping is read and answered — so the silence measured \
above was head-of-line blocking, not an unwired answer"
);
fixture.finish().await
}