use serde::{Deserialize, Serialize};
use crate::{ActivityId, Event, WorkerAttribution};
#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq)]
pub struct OutstandingLease {
pub activity_id: ActivityId,
pub attempt: u32,
pub worker: WorkerAttribution,
}
pub fn apply_lease_transition(leases: &mut Vec<OutstandingLease>, event: &Event) {
match event {
Event::ActivityLeased {
activity_id,
attempt,
worker,
..
} => {
leases
.retain(|lease| !(lease.activity_id == *activity_id && lease.attempt == *attempt));
leases.push(OutstandingLease {
activity_id: activity_id.clone(),
attempt: *attempt,
worker: worker.clone(),
});
}
Event::ActivityCompleted {
activity_id,
attempt,
..
}
| Event::ActivityFailed {
activity_id,
attempt,
..
}
| Event::ActivityCancelled {
activity_id,
attempt,
..
} => {
leases
.retain(|lease| !(lease.activity_id == *activity_id && lease.attempt == *attempt));
}
_ => {}
}
}
#[must_use]
pub fn outstanding_leases(events: &[Event]) -> Vec<OutstandingLease> {
let segment_start = events
.iter()
.rposition(|event| {
matches!(
event,
Event::WorkflowStarted { .. } | Event::WorkflowReopened { .. }
)
})
.map_or(0, |index| index + 1);
let mut leases = Vec::new();
for event in &events[segment_start..] {
apply_lease_transition(&mut leases, event);
}
leases
}
#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct LeaseRecording {
pub leases_recorded: u64,
pub dispatched_attempts: u64,
pub attempts_with_a_lease: u64,
}
#[must_use]
pub fn lease_recording(events: &[Event]) -> LeaseRecording {
let mut counts = LeaseRecording::default();
let mut attributed: std::collections::HashSet<(&ActivityId, u32)> =
std::collections::HashSet::new();
for event in events {
match event {
Event::ActivityLeased {
activity_id,
attempt,
..
} => {
counts.leases_recorded += 1;
if attributed.insert((activity_id, *attempt)) {
counts.attempts_with_a_lease += 1;
}
}
Event::ActivityStarted { .. } => counts.dispatched_attempts += 1,
_ => {}
}
}
counts
}
#[must_use]
pub fn current_worker(leases: &[OutstandingLease]) -> Option<WorkerAttribution> {
leases.last().map(|lease| lease.worker.clone())
}
#[cfg(test)]
mod tests {
use chrono::{DateTime, Utc};
use super::{OutstandingLease, apply_lease_transition, current_worker, outstanding_leases};
use crate::{
ActivityError, ActivityErrorKind, ActivityId, Event, EventEnvelope, Payload, RunId,
WorkerAttribution, WorkerTransport, WorkflowId,
};
fn workflow_id() -> WorkflowId {
WorkflowId::new(uuid::Uuid::from_u128(7))
}
fn envelope(seq: u64) -> EventEnvelope {
EventEnvelope {
seq,
recorded_at: DateTime::<Utc>::from_timestamp(
1_700_000_000 + i64::try_from(seq).unwrap_or(0),
0,
)
.unwrap_or_default(),
workflow_id: workflow_id(),
}
}
fn worker(identity: &str) -> WorkerAttribution {
WorkerAttribution {
identity: identity.to_owned(),
task_queue: String::from("billing"),
node: Some(String::from("n1")),
deployment: None,
instance_id: None,
transport: WorkerTransport::Grpc,
}
}
fn started(seq: u64) -> Result<Event, Box<dyn std::error::Error>> {
Ok(Event::WorkflowStarted {
envelope: envelope(seq),
workflow_type: String::from("checkout"),
input: Payload::from_json(&serde_json::json!({}))?,
run_id: RunId::new(uuid::Uuid::from_u128(1)),
parent_run_id: None,
parent_workflow_id: None,
package_version: crate::PackageVersion::new("a".repeat(64)),
})
}
fn leased(seq: u64, activity: u64, attempt: u32, identity: &str) -> Event {
Event::ActivityLeased {
envelope: envelope(seq),
activity_id: ActivityId::from_sequence_position(activity),
attempt,
worker: worker(identity),
}
}
fn completed(
seq: u64,
activity: u64,
attempt: u32,
) -> Result<Event, Box<dyn std::error::Error>> {
Ok(Event::ActivityCompleted {
envelope: envelope(seq),
activity_id: ActivityId::from_sequence_position(activity),
attempt,
result: Payload::from_json(&serde_json::json!({}))?,
})
}
fn failed(seq: u64, activity: u64, attempt: u32) -> Event {
Event::ActivityFailed {
envelope: envelope(seq),
activity_id: ActivityId::from_sequence_position(activity),
attempt,
error: ActivityError {
kind: ActivityErrorKind::Terminal,
message: String::from("boom"),
details: None,
},
}
}
fn identities(leases: &[OutstandingLease]) -> Vec<(u64, u32, &str)> {
leases
.iter()
.map(|lease| {
(
lease.activity_id.sequence_position(),
lease.attempt,
lease.worker.identity.as_str(),
)
})
.collect()
}
#[test]
fn a_lease_sets_the_current_worker_and_its_terminal_clears_it()
-> Result<(), Box<dyn std::error::Error>> {
let mut history = vec![started(1)?, leased(2, 2, 1, "w-a")];
let leases = outstanding_leases(&history);
assert_eq!(identities(&leases), vec![(2, 1, "w-a")]);
assert_eq!(
current_worker(&leases).map(|w| w.identity),
Some(String::from("w-a"))
);
history.push(completed(3, 2, 1)?);
let leases = outstanding_leases(&history);
assert!(
leases.is_empty(),
"the completion clears the attempt's lease"
);
assert_eq!(current_worker(&leases), None);
Ok(())
}
#[test]
fn a_second_lease_of_the_same_attempt_replaces_the_first()
-> Result<(), Box<dyn std::error::Error>> {
let history = vec![started(1)?, leased(2, 2, 1, "w-a"), leased(3, 2, 1, "w-b")];
let leases = outstanding_leases(&history);
assert_eq!(
identities(&leases),
vec![(2, 1, "w-b")],
"last lease per attempt wins"
);
Ok(())
}
#[test]
fn a_terminal_for_another_attempt_leaves_the_lease_standing()
-> Result<(), Box<dyn std::error::Error>> {
let history = vec![started(1)?, leased(2, 2, 2, "w-b"), failed(3, 2, 1)];
assert_eq!(
identities(&outstanding_leases(&history)),
vec![(2, 2, "w-b")]
);
Ok(())
}
#[test]
fn when_the_latest_attempt_terminates_the_earlier_outstanding_one_is_shown()
-> Result<(), Box<dyn std::error::Error>> {
let history = vec![
started(1)?,
leased(2, 2, 1, "w-a"),
leased(3, 3, 1, "w-b"),
completed(4, 3, 1)?,
];
let leases = outstanding_leases(&history);
assert_eq!(identities(&leases), vec![(2, 1, "w-a")]);
assert_eq!(
current_worker(&leases).map(|w| w.identity),
Some(String::from("w-a"))
);
Ok(())
}
#[test]
fn a_history_with_no_lease_is_unattributed() -> Result<(), Box<dyn std::error::Error>> {
let history = vec![started(1)?];
assert!(outstanding_leases(&history).is_empty());
assert_eq!(current_worker(&[]), None);
Ok(())
}
#[test]
fn lease_recording_counts_the_whole_history_across_segments()
-> Result<(), Box<dyn std::error::Error>> {
let started_attempt = |seq: u64, activity: u64| Event::ActivityStarted {
envelope: envelope(seq),
activity_id: ActivityId::from_sequence_position(activity),
attempt: 1,
};
let history = vec![
started(1)?,
started_attempt(2, 2),
leased(3, 2, 1, "w-a"),
Event::WorkflowReopened {
envelope: envelope(4),
run_id: RunId::new(uuid::Uuid::from_u128(1)),
reopened: vec![ActivityId::from_sequence_position(2)],
},
started_attempt(5, 2),
];
let counts = super::lease_recording(&history);
assert_eq!(
counts.leases_recorded, 1,
"the lease before the reopen still counts"
);
assert_eq!(counts.dispatched_attempts, 2, "both dispatches count");
assert_eq!(counts.attempts_with_a_lease, 1);
let mut redelivered = history.clone();
redelivered.push(leased(6, 2, 1, "w-b"));
let counts = super::lease_recording(&redelivered);
assert_eq!(
(counts.leases_recorded, counts.attempts_with_a_lease),
(2, 1)
);
assert!(
super::outstanding_leases(&history).is_empty(),
"while the current segment's attribution is empty"
);
Ok(())
}
#[test]
fn a_reopen_starts_a_fresh_segment() -> Result<(), Box<dyn std::error::Error>> {
let history = vec![
started(1)?,
leased(2, 2, 1, "w-a"),
Event::WorkflowReopened {
envelope: envelope(3),
run_id: RunId::new(uuid::Uuid::from_u128(1)),
reopened: vec![ActivityId::from_sequence_position(2)],
},
];
assert!(
outstanding_leases(&history).is_empty(),
"a lease recorded before the reopen belongs to a superseded delivery"
);
Ok(())
}
#[test]
fn the_incremental_transition_matches_the_fold() -> Result<(), Box<dyn std::error::Error>> {
let history = vec![
started(1)?,
leased(2, 2, 1, "w-a"),
leased(3, 3, 1, "w-b"),
leased(4, 2, 1, "w-c"),
failed(5, 3, 1),
];
let mut incremental = Vec::new();
for event in &history[1..] {
apply_lease_transition(&mut incremental, event);
}
assert_eq!(incremental, outstanding_leases(&history));
assert_eq!(identities(&incremental), vec![(2, 1, "w-c")]);
Ok(())
}
}