use std::collections::HashMap;
use std::sync::Arc;
use aion_core::{
ActivityId, Event, EventEnvelope, OutstandingLease, Payload, RunId, WorkerAttribution,
WorkerTransport, WorkflowId, WorkflowStatus, apply_lease_transition,
};
use chrono::{DateTime, Utc};
use super::expect_eq;
use crate::StoreError;
use crate::visibility::{VisibilityRecord, VisibilityStore};
const NAMESPACE: &str = "conformance-leases";
fn workflow_id() -> WorkflowId {
WorkflowId::new(uuid::Uuid::from_u128(0x1ea5e))
}
fn run_id() -> RunId {
RunId::new(uuid::Uuid::from_u128(0x0001_ea5e_0001))
}
fn instant(seq: u64) -> DateTime<Utc> {
DateTime::<Utc>::default() + chrono::Duration::seconds(i64::try_from(seq).unwrap_or(0))
}
fn envelope(seq: u64) -> EventEnvelope {
EventEnvelope {
seq,
recorded_at: instant(seq),
workflow_id: workflow_id(),
}
}
fn worker(identity: &str, transport: WorkerTransport) -> WorkerAttribution {
WorkerAttribution {
identity: identity.to_owned(),
task_queue: String::from("billing"),
node: Some(String::from("n1")),
deployment: Some(String::from("billing-workers")),
instance_id: None,
transport,
}
}
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,
if seq.is_multiple_of(2) {
WorkerTransport::Grpc
} else {
WorkerTransport::Liminal
},
),
}
}
fn completed(seq: u64, activity: u64, attempt: u32) -> Result<Event, StoreError> {
Ok(Event::ActivityCompleted {
envelope: envelope(seq),
activity_id: ActivityId::from_sequence_position(activity),
attempt,
result: Payload::from_json(&serde_json::json!({ "ok": true }))
.map_err(|error| StoreError::Backend(error.to_string()))?,
})
}
fn fresh_row() -> VisibilityRecord {
VisibilityRecord {
namespace: NAMESPACE.to_owned(),
workflow_id: workflow_id(),
run_id: run_id(),
workflow_type: String::from("checkout"),
status: WorkflowStatus::Running,
started_at: instant(1),
updated_at: instant(1),
ended_at: None,
parent: None,
display_name: None,
kind: None,
failed_step: None,
failure_reason: None,
search_attributes: HashMap::new(),
outstanding_leases: Vec::new(),
package_version: None,
}
}
async fn touch_through(
store: &Arc<dyn VisibilityStore>,
events: &[Event],
) -> Result<VisibilityRecord, StoreError> {
store.record_visibility(fresh_row()).await?;
for event in events {
let mut row = store
.get_visibility(&workflow_id(), &run_id())
.await?
.ok_or_else(|| StoreError::Backend(String::from("the row written above is gone")))?;
row.updated_at = *event.recorded_at();
apply_lease_transition(&mut row.outstanding_leases, event);
store.record_visibility(row).await?;
}
store
.get_visibility(&workflow_id(), &run_id())
.await?
.ok_or_else(|| StoreError::Backend(String::from("the row written above is gone")))
}
fn current_identity(row: &VisibilityRecord) -> Option<String> {
row.summary().current_worker.map(|worker| worker.identity)
}
pub(super) async fn a_lease_sets_current_worker_and_its_terminal_clears_it(
store: Arc<dyn VisibilityStore>,
) -> Result<(), StoreError> {
let row = touch_through(&store, &[leased(2, 2, 1, "worker-a@host-1")]).await?;
expect_eq(
current_identity(&row),
Some(String::from("worker-a@host-1")),
"a recorded lease names the worker on the row",
)?;
expect_eq(
row.summary().current_worker.map(|worker| worker.transport),
Some(WorkerTransport::Grpc),
"the attribution round-trips whole, transport included",
)?;
let row = touch_through(
&store,
&[leased(2, 2, 1, "worker-a@host-1"), completed(3, 2, 1)?],
)
.await?;
expect_eq(
current_identity(&row),
None,
"the attempt's completion clears its lease",
)?;
expect_eq(
row.outstanding_leases,
Vec::<OutstandingLease>::new(),
"nothing is outstanding after the only attempt completed",
)
}
pub(super) async fn the_last_lease_of_an_attempt_wins(
store: Arc<dyn VisibilityStore>,
) -> Result<(), StoreError> {
let row = touch_through(
&store,
&[
leased(2, 2, 1, "worker-a@host-1"),
leased(3, 2, 1, "worker-b@host-2"),
],
)
.await?;
expect_eq(
current_identity(&row),
Some(String::from("worker-b@host-2")),
"an at-least-once redelivery's later lease supersedes the earlier one",
)?;
expect_eq(
row.outstanding_leases.len(),
1,
"the same attempt is never listed twice",
)
}
pub(super) async fn an_earlier_outstanding_attempt_is_shown_when_the_later_one_terminates(
store: Arc<dyn VisibilityStore>,
) -> Result<(), StoreError> {
let row = touch_through(
&store,
&[
leased(2, 2, 1, "worker-a@host-1"),
leased(3, 3, 1, "worker-b@host-2"),
completed(4, 3, 1)?,
],
)
.await?;
expect_eq(
current_identity(&row),
Some(String::from("worker-a@host-1")),
"with two attempts outstanding, the later one's terminal falls back to the earlier",
)
}
pub(super) async fn a_row_with_no_lease_is_unattributed(
store: Arc<dyn VisibilityStore>,
) -> Result<(), StoreError> {
let row = touch_through(&store, &[]).await?;
expect_eq(
row.summary().current_worker,
None,
"a run nothing has leased carries no worker",
)?;
let mut legacy = serde_json::to_value(fresh_row())
.map_err(|error| StoreError::Backend(error.to_string()))?;
if let serde_json::Value::Object(map) = &mut legacy {
drop(map.remove("outstanding_leases"));
}
let decoded: VisibilityRecord = serde_json::from_value(legacy)
.map_err(|error| StoreError::Backend(format!("a pre-field row must decode: {error}")))?;
expect_eq(
decoded.outstanding_leases,
Vec::new(),
"a row recorded before lease events existed decodes as unattributed",
)
}
pub(super) async fn a_touched_row_matches_the_fold_from_history(
store: Arc<dyn VisibilityStore>,
) -> Result<(), StoreError> {
let events = [
leased(2, 2, 1, "worker-a@host-1"),
leased(3, 3, 1, "worker-b@host-2"),
leased(4, 2, 1, "worker-c@host-3"),
completed(5, 3, 1)?,
];
let row = touch_through(&store, &events).await?;
expect_eq(
row.outstanding_leases,
aion_core::outstanding_leases(&events),
"the row's leases after per-event touches equal the whole-history fold",
)
}