use crate::client::{self, EngineClient, WorkerCoordinatorClient};
use crate::error::{Result, SdkError};
use crate::journal_queue::{JournalEventMessage, JournalEventQueue, JournalQueueConfig};
use crate::pb::{
execution_engine_service_client::ExecutionEngineServiceClient, runtime_message,
runtime_service_request, runtime_service_response, service_message, CompleteJobRequest,
CompleteJobResponse, ComponentInfo, DispatchComponentRequest, DispatchComponentResponse,
EntityStateLoadResult, EntityStateSaveResult, EventStreamMessage, GetEntityStateRequest,
GetEntityStateResponse, HealthCheck, JobAssignment, LeaseRenewalOutcome, PollJobRequest,
PutEntityStateRequest, PutEntityStateResponse, RegisterService, RegisterWorkerSessionRequest,
RenewJobLeaseRequest, ReportWorkerCapacityRequest, RuntimeMessage, RuntimeMessageType,
RuntimeServiceResponse, ServiceMessage, UnregisterService, WorkerCapability,
WorkerHealthStatus, WorkerMode, WorkerSlotPolicy, WriteCheckpointRequest,
};
use sha2::{Digest as _, Sha256};
use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use tokio::sync::Mutex as TokioMutex;
use tonic::transport::Channel;
use tracing::{debug, error, info, warn};
use uuid::Uuid;
const PARKED_WORKER_SESSION_REGISTER_ATTEMPTS: usize = 3;
const PARKED_WORKER_SESSION_REGISTER_RETRY_MS: u64 = 1_000;
const PARKED_WORKER_SESSION_TRANSIENT_RETRY_MAX_MS: u64 = 32_000;
const PARKED_COMPLETION_FLUSH_TIMEOUT: Duration = Duration::from_secs(5);
const PARKED_COMPLETE_JOB_ATTEMPTS: usize = 3;
const PARKED_COMPLETE_JOB_ATTEMPT_TIMEOUT: Duration = Duration::from_secs(3);
const PARKED_COMPLETE_JOB_RETRY_DELAY: Duration = Duration::from_millis(100);
const DEPLOYMENT_ARTIFACT_DOMAIN: &[u8] = b"agnt5.deployment-artifact.v1\0";
const ASSIGNMENT_AUTHORED_RETRY_METADATA_KEYS: &[&str] = &[
"attempt",
"max_attempts",
"initial_interval_ms",
"max_interval_ms",
"backoff_type",
"backoff_multiplier",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ParkedSlotEvent {
Started { active_started: usize },
Retired,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum WorkerSlotPhase {
ClaimedNotStarted,
Executing,
Terminalizing,
}
#[derive(Debug)]
struct WorkerSlotEntry {
generation: u64,
phase: WorkerSlotPhase,
claimed_at: Instant,
}
#[derive(Default)]
struct WorkerSlotPhaseState {
entries: HashMap<String, WorkerSlotEntry>,
next_generation: u64,
started_notifier: Option<tokio::sync::mpsc::UnboundedSender<ParkedSlotEvent>>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
struct WorkerSlotPhaseSnapshot {
claimed_not_started: usize,
executing: usize,
terminalizing: usize,
}
impl WorkerSlotPhaseState {
fn snapshot(&self) -> WorkerSlotPhaseSnapshot {
let mut snapshot = WorkerSlotPhaseSnapshot::default();
for entry in self.entries.values() {
match entry.phase {
WorkerSlotPhase::ClaimedNotStarted => snapshot.claimed_not_started += 1,
WorkerSlotPhase::Executing => snapshot.executing += 1,
WorkerSlotPhase::Terminalizing => snapshot.terminalizing += 1,
}
}
snapshot
}
}
#[derive(Clone, Default)]
struct WorkerSlotPhases {
state: Arc<std::sync::Mutex<WorkerSlotPhaseState>>,
}
impl WorkerSlotPhases {
fn lock_state(&self) -> std::sync::MutexGuard<'_, WorkerSlotPhaseState> {
self.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn publish(snapshot: WorkerSlotPhaseSnapshot) {
crate::telemetry::record_worker_slot_phases(
"pull",
snapshot.claimed_not_started as u64,
snapshot.executing as u64,
snapshot.terminalizing as u64,
);
}
fn set_started_notifier(&self, notifier: tokio::sync::mpsc::UnboundedSender<ParkedSlotEvent>) {
self.lock_state().started_notifier = Some(notifier);
}
fn claim(&self, run_id: String) -> WorkerSlotPhaseGuard {
let (generation, replaced, snapshot) = {
let mut state = self.lock_state();
let generation = state.next_generation;
state.next_generation = state.next_generation.wrapping_add(1);
let replaced = state
.entries
.insert(
run_id.clone(),
WorkerSlotEntry {
generation,
phase: WorkerSlotPhase::ClaimedNotStarted,
claimed_at: Instant::now(),
},
)
.is_some();
(generation, replaced, state.snapshot())
};
if replaced {
warn!(run_id, "Replacing duplicate active pull-slot phase entry");
}
Self::publish(snapshot);
WorkerSlotPhaseGuard {
phases: self.clone(),
run_id,
generation,
}
}
fn mark_started(&self, run_id: &str) {
let transition = {
let mut state = self.lock_state();
let Some(entry) = state.entries.get_mut(run_id) else {
return;
};
if entry.phase != WorkerSlotPhase::ClaimedNotStarted {
return;
}
entry.phase = WorkerSlotPhase::Executing;
let claim_to_start = entry.claimed_at.elapsed();
let notifier = state.started_notifier.clone();
(claim_to_start, notifier, state.snapshot())
};
crate::telemetry::record_worker_claim_to_start("pull", transition.0.as_secs_f64());
Self::publish(transition.2);
if let Some(notifier) = transition.1 {
let active_started = transition.2.executing + transition.2.terminalizing;
let _ = notifier.send(ParkedSlotEvent::Started { active_started });
}
}
fn mark_terminalizing(&self, run_id: &str) {
let snapshot = {
let mut state = self.lock_state();
let Some(entry) = state.entries.get_mut(run_id) else {
return;
};
if entry.phase == WorkerSlotPhase::Terminalizing {
return;
}
entry.phase = WorkerSlotPhase::Terminalizing;
state.snapshot()
};
Self::publish(snapshot);
}
fn finish(&self, run_id: &str, generation: u64) {
let result = {
let mut state = self.lock_state();
let Some(entry) = state.entries.get(run_id) else {
return;
};
if entry.generation != generation {
return;
}
let residency = entry.claimed_at.elapsed();
state.entries.remove(run_id);
(residency, state.snapshot())
};
crate::telemetry::record_worker_slot_residency("pull", result.0.as_secs_f64());
Self::publish(result.1);
}
#[cfg(test)]
fn snapshot(&self) -> WorkerSlotPhaseSnapshot {
self.lock_state().snapshot()
}
}
struct WorkerSlotPhaseGuard {
phases: WorkerSlotPhases,
run_id: String,
generation: u64,
}
impl Drop for WorkerSlotPhaseGuard {
fn drop(&mut self) {
self.phases.finish(&self.run_id, self.generation);
}
}
#[derive(Clone, Default)]
struct RunFlushLocks {
locks: Arc<std::sync::Mutex<HashMap<String, std::sync::Weak<TokioMutex<()>>>>>,
}
impl RunFlushLocks {
fn lock_for_run(&self, run_id: &str) -> Arc<TokioMutex<()>> {
let mut locks = match self.locks.lock() {
Ok(locks) => locks,
Err(poisoned) => poisoned.into_inner(),
};
if let Some(lock) = locks.get(run_id).and_then(std::sync::Weak::upgrade) {
return lock;
}
if locks.len() >= 4096 {
locks.retain(|_, lock| lock.strong_count() > 0);
}
let lock = Arc::new(TokioMutex::new(()));
locks.insert(run_id.to_string(), Arc::downgrade(&lock));
lock
}
async fn lock_run(&self, run_id: &str) -> tokio::sync::OwnedMutexGuard<()> {
self.lock_for_run(run_id).lock_owned().await
}
async fn lock_runs<I>(&self, run_ids: I) -> Vec<tokio::sync::OwnedMutexGuard<()>>
where
I: IntoIterator<Item = String>,
{
let mut run_ids: Vec<_> = run_ids.into_iter().collect();
run_ids.sort_unstable();
run_ids.dedup();
let locks: Vec<_> = run_ids
.iter()
.map(|run_id| self.lock_for_run(run_id))
.collect();
let mut guards = Vec::with_capacity(locks.len());
for lock in locks {
guards.push(lock.lock_owned().await);
}
guards
}
}
async fn await_checkpoint_ack<F, T>(
future: F,
timeout_ms: u64,
operation: &str,
run_id: &str,
event_type: &str,
sequence_number: i64,
) -> Result<T>
where
F: Future<Output = T>,
{
tokio::time::timeout(Duration::from_millis(timeout_ms), future)
.await
.map_err(|_| SdkError::Timeout {
message: format!(
"{operation} acknowledgement timed out after {timeout_ms}ms for \
run_id={run_id} event_type={event_type} seq={sequence_number}; \
persistence outcome is unknown"
),
operation: operation.to_string(),
duration_ms: Some(timeout_ms),
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum ParkedWorkerSessionRegistrationResult {
Registered(String),
Rejected,
}
#[derive(Clone)]
struct ParkedWorkerSessionRegistration {
worker_id: String,
project_id: String,
deployment_id: String,
min_slots: usize,
max_slots: usize,
capabilities: Vec<WorkerCapability>,
components: Vec<ComponentInfo>,
service_name: String,
service_version: String,
service_type: String,
supported_protocol_capabilities: Vec<String>,
required_protocol_capabilities: Vec<String>,
}
impl ParkedWorkerSessionRegistration {
fn request(&self) -> RegisterWorkerSessionRequest {
RegisterWorkerSessionRequest {
worker_id: self.worker_id.clone(),
project_id: self.project_id.clone(),
deployment_id: self.deployment_id.clone(),
max_slots: self.max_slots as u32,
slot_policy: Some(WorkerSlotPolicy {
min_slots: self.min_slots as u32,
max_slots: self.max_slots as u32,
target_cpu_usage: 0.75,
target_memory_usage: 0.80,
ramp_throttle_ms: 1_000,
}),
capabilities: self.capabilities.clone(),
components: self.components.clone(),
service_name: self.service_name.clone(),
service_version: self.service_version.clone(),
service_type: self.service_type.clone(),
supported_protocol_capabilities: self.supported_protocol_capabilities.clone(),
required_protocol_capabilities: self.required_protocol_capabilities.clone(),
}
}
}
fn take_correlation_ids(metadata: &mut HashMap<String, String>) -> (String, String) {
let correlation_id = metadata
.remove("cid")
.or_else(|| metadata.remove("correlation_id"))
.unwrap_or_default();
let parent_correlation_id = metadata
.remove("pcid")
.or_else(|| metadata.remove("parent_correlation_id"))
.unwrap_or_default();
(correlation_id, parent_correlation_id)
}
fn record_groups_by_run(records: &[crate::pb::Record]) -> Vec<Vec<usize>> {
let mut group_by_run = HashMap::<String, usize>::new();
let mut groups = Vec::<Vec<usize>>::new();
for (index, record) in records.iter().enumerate() {
let group_index = match group_by_run.get(&record.run_id) {
Some(index) => *index,
None => {
let index = groups.len();
group_by_run.insert(record.run_id.clone(), index);
groups.push(Vec::new());
index
}
};
groups[group_index].push(index);
}
groups
}
struct AppendGroupProgress {
committed: Vec<bool>,
written_total: i32,
}
impl AppendGroupProgress {
fn new(record_count: usize) -> Self {
Self {
committed: vec![false; record_count],
written_total: 0,
}
}
fn acknowledge(&mut self, group: &[usize], written: i32) {
self.written_total = self.written_total.saturating_add(written);
for &index in group {
self.committed[index] = true;
}
}
fn failure(self, error: SdkError) -> (SdkError, Vec<bool>, i32) {
(error, self.committed, self.written_total)
}
}
fn uncommitted_records_in_reverse<T>(records: Vec<T>, committed: &[bool]) -> Vec<T> {
records
.into_iter()
.enumerate()
.rev()
.filter_map(|(index, record)| (!committed[index]).then_some(record))
.collect()
}
async fn append_records_by_run(
engine: &mut EngineClient,
records: &[crate::pb::Record],
) -> std::result::Result<i32, (SdkError, Vec<bool>, i32)> {
let mut progress = AppendGroupProgress::new(records.len());
for group in record_groups_by_run(records) {
let batch = group.iter().map(|index| records[*index].clone()).collect();
match engine.append_batch(batch).await {
Ok(written) => progress.acknowledge(&group, written),
Err(error) => return Err(progress.failure(error)),
}
}
Ok(progress.written_total)
}
fn polled_job_attempt(job: &JobAssignment) -> Result<u32> {
u32::try_from(job.attempt).map_err(|_| SdkError::InvalidMessage {
message: format!(
"PollJob returned negative attempt {} for job {}",
job.attempt, job.job_id
),
field: Some("attempt".to_string()),
})
}
fn runtime_message_from_job_assignment(
job: JobAssignment,
configured_claim_timeout_ms: i64,
) -> Result<(
RuntimeMessage,
bool,
String,
String,
u32,
i64,
i64,
HashMap<String, String>,
)> {
if job.job_id.is_empty() || job.run_id.is_empty() {
return Err(SdkError::InvalidMessage {
message: "PollJob assignment requires nonempty job_id and run_id".to_string(),
field: Some("job_id/run_id".to_string()),
});
}
if job.job_id != job.run_id {
return Err(SdkError::InvalidMessage {
message: format!(
"PollJob assignment job_id {} does not match run_id {}",
job.job_id, job.run_id
),
field: Some("job_id/run_id".to_string()),
});
}
if job.lease_id.is_empty() {
return Err(SdkError::InvalidMessage {
message: format!(
"PollJob assignment for job {} has no typed lease_id",
job.job_id
),
field: Some("lease_id".to_string()),
});
}
if configured_claim_timeout_ms <= 0 {
return Err(SdkError::Configuration {
message: "parked pull claim timeout must be positive".to_string(),
field: Some("AGNT5_CLAIM_TIMEOUT_MS".to_string()),
});
}
let attempt = polled_job_attempt(&job)?;
let mut metadata = job.metadata.clone();
metadata.insert("dispatch_mode".to_string(), "pull".to_string());
if !job.trace_id.is_empty() {
metadata.insert("trace_id".to_string(), job.trace_id.clone());
}
metadata.insert("lease_id".to_string(), job.lease_id.clone());
metadata.remove("lease_expires_at_ms");
if job.lease_expires_at_ms > 0 {
metadata.insert(
"lease_expires_at_ms".to_string(),
job.lease_expires_at_ms.to_string(),
);
}
let is_streaming = metadata.get("stream_mode").map_or(false, |m| m == "full");
let session_id = metadata.get("session_id").cloned().unwrap_or_default();
let user_id = metadata.get("user_id").cloned().unwrap_or_default();
let lease_id = job.lease_id.clone();
let deployment_id = metadata.get("deployment_id").cloned().unwrap_or_default();
let priority = metadata
.get("priority")
.and_then(|v| v.parse::<i32>().ok())
.unwrap_or(0);
let run_id = job.run_id.clone();
let mut completion_metadata = HashMap::new();
for key in &ASSIGNMENT_AUTHORED_RETRY_METADATA_KEYS[1..] {
if let Some(value) = metadata.get(*key) {
completion_metadata.insert((*key).to_string(), value.clone());
}
}
completion_metadata.insert("attempt".to_string(), attempt.to_string());
let runtime_message = RuntimeMessage {
worker_id: String::new(),
message_type: RuntimeMessageType::InvokeFunction as i32,
metadata: HashMap::new(),
message_data: Some(crate::pb::runtime_message::MessageData::DispatchComponent(
crate::pb::DispatchComponentRequest {
invocation_id: job.run_id,
service_name: String::new(),
component_type: job.component_type,
component_name: job.component_name,
input_data: job.input_data,
metadata,
attempt: job.attempt,
object_id: String::new(),
method_name: String::new(),
flow_instance_id: String::new(),
flow_step: 0,
state_snapshot: Vec::new(),
journal_position: 0,
step_checkpoints: Vec::new(),
session_id,
user_id,
is_streaming,
priority,
deployment_id,
lease_id: lease_id.clone(),
retry_policy: None,
},
)),
};
Ok((
runtime_message,
is_streaming,
run_id,
lease_id,
attempt,
configured_claim_timeout_ms,
job.lease_expires_at_ms,
completion_metadata,
))
}
fn env_usize(name: &str) -> Option<usize> {
std::env::var(name).ok().and_then(|v| v.parse().ok())
}
fn current_time_ms() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or_default()
}
fn worker_capabilities(components: &[ComponentInfo]) -> Vec<WorkerCapability> {
let mut seen = HashSet::new();
let mut capabilities = Vec::with_capacity(
components.len() + crate::eval::builtin_scorer::BUILTIN_SCORER_NAMES.len(),
);
for component in components {
if !component.name.is_empty() && seen.insert(component.name.clone()) {
capabilities.push(WorkerCapability {
component_type: component.component_type,
component_name: component.name.clone(),
});
}
}
for scorer in crate::eval::builtin_scorer::BUILTIN_SCORER_NAMES {
if crate::eval::builtin_scorer::can_execute_locally(scorer)
&& seen.insert((*scorer).to_string())
{
capabilities.push(WorkerCapability {
component_type: crate::pb::ComponentType::Scorer as i32,
component_name: (*scorer).to_string(),
});
}
}
capabilities
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConnectionState {
Disconnected,
Connecting,
Connected,
Error(String),
}
#[derive(Debug, Clone, PartialEq)]
pub struct WorkerConfig {
pub service_name: String,
pub service_version: String,
pub service_type: String,
pub worker_id: String,
pub coordinator_endpoint: String,
pub ee_endpoint: String,
pub max_retries: u32,
pub engine_endpoint: Option<String>,
pub max_concurrency: Option<u32>,
}
impl WorkerConfig {
pub fn new(service_name: String, service_version: String, service_type: String) -> Self {
let default_worker_id = Uuid::new_v4().to_string();
let worker_id = std::env::var("AGNT5_WORKER_ID").unwrap_or_else(|_| default_worker_id);
let coordinator_endpoint = std::env::var("AGNT5_COORDINATOR_ENDPOINT")
.unwrap_or_else(|_| "http://localhost:34186".to_string());
let ee_endpoint =
std::env::var("AGNT5_EE_ENDPOINT").unwrap_or_else(|_| coordinator_endpoint.clone());
let max_retries = std::env::var("AGNT5_MAX_RETRIES")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(5);
let engine_endpoint = std::env::var("AGNT5_ENGINE_URL").ok();
let max_concurrency = std::env::var("AGNT5_MAX_CONCURRENCY")
.ok()
.and_then(|v| v.parse().ok());
Self {
service_name,
service_version,
service_type,
worker_id,
coordinator_endpoint,
ee_endpoint,
max_retries,
engine_endpoint,
max_concurrency,
}
}
pub fn resolved_coordinator_endpoint(&self) -> String {
self.coordinator_endpoint.clone()
}
}
pub const AGNT5_METADATA_BLACKLIST_PATTERNS: &[&str] = &[
"_KEY",
"_SECRET",
"_TOKEN",
"_PASSWORD",
"_CREDENTIAL",
"_API_KEY",
"_AUTH_TOKEN",
"_PRIVATE_KEY",
];
pub fn is_sensitive_env_var(key: &str) -> bool {
let key_upper = key.to_uppercase();
AGNT5_METADATA_BLACKLIST_PATTERNS
.iter()
.any(|pattern| key_upper.ends_with(pattern))
}
pub fn collect_agnt5_env_vars() -> HashMap<String, String> {
let mut metadata = HashMap::new();
for (key, value) in std::env::vars() {
if key.starts_with("AGNT5_") && !is_sensitive_env_var(&key) {
metadata.insert(key, value);
}
}
if let Ok(h) = hostname::get() {
metadata.insert(
"AGNT5_SYS_HOSTNAME".into(),
h.to_string_lossy().into_owned(),
);
}
metadata.insert("AGNT5_SYS_OS".into(), std::env::consts::OS.into());
metadata.insert("AGNT5_SYS_ARCH".into(), std::env::consts::ARCH.into());
metadata
}
fn canonical_project_id_from_metadata(metadata: &HashMap<String, String>) -> Option<String> {
metadata.get("project_id").cloned()
}
fn canonical_project_id_from_env() -> String {
std::env::var("AGNT5_PROJECT_ID").ok().unwrap_or_default()
}
fn with_project_metadata(
mut metadata: HashMap<String, String>,
project_id: &str,
) -> HashMap<String, String> {
if !project_id.is_empty() {
metadata
.entry("project_id".to_string())
.or_insert_with(|| project_id.to_string());
}
metadata
}
#[derive(Clone)]
pub struct Worker {
config: WorkerConfig,
components: Vec<ComponentInfo>,
metadata: HashMap<String, String>,
connection_state: Arc<std::sync::Mutex<ConnectionState>>,
journal_queue: JournalEventQueue,
ee_client: Arc<TokioMutex<Option<ExecutionEngineServiceClient<Channel>>>>,
tokio_handle: Arc<std::sync::Mutex<Option<tokio::runtime::Handle>>>,
streaming_runs: Arc<std::sync::Mutex<HashMap<String, bool>>>,
pending_lease_ids: Arc<std::sync::Mutex<HashMap<String, String>>>,
revoked_executions: Arc<std::sync::Mutex<HashSet<String>>>,
lease_renewal_stops: Arc<std::sync::Mutex<HashMap<String, tokio::sync::oneshot::Sender<()>>>>,
cancel_tokens: Arc<std::sync::Mutex<HashMap<String, tokio::sync::oneshot::Sender<()>>>>,
cancel_hook: Arc<std::sync::Mutex<Option<Box<dyn Fn(String) + Send + Sync>>>>,
event_stream_tx: Arc<std::sync::Mutex<Option<flume::Sender<EventStreamMessage>>>>,
dispatch_tx: Arc<std::sync::Mutex<Option<flume::Sender<ServiceMessage>>>>,
engine_client: Arc<TokioMutex<Option<EngineClient>>>,
journal_flush_locks: RunFlushLocks,
slot_phases: WorkerSlotPhases,
}
impl std::fmt::Debug for Worker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Worker")
.field("config", &self.config)
.field("components", &self.components)
.field("metadata", &self.metadata)
.field("connection_state", &self.connection_state)
.field("journal_queue_size", &self.journal_queue.len())
.field("streaming_runs", &self.streaming_runs)
.finish()
}
}
fn dispatch_run_key(msg: &RuntimeMessage) -> Option<String> {
match &msg.message_data {
Some(crate::pb::runtime_message::MessageData::DispatchComponent(req)) => Some(
req.invocation_id
.split(':')
.next()
.unwrap_or(&req.invocation_id)
.to_string(),
),
_ => None,
}
}
fn stamp_dispatch_mode(runtime_message: &mut RuntimeMessage, dispatch_mode: &str) {
if let Some(crate::pb::runtime_message::MessageData::DispatchComponent(request)) =
runtime_message.message_data.as_mut()
{
request
.metadata
.insert("dispatch_mode".to_string(), dispatch_mode.to_string());
}
}
fn stamp_execution_authority_metadata(
runtime_message: &mut RuntimeMessage,
worker_id: &str,
worker_session_id: &str,
dispatch_mode: &str,
) {
let Some(crate::pb::runtime_message::MessageData::DispatchComponent(request)) =
runtime_message.message_data.as_mut()
else {
return;
};
request
.metadata
.insert("dispatch_mode".to_string(), dispatch_mode.to_string());
request
.metadata
.insert("worker_id".to_string(), worker_id.to_string());
request.metadata.insert(
"worker_session_id".to_string(),
worker_session_id.to_string(),
);
request
.metadata
.insert("lease_id".to_string(), request.lease_id.clone());
request
.metadata
.insert("lease_attempt".to_string(), request.attempt.to_string());
}
fn stamp_protocol_capability(runtime_message: &mut RuntimeMessage, capability: &str) {
let Some(crate::pb::runtime_message::MessageData::DispatchComponent(request)) =
runtime_message.message_data.as_mut()
else {
return;
};
request
.metadata
.insert(capability.to_string(), "true".to_string());
}
fn canonical_activation_component_config(config: &HashMap<String, String>) -> String {
let mut entries = config.iter().collect::<Vec<_>>();
entries.sort_by(|(left, _), (right, _)| left.as_bytes().cmp(right.as_bytes()));
serde_json::to_string(&serde_json::json!([
"object",
entries
.into_iter()
.map(|(key, value)| serde_json::json!([key, ["string", value]]))
.collect::<Vec<_>>()
]))
.expect("canonical activation component config must serialize")
}
fn configured_activation_artifact_sha256(metadata: &HashMap<String, String>) -> Option<String> {
metadata
.get("activation_artifact_sha256")
.cloned()
.or_else(|| std::env::var("AGNT5_ACTIVATION_ARTIFACT_SHA256").ok())
.filter(|value| !value.is_empty())
}
fn decode_activation_artifact_sha256(value: &str) -> Option<[u8; 32]> {
use base64::Engine as _;
if let Ok(decoded) = hex::decode(value) {
if let Ok(digest) = decoded.try_into() {
return Some(digest);
}
}
[
base64::engine::general_purpose::STANDARD.decode(value),
base64::engine::general_purpose::STANDARD_NO_PAD.decode(value),
base64::engine::general_purpose::URL_SAFE.decode(value),
base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(value),
]
.into_iter()
.filter_map(|decoded| decoded.ok())
.find_map(|decoded| decoded.try_into().ok())
}
fn valid_activation_artifact_sha256(value: &str) -> bool {
decode_activation_artifact_sha256(value).is_some()
}
fn deployment_artifact_sha256(deployment_id: &str) -> Option<[u8; 32]> {
let deployment_id = Uuid::parse_str(deployment_id).ok()?.to_string();
let mut digest = Sha256::new();
digest.update(DEPLOYMENT_ARTIFACT_DOMAIN);
digest.update(deployment_id.as_bytes());
Some(digest.finalize().into())
}
fn configured_deployment_artifact_sha256(metadata: &HashMap<String, String>) -> Option<[u8; 32]> {
["AGNT5_DEPLOYMENT_ID", "deployment_id"]
.into_iter()
.filter_map(|key| metadata.get(key))
.map(String::as_str)
.find_map(deployment_artifact_sha256)
.or_else(|| {
std::env::var("AGNT5_DEPLOYMENT_ID")
.ok()
.and_then(|value| deployment_artifact_sha256(&value))
})
}
fn worker_protocol_capabilities_for_metadata(
metadata: &HashMap<String, String>,
) -> Result<(Vec<String>, Vec<String>)> {
let (supported, required) = crate::client::worker_protocol_capabilities();
if !supported
.iter()
.any(|capability| capability == crate::client::DURABLE_ACTIVATION_V1_CAPABILITY)
{
return Ok((supported, required));
}
if let Some(configured) = configured_activation_artifact_sha256(metadata) {
if valid_activation_artifact_sha256(&configured) {
let configured = decode_activation_artifact_sha256(&configured)
.expect("validated activation artifact digest");
if configured_deployment_artifact_sha256(metadata)
.is_none_or(|expected| expected == configured)
{
return Ok((supported, required));
}
}
}
let message = "durable_activation_v1 requires the control-plane deployment artifact identity";
if required
.iter()
.any(|capability| capability == crate::client::DURABLE_ACTIVATION_V1_CAPABILITY)
{
return Err(SdkError::Activation {
message: message.to_string(),
code: crate::error::ErrorCode::DurabilityUnavailable,
activation_id: None,
attempt: None,
});
}
eprintln!(
"[WARN] agnt5 durable activation degraded: {message}; legacy checkpoints remain enabled"
);
Ok((Vec::new(), Vec::new()))
}
fn activation_definition_configs(components: &[ComponentInfo]) -> HashMap<String, String> {
components
.iter()
.map(|component| {
(
component.name.clone(),
canonical_activation_component_config(&component.config),
)
})
.collect()
}
fn stamp_activation_dispatch_metadata(
runtime_message: &mut RuntimeMessage,
worker_id: &str,
worker_session_id: &str,
service_version: &str,
worker_metadata: &HashMap<String, String>,
definition_configs: &HashMap<String, String>,
) -> Result<()> {
let Some(crate::pb::runtime_message::MessageData::DispatchComponent(request)) =
runtime_message.message_data.as_mut()
else {
return Ok(());
};
request.metadata.insert(
crate::client::DURABLE_ACTIVATION_V1_CAPABILITY.to_string(),
"true".to_string(),
);
request
.metadata
.insert("worker_id".to_string(), worker_id.to_string());
request.metadata.insert(
"worker_session_id".to_string(),
worker_session_id.to_string(),
);
request
.metadata
.insert("lease_id".to_string(), request.lease_id.clone());
request
.metadata
.entry("run_authority".to_string())
.or_insert_with(|| request.invocation_id.clone());
request
.metadata
.entry("lease_authority".to_string())
.or_insert_with(|| request.lease_id.clone());
request
.metadata
.insert("component_name".to_string(), request.component_name.clone());
request.metadata.insert(
"activation_definition_version".to_string(),
service_version.to_string(),
);
request.metadata.insert(
"activation_definition_config".to_string(),
definition_configs
.get(&request.component_name)
.cloned()
.unwrap_or_else(|| "[\"object\",[]]".to_string()),
);
if let Some(value) = worker_metadata.get("project_id") {
request
.metadata
.entry("project_id".to_string())
.or_insert_with(|| value.clone());
}
let worker_artifact =
configured_activation_artifact_sha256(worker_metadata).ok_or_else(|| {
SdkError::Activation {
message: "negotiated durable activation is missing the worker artifact identity"
.to_string(),
code: crate::error::ErrorCode::DurabilityUnavailable,
activation_id: None,
attempt: None,
}
})?;
let worker_digest = decode_activation_artifact_sha256(&worker_artifact).ok_or_else(|| {
SdkError::Activation {
message: "negotiated durable activation has an invalid worker artifact identity"
.to_string(),
code: crate::error::ErrorCode::DurabilityUnavailable,
activation_id: None,
attempt: None,
}
})?;
if let Some(run_artifact) = request.metadata.get("activation_artifact_sha256") {
if decode_activation_artifact_sha256(run_artifact) != Some(worker_digest) {
return Err(SdkError::Activation {
message:
"worker artifact identity does not match the run's pinned deployment artifact"
.to_string(),
code: crate::error::ErrorCode::NondeterministicReplay,
activation_id: None,
attempt: None,
});
}
} else {
request
.metadata
.insert("activation_artifact_sha256".to_string(), worker_artifact);
}
Ok(())
}
struct InFlightGuard(Arc<std::sync::atomic::AtomicUsize>);
impl InFlightGuard {
fn enter(c: &Arc<std::sync::atomic::AtomicUsize>) -> Self {
c.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
InFlightGuard(c.clone())
}
}
impl Drop for InFlightGuard {
fn drop(&mut self) {
self.0.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
}
}
struct DurableSuspensionEnvelope {
invocation_id: String,
metadata: HashMap<String, String>,
attempt: i32,
lease_id: String,
}
fn durable_suspension_envelope(
runtime_message: &RuntimeMessage,
) -> Option<DurableSuspensionEnvelope> {
let crate::pb::runtime_message::MessageData::DispatchComponent(request) =
runtime_message.message_data.as_ref()?
else {
return None;
};
Some(DurableSuspensionEnvelope {
invocation_id: request.invocation_id.clone(),
metadata: request.metadata.clone(),
attempt: request.attempt,
lease_id: request.lease_id.clone(),
})
}
async fn execute_runtime_message_for_response<F, Fut>(
worker_name: &str,
response_worker_id: &str,
mut runtime_message: RuntimeMessage,
response_tx: flume::Sender<ServiceMessage>,
handler: F,
in_flight: Arc<std::sync::atomic::AtomicUsize>,
cancel_tokens: Arc<std::sync::Mutex<HashMap<String, tokio::sync::oneshot::Sender<()>>>>,
revoked_executions: Arc<std::sync::Mutex<HashSet<String>>>,
dispatch_mode: &'static str,
) -> Option<ServiceMessage>
where
F: Fn(RuntimeMessage, flume::Sender<ServiceMessage>) -> Fut + Send + Clone + 'static,
Fut: std::future::Future<Output = Result<Option<ServiceMessage>>> + Send + 'static,
{
let _in_flight = InFlightGuard::enter(&in_flight);
let tx_clone = response_tx.clone();
stamp_dispatch_mode(&mut runtime_message, dispatch_mode);
let suspension_envelope = durable_suspension_envelope(&runtime_message);
let run_key = dispatch_run_key(&runtime_message);
if run_key
.as_deref()
.is_some_and(|run_id| execution_is_revoked(&revoked_executions, run_id))
{
warn!(
"Worker {} refusing dispatch after local execution authority was revoked",
worker_name
);
return None;
}
let result = if let Some(key) = run_key.clone() {
let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>();
if let Ok(mut m) = cancel_tokens.lock() {
m.insert(key.clone(), cancel_tx);
}
let outcome = tokio::select! {
res = handler(runtime_message, tx_clone) => Some(res),
_ = cancel_rx => None,
};
if let Ok(mut m) = cancel_tokens.lock() {
m.remove(&key);
}
outcome
} else {
Some(handler(runtime_message, tx_clone).await)
};
if run_key
.as_deref()
.is_some_and(|run_id| execution_is_revoked(&revoked_executions, run_id))
{
warn!(
"Worker {} suppressing response after execution authority loss",
worker_name
);
return None;
}
let response = match result {
Some(Ok(Some(response))) => Some(response),
Some(Ok(None)) => None,
Some(Err(SdkError::DurableSuspension { suspension })) => {
suspension_envelope.as_ref().map(|envelope| {
durable_suspension_service_message(response_worker_id, envelope, *suspension)
})
}
Some(Err(e)) => {
error!("Worker {} handler error: {}", worker_name, e);
None
}
None => {
debug!("Worker {} invocation cancelled by request", worker_name);
None
}
};
response
}
async fn execute_runtime_message<F, Fut>(
worker_name: &str,
response_worker_id: &str,
runtime_message: RuntimeMessage,
response_tx: flume::Sender<ServiceMessage>,
handler: F,
in_flight: Arc<std::sync::atomic::AtomicUsize>,
cancel_tokens: Arc<std::sync::Mutex<HashMap<String, tokio::sync::oneshot::Sender<()>>>>,
revoked_executions: Arc<std::sync::Mutex<HashSet<String>>>,
) where
F: Fn(RuntimeMessage, flume::Sender<ServiceMessage>) -> Fut + Send + Clone + 'static,
Fut: std::future::Future<Output = Result<Option<ServiceMessage>>> + Send + 'static,
{
if let Some(response) = execute_runtime_message_for_response(
worker_name,
response_worker_id,
runtime_message,
response_tx.clone(),
handler,
in_flight,
cancel_tokens,
revoked_executions,
"push",
)
.await
{
if let Err(e) = response_tx.send_async(response).await {
error!("Worker {} failed to send response: {}", worker_name, e);
}
}
}
fn durable_suspension_service_message(
worker_id: &str,
envelope: &DurableSuspensionEnvelope,
suspension: crate::pb::WorkerSuspension,
) -> ServiceMessage {
ServiceMessage {
worker_id: worker_id.to_string(),
metadata: HashMap::new(),
message_type: Some(crate::pb::service_message::MessageType::FunctionResponse(
DispatchComponentResponse {
invocation_id: envelope.invocation_id.clone(),
success: true,
result: Some(
crate::pb::dispatch_component_response::Result::WorkerSuspension(suspension),
),
error_message: String::new(),
metadata: envelope.metadata.clone(),
event_type: "workflow.paused".to_string(),
content_index: 0,
sequence: 0,
attempt: envelope.attempt,
source_timestamp_ns: 0,
lease_id: envelope.lease_id.clone(),
},
)),
}
}
struct PolledJobCompletion {
job_id: String,
success: bool,
output_data: Vec<u8>,
error_message: String,
error_code: String,
event_type: String,
metadata: HashMap<String, String>,
lease_id: String,
attempt: u32,
}
#[async_trait::async_trait]
trait CompleteJobSender: Send {
async fn send_complete_job(
&mut self,
request: CompleteJobRequest,
) -> Result<CompleteJobResponse>;
}
#[async_trait::async_trait]
impl CompleteJobSender for WorkerCoordinatorClient {
async fn send_complete_job(
&mut self,
request: CompleteJobRequest,
) -> Result<CompleteJobResponse> {
self.complete_job(request).await
}
}
#[async_trait::async_trait]
trait EntityStateSender: Send {
async fn send_get_entity_state(
&mut self,
request: GetEntityStateRequest,
) -> Result<GetEntityStateResponse>;
async fn send_put_entity_state(
&mut self,
request: PutEntityStateRequest,
) -> Result<PutEntityStateResponse>;
}
#[async_trait::async_trait]
impl EntityStateSender for WorkerCoordinatorClient {
async fn send_get_entity_state(
&mut self,
request: GetEntityStateRequest,
) -> Result<GetEntityStateResponse> {
self.get_entity_state(request).await
}
async fn send_put_entity_state(
&mut self,
request: PutEntityStateRequest,
) -> Result<PutEntityStateResponse> {
self.put_entity_state(request).await
}
}
#[allow(clippy::too_many_arguments)]
async fn parked_runtime_service_response<S: EntityStateSender>(
sender: &mut S,
service_message: &ServiceMessage,
project_id: &str,
run_id: &str,
worker_id: &str,
worker_session_id: &str,
lease_id: &str,
attempt: u32,
) -> Option<RuntimeMessage> {
let service_message::MessageType::RuntimeService(request) =
service_message.message_type.as_ref()?
else {
return None;
};
let request_id = request.request_id.clone();
let response = match request.operation.as_ref() {
Some(runtime_service_request::Operation::EntityStateLoad(load)) => {
match sender
.send_get_entity_state(GetEntityStateRequest {
project_id: project_id.to_string(),
entity_type: load.entity_type.clone(),
entity_key: load.entity_key.clone(),
scope: load.scope.clone(),
scope_id: load.scope_id.clone(),
})
.await
{
Ok(result) => RuntimeServiceResponse {
request_id,
success: true,
error_message: String::new(),
result: Some(runtime_service_response::Result::EntityStateLoad(
EntityStateLoadResult {
found: result.found,
state_json: result.state_json,
version: result.version,
},
)),
},
Err(error) => RuntimeServiceResponse {
request_id,
success: false,
error_message: error.to_string(),
result: None,
},
}
}
Some(runtime_service_request::Operation::EntityStateSave(save)) => {
match sender
.send_put_entity_state(PutEntityStateRequest {
project_id: project_id.to_string(),
entity_type: save.entity_type.clone(),
entity_key: save.entity_key.clone(),
scope: save.scope.clone(),
scope_id: save.scope_id.clone(),
state_json: save.state_json.clone(),
expected_version: save.expected_version,
run_id: run_id.to_string(),
worker_id: worker_id.to_string(),
worker_session_id: worker_session_id.to_string(),
lease_id: lease_id.to_string(),
attempt: Some(attempt),
operation_id: request.request_id.clone(),
})
.await
{
Ok(result) => RuntimeServiceResponse {
request_id,
success: true,
error_message: String::new(),
result: Some(runtime_service_response::Result::EntityStateSave(
EntityStateSaveResult {
new_version: result.new_version,
},
)),
},
Err(error) => RuntimeServiceResponse {
request_id,
success: false,
error_message: error.to_string(),
result: None,
},
}
}
_ => RuntimeServiceResponse {
request_id,
success: false,
error_message: "runtime service operation is not available through parked polling yet"
.to_string(),
result: None,
},
};
Some(RuntimeMessage {
worker_id: worker_id.to_string(),
message_type: RuntimeMessageType::RuntimeService as i32,
metadata: HashMap::new(),
message_data: Some(runtime_message::MessageData::RuntimeServiceResponse(
response,
)),
})
}
async fn complete_job_with_retry<S: CompleteJobSender>(
sender: &mut S,
request: CompleteJobRequest,
attempts: usize,
attempt_timeout: Duration,
retry_delay: Duration,
) -> Result<()> {
let attempts = attempts.max(1);
let job_id = request.job_id.clone();
let mut last_error = None;
for attempt in 1..=attempts {
let outcome =
tokio::time::timeout(attempt_timeout, sender.send_complete_job(request.clone())).await;
match outcome {
Ok(Ok(response)) if response.acknowledged => return Ok(()),
Ok(Ok(_)) => {
last_error = Some(SdkError::Internal(format!(
"CompleteJob was not acknowledged for job {job_id}"
)));
}
Ok(Err(error)) => {
last_error = Some(error);
}
Err(_) => {
last_error = Some(SdkError::Timeout {
message: format!("CompleteJob timed out for job {job_id}"),
operation: "CompleteJob".to_string(),
duration_ms: Some(attempt_timeout.as_millis() as u64),
});
}
}
if attempt < attempts {
warn!(
"CompleteJob attempt {}/{} failed for job_id={}; retrying",
attempt, attempts, job_id
);
tokio::time::sleep(retry_delay).await;
}
}
Err(last_error.unwrap_or_else(|| {
SdkError::Internal(format!(
"CompleteJob failed without an outcome for job {job_id}"
))
}))
}
fn polled_job_completion_from_service_message(
service_message: &ServiceMessage,
assigned_job_id: &str,
assigned_lease_id: &str,
assigned_attempt: u32,
assigned_completion_metadata: &HashMap<String, String>,
) -> Option<PolledJobCompletion> {
match &service_message.message_type {
Some(crate::pb::service_message::MessageType::FunctionResponse(resp))
if is_terminal_worker_response(&resp.event_type) =>
{
let output_data = match &resp.result {
Some(crate::pb::dispatch_component_response::Result::OutputData(data)) => {
data.clone()
}
_ => Vec::new(),
};
let mut metadata = resp.metadata.clone();
for key in ASSIGNMENT_AUTHORED_RETRY_METADATA_KEYS {
metadata.remove(*key);
}
metadata.extend(assigned_completion_metadata.clone());
Some(PolledJobCompletion {
job_id: assigned_job_id.to_string(),
success: resp.success,
output_data,
error_message: resp.error_message.clone(),
error_code: metadata.get("error_code").cloned().unwrap_or_default(),
event_type: resp.event_type.clone(),
metadata,
lease_id: assigned_lease_id.to_string(),
attempt: assigned_attempt,
})
}
_ => None,
}
}
fn polled_job_suspension_request(
service_message: &ServiceMessage,
project_id: &str,
run_id: &str,
) -> Option<crate::pb::SuspendActivationRequest> {
let Some(crate::pb::service_message::MessageType::FunctionResponse(response)) =
&service_message.message_type
else {
return None;
};
let Some(crate::pb::dispatch_component_response::Result::WorkerSuspension(suspension)) =
&response.result
else {
return None;
};
Some(crate::pb::SuspendActivationRequest {
project_id: project_id.to_string(),
run_id: run_id.to_string(),
activation_id: suspension.activation_id.clone(),
attempt: suspension.attempt,
fence_token: suspension.fence_token.clone(),
timer_key: suspension.timer_key.clone(),
ready_at_ms: suspension.ready_at_ms,
input_digest: suspension.input_digest.clone(),
definition_digest: suspension.definition_digest.clone(),
continuation: suspension.continuation.clone(),
delay_ms: suspension.delay_ms,
})
}
async fn wait_for_parked_run_events_flush(
journal_queue: &JournalEventQueue,
journal_flush_locks: &RunFlushLocks,
run_id: &str,
) -> bool {
let deadline = tokio::time::Instant::now() + PARKED_COMPLETION_FLUSH_TIMEOUT;
loop {
{
let _flush_guard = journal_flush_locks.lock_run(run_id).await;
if !journal_queue.contains_run(run_id) {
return true;
}
}
if tokio::time::Instant::now() >= deadline {
return false;
}
tokio::time::sleep(Duration::from_millis(
journal_queue.flush_interval_ms().max(1),
))
.await;
}
}
fn complete_job_request_from_polled_completion(
worker_id: &str,
worker_session_id: &str,
tenant_id: &str,
completion: PolledJobCompletion,
) -> CompleteJobRequest {
let mut metadata = completion.metadata;
metadata.insert("completion_event_type".to_string(), completion.event_type);
CompleteJobRequest {
job_id: completion.job_id,
worker_id: worker_id.to_string(),
success: completion.success,
output_data: completion.output_data,
error_message: completion.error_message,
error_code: completion.error_code,
metadata,
project_id: tenant_id.to_string(),
lease_id: completion.lease_id,
worker_session_id: worker_session_id.to_string(),
attempt: Some(completion.attempt),
}
}
async fn complete_polled_job_with_client(
client: &mut WorkerCoordinatorClient,
worker_id: &str,
worker_session_id: &str,
tenant_id: &str,
completion: PolledJobCompletion,
) -> Result<()> {
let job_id = completion.job_id.clone();
let request = complete_job_request_from_polled_completion(
worker_id,
worker_session_id,
tenant_id,
completion,
);
match complete_job_with_retry(
client,
request,
PARKED_COMPLETE_JOB_ATTEMPTS,
PARKED_COMPLETE_JOB_ATTEMPT_TIMEOUT,
PARKED_COMPLETE_JOB_RETRY_DELAY,
)
.await
{
Ok(()) => {
debug!("CompleteJob succeeded: job_id={}", job_id);
Ok(())
}
Err(e) => {
error!("CompleteJob failed: job_id={} error={}", job_id, e);
Err(e)
}
}
}
async fn complete_or_forward_parked_response(
client: &mut WorkerCoordinatorClient,
service_message: ServiceMessage,
assigned_job_id: &str,
assigned_lease_id: &str,
assigned_attempt: u32,
assigned_completion_metadata: &HashMap<String, String>,
worker_id: &str,
worker_session_id: &Arc<TokioMutex<String>>,
tenant_id: &str,
slot_idx: usize,
response_tx: &flume::Sender<ServiceMessage>,
journal_queue: &JournalEventQueue,
journal_flush_locks: &RunFlushLocks,
) -> bool {
if is_cancelled_worker_response(&service_message) {
debug!(
"Parked poll slot {} observed runtime-authored cancellation for job_id={}",
slot_idx, assigned_job_id
);
return true;
}
if let Some(request) =
polled_job_suspension_request(&service_message, tenant_id, assigned_job_id)
{
match client.suspend_activation(request).await {
Ok(receipt) if receipt.accepted => {
debug!(
"Parked poll slot {} suspended job_id={} timer_id={}",
slot_idx, assigned_job_id, receipt.timer_id
);
return true;
}
Ok(_) => {
warn!(
"Parked poll slot {} received an unaccepted suspension receipt for job_id={}",
slot_idx, assigned_job_id
);
return false;
}
Err(error) => {
warn!(
"Parked poll slot {} failed to suspend job_id={}: {}",
slot_idx, assigned_job_id, error
);
return false;
}
}
}
let Some(completion) = polled_job_completion_from_service_message(
&service_message,
assigned_job_id,
assigned_lease_id,
assigned_attempt,
assigned_completion_metadata,
) else {
if let Err(e) = response_tx.send_async(service_message).await {
error!(
"Parked poll slot {} failed to send response: {}",
slot_idx, e
);
}
return false;
};
let job_id = completion.job_id.clone();
if !wait_for_parked_run_events_flush(journal_queue, journal_flush_locks, &job_id).await {
warn!(
"Parked poll slot {} refusing to overtake unflushed events for job_id={}",
slot_idx, job_id
);
return false;
}
let started = Instant::now();
let current_session_id = worker_session_id.lock().await.clone();
if let Err(e) = complete_polled_job_with_client(
client,
worker_id,
¤t_session_id,
tenant_id,
completion,
)
.await
{
warn!("Parked poll slot {} CompleteJob failed: {}", slot_idx, e);
return false;
} else {
let elapsed = started.elapsed();
if elapsed > Duration::from_millis(500) {
warn!(
"Parked poll slot {} CompleteJob was slow: job_id={} elapsed_ms={}",
slot_idx,
job_id,
elapsed.as_millis()
);
} else {
debug!(
"Parked poll slot {} CompleteJob acked: job_id={} elapsed_ms={}",
slot_idx,
job_id,
elapsed.as_millis()
);
}
}
true
}
fn active_lease_renew_interval_ms(lease_timeout_ms: i64) -> u64 {
let timeout_ms = lease_timeout_ms.max(2) as u64;
(timeout_ms / 2).clamp(1, 60_000)
}
fn active_lease_renew_interval_with_jitter_ms(lease_timeout_ms: i64) -> u64 {
let base = active_lease_renew_interval_ms(lease_timeout_ms);
let jitter = rand::random::<f64>() * 0.20 - 0.10;
((base as f64) * (1.0 + jitter)).round().max(1.0) as u64
}
fn active_lease_danger_retry_ms(lease_timeout_ms: i64) -> u64 {
let timeout_ms = lease_timeout_ms.max(2) as u64;
(timeout_ms / 10).clamp(1, 5_000)
}
#[derive(Clone)]
enum ActiveLeaseSession {
Push,
Pull(Arc<TokioMutex<String>>),
}
#[derive(Clone)]
struct ActiveLeaseAuthority {
worker_id: String,
project_id: String,
deployment_id: String,
run_id: String,
lease_id: String,
attempt: u32,
lease_timeout_ms: i64,
lease_expires_at_ms: i64,
session: ActiveLeaseSession,
}
impl ActiveLeaseAuthority {
fn mode_label(&self) -> &'static str {
match &self.session {
ActiveLeaseSession::Push => "push",
ActiveLeaseSession::Pull(_) => "pull",
}
}
async fn renewal_request(&self) -> RenewJobLeaseRequest {
let (worker_session_id, mode) = match &self.session {
ActiveLeaseSession::Push => (String::new(), WorkerMode::Push),
ActiveLeaseSession::Pull(session_id) => {
(session_id.lock().await.clone(), WorkerMode::Pull)
}
};
RenewJobLeaseRequest {
worker_id: self.worker_id.clone(),
worker_session_id,
run_id: self.run_id.clone(),
lease_id: self.lease_id.clone(),
lease_timeout_ms: self.lease_timeout_ms,
attempt: Some(self.attempt),
mode: mode as i32,
project_id: self.project_id.clone(),
deployment_id: self.deployment_id.clone(),
}
}
}
fn execution_is_revoked(
revoked_executions: &Arc<std::sync::Mutex<HashSet<String>>>,
run_id: &str,
) -> bool {
revoked_executions
.lock()
.map(|runs| runs.contains(run_id))
.unwrap_or(true)
}
fn revoke_execution_authority(
run_id: &str,
revoked_executions: &Arc<std::sync::Mutex<HashSet<String>>>,
cancel_tokens: &Arc<std::sync::Mutex<HashMap<String, tokio::sync::oneshot::Sender<()>>>>,
cancel_hook: &Arc<std::sync::Mutex<Option<Box<dyn Fn(String) + Send + Sync>>>>,
) {
if let Ok(mut runs) = revoked_executions.lock() {
runs.insert(run_id.to_string());
}
let hooked = cancel_hook
.lock()
.ok()
.and_then(|hook| hook.as_ref().map(|hook| hook(run_id.to_string())))
.is_some();
if !hooked {
if let Some(cancel) = cancel_tokens
.lock()
.ok()
.and_then(|mut tokens| tokens.remove(run_id))
{
let _ = cancel.send(());
}
}
}
async fn report_worker_capacity_with_client(
client: &mut WorkerCoordinatorClient,
worker_id: &str,
worker_session_id: &str,
open_poll_slots: Arc<std::sync::atomic::AtomicUsize>,
active_slots: Arc<std::sync::atomic::AtomicUsize>,
desired_slots: Arc<std::sync::atomic::AtomicUsize>,
effective_max_slots: usize,
) {
let open_poll_slots = open_poll_slots.load(std::sync::atomic::Ordering::Relaxed) as u32;
let active_slots = active_slots.load(std::sync::atomic::Ordering::Relaxed) as u32;
let desired_slots = desired_slots.load(std::sync::atomic::Ordering::Relaxed);
if let Err(e) = client
.report_worker_capacity(ReportWorkerCapacityRequest {
worker_id: worker_id.to_string(),
worker_session_id: worker_session_id.to_string(),
open_poll_slots,
active_slots,
desired_slots: desired_slots as u32,
effective_max_slots: effective_max_slots as u32,
cpu_usage: 0.0,
memory_usage: 0.0,
observed_at_ms: current_time_ms(),
})
.await
{
debug!("ReportWorkerCapacity failed: {}", e);
}
}
fn spawn_parked_capacity_reporter(
mut client: WorkerCoordinatorClient,
worker_id: String,
worker_session_id: Arc<TokioMutex<String>>,
open_poll_slots: Arc<std::sync::atomic::AtomicUsize>,
active_slots: Arc<std::sync::atomic::AtomicUsize>,
desired_slots: Arc<std::sync::atomic::AtomicUsize>,
effective_max_slots: usize,
mut shutdown_rx: tokio::sync::broadcast::Receiver<()>,
) -> tokio::task::JoinHandle<()> {
let interval_ms = env_usize("AGNT5_CAPACITY_REPORT_INTERVAL_MS")
.unwrap_or(5_000)
.clamp(1_000, 60_000) as u64;
tokio::spawn(async move {
let current_session_id = worker_session_id.lock().await.clone();
report_worker_capacity_with_client(
&mut client,
&worker_id,
¤t_session_id,
open_poll_slots.clone(),
active_slots.clone(),
desired_slots.clone(),
effective_max_slots,
)
.await;
let mut interval = tokio::time::interval(Duration::from_millis(interval_ms));
loop {
tokio::select! {
_ = shutdown_rx.recv() => return,
_ = interval.tick() => {
let current_session_id = worker_session_id.lock().await.clone();
report_worker_capacity_with_client(
&mut client,
&worker_id,
¤t_session_id,
open_poll_slots.clone(),
active_slots.clone(),
desired_slots.clone(),
effective_max_slots,
)
.await;
}
}
}
})
}
fn is_worker_session_inactive_error(error: &SdkError) -> bool {
error
.to_string()
.to_ascii_lowercase()
.contains("worker session is not active")
}
fn is_parked_worker_session_registration_rejection(error: &SdkError) -> bool {
let error = error.to_string().to_ascii_lowercase();
[
"invalid argument",
"permission denied",
"unauthenticated",
"failed precondition",
"out of range",
"unimplemented",
]
.iter()
.any(|needle| error.contains(needle))
}
fn parked_worker_session_was_refreshed(
current_session_id: &str,
observed_session_id: &str,
) -> bool {
current_session_id != observed_session_id
}
fn exit_parked_worker_process(reason: &str) -> ! {
error!("{}", reason);
std::process::exit(1);
}
async fn register_parked_worker_session_with_retries(
client: &mut WorkerCoordinatorClient,
registration: &ParkedWorkerSessionRegistration,
reason: &str,
) -> ParkedWorkerSessionRegistrationResult {
let mut rejection_attempts = 0;
let mut transient_attempts = 0;
let mut transient_delay = Duration::from_millis(PARKED_WORKER_SESSION_REGISTER_RETRY_MS);
loop {
match client.register_worker_session(registration.request()).await {
Ok(session) => {
if let Err(error) = crate::client::validate_protocol_capabilities(
®istration.supported_protocol_capabilities,
®istration.required_protocol_capabilities,
&session.supported_protocol_capabilities,
&session.required_protocol_capabilities,
) {
warn!("{} protocol negotiation failed: {}", reason, error);
return ParkedWorkerSessionRegistrationResult::Rejected;
}
client.retain_negotiated_protocol_capabilities(
®istration.supported_protocol_capabilities,
&session.supported_protocol_capabilities,
);
return ParkedWorkerSessionRegistrationResult::Registered(
session.worker_session_id,
);
}
Err(e) if is_parked_worker_session_registration_rejection(&e) => {
rejection_attempts += 1;
warn!(
"{} attempt {}/{} failed: {}",
reason, rejection_attempts, PARKED_WORKER_SESSION_REGISTER_ATTEMPTS, e
);
if rejection_attempts >= PARKED_WORKER_SESSION_REGISTER_ATTEMPTS {
return ParkedWorkerSessionRegistrationResult::Rejected;
}
tokio::time::sleep(Duration::from_millis(
PARKED_WORKER_SESSION_REGISTER_RETRY_MS,
))
.await;
}
Err(e) => {
transient_attempts += 1;
warn!(
"{} transient attempt {} failed: {}; retrying in {}ms",
reason,
transient_attempts,
e,
transient_delay.as_millis()
);
tokio::time::sleep(transient_delay).await;
transient_delay = (transient_delay * 2).min(Duration::from_millis(
PARKED_WORKER_SESSION_TRANSIENT_RETRY_MAX_MS,
));
}
}
}
}
async fn try_refresh_parked_worker_session_once(
client: &mut WorkerCoordinatorClient,
session_id: &Arc<TokioMutex<String>>,
observed_session_id: &str,
registration: &ParkedWorkerSessionRegistration,
slot_idx: usize,
) -> bool {
let current_session_id = session_id.lock().await.clone();
if parked_worker_session_was_refreshed(¤t_session_id, observed_session_id) {
debug!(
"Parked poll slot {} observed stale worker session; another slot refreshed it",
slot_idx
);
return true;
}
warn!(
"Parked poll slot {} detected inactive worker session; re-registering parked worker session",
slot_idx
);
match register_parked_worker_session_with_retries(
client,
registration,
"RegisterWorkerSession retry after inactive worker session",
)
.await
{
ParkedWorkerSessionRegistrationResult::Registered(new_session_id) => {
let mut current_session_id = session_id.lock().await;
if parked_worker_session_was_refreshed(¤t_session_id, observed_session_id) {
debug!(
"Parked poll slot {} discarding refreshed session; another slot already updated it",
slot_idx
);
return true;
}
*current_session_id = new_session_id;
info!(
"Parked poll slot {} refreshed inactive worker session",
slot_idx
);
true
}
ParkedWorkerSessionRegistrationResult::Rejected => false,
}
}
async fn refresh_parked_worker_session(
client: &mut WorkerCoordinatorClient,
session_id: &Arc<TokioMutex<String>>,
observed_session_id: &str,
registration: &ParkedWorkerSessionRegistration,
slot_idx: usize,
refresh_lock: &Arc<TokioMutex<()>>,
) -> bool {
let _refresh_guard = refresh_lock.lock().await;
try_refresh_parked_worker_session_once(
client,
session_id,
observed_session_id,
registration,
slot_idx,
)
.await
}
struct ParkedPollContext {
client: WorkerCoordinatorClient,
worker_id: String,
worker_session_id: Arc<TokioMutex<String>>,
registration: ParkedWorkerSessionRegistration,
service_version: String,
worker_metadata: HashMap<String, String>,
activation_definition_configs: HashMap<String, String>,
project_id: String,
response_tx: flume::Sender<ServiceMessage>,
in_flight: Arc<std::sync::atomic::AtomicUsize>,
cancel_tokens: Arc<std::sync::Mutex<HashMap<String, tokio::sync::oneshot::Sender<()>>>>,
cancel_hook: Arc<std::sync::Mutex<Option<Box<dyn Fn(String) + Send + Sync>>>>,
revoked_executions: Arc<std::sync::Mutex<HashSet<String>>>,
streaming_runs: Arc<std::sync::Mutex<HashMap<String, bool>>>,
pending_lease_ids: Arc<std::sync::Mutex<HashMap<String, String>>>,
journal_queue: JournalEventQueue,
journal_flush_locks: RunFlushLocks,
slot_phases: WorkerSlotPhases,
open_poll_slots: Arc<std::sync::atomic::AtomicUsize>,
total_slots: Arc<std::sync::atomic::AtomicUsize>,
busy_slots: Arc<std::sync::atomic::AtomicUsize>,
events_tx: tokio::sync::mpsc::UnboundedSender<ParkedSlotEvent>,
session_refresh_lock: Arc<TokioMutex<()>>,
claim_timeout_ms: i64,
min_slots: usize,
retire_empty_polls: usize,
}
fn parked_ramp_spawn_count(total_slots: usize, busy_slots: usize, max_slots: usize) -> usize {
busy_slots
.saturating_mul(2)
.min(max_slots)
.saturating_sub(total_slots)
}
fn try_retire_parked_slot(
total_slots: &std::sync::atomic::AtomicUsize,
busy_slots: &std::sync::atomic::AtomicUsize,
min_slots: usize,
) -> bool {
total_slots
.fetch_update(
std::sync::atomic::Ordering::Relaxed,
std::sync::atomic::Ordering::Relaxed,
|current| {
let busy = busy_slots.load(std::sync::atomic::Ordering::Relaxed);
(current > min_slots.saturating_add(busy)).then(|| current - 1)
},
)
.is_ok()
}
fn spawn_parked_slot<F, Fut>(
slots: &mut tokio::task::JoinSet<()>,
ctx: &Arc<ParkedPollContext>,
handler: F,
next_slot_id: &mut usize,
) where
F: Fn(RuntimeMessage, flume::Sender<ServiceMessage>) -> Fut + Send + Clone + 'static,
Fut: std::future::Future<Output = Result<Option<ServiceMessage>>> + Send + 'static,
{
ctx.total_slots
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let slot_id = *next_slot_id;
*next_slot_id += 1;
slots.spawn(run_parked_poll_slot(ctx.clone(), handler, slot_id));
}
async fn run_parked_poll_slot<F, Fut>(ctx: Arc<ParkedPollContext>, handler: F, slot_id: usize)
where
F: Fn(RuntimeMessage, flume::Sender<ServiceMessage>) -> Fut + Send + Clone + 'static,
Fut: std::future::Future<Output = Result<Option<ServiceMessage>>> + Send + 'static,
{
let mut client = ctx.client.clone();
let worker_name = format!("{}-parked-{}", ctx.worker_id, slot_id);
let retire_threshold = ctx.retire_empty_polls + (slot_id % 2);
let mut consecutive_empty = 0usize;
loop {
let current_session_id = ctx.worker_session_id.lock().await.clone();
ctx.open_poll_slots
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let poll_result = client
.poll_job(PollJobRequest {
worker_id: ctx.worker_id.clone(),
worker_session_id: current_session_id.clone(),
wait_ms: 30_000,
claim_timeout_ms: ctx.claim_timeout_ms,
})
.await;
ctx.open_poll_slots
.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
match poll_result {
Ok(resp) => {
let Some(job) = resp.job else {
consecutive_empty += 1;
if consecutive_empty >= retire_threshold
&& try_retire_parked_slot(&ctx.total_slots, &ctx.busy_slots, ctx.min_slots)
{
debug!(
"Parked poll slot {} retiring after {} empty polls",
slot_id, consecutive_empty
);
let _ = ctx.events_tx.send(ParkedSlotEvent::Retired);
return;
}
continue;
};
consecutive_empty = 0;
let _busy = InFlightGuard::enter(&ctx.busy_slots);
let (
mut runtime_message,
is_streaming,
run_id,
lease_id,
completion_attempt,
lease_timeout_ms,
lease_expires_at_ms,
completion_metadata,
) = match runtime_message_from_job_assignment(job, ctx.claim_timeout_ms) {
Ok(assignment) => assignment,
Err(error) => {
error!(
"Parked poll slot {} refusing invalid assignment: {}",
slot_id, error
);
continue;
}
};
let _slot_phase = ctx.slot_phases.claim(run_id.clone());
stamp_execution_authority_metadata(
&mut runtime_message,
&ctx.worker_id,
¤t_session_id,
"pull",
);
if client
.negotiated_protocol_capability(crate::client::DURABLE_ACTIVATION_V1_CAPABILITY)
{
if let Err(error) = stamp_activation_dispatch_metadata(
&mut runtime_message,
&ctx.worker_id,
¤t_session_id,
&ctx.service_version,
&ctx.worker_metadata,
&ctx.activation_definition_configs,
) {
error!(
run_id,
error = %error,
"Parked poll slot refusing assignment with mismatched artifact identity"
);
continue;
}
}
if client
.negotiated_protocol_capability(crate::client::DURABLE_SUSPENSION_V1_CAPABILITY)
{
stamp_protocol_capability(
&mut runtime_message,
crate::client::DURABLE_SUSPENSION_V1_CAPABILITY,
);
}
let completion_run_id = run_id.clone();
let completion_lease_id = lease_id.clone();
if !lease_id.is_empty() {
if let Ok(mut map) = ctx.pending_lease_ids.lock() {
map.insert(run_id.clone(), lease_id.clone());
}
}
if is_streaming {
if let Ok(mut map) = ctx.streaming_runs.lock() {
map.insert(run_id.clone(), true);
}
}
if let Ok(mut revoked) = ctx.revoked_executions.lock() {
revoked.remove(&run_id);
}
let (renew_stop_tx, renew_handle) = tokio::sync::oneshot::channel::<()>();
let renewal = spawn_active_lease_renewal(
client.clone(),
ActiveLeaseAuthority {
worker_id: ctx.worker_id.clone(),
project_id: ctx.project_id.clone(),
deployment_id: ctx.registration.deployment_id.clone(),
run_id: run_id.clone(),
lease_id: lease_id.clone(),
attempt: completion_attempt,
lease_timeout_ms,
lease_expires_at_ms,
session: ActiveLeaseSession::Pull(ctx.worker_session_id.clone()),
},
ctx.revoked_executions.clone(),
ctx.cancel_tokens.clone(),
ctx.cancel_hook.clone(),
renew_handle,
);
let (slot_response_tx, slot_response_rx) = flume::unbounded::<ServiceMessage>();
let handler_future = execute_runtime_message_for_response(
&worker_name,
&ctx.worker_id,
runtime_message,
slot_response_tx.clone(),
handler.clone(),
ctx.in_flight.clone(),
ctx.cancel_tokens.clone(),
ctx.revoked_executions.clone(),
"pull",
);
tokio::pin!(handler_future);
let mut buffered_responses = Vec::new();
let returned_response = loop {
tokio::select! {
response = &mut handler_future => break response,
outgoing = slot_response_rx.recv_async() => {
let Ok(outgoing) = outgoing else {
break handler_future.await;
};
let current_session_id =
ctx.worker_session_id.lock().await.clone();
if let Some(runtime_response) = parked_runtime_service_response(
&mut client,
&outgoing,
&ctx.project_id,
&completion_run_id,
&ctx.worker_id,
¤t_session_id,
&completion_lease_id,
completion_attempt,
)
.await
{
match handler.clone()(
runtime_response,
slot_response_tx.clone(),
)
.await
{
Ok(Some(response)) => buffered_responses.push(response),
Ok(None) => {}
Err(error) => {
error!(
"Parked poll slot {} failed to deliver unary runtime response: {}",
slot_id, error
);
}
}
} else {
buffered_responses.push(outgoing);
}
}
}
};
drop(slot_response_tx);
while let Ok(service_message) = slot_response_rx.try_recv() {
buffered_responses.push(service_message);
}
ctx.slot_phases.mark_terminalizing(&run_id);
let mut completed = false;
if let Some(service_message) = returned_response
.filter(|_| !execution_is_revoked(&ctx.revoked_executions, &run_id))
{
completed = complete_or_forward_parked_response(
&mut client,
service_message,
&completion_run_id,
&completion_lease_id,
completion_attempt,
&completion_metadata,
&ctx.worker_id,
&ctx.worker_session_id,
&ctx.project_id,
slot_id,
&ctx.response_tx,
&ctx.journal_queue,
&ctx.journal_flush_locks,
)
.await;
}
for service_message in buffered_responses {
if execution_is_revoked(&ctx.revoked_executions, &run_id) {
warn!(
"Parked poll slot {} suppressing response after lease authority loss for run_id={}",
slot_id, completion_run_id
);
continue;
}
if completed
&& polled_job_completion_from_service_message(
&service_message,
&completion_run_id,
&completion_lease_id,
completion_attempt,
&completion_metadata,
)
.is_some()
{
debug!(
"Parked poll slot {} dropping duplicate completion for job_id={}",
slot_id, completion_run_id
);
continue;
}
completed = complete_or_forward_parked_response(
&mut client,
service_message,
&completion_run_id,
&completion_lease_id,
completion_attempt,
&completion_metadata,
&ctx.worker_id,
&ctx.worker_session_id,
&ctx.project_id,
slot_id,
&ctx.response_tx,
&ctx.journal_queue,
&ctx.journal_flush_locks,
)
.await
|| completed;
}
if completed || execution_is_revoked(&ctx.revoked_executions, &run_id) {
if let Ok(mut map) = ctx.pending_lease_ids.lock() {
map.remove(&completion_run_id);
}
if let Ok(mut map) = ctx.streaming_runs.lock() {
map.remove(&completion_run_id);
}
}
let _ = renew_stop_tx.send(());
if let Some(handle) = renewal {
let _ = handle.await;
}
}
Err(e) if is_worker_session_inactive_error(&e) => {
consecutive_empty = 0;
warn!("Parked poll slot {} error: {}", slot_id, e);
if !refresh_parked_worker_session(
&mut client,
&ctx.worker_session_id,
¤t_session_id,
&ctx.registration,
slot_id,
&ctx.session_refresh_lock,
)
.await
{
exit_parked_worker_process(
"RegisterWorkerSession retry was rejected after 3 attempts; exiting worker process",
);
}
}
Err(e) => {
consecutive_empty = 0;
warn!("Parked poll slot {} error: {}", slot_id, e);
tokio::time::sleep(Duration::from_millis(1_000)).await;
}
}
}
}
fn spawn_active_lease_renewal(
mut client: WorkerCoordinatorClient,
authority: ActiveLeaseAuthority,
revoked_executions: Arc<std::sync::Mutex<HashSet<String>>>,
cancel_tokens: Arc<std::sync::Mutex<HashMap<String, tokio::sync::oneshot::Sender<()>>>>,
cancel_hook: Arc<std::sync::Mutex<Option<Box<dyn Fn(String) + Send + Sync>>>>,
mut stop_rx: tokio::sync::oneshot::Receiver<()>,
) -> Option<tokio::task::JoinHandle<()>> {
if authority.lease_id.is_empty() {
return None;
}
Some(tokio::spawn(async move {
let mut lease_expires_at_ms = if authority.lease_expires_at_ms > 0 {
authority.lease_expires_at_ms
} else {
current_time_ms().saturating_add(authority.lease_timeout_ms)
};
let mut delay_ms = active_lease_renew_interval_with_jitter_ms(authority.lease_timeout_ms);
loop {
let now_ms = current_time_ms();
if now_ms >= lease_expires_at_ms {
crate::telemetry::record_lease_renewal(authority.mode_label(), "expired");
warn!(
"Execution lease expired before renewal was confirmed: run_id={} lease_id={}",
authority.run_id, authority.lease_id
);
revoke_execution_authority(
&authority.run_id,
&revoked_executions,
&cancel_tokens,
&cancel_hook,
);
return;
}
let remaining_ms = lease_expires_at_ms.saturating_sub(now_ms) as u64;
let sleep_ms = delay_ms.min(remaining_ms.max(1));
tokio::select! {
_ = &mut stop_rx => {
return;
}
_ = tokio::time::sleep(Duration::from_millis(sleep_ms)) => {}
}
if current_time_ms() >= lease_expires_at_ms {
crate::telemetry::record_lease_renewal(authority.mode_label(), "danger_expired");
warn!(
"Execution entered the lease danger boundary without a confirmed renewal: run_id={} lease_id={}",
authority.run_id, authority.lease_id
);
revoke_execution_authority(
&authority.run_id,
&revoked_executions,
&cancel_tokens,
&cancel_hook,
);
return;
}
match client
.renew_job_lease(authority.renewal_request().await)
.await
{
Ok(resp) if resp.renewed => {
crate::telemetry::record_lease_renewal(authority.mode_label(), "renewed");
debug!(
"Renewed execution lease run_id={} lease_id={} expires_at_ms={}",
authority.run_id, authority.lease_id, resp.lease_expires_at_ms
);
lease_expires_at_ms = resp.lease_expires_at_ms;
delay_ms =
active_lease_renew_interval_with_jitter_ms(authority.lease_timeout_ms);
}
Ok(resp) => {
let outcome = LeaseRenewalOutcome::try_from(resp.outcome)
.unwrap_or(LeaseRenewalOutcome::Unspecified);
warn!(
"Execution lease authority was revoked: run_id={} lease_id={} outcome={}",
authority.run_id,
authority.lease_id,
outcome.as_str_name(),
);
crate::telemetry::record_lease_renewal(
authority.mode_label(),
match outcome {
LeaseRenewalOutcome::AuthorityLost => "authority_lost",
LeaseRenewalOutcome::Terminal => "terminal",
LeaseRenewalOutcome::SessionInactive => "session_inactive",
LeaseRenewalOutcome::Unspecified | LeaseRenewalOutcome::Renewed => {
"rejected"
}
},
);
revoke_execution_authority(
&authority.run_id,
&revoked_executions,
&cancel_tokens,
&cancel_hook,
);
return;
}
Err(e) => {
crate::telemetry::record_lease_renewal(authority.mode_label(), "indeterminate");
warn!(
"Execution lease renewal is indeterminate; retrying inside the known lease window: run_id={} lease_id={} error={}",
authority.run_id, authority.lease_id, e
);
delay_ms = active_lease_danger_retry_ms(authority.lease_timeout_ms);
}
}
}
}))
}
impl Worker {
pub fn new(
config: WorkerConfig,
components: Vec<ComponentInfo>,
metadata: HashMap<String, String>,
) -> Self {
let journal_config = JournalQueueConfig::from_env();
debug!(
"Creating worker with unified journal queue: max_size={}, batch_size={}, flush_interval_ms={}",
journal_config.max_size, journal_config.batch_size, journal_config.flush_interval_ms
);
Self {
config,
components,
metadata,
connection_state: Arc::new(std::sync::Mutex::new(ConnectionState::Disconnected)),
journal_queue: JournalEventQueue::new(journal_config),
ee_client: Arc::new(TokioMutex::new(None)),
tokio_handle: Arc::new(std::sync::Mutex::new(None)),
streaming_runs: Arc::new(std::sync::Mutex::new(HashMap::new())),
pending_lease_ids: Arc::new(std::sync::Mutex::new(HashMap::new())),
revoked_executions: Arc::new(std::sync::Mutex::new(HashSet::new())),
lease_renewal_stops: Arc::new(std::sync::Mutex::new(HashMap::new())),
cancel_tokens: Arc::new(std::sync::Mutex::new(HashMap::new())),
cancel_hook: Arc::new(std::sync::Mutex::new(None)),
event_stream_tx: Arc::new(std::sync::Mutex::new(None)),
dispatch_tx: Arc::new(std::sync::Mutex::new(None)),
engine_client: Arc::new(TokioMutex::new(None)),
journal_flush_locks: RunFlushLocks::default(),
slot_phases: WorkerSlotPhases::default(),
}
}
pub fn journal_queue(&self) -> JournalEventQueue {
self.journal_queue.clone()
}
pub fn set_components(&mut self, components: Vec<ComponentInfo>) {
self.components = components;
}
pub fn set_metadata(&mut self, metadata: HashMap<String, String>) {
self.metadata = metadata;
}
pub fn connection_state(&self) -> ConnectionState {
self.connection_state
.lock()
.unwrap_or_else(|poisoned| {
warn!("Connection state mutex poisoned, recovering");
poisoned.into_inner()
})
.clone()
}
fn set_connection_state(&self, state: ConnectionState) {
let mut guard = self.connection_state.lock().unwrap_or_else(|poisoned| {
warn!("Connection state mutex poisoned during set, recovering");
poisoned.into_inner()
});
*guard = state;
}
pub fn queue_event(&self, event: JournalEventMessage) -> Result<()> {
if event.event_type == "run.started" {
self.mark_execution_started(&event.run_id);
}
self.journal_queue.push(event).map_err(|e| {
crate::error::SdkError::Internal(format!("Failed to queue journal event: {}", e))
})?;
Ok(())
}
pub fn mark_execution_started(&self, run_id: &str) {
let run_id = run_id.split(':').next().unwrap_or(run_id);
self.slot_phases.mark_started(run_id);
}
pub fn queue_checkpoint(
&self,
invocation_id: String,
checkpoint_type: String,
checkpoint_data: Vec<u8>,
sequence_number: i64,
metadata: HashMap<String, String>,
source_timestamp_ns: i64,
correlation_id: String,
parent_correlation_id: String,
) -> Result<()> {
let tenant_id = canonical_project_id_from_metadata(&metadata);
let event = JournalEventMessage {
run_id: invocation_id,
event_type: checkpoint_type,
data: checkpoint_data,
sequence: sequence_number,
metadata,
source_timestamp_ns,
correlation_id,
parent_correlation_id,
tenant_id,
is_sse_only: false, queued_at: std::time::Instant::now(),
..Default::default()
};
self.queue_event(event)
}
pub fn journal_metrics(&self) -> (u64, u64, u64, u64) {
self.journal_queue.get_metrics()
}
pub fn drain_events(&self) -> Vec<JournalEventMessage> {
self.journal_queue.drain_all()
}
async fn ensure_ee_client(&self) -> Result<ExecutionEngineServiceClient<Channel>> {
let mut guard = self.ee_client.lock().await;
if let Some(ref client) = *guard {
return Ok(client.clone());
}
let endpoint = &self.config.ee_endpoint;
debug!("Connecting EE client to {}", endpoint);
let channel = Channel::from_shared(endpoint.clone())
.map_err(|e| crate::error::SdkError::Connection {
message: format!("Invalid EE endpoint {}: {}", endpoint, e),
code: crate::error::ErrorCode::ConnectionFailed,
source: None,
})?
.connect_timeout(Duration::from_secs(10))
.timeout(Duration::from_secs(30))
.connect()
.await
.map_err(|e| {
debug!("EE connection to {} failed: {:?}", endpoint, e);
crate::error::SdkError::Connection {
message: format!("EE connection failed: {}", e),
code: crate::error::ErrorCode::ConnectionFailed,
source: None,
}
})?;
let client = ExecutionEngineServiceClient::new(channel);
*guard = Some(client.clone());
debug!("EE client connected to {}", endpoint);
Ok(client)
}
async fn ensure_engine_client(&self) -> Result<Option<EngineClient>> {
let endpoint = match &self.config.engine_endpoint {
Some(ep) => ep.clone(),
None => return Ok(None),
};
let mut guard = self.engine_client.lock().await;
if let Some(ref client) = *guard {
return Ok(Some(client.clone()));
}
debug!("Connecting Engine client to {}", endpoint);
let client = EngineClient::connect(&endpoint).await?;
*guard = Some(client.clone());
debug!("Engine client connected to {}", endpoint);
Ok(Some(client))
}
fn start_push_lease_renewal(
&self,
client: WorkerCoordinatorClient,
request: &DispatchComponentRequest,
) {
if request.lease_id.is_empty() {
return;
}
let run_id = request
.invocation_id
.split(':')
.next()
.unwrap_or(&request.invocation_id)
.to_string();
let Ok(attempt) = u32::try_from(request.attempt) else {
warn!(
"Refusing push lease renewal with negative attempt: run_id={} attempt={}",
run_id, request.attempt
);
revoke_execution_authority(
&run_id,
&self.revoked_executions,
&self.cancel_tokens,
&self.cancel_hook,
);
return;
};
let project_id = request
.metadata
.get("project_id")
.cloned()
.or_else(|| canonical_project_id_from_metadata(&self.metadata))
.unwrap_or_default();
let deployment_id = if request.deployment_id.is_empty() {
request
.metadata
.get("deployment_id")
.cloned()
.or_else(|| self.metadata.get("deployment_id").cloned())
.unwrap_or_default()
} else {
request.deployment_id.clone()
};
if project_id.is_empty() || deployment_id.is_empty() {
warn!(
"Push lease has incomplete routing authority; cancelling execution: run_id={}",
run_id
);
revoke_execution_authority(
&run_id,
&self.revoked_executions,
&self.cancel_tokens,
&self.cancel_hook,
);
return;
}
let lease_timeout_ms = request
.metadata
.get("lease_timeout_ms")
.and_then(|value| value.parse::<i64>().ok())
.filter(|value| *value > 0)
.unwrap_or(120_000);
let lease_expires_at_ms = request
.metadata
.get("lease_expires_at_ms")
.and_then(|value| value.parse::<i64>().ok())
.filter(|value| *value > 0)
.unwrap_or_else(|| current_time_ms().saturating_add(lease_timeout_ms));
if let Ok(mut revoked) = self.revoked_executions.lock() {
revoked.remove(&run_id);
}
let (stop_tx, stop_rx) = tokio::sync::oneshot::channel();
if let Ok(mut stops) = self.lease_renewal_stops.lock() {
if let Some(previous) = stops.insert(run_id.clone(), stop_tx) {
let _ = previous.send(());
}
}
let _ = spawn_active_lease_renewal(
client,
ActiveLeaseAuthority {
worker_id: self.config.worker_id.clone(),
project_id,
deployment_id,
run_id,
lease_id: request.lease_id.clone(),
attempt,
lease_timeout_ms,
lease_expires_at_ms,
session: ActiveLeaseSession::Push,
},
self.revoked_executions.clone(),
self.cancel_tokens.clone(),
self.cancel_hook.clone(),
stop_rx,
);
}
fn stop_execution_lease_renewal(&self, run_id: &str) {
if let Some(stop) = self
.lease_renewal_stops
.lock()
.ok()
.and_then(|mut stops| stops.remove(run_id))
{
let _ = stop.send(());
}
}
fn cleanup_run_tracking(&self, invocation_id: &str) {
if let Ok(mut map) = self.pending_lease_ids.lock() {
map.remove(invocation_id);
}
let run_id = invocation_id.split(':').next().unwrap_or(invocation_id);
self.stop_execution_lease_renewal(run_id);
if let Ok(mut map) = self.streaming_runs.lock() {
map.remove(run_id);
}
}
pub async fn emit_checkpoint_sync(
&self,
run_id: String,
event_type: String,
event_data: Vec<u8>,
sequence_number: i64,
metadata: HashMap<String, String>,
source_timestamp_ns: i64,
timeout_ms: u64,
) -> Result<()> {
let authority_run_id = run_id.split(':').next().unwrap_or(&run_id);
if execution_is_revoked(&self.revoked_executions, authority_run_id) {
return Err(SdkError::Internal(format!(
"execution authority was revoked for run_id={authority_run_id}"
)));
}
if event_type == "run.started" {
self.mark_execution_started(&run_id);
}
let is_terminal = event_type == "run.completed" || event_type == "run.failed";
let is_durable_checkpoint = JournalEventMessage::is_checkpoint_event_type(&event_type);
let _journal_flush_guard = if is_durable_checkpoint {
Some(self.journal_flush_locks.lock_run(&run_id).await)
} else {
None
};
if let Some(mut engine) = self.ensure_engine_client().await? {
if is_durable_checkpoint {
let pending = self.journal_queue.drain_run_events(&run_id);
if !pending.is_empty() {
let tenant_id = canonical_project_id_from_metadata(&metadata)
.or_else(|| canonical_project_id_from_metadata(&self.metadata))
.unwrap_or_default();
let transient: Vec<_> = pending
.iter()
.filter(|event| event.is_sse_only)
.map(|event| EventStreamMessage {
run_id: event.run_id.clone(),
event_type: event.event_type.clone(),
data: event.data.clone(),
trace_id: event.correlation_id.clone(),
span_id: event.parent_correlation_id.clone(),
project_id: canonical_project_id_from_metadata(&event.metadata)
.unwrap_or_else(|| tenant_id.clone()),
source_timestamp_ns: event.source_timestamp_ns,
worker_id: self.config.worker_id.clone(),
})
.collect();
let durable_records: Vec<_> = pending
.iter()
.filter(|event| !event.is_sse_only)
.map(|e| {
client::build_engine_record(
tenant_id.clone(),
e.run_id.clone(),
e.event_type.clone(),
e.data.clone(),
e.source_timestamp_ns,
String::new(),
e.correlation_id.clone(),
e.parent_correlation_id.clone(),
e.metadata.clone(),
)
})
.collect();
if let Err(error) = engine.stream_events(transient).await {
for event in pending.into_iter().rev() {
self.journal_queue.push_front(event).ok();
}
self.journal_queue.record_error();
let mut guard = self.engine_client.lock().await;
*guard = None;
return Err(error);
}
if !durable_records.is_empty() {
if let Err(error) = engine.append_batch(durable_records).await {
for event in
pending.into_iter().rev().filter(|event| !event.is_sse_only)
{
self.journal_queue.push_front(event).ok();
}
self.journal_queue.record_error();
let mut guard = self.engine_client.lock().await;
*guard = None;
return Err(error);
}
}
debug!(
"Engine: flushed {} events before {} for run_id={}",
pending.len(),
event_type,
run_id
);
}
}
let mut merged_metadata = metadata;
for (k, v) in &self.metadata {
if !merged_metadata.contains_key(k) {
merged_metadata.insert(k.clone(), v.clone());
}
}
let canonical_project_id =
canonical_project_id_from_metadata(&merged_metadata).unwrap_or_default();
merged_metadata = with_project_metadata(merged_metadata, &canonical_project_id);
let (correlation_id, parent_event_id) = take_correlation_ids(&mut merged_metadata);
let tenant_id = merged_metadata
.remove("project_id")
.or_else(|| merged_metadata.remove("tenant_id"))
.unwrap_or_default();
let experiment_id = merged_metadata.get("experiment_id").cloned();
let record = client::build_engine_record(
tenant_id,
run_id.clone(),
event_type.clone(),
event_data,
source_timestamp_ns,
String::new(), correlation_id,
parent_event_id,
merged_metadata,
);
let start = Instant::now();
let result = match await_checkpoint_ack(
engine.append(record),
timeout_ms,
"Engine.Append",
&run_id,
&event_type,
sequence_number,
)
.await
{
Ok(Ok((_offset, _ts))) => {
debug!(
"Engine checkpoint persisted: run_id={} event_type={} seq={}",
run_id, event_type, sequence_number
);
Ok(())
}
Ok(Err(e)) => {
warn!(
"Engine Append failed: run_id={} event_type={} seq={} error={}",
run_id, event_type, sequence_number, e
);
{
let mut guard = self.engine_client.lock().await;
*guard = None;
}
Err(e)
}
Err(error) => {
warn!("{error}");
Err(error)
}
};
let duration_secs = start.elapsed().as_secs_f64();
crate::telemetry::record_checkpoint(
&event_type,
duration_secs,
result.is_ok(),
experiment_id.as_deref(),
);
if is_terminal && result.is_ok() {
self.cleanup_run_tracking(&run_id);
}
return result;
}
if is_durable_checkpoint {
let pending = self.journal_queue.drain_run_events(&run_id);
if !pending.is_empty() {
let es_tx = self.event_stream_tx.lock().ok().and_then(|g| g.clone());
let dispatch = self.dispatch_tx.lock().ok().and_then(|g| g.clone());
for event in &pending {
if let Some(ref es) = es_tx {
let es_msg = EventStreamMessage {
run_id: event.run_id.clone(),
event_type: event.event_type.clone(),
data: event.data.clone(),
trace_id: String::new(),
span_id: String::new(),
project_id: canonical_project_id_from_metadata(&event.metadata)
.unwrap_or_default(),
source_timestamp_ns: event.source_timestamp_ns,
worker_id: self.config.worker_id.clone(),
};
if let Err(e) = es.send_async(es_msg).await {
warn!(
"Failed to flush pre-checkpoint event via EventStream: type={} run_id={} error={}",
event.event_type, event.run_id, e
);
}
} else if let Some(ref dtx) = dispatch {
let mut meta = event.metadata.clone();
if !event.correlation_id.is_empty() {
meta.insert("cid".to_string(), event.correlation_id.clone());
}
if !event.parent_correlation_id.is_empty() {
meta.insert("pcid".to_string(), event.parent_correlation_id.clone());
}
let stashed_lease_id = if let Ok(map) = self.pending_lease_ids.lock() {
map.get(&event.run_id).cloned().unwrap_or_default()
} else {
String::new()
};
let response = DispatchComponentResponse {
invocation_id: event.run_id.clone(),
success: true,
result: Some(
crate::pb::dispatch_component_response::Result::OutputData(
event.data.clone(),
),
),
error_message: String::new(),
metadata: meta,
event_type: event.event_type.clone(),
content_index: event.content_index,
sequence: event.sequence,
attempt: 0,
source_timestamp_ns: event.source_timestamp_ns,
lease_id: stashed_lease_id,
};
let service_message = ServiceMessage {
worker_id: self.config.worker_id.clone(),
metadata: std::collections::HashMap::new(),
message_type: Some(
crate::pb::service_message::MessageType::FunctionResponse(response),
),
};
if let Err(e) = dtx.send_async(service_message).await {
warn!(
"Failed to flush pre-checkpoint event via dispatch: type={} run_id={} error={}",
event.event_type, event.run_id, e
);
}
}
}
debug!(
"Flushed {} SSE-only events before {}: run_id={}",
pending.len(),
event_type,
run_id
);
}
}
let mut merged_metadata = metadata;
for (k, v) in &self.metadata {
if !merged_metadata.contains_key(k) {
merged_metadata.insert(k.clone(), v.clone());
}
}
let canonical_project_id =
canonical_project_id_from_metadata(&merged_metadata).unwrap_or_default();
merged_metadata = with_project_metadata(merged_metadata, &canonical_project_id);
let (correlation_id, parent_event_id) = take_correlation_ids(&mut merged_metadata);
let experiment_id = merged_metadata.get("experiment_id").cloned();
let request = WriteCheckpointRequest {
run_id: run_id.clone(),
checkpoint_type: event_type.clone(),
checkpoint_data: event_data,
sequence_number,
trace_id: String::new(),
project_id: canonical_project_id,
source_timestamp_ns,
correlation_id,
parent_event_id,
metadata: merged_metadata,
};
let mut ee_client = self.ensure_ee_client().await?;
let start = Instant::now();
let result = match await_checkpoint_ack(
ee_client.write_checkpoint(request),
timeout_ms,
"ExecutionEngine.WriteCheckpoint",
&run_id,
&event_type,
sequence_number,
)
.await
{
Ok(Ok(response)) => {
let resp = response.into_inner();
if resp.success {
debug!(
"Checkpoint persisted: run_id={} event_type={} seq={} journal_seq={}",
run_id, event_type, sequence_number, resp.sequence_number
);
Ok(())
} else {
warn!(
"Checkpoint rejected: run_id={} event_type={} seq={} error={}",
run_id, event_type, sequence_number, resp.error_message
);
Err(crate::error::SdkError::Internal(format!(
"Checkpoint rejected: {}",
resp.error_message
)))
}
}
Ok(Err(e)) => {
warn!(
"WriteCheckpoint RPC failed: run_id={} event_type={} seq={} error={}",
run_id, event_type, sequence_number, e
);
{
let mut guard = self.ee_client.lock().await;
*guard = None;
}
Err(crate::error::SdkError::Connection {
message: format!("WriteCheckpoint failed: {}", e),
code: crate::error::ErrorCode::ConnectionFailed,
source: None,
})
}
Err(error) => {
warn!("{error}");
Err(error)
}
};
let duration_secs = start.elapsed().as_secs_f64();
crate::telemetry::record_checkpoint(
&event_type,
duration_secs,
result.is_ok(),
experiment_id.as_deref(),
);
if is_terminal && result.is_ok() {
self.cleanup_run_tracking(&run_id);
}
result
}
pub fn emit_checkpoint_sync_blocking(
&self,
run_id: String,
event_type: String,
event_data: Vec<u8>,
sequence_number: i64,
metadata: HashMap<String, String>,
source_timestamp_ns: i64,
timeout_ms: u64,
) -> Result<()> {
let worker = self.clone();
if let Ok(handle) = tokio::runtime::Handle::try_current() {
tokio::task::block_in_place(move || {
handle.block_on(async move {
worker
.emit_checkpoint_sync(
run_id,
event_type,
event_data,
sequence_number,
metadata,
source_timestamp_ns,
timeout_ms,
)
.await
})
})
} else {
let handle = {
let guard = self.tokio_handle.lock().map_err(|e| {
crate::error::SdkError::Internal(format!("Failed to lock tokio_handle: {}", e))
})?;
guard
.clone()
.ok_or_else(|| crate::error::SdkError::Connection {
message: "Worker not running, cannot emit checkpoint".to_string(),
code: crate::error::ErrorCode::ConnectionFailed,
source: None,
})?
};
handle.block_on(async move {
worker
.emit_checkpoint_sync(
run_id,
event_type,
event_data,
sequence_number,
metadata,
source_timestamp_ns,
timeout_ms,
)
.await
})
}
}
pub async fn emit_checkpoint_batch(
&self,
events: Vec<(String, String, Vec<u8>, i64, HashMap<String, String>, i64)>,
) -> Result<()> {
if events.is_empty() {
return Ok(());
}
for (run_id, event_type, ..) in &events {
if event_type == "run.started" {
self.mark_execution_started(run_id);
}
}
let mut run_ids: Vec<_> = events.iter().map(|event| event.0.clone()).collect();
run_ids.sort_unstable();
run_ids.dedup();
let _journal_flush_guards = self
.journal_flush_locks
.lock_runs(run_ids.iter().cloned())
.await;
if let Some(mut engine) = self.ensure_engine_client().await? {
for run_id in &run_ids {
let pending = self.journal_queue.drain_run_events(run_id);
if pending.is_empty() {
continue;
}
let tenant_id = pending
.iter()
.find_map(|event| canonical_project_id_from_metadata(&event.metadata))
.or_else(|| canonical_project_id_from_metadata(&self.metadata))
.unwrap_or_default();
let transient: Vec<_> = pending
.iter()
.filter(|event| event.is_sse_only)
.map(|event| EventStreamMessage {
run_id: event.run_id.clone(),
event_type: event.event_type.clone(),
data: event.data.clone(),
trace_id: event.correlation_id.clone(),
span_id: event.parent_correlation_id.clone(),
project_id: canonical_project_id_from_metadata(&event.metadata)
.unwrap_or_else(|| tenant_id.clone()),
source_timestamp_ns: event.source_timestamp_ns,
worker_id: self.config.worker_id.clone(),
})
.collect();
let durable_records: Vec<_> = pending
.iter()
.filter(|event| !event.is_sse_only)
.map(|event| {
client::build_engine_record(
tenant_id.clone(),
event.run_id.clone(),
event.event_type.clone(),
event.data.clone(),
event.source_timestamp_ns,
String::new(),
event.correlation_id.clone(),
event.parent_correlation_id.clone(),
event.metadata.clone(),
)
})
.collect();
if let Err(error) = engine.stream_events(transient).await {
for event in pending.into_iter().rev() {
self.journal_queue.push_front(event).ok();
}
self.journal_queue.record_error();
let mut guard = self.engine_client.lock().await;
*guard = None;
return Err(error);
}
if !durable_records.is_empty() {
if let Err(error) = engine.append_batch(durable_records).await {
for event in pending.into_iter().rev().filter(|event| !event.is_sse_only) {
self.journal_queue.push_front(event).ok();
}
self.journal_queue.record_error();
let mut guard = self.engine_client.lock().await;
*guard = None;
return Err(error);
}
}
}
let originals: Vec<_> = events
.into_iter()
.map(|(run_id, event_type, data, sequence, metadata, ts)| {
let mut merged = metadata;
for (k, v) in &self.metadata {
if !merged.contains_key(k) {
merged.insert(k.clone(), v.clone());
}
}
let (cid, pcid) = take_correlation_ids(&mut merged);
JournalEventMessage {
run_id,
event_type,
data,
correlation_id: cid,
parent_correlation_id: pcid,
tenant_id: None,
source_timestamp_ns: ts,
metadata: merged,
queued_at: Instant::now(),
is_streaming: false,
is_sse_only: false,
content_index: 0,
sequence,
}
})
.collect();
let records: Vec<_> = originals
.iter()
.map(|event| {
let canonical_project_id =
canonical_project_id_from_metadata(&event.metadata).unwrap_or_default();
let mut metadata =
with_project_metadata(event.metadata.clone(), &canonical_project_id);
let tenant_id = metadata
.remove("project_id")
.or_else(|| metadata.remove("tenant_id"))
.unwrap_or_default();
client::build_engine_record(
tenant_id,
event.run_id.clone(),
event.event_type.clone(),
event.data.clone(),
event.source_timestamp_ns,
String::new(),
event.correlation_id.clone(),
event.parent_correlation_id.clone(),
metadata,
)
})
.collect();
let count = originals.len();
match append_records_by_run(&mut engine, &records).await {
Ok(_) => {
debug!("Engine batch checkpoint: {} events persisted", count);
}
Err((e, committed, _written)) => {
warn!(
"Engine batch checkpoint failed for {} non-terminal events; queued for retry: {}",
count, e
);
{
let mut guard = self.engine_client.lock().await;
*guard = None;
}
for event in uncommitted_records_in_reverse(originals, &committed) {
self.journal_queue.push_front(event).ok();
}
self.journal_queue.record_error();
return Err(e);
}
}
return Ok(());
}
warn!("emit_checkpoint_batch requires AGNT5_ENGINE_URL, events will be dropped");
Ok(())
}
pub fn queue_delta(
&self,
invocation_id: String,
event_type: String,
output_data: Vec<u8>,
content_index: i32,
sequence: i64,
metadata: HashMap<String, String>,
source_timestamp_ns: i64,
correlation_id: String,
parent_correlation_id: String,
) -> Result<()> {
let is_sse_only = JournalEventMessage::is_sse_only_event_type(&event_type);
let tenant_id = canonical_project_id_from_metadata(&metadata);
let event = JournalEventMessage {
run_id: invocation_id,
event_type,
data: output_data,
content_index,
sequence,
metadata,
source_timestamp_ns,
correlation_id,
parent_correlation_id,
tenant_id,
is_sse_only,
queued_at: std::time::Instant::now(),
..Default::default()
};
self.queue_event(event)
}
pub fn set_cancel_hook<F>(&self, hook: F)
where
F: Fn(String) + Send + Sync + 'static,
{
if let Ok(mut guard) = self.cancel_hook.lock() {
*guard = Some(Box::new(hook));
}
}
pub async fn run<F, Fut>(&self, message_handler: F) -> Result<()>
where
F: Fn(RuntimeMessage, flume::Sender<ServiceMessage>) -> Fut + Send + Clone + 'static,
Fut: std::future::Future<Output = Result<Option<ServiceMessage>>> + Send + 'static,
{
info!("Starting worker {}", self.config.worker_id);
{
if let Ok(mut guard) = self.tokio_handle.lock() {
*guard = Some(tokio::runtime::Handle::current());
}
}
if let Err(e) = crate::telemetry::init_telemetry(
&self.config.service_name,
&self.config.service_version,
) {
warn!("Failed to initialize telemetry: {}", e);
}
let (shutdown_tx, _) = tokio::sync::broadcast::channel::<()>(1);
let mut shutdown_rx = shutdown_tx.subscribe();
let shutdown_tx = Arc::new(shutdown_tx);
let shutdown_tx_clone = shutdown_tx.clone();
tokio::spawn(async move {
let _ = tokio::signal::ctrl_c().await;
info!("Received shutdown signal (Ctrl+C)");
let _ = shutdown_tx_clone.send(()); });
let max_retries = self.config.max_retries;
let infinite_retry = max_retries == 0;
let base_delay = std::time::Duration::from_secs(1);
let mut retry_count: u32 = 0;
let mut disconnect_instant: Option<Instant> = None;
loop {
if let Ok(()) = shutdown_rx.try_recv() {
info!(
"Worker {} shutting down due to signal",
self.config.worker_id
);
return Ok(());
}
if retry_count > 0 {
let exp_delay = base_delay * 2_u32.pow((retry_count - 1).min(5));
let jitter = rand::random::<f64>() * 0.5 - 0.25;
let jitter_ms = (exp_delay.as_millis() as f64 * jitter) as u64;
let delay = exp_delay + std::time::Duration::from_millis(jitter_ms);
let delay_secs = delay.as_secs_f64();
const QUIET_RETRY_THRESHOLD: u32 = 3;
if retry_count >= QUIET_RETRY_THRESHOLD {
if infinite_retry {
eprintln!(
"[WARN] Reconnecting in {:.1}s... (attempt {})",
delay_secs, retry_count
);
} else {
eprintln!(
"[WARN] Reconnecting in {:.1}s... (attempt {}/{})",
delay_secs, retry_count, max_retries
);
}
} else {
debug!(
retry = retry_count,
delay_secs, "Reconnecting silently before user-visible warning"
);
}
tokio::select! {
_ = tokio::time::sleep(delay) => {},
_ = shutdown_rx.recv() => {
info!("Worker {} shutting down during reconnect delay", self.config.worker_id);
return Ok(());
}
}
}
self.set_connection_state(ConnectionState::Connecting);
crate::telemetry::update_connection_state(1); if disconnect_instant.is_none() && retry_count > 0 {
disconnect_instant = Some(Instant::now());
}
let was_reconnecting = retry_count > 0;
let shutdown_rx_inner = shutdown_tx.subscribe();
match self
.try_connect_and_run(
message_handler.clone(),
shutdown_rx_inner,
was_reconnecting,
disconnect_instant,
)
.await
{
Ok(()) => {
self.set_connection_state(ConnectionState::Disconnected);
crate::telemetry::update_connection_state(0); return Ok(());
}
Err(e) => {
let was_connected =
matches!(self.connection_state(), ConnectionState::Connected);
if retry_count > 0 && !was_connected {
crate::telemetry::record_reconnection_attempt(false);
}
let error_msg =
format!("Connection failed (attempt {}): {}", retry_count + 1, e);
if retry_count == 0 && !was_connected {
eprintln!("[ERROR] Connection failed: {}", e);
}
debug!("{}", error_msg);
self.set_connection_state(ConnectionState::Error(error_msg));
crate::telemetry::update_connection_state(0);
if was_connected {
retry_count = 1;
disconnect_instant = Some(Instant::now());
} else {
retry_count += 1;
}
if !infinite_retry && retry_count >= max_retries {
error!("Failed to connect after {} attempts, exiting", max_retries);
self.set_connection_state(ConnectionState::Error(format!(
"Failed to connect after {} attempts",
max_retries
)));
return Err(anyhow::anyhow!(
"Worker failed to connect to coordinator after {} attempts",
max_retries
)
.into());
}
}
}
}
}
async fn try_connect_and_run<F, Fut>(
&self,
message_handler: F,
mut shutdown_rx: tokio::sync::broadcast::Receiver<()>,
is_reconnect: bool,
disconnect_instant: Option<Instant>,
) -> Result<()>
where
F: Fn(RuntimeMessage, flume::Sender<ServiceMessage>) -> Fut + Send + Clone + 'static,
Fut: std::future::Future<Output = Result<Option<ServiceMessage>>> + Send + 'static,
{
let coordinator_endpoint = self.config.resolved_coordinator_endpoint();
if is_reconnect {
eprintln!(
"[INFO] Reconnecting to coordinator ({})...",
coordinator_endpoint
);
} else {
eprintln!(
"[INFO] Connecting to coordinator ({})...",
coordinator_endpoint
);
}
let mut client = WorkerCoordinatorClient::connect(coordinator_endpoint.clone()).await?;
let mut metadata = self.metadata.clone();
metadata.extend(collect_agnt5_env_vars());
if let Some(artifact) = configured_activation_artifact_sha256(&metadata) {
metadata.insert("activation_artifact_sha256".to_string(), artifact);
}
let is_pull_mode = matches!(
std::env::var("AGNT5_WORKER_MODE").ok().as_deref(),
Some("pull") | Some("PULL")
);
let mode = if is_pull_mode {
crate::pb::WorkerMode::Pull as i32
} else {
crate::pb::WorkerMode::Push as i32
};
let deployment_id = std::env::var("AGNT5_DEPLOYMENT_ID").unwrap_or_default();
let max_concurrency: u32 = self.config.max_concurrency.unwrap_or(100);
let capabilities = worker_capabilities(&self.components);
let (supported_protocol_capabilities, required_protocol_capabilities) =
worker_protocol_capabilities_for_metadata(&metadata)?;
let dispatch_worker_metadata = metadata.clone();
let dispatch_activation_definition_configs =
activation_definition_configs(&self.components);
let registration = RegisterService {
service_name: self.config.service_name.clone(),
service_version: self.config.service_version.clone(),
service_type: self.config.service_type.clone(),
components: self.components.clone(),
metadata,
mode,
deployment_id,
max_concurrency,
capabilities,
supported_protocol_capabilities: supported_protocol_capabilities.clone(),
required_protocol_capabilities: required_protocol_capabilities.clone(),
};
let (tx, rx, _runtime_msg_tx_hold) = if is_pull_mode {
let (tx, _outgoing_rx) = flume::bounded::<ServiceMessage>(1000);
let (runtime_msg_tx, runtime_msg_rx) = flume::bounded::<RuntimeMessage>(1000);
(tx, runtime_msg_rx, Some(runtime_msg_tx))
} else {
let (tx, rx) = client
.create_worker_stream_with_registration(self.config.worker_id.clone(), registration)
.await?;
(tx, rx, None)
};
if is_reconnect {
if is_pull_mode {
eprintln!(
"[INFO] Reconnected to coordinator ({}) for parked polling",
coordinator_endpoint
);
} else {
eprintln!(
"[INFO] Reconnected to coordinator ({})",
coordinator_endpoint
);
}
} else if is_pull_mode {
eprintln!(
"[INFO] Connected to coordinator ({}) for parked polling",
coordinator_endpoint
);
} else {
eprintln!("[INFO] Connected to coordinator ({})", coordinator_endpoint);
}
if is_pull_mode {
debug!(
"Worker {} connected for parked polling",
self.config.worker_id
);
} else {
debug!("Worker {} registered successfully", self.config.worker_id);
}
self.set_connection_state(ConnectionState::Connected);
crate::telemetry::update_connection_state(2);
self.write_health_marker();
if is_reconnect {
crate::telemetry::record_reconnection_attempt(true);
if let Some(disc_instant) = disconnect_instant {
crate::telemetry::record_reconnection_duration(
disc_instant.elapsed().as_secs_f64(),
);
}
}
let event_stream_tx = match self.ensure_ee_client().await {
Ok(mut ee_client) => {
match crate::client::create_ee_event_stream(
&mut ee_client,
self.config.worker_id.clone(),
)
.await
{
Ok(es_tx) => {
debug!("EE EventStream opened for SSE-only events");
Some(es_tx)
}
Err(e) => {
warn!(
"Failed to open EE EventStream, SSE-only events will use dispatch stream: {}",
e
);
None
}
}
}
Err(e) => {
warn!(
"Failed to get EE client for EventStream, SSE-only events will use dispatch stream: {}",
e
);
None
}
};
if let Ok(mut guard) = self.event_stream_tx.lock() {
*guard = event_stream_tx.clone();
}
if let Ok(mut guard) = self.dispatch_tx.lock() {
*guard = Some(tx.clone());
}
let in_flight = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let heartbeat_task = if is_pull_mode {
None
} else {
Some(self.spawn_heartbeat_task(tx.clone(), in_flight.clone()))
};
let journal_flush_task = self.spawn_journal_flush_task(tx.clone(), event_stream_tx.clone());
let max_concurrency = max_concurrency as usize;
debug!(
"Worker {} starting with concurrency limit: {}",
self.config.worker_id, max_concurrency
);
let (task_tx, task_rx) = flume::bounded::<RuntimeMessage>(max_concurrency * 2);
let (response_tx, response_rx) = flume::unbounded::<ServiceMessage>();
let mut worker_handles = Vec::new();
for worker_id in 0..max_concurrency {
let task_rx = task_rx.clone();
let response_tx = response_tx.clone();
let handler = message_handler.clone();
let worker_name = format!("{}-{}", self.config.worker_id, worker_id);
let response_worker_id = self.config.worker_id.clone();
let in_flight = in_flight.clone();
let cancel_tokens = self.cancel_tokens.clone();
let revoked_executions = self.revoked_executions.clone();
let handle = tokio::spawn(async move {
while let Ok(runtime_message) = task_rx.recv_async().await {
execute_runtime_message(
&worker_name,
&response_worker_id,
runtime_message,
response_tx.clone(),
handler.clone(),
in_flight.clone(),
cancel_tokens.clone(),
revoked_executions.clone(),
)
.await;
}
});
worker_handles.push(handle);
}
let poll_task = if is_pull_mode {
let poll_shutdown = shutdown_rx.resubscribe();
Some(self.spawn_parked_poll_task(
response_tx.clone(),
message_handler.clone(),
poll_shutdown,
max_concurrency,
in_flight.clone(),
supported_protocol_capabilities.clone(),
required_protocol_capabilities.clone(),
))
} else {
None
};
let dispatch_result = loop {
tokio::select! {
result = rx.recv_async() => {
match result {
Ok(mut runtime_message) => {
if runtime_message.message_type == RuntimeMessageType::CheckpointAck as i32 {
debug!("Ignoring legacy CheckpointAck on dispatch stream");
continue;
}
if runtime_message.message_type == RuntimeMessageType::WorkerReplaced as i32 {
warn!(
"Worker {} received WORKER_REPLACED — another instance took over. Shutting down.",
self.config.worker_id
);
eprintln!(
"[WARN] Another worker instance connected with the same worker ID. This worker is shutting down."
);
break Ok(());
}
if runtime_message.message_type == RuntimeMessageType::CoordinatorDraining as i32 {
warn!(
"Worker {} received COORDINATOR_DRAINING — draining local work before reconnect.",
self.config.worker_id
);
eprintln!(
"[INFO] Coordinator is draining. Worker will reconnect after in-flight work completes."
);
break Err(crate::error::SdkError::Connection {
message: "coordinator draining".to_string(),
code: crate::error::ErrorCode::ConnectionFailed,
source: None,
});
}
if client.negotiated_protocol_capability(
crate::client::DURABLE_ACTIVATION_V1_CAPABILITY,
) {
if let Err(error) = stamp_activation_dispatch_metadata(
&mut runtime_message,
&self.config.worker_id,
&self.config.worker_id,
&self.config.service_version,
&dispatch_worker_metadata,
&dispatch_activation_definition_configs,
) {
break Err(error);
}
}
if client.negotiated_protocol_capability(
crate::client::DURABLE_SUSPENSION_V1_CAPABILITY,
) {
stamp_protocol_capability(
&mut runtime_message,
crate::client::DURABLE_SUSPENSION_V1_CAPABILITY,
);
}
stamp_execution_authority_metadata(
&mut runtime_message,
&self.config.worker_id,
&self.config.worker_id,
"push",
);
if runtime_message.message_type == RuntimeMessageType::CancelExecution as i32 {
if let Some(crate::pb::runtime_message::MessageData::CancelExecution(ref req)) =
runtime_message.message_data
{
let run_key = req
.invocation_id
.split(':')
.next()
.unwrap_or(&req.invocation_id)
.to_string();
let hooked = self
.cancel_hook
.lock()
.ok()
.and_then(|g| g.as_ref().map(|h| h(run_key.clone())))
.is_some();
if hooked {
info!(
"Worker {} cancel hook invoked for {}",
self.config.worker_id, run_key
);
} else {
let fired = self
.cancel_tokens
.lock()
.ok()
.and_then(|mut m| m.remove(&run_key))
.map(|tx| {
let _ = tx.send(());
})
.is_some();
if fired {
info!(
"Worker {} soft-cancelling invocation {}",
self.config.worker_id, run_key
);
} else {
debug!(
"Worker {} CancelExecution for {} — no in-flight invocation",
self.config.worker_id, run_key
);
}
}
self.cleanup_run_tracking(&req.invocation_id);
}
continue;
}
if let Some(ref msg_data) = runtime_message.message_data {
if let crate::pb::runtime_message::MessageData::DispatchComponent(ref req) = msg_data {
self.start_push_lease_renewal(client.clone(), req);
if req.is_streaming {
let run_id = if let Some(idx) = req.invocation_id.find(':') {
req.invocation_id[..idx].to_string()
} else {
req.invocation_id.clone()
};
if let Ok(mut map) = self.streaming_runs.lock() {
map.insert(run_id, true);
}
}
if !req.lease_id.is_empty() {
if let Ok(mut map) = self.pending_lease_ids.lock() {
map.insert(req.invocation_id.clone(), req.lease_id.clone());
}
}
}
}
if let Some(ref msg_data) = runtime_message.message_data {
if let crate::pb::runtime_message::MessageData::DispatchComponent(ref req) = msg_data {
if req.component_type == 10 {
if let Some(result) = crate::eval::builtin_scorer::execute(&req.component_name, &req.input_data) {
let output_data = serde_json::to_vec(&result).unwrap_or_default();
let lease_id = if !req.lease_id.is_empty() {
if let Ok(mut map) = self.pending_lease_ids.lock() {
map.remove(&req.invocation_id);
}
req.lease_id.clone()
} else {
String::new()
};
let response = DispatchComponentResponse {
invocation_id: req.invocation_id.clone(),
success: true,
result: Some(
crate::pb::dispatch_component_response::Result::OutputData(output_data.clone()),
),
error_message: String::new(),
metadata: req.metadata.clone(),
event_type: "run.completed".to_string(),
content_index: 0,
sequence: 0,
attempt: 0,
source_timestamp_ns: 0,
lease_id,
};
let service_message = ServiceMessage {
worker_id: self.config.worker_id.clone(),
metadata: std::collections::HashMap::new(),
message_type: Some(
crate::pb::service_message::MessageType::FunctionResponse(response),
),
};
let run_id = if let Some(idx) = req.invocation_id.find(':') {
req.invocation_id[..idx].to_string()
} else {
req.invocation_id.clone()
};
let timestamp_ns = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as i64;
let checkpoint_worker = self.clone();
let response_tx = response_tx.clone();
let input_data = req.input_data.clone();
let metadata = req.metadata.clone();
tokio::spawn(async move {
if let Err(e) = checkpoint_worker.emit_checkpoint_sync(
run_id.clone(),
"run.started".to_string(),
input_data,
0,
metadata.clone(),
timestamp_ns,
5000,
).await {
warn!("Built-in scorer: failed to emit run.started checkpoint: {}", e);
}
if let Err(e) = checkpoint_worker.emit_checkpoint_sync(
run_id,
"run.completed".to_string(),
output_data,
1,
metadata,
timestamp_ns,
5000,
).await {
warn!("Built-in scorer: failed to emit run.completed checkpoint: {}", e);
}
if let Err(e) = response_tx.send_async(service_message).await {
error!("Failed to send built-in scorer response: {}", e);
}
});
continue;
}
}
}
}
if let Err(e) = task_tx.send_async(runtime_message).await {
error!("Failed to dispatch message to worker pool: {}", e);
break Err(crate::error::SdkError::Connection {
message: format!("Task dispatch failed: {}", e),
code: crate::error::ErrorCode::ConnectionFailed,
source: None,
});
}
}
Err(e) => {
debug!("Channel closed for worker {}, will reconnect: {}", self.config.worker_id, e);
break Err(crate::error::SdkError::Connection {
message: format!("Receive failed: {}", e),
code: crate::error::ErrorCode::ConnectionFailed,
source: None,
});
}
}
}
response = response_rx.recv_async() => {
match response {
Ok(service_message) => {
if let Err(e) = self.forward_worker_response(service_message, is_pull_mode, &tx).await {
error!("Failed to send response to coordinator: {}", e);
break Err(e);
}
}
Err(e) => {
error!("Response channel error: {}", e);
break Err(crate::error::SdkError::Connection {
message: format!("Response receive failed: {}", e),
code: crate::error::ErrorCode::ConnectionFailed,
source: None,
});
}
}
}
_ = shutdown_rx.recv() => {
info!("Worker {} received shutdown signal, stopping gracefully", self.config.worker_id);
break Ok(());
}
}
};
if let Some(task) = poll_task {
task.abort();
}
info!("Worker {} shutting down task pool", self.config.worker_id);
drop(task_tx); drop(task_rx); drop(response_tx);
{
let mut guard = self.ee_client.lock().await;
*guard = None;
}
for handle in worker_handles {
let _ = handle.await;
}
while let Ok(service_message) = response_rx.try_recv() {
if let Err(e) = self
.forward_worker_response(service_message, is_pull_mode, &tx)
.await
{
warn!("Failed to flush drained worker response: {}", e);
break;
}
}
self.remove_health_marker();
let _ = self.send_shutdown_message(&tx).await;
if let Some(task) = heartbeat_task {
task.abort();
}
journal_flush_task.abort();
if let Ok(mut map) = self.streaming_runs.lock() {
map.clear();
}
if let Ok(mut map) = self.pending_lease_ids.lock() {
map.clear();
}
if let Ok(mut stops) = self.lease_renewal_stops.lock() {
for (_, stop) in stops.drain() {
let _ = stop.send(());
}
}
if let Ok(mut revoked) = self.revoked_executions.lock() {
revoked.clear();
}
dispatch_result
}
async fn forward_worker_response(
&self,
mut service_message: ServiceMessage,
is_pull_mode: bool,
tx: &flume::Sender<ServiceMessage>,
) -> Result<()> {
if let Some(crate::pb::service_message::MessageType::FunctionResponse(ref mut resp)) =
service_message.message_type
{
let run_id = resp
.invocation_id
.split(':')
.next()
.unwrap_or(&resp.invocation_id)
.to_string();
if execution_is_revoked(&self.revoked_executions, &run_id) {
warn!(
"Suppressing worker response after lease authority loss: run_id={}",
run_id
);
self.cleanup_run_tracking(&resp.invocation_id);
return Ok(());
}
let is_terminal = is_terminal_worker_response(&resp.event_type);
if resp.lease_id.is_empty() {
if let Ok(mut map) = self.pending_lease_ids.lock() {
if is_terminal {
if let Some(lease_id) = map.remove(&resp.invocation_id) {
resp.lease_id = lease_id;
}
} else if let Some(lease_id) = map.get(&resp.invocation_id) {
resp.lease_id = lease_id.clone();
}
}
}
if is_terminal {
self.cleanup_run_tracking(&resp.invocation_id);
}
}
if is_pull_mode {
self.handle_polled_job_response(service_message).await;
} else {
tx.send_async(service_message).await.map_err(|e| {
crate::error::SdkError::Connection {
message: format!("Send failed: {}", e),
code: crate::error::ErrorCode::ConnectionFailed,
source: None,
}
})?;
}
Ok(())
}
fn spawn_heartbeat_task(
&self,
tx: flume::Sender<ServiceMessage>,
in_flight: std::sync::Arc<std::sync::atomic::AtomicUsize>,
) -> tokio::task::JoinHandle<()> {
let worker_id = self.config.worker_id.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(30));
loop {
interval.tick().await;
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
let health_check = HealthCheck {
timestamp,
status: WorkerHealthStatus::WorkerHealthHealthy.into(),
metrics: std::collections::HashMap::new(),
message: "Worker healthy".to_string(),
in_flight: Some(in_flight.load(std::sync::atomic::Ordering::Relaxed) as u32),
};
let service_message = ServiceMessage {
worker_id: worker_id.clone(),
metadata: std::collections::HashMap::new(),
message_type: Some(crate::pb::service_message::MessageType::HealthCheck(
health_check,
)),
};
if tx.send_async(service_message).await.is_err() {
break;
}
}
})
}
fn spawn_journal_flush_task(
&self,
dispatch_tx: flume::Sender<ServiceMessage>,
event_stream_tx: Option<flume::Sender<EventStreamMessage>>,
) -> tokio::task::JoinHandle<()> {
let worker_id_outer = self.config.worker_id.clone();
let journal_queue_outer = self.journal_queue.clone();
let streaming_runs_outer = self.streaming_runs.clone();
let pending_lease_ids_outer = self.pending_lease_ids.clone();
let revoked_executions_outer = self.revoked_executions.clone();
let journal_flush_locks_outer = self.journal_flush_locks.clone();
let ee_endpoint_outer = self.config.ee_endpoint.clone();
let engine_endpoint_outer = self.config.engine_endpoint.clone();
tokio::spawn(async move {
let mut backoff = std::time::Duration::from_millis(100);
const MAX_BACKOFF: std::time::Duration = std::time::Duration::from_secs(30);
loop {
let worker_id = worker_id_outer.clone();
let journal_queue = journal_queue_outer.clone();
let flush_interval_ms = journal_queue.flush_interval_ms();
let batch_size = journal_queue.batch_size().max(1);
let max_batches_per_tick = std::env::var("AGNT5_JOURNAL_MAX_BATCHES_PER_TICK")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(32)
.max(1);
let streaming_runs = streaming_runs_outer.clone();
let pending_lease_ids = pending_lease_ids_outer.clone();
let revoked_executions = revoked_executions_outer.clone();
let journal_flush_locks = journal_flush_locks_outer.clone();
let ee_endpoint = ee_endpoint_outer.clone();
let engine_endpoint = engine_endpoint_outer.clone();
let dispatch_tx = dispatch_tx.clone();
let event_stream_tx = event_stream_tx.clone();
let cached_project_id = canonical_project_id_from_env();
let cached_deployment_id = std::env::var("AGNT5_DEPLOYMENT_ID").unwrap_or_default();
let inner = tokio::spawn(async move {
let mut interval =
tokio::time::interval(Duration::from_millis(flush_interval_ms));
let mut ee_client: Option<ExecutionEngineServiceClient<Channel>> = None;
let mut engine: Option<EngineClient> = None;
loop {
interval.tick().await;
let queued = journal_queue.len();
let drain_limit = if queued > batch_size {
queued.min(batch_size.saturating_mul(max_batches_per_tick))
} else {
batch_size
};
let run_ids = journal_queue.peek_batch_run_ids(drain_limit);
let _flush_guards = journal_flush_locks.lock_runs(run_ids.clone()).await;
let protected_runs: HashSet<_> = run_ids.into_iter().collect();
let batch: Vec<_> = journal_queue
.drain_batch_for_runs(drain_limit, &protected_runs)
.into_iter()
.filter(|event| {
let run_id =
event.run_id.split(':').next().unwrap_or(&event.run_id);
!execution_is_revoked(&revoked_executions, run_id)
})
.collect();
if batch.is_empty() {
continue;
}
if let Some(ref ep) = engine_endpoint {
if engine.is_none() {
match EngineClient::connect(ep).await {
Ok(c) => {
debug!("Flush task: Engine client connected to {}", ep);
engine = Some(c);
}
Err(e) => {
warn!(
"Flush task: failed to connect to Engine {}: {}",
ep, e
);
for event in batch.into_iter().rev() {
journal_queue.push_front(event).ok();
}
journal_queue.record_error();
continue;
}
}
}
let mut transient_events = Vec::new();
let mut durable_originals = Vec::new();
for event in batch {
if event.is_sse_only {
let is_run_streaming = match streaming_runs.lock() {
Ok(map) => map.get(&event.run_id).copied().unwrap_or(false),
Err(poisoned) => poisoned
.into_inner()
.get(&event.run_id)
.copied()
.unwrap_or(false),
};
if is_run_streaming {
transient_events.push(EventStreamMessage {
run_id: event.run_id.clone(),
event_type: event.event_type.clone(),
data: event.data.clone(),
trace_id: event.correlation_id.clone(),
span_id: event.parent_correlation_id.clone(),
project_id: canonical_project_id_from_metadata(
&event.metadata,
)
.or_else(|| event.tenant_id.clone())
.unwrap_or_else(|| cached_project_id.clone()),
source_timestamp_ns: event.source_timestamp_ns,
worker_id: worker_id.clone(),
});
}
} else {
durable_originals.push(event);
}
}
let records: Vec<_> = durable_originals
.iter()
.map(|e| {
let tenant = if let Some(ref tid) = e.tenant_id {
tid.clone()
} else {
cached_project_id.clone()
};
client::build_engine_record(
tenant,
e.run_id.clone(),
e.event_type.clone(),
e.data.clone(),
e.source_timestamp_ns,
String::new(),
e.correlation_id.clone(),
e.parent_correlation_id.clone(),
e.metadata.clone(),
)
})
.collect();
if let Some(ref mut eng) = engine {
let streamed = match eng.stream_events(transient_events).await {
Ok(count) => count as usize,
Err(e) => {
warn!("Flush task: Engine EventStream failed: {}", e);
journal_queue.record_error();
0
}
};
if records.is_empty() {
if streamed > 0 {
journal_queue.record_sent_batch(streamed, streamed);
}
continue;
}
match append_records_by_run(eng, &records).await {
Ok(written) => {
journal_queue.record_sent_batch(
written as usize + streamed,
streamed,
);
debug!(
"Flush task: published {} transient and wrote {} durable events to Engine (queue_size={})",
streamed, written,
journal_queue.len()
);
}
Err((e, committed, written)) => {
warn!("Flush task: Engine AppendBatch failed: {}", e);
engine = None; for event in uncommitted_records_in_reverse(
durable_originals,
&committed,
) {
journal_queue.push_front(event).ok();
}
if written > 0 || streamed > 0 {
journal_queue.record_sent_batch(
written as usize + streamed,
streamed,
);
}
journal_queue.record_error();
}
}
}
continue; }
let mut sent_count = 0;
let mut sse_only_count = 0;
let mut boundary_events: Vec<(usize, crate::pb::WriteJournalEventRequest)> =
Vec::new();
let mut boundary_originals: Vec<JournalEventMessage> = Vec::new();
for event in batch {
let is_sse_only = event.is_sse_only;
if is_sse_only {
let is_run_streaming = match streaming_runs.lock() {
Ok(map) => map.get(&event.run_id).copied().unwrap_or(false),
Err(poisoned) => {
warn!(
"streaming_runs mutex poisoned, assuming non-streaming for run_id={}",
event.run_id
);
poisoned
.into_inner()
.get(&event.run_id)
.copied()
.unwrap_or(false)
}
};
if !is_run_streaming {
continue; }
if let Some(ref es_tx) = event_stream_tx {
let es_msg = EventStreamMessage {
run_id: event.run_id.clone(),
event_type: event.event_type.clone(),
data: event.data.clone(),
trace_id: String::new(),
span_id: String::new(),
project_id: cached_project_id.clone(),
source_timestamp_ns: event.source_timestamp_ns,
worker_id: worker_id.clone(),
};
if let Err(e) = es_tx.send_async(es_msg).await {
warn!(
"EventStream send failed, falling back to dispatch stream: type={} run_id={} error={}",
event.event_type, event.run_id, e
);
} else {
sse_only_count += 1;
sent_count += 1;
continue; }
}
let mut metadata = event.metadata.clone();
metadata = with_project_metadata(metadata, &cached_project_id);
if !cached_deployment_id.is_empty() {
metadata.insert(
"deployment_id".to_string(),
cached_deployment_id.clone(),
);
}
let stashed_lease_id = match pending_lease_ids.lock() {
Ok(map) => map.get(&event.run_id).cloned().unwrap_or_default(),
Err(poisoned) => poisoned
.into_inner()
.get(&event.run_id)
.cloned()
.unwrap_or_default(),
};
let response = DispatchComponentResponse {
invocation_id: event.run_id.clone(),
success: true,
result: Some(
crate::pb::dispatch_component_response::Result::OutputData(
event.data.clone(),
),
),
error_message: String::new(),
metadata,
event_type: event.event_type.clone(),
content_index: event.content_index,
sequence: event.sequence,
attempt: 0,
source_timestamp_ns: event.source_timestamp_ns,
lease_id: stashed_lease_id,
};
let service_message = ServiceMessage {
worker_id: worker_id.clone(),
metadata: std::collections::HashMap::new(),
message_type: Some(
crate::pb::service_message::MessageType::FunctionResponse(
response,
),
),
};
if let Err(e) = dispatch_tx.send_async(service_message).await {
warn!(
"Failed to send SSE-only event via dispatch fallback: type={} run_id={} error={}",
event.event_type, event.run_id, e
);
journal_queue.push_front(event).ok();
journal_queue.record_error();
break;
}
sse_only_count += 1;
sent_count += 1;
continue;
}
let mut metadata = event.metadata.clone();
metadata = with_project_metadata(metadata, &cached_project_id);
if !cached_deployment_id.is_empty() {
metadata
.entry("deployment_id".to_string())
.or_insert_with(|| cached_deployment_id.clone());
}
let tenant_id = metadata
.remove("project_id")
.or_else(|| metadata.remove("tenant_id"))
.unwrap_or_default();
let req = crate::pb::WriteJournalEventRequest {
run_id: event.run_id.clone(),
event_type: event.event_type.clone(),
data: event.data.clone(),
trace_id: String::new(),
span_id: String::new(),
project_id: tenant_id,
source_timestamp_ns: event.source_timestamp_ns,
correlation_id: event.correlation_id.clone(),
parent_event_id: event.parent_correlation_id.clone(),
metadata,
};
boundary_events.push((boundary_originals.len(), req));
boundary_originals.push(event);
}
if !boundary_events.is_empty() {
let requests: Vec<crate::pb::WriteJournalEventRequest> =
boundary_events.into_iter().map(|(_, req)| req).collect();
let batch_count = requests.len();
if ee_client.is_none() {
match Channel::from_shared(ee_endpoint.clone()) {
Ok(ch) => {
match ch
.connect_timeout(Duration::from_secs(10))
.timeout(Duration::from_secs(30))
.connect()
.await
{
Ok(channel) => {
debug!(
"Flush task: EE client connected to {}",
ee_endpoint
);
ee_client = Some(
ExecutionEngineServiceClient::new(channel),
);
}
Err(e) => {
warn!(
"Flush task: failed to connect to EE {}: {}",
ee_endpoint, e
);
for event in boundary_originals.into_iter().rev() {
journal_queue.push_front(event).ok();
}
journal_queue.record_error();
if sent_count > 0 {
journal_queue.record_sent_batch(
sent_count,
sse_only_count,
);
}
continue;
}
}
}
Err(e) => {
error!(
"Flush task: invalid EE endpoint {}: {}",
ee_endpoint, e
);
for event in boundary_originals.into_iter().rev() {
journal_queue.push_front(event).ok();
}
journal_queue.record_error();
if sent_count > 0 {
journal_queue
.record_sent_batch(sent_count, sse_only_count);
}
continue;
}
}
}
if let Some(ref mut client) = ee_client {
let batch_req =
crate::pb::WriteJournalEventsBatchRequest { events: requests };
match client.write_journal_events_batch(batch_req).await {
Ok(resp) => {
let r = resp.into_inner();
sent_count += r.written_count as usize;
if !r.errors.is_empty() {
warn!(
"Flush task: {} boundary events had errors (written={})",
r.errors.len(),
r.written_count
);
for err in &r.errors {
warn!(
" event[{}]: {}",
err.index, err.error_message
);
}
} else {
debug!(
"Flush task: wrote {} boundary events to EE",
batch_count
);
}
}
Err(e) => {
warn!("Flush task: WriteJournalEventsBatch failed: {}", e);
ee_client = None;
for event in boundary_originals.into_iter().rev() {
journal_queue.push_front(event).ok();
}
journal_queue.record_error();
}
}
}
}
if sent_count > 0 {
journal_queue.record_sent_batch(sent_count, sse_only_count);
debug!(
"Flushed {} journal events (boundary={}, sse_only={}, queue_size={})",
sent_count,
sent_count - sse_only_count,
sse_only_count,
journal_queue.len()
);
}
}
});
match inner.await {
Ok(()) => {
debug!(
worker_id = %worker_id_outer,
"Journal flush task exited cleanly; supervisor shutting down"
);
return;
}
Err(e) if e.is_panic() => {
error!(
worker_id = %worker_id_outer,
error = ?e,
backoff_ms = backoff.as_millis() as u64,
"Journal flush task panicked (likely h2 transport); restarting after backoff"
);
tokio::time::sleep(backoff).await;
backoff = (backoff * 2).min(MAX_BACKOFF);
continue;
}
Err(_cancelled) => {
debug!(
worker_id = %worker_id_outer,
"Journal flush task cancelled; supervisor shutting down"
);
return;
}
}
}
})
}
fn spawn_parked_poll_task<F, Fut>(
&self,
response_tx: flume::Sender<ServiceMessage>,
message_handler: F,
mut shutdown_rx: tokio::sync::broadcast::Receiver<()>,
max_concurrency: usize,
in_flight: Arc<std::sync::atomic::AtomicUsize>,
supported_protocol_capabilities: Vec<String>,
required_protocol_capabilities: Vec<String>,
) -> tokio::task::JoinHandle<()>
where
F: Fn(RuntimeMessage, flume::Sender<ServiceMessage>) -> Fut + Send + Clone + 'static,
Fut: std::future::Future<Output = Result<Option<ServiceMessage>>> + Send + 'static,
{
let worker_id = self.config.worker_id.clone();
let endpoint = self.config.resolved_coordinator_endpoint();
let project_id = canonical_project_id_from_env();
let deployment_id = std::env::var("AGNT5_DEPLOYMENT_ID").unwrap_or_default();
let capabilities = worker_capabilities(&self.components);
let components = self.components.clone();
let activation_definition_configs = activation_definition_configs(&components);
let mut worker_metadata = self.metadata.clone();
if let Some(artifact) = configured_activation_artifact_sha256(&worker_metadata) {
worker_metadata.insert("activation_artifact_sha256".to_string(), artifact);
}
let service_name = self.config.service_name.clone();
let service_version = self.config.service_version.clone();
let service_type = self.config.service_type.clone();
let streaming_runs = self.streaming_runs.clone();
let pending_lease_ids = self.pending_lease_ids.clone();
let journal_queue = self.journal_queue.clone();
let journal_flush_locks = self.journal_flush_locks.clone();
let slot_phases = self.slot_phases.clone();
let cancel_tokens = self.cancel_tokens.clone();
let cancel_hook = self.cancel_hook.clone();
let revoked_executions = self.revoked_executions.clone();
let configured_max_slots = env_usize("AGNT5_MAX_SLOTS").unwrap_or(max_concurrency);
let max_slots = configured_max_slots
.clamp(1, max_concurrency.max(1))
.min(100);
let configured_min_slots = env_usize("AGNT5_MIN_SLOTS").unwrap_or(1);
let min_slots = configured_min_slots.clamp(1, max_slots);
let claim_timeout_ms = std::env::var("AGNT5_CLAIM_TIMEOUT_MS")
.ok()
.and_then(|s| s.parse::<i64>().ok())
.filter(|v| *v > 0)
.unwrap_or(300_000);
let retire_empty_polls = env_usize("AGNT5_SLOT_RETIRE_EMPTY_POLLS")
.filter(|v| *v > 0)
.unwrap_or(2);
tokio::spawn(async move {
if project_id.is_empty() {
eprintln!("[INFO] Parked polling disabled (AGNT5_PROJECT_ID not set)");
return;
}
if deployment_id.is_empty() {
eprintln!("[INFO] Parked polling disabled (AGNT5_DEPLOYMENT_ID not set)");
return;
}
let mut client = match WorkerCoordinatorClient::connect(endpoint.clone()).await {
Ok(c) => c,
Err(e) => {
eprintln!("[WARN] Parked poll task failed to connect: {}", e);
return;
}
};
let registration = ParkedWorkerSessionRegistration {
worker_id: worker_id.clone(),
project_id: project_id.clone(),
deployment_id: deployment_id.clone(),
min_slots,
max_slots,
capabilities,
components,
service_name,
service_version: service_version.clone(),
service_type,
supported_protocol_capabilities,
required_protocol_capabilities,
};
let initial_session_id = match register_parked_worker_session_with_retries(
&mut client,
®istration,
"RegisterWorkerSession",
)
.await
{
ParkedWorkerSessionRegistrationResult::Registered(session_id) => session_id,
ParkedWorkerSessionRegistrationResult::Rejected => exit_parked_worker_process(
"RegisterWorkerSession was rejected after 3 attempts; exiting worker process",
),
};
let worker_session_id = Arc::new(TokioMutex::new(initial_session_id));
eprintln!(
"[INFO] Parked polling started (deployment={}, min_slots={}, max_slots={})",
deployment_id, min_slots, max_slots
);
let open_poll_slots = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let total_slots = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let busy_slots = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let capacity_reporter = spawn_parked_capacity_reporter(
client.clone(),
worker_id.clone(),
worker_session_id.clone(),
open_poll_slots.clone(),
busy_slots.clone(),
total_slots.clone(),
max_slots,
shutdown_rx.resubscribe(),
);
let (events_tx, mut events_rx) =
tokio::sync::mpsc::unbounded_channel::<ParkedSlotEvent>();
slot_phases.set_started_notifier(events_tx.clone());
let ctx = Arc::new(ParkedPollContext {
client,
worker_id,
worker_session_id,
registration,
service_version,
worker_metadata,
activation_definition_configs,
project_id,
response_tx,
in_flight,
cancel_tokens,
cancel_hook,
revoked_executions,
streaming_runs,
pending_lease_ids,
journal_queue,
journal_flush_locks,
slot_phases,
open_poll_slots,
total_slots: total_slots.clone(),
busy_slots: busy_slots.clone(),
events_tx,
session_refresh_lock: Arc::new(TokioMutex::new(())),
claim_timeout_ms,
min_slots,
retire_empty_polls,
});
let mut slots = tokio::task::JoinSet::new();
let mut next_slot_id = 0usize;
for _ in 0..min_slots {
spawn_parked_slot(&mut slots, &ctx, message_handler.clone(), &mut next_slot_id);
}
loop {
tokio::select! {
_ = shutdown_rx.recv() => {
info!("Parked poll task shutting down");
slots.abort_all();
while slots.join_next().await.is_some() {}
capacity_reporter.abort();
return;
}
event = events_rx.recv() => {
if let Some(ParkedSlotEvent::Started { active_started }) = event {
let spawn = parked_ramp_spawn_count(
total_slots.load(std::sync::atomic::Ordering::Relaxed),
active_started,
max_slots,
);
for _ in 0..spawn {
spawn_parked_slot(
&mut slots,
&ctx,
message_handler.clone(),
&mut next_slot_id,
);
}
if spawn > 0 {
debug!(
"Parked poll ramp: spawned {} slot(s) (total={})",
spawn,
total_slots.load(std::sync::atomic::Ordering::Relaxed)
);
}
}
}
result = slots.join_next() => {
match result {
Some(Ok(())) => {}
Some(Err(e)) => {
warn!("Parked poll slot exited abnormally: {}", e);
let _ = total_slots.fetch_update(
std::sync::atomic::Ordering::Relaxed,
std::sync::atomic::Ordering::Relaxed,
|current| current.checked_sub(1),
);
}
None => total_slots.store(0, std::sync::atomic::Ordering::Relaxed),
}
while total_slots.load(std::sync::atomic::Ordering::Relaxed) < min_slots {
spawn_parked_slot(
&mut slots,
&ctx,
message_handler.clone(),
&mut next_slot_id,
);
}
}
}
}
})
}
async fn handle_polled_job_response(&self, service_message: ServiceMessage) {
let invocation_id = match &service_message.message_type {
Some(crate::pb::service_message::MessageType::FunctionResponse(response)) => {
response.invocation_id.as_str()
}
_ => "",
};
error!(
invocation_id,
"Dropping unfenced legacy pull completion; parked PollJob completion is required"
);
}
fn write_health_marker(&self) {
let health_dir = std::env::var("AGNT5_HEALTH_DIR").unwrap_or_else(|_| "/tmp/health".into());
if let Err(e) = std::fs::create_dir_all(&health_dir) {
warn!("Failed to create health dir {}: {}", health_dir, e);
return;
}
let path = format!("{}/worker_{}.txt", health_dir, self.config.worker_id);
if let Err(e) = std::fs::write(&path, "") {
warn!("Failed to write health marker {}: {}", path, e);
} else {
debug!("Wrote health marker file {}", path);
}
}
fn remove_health_marker(&self) {
let health_dir = std::env::var("AGNT5_HEALTH_DIR").unwrap_or_else(|_| "/tmp/health".into());
let path = format!("{}/worker_{}.txt", health_dir, self.config.worker_id);
if let Err(e) = std::fs::remove_file(&path) {
if e.kind() != std::io::ErrorKind::NotFound {
warn!("Failed to remove health marker {}: {}", path, e);
}
} else {
debug!("Removed health marker file {}", path);
}
}
async fn send_shutdown_message(&self, tx: &flume::Sender<ServiceMessage>) -> Result<()> {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
let unregister = UnregisterService {
reason: "Worker shutdown".to_string(),
timestamp,
};
let service_message = ServiceMessage {
worker_id: self.config.worker_id.clone(),
metadata: std::collections::HashMap::new(),
message_type: Some(crate::pb::service_message::MessageType::UnregisterService(
unregister,
)),
};
match tx.send_async(service_message).await {
Ok(_) => {
info!(
"Sent graceful shutdown message for worker {}",
self.config.worker_id
);
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
Ok(())
}
Err(e) => {
debug!(
"Failed to send shutdown message for worker {}: {}",
self.config.worker_id, e
);
Err(crate::error::SdkError::Connection {
message: format!("Shutdown message failed: {}", e),
code: crate::error::ErrorCode::ConnectionFailed,
source: None,
})
}
}
}
}
fn is_terminal_worker_response(event_type: &str) -> bool {
matches!(
event_type,
"run.completed" | "run.failed" | "run.paused" | "workflow.paused"
)
}
fn is_cancelled_worker_response(service_message: &ServiceMessage) -> bool {
matches!(
&service_message.message_type,
Some(crate::pb::service_message::MessageType::FunctionResponse(resp))
if resp.event_type == "run.cancelled"
)
}
#[cfg(test)]
mod tests {
use super::{
activation_definition_configs, active_lease_danger_retry_ms,
active_lease_renew_interval_ms, active_lease_renew_interval_with_jitter_ms,
await_checkpoint_ack, canonical_activation_component_config,
complete_job_request_from_polled_completion, complete_job_with_retry,
deployment_artifact_sha256, durable_suspension_service_message, execution_is_revoked,
is_cancelled_worker_response, is_parked_worker_session_registration_rejection,
is_terminal_worker_response, is_worker_session_inactive_error, parked_ramp_spawn_count,
parked_runtime_service_response, parked_worker_session_was_refreshed,
polled_job_completion_from_service_message, polled_job_suspension_request,
record_groups_by_run, runtime_message_from_job_assignment,
stamp_activation_dispatch_metadata, stamp_dispatch_mode,
stamp_execution_authority_metadata, stamp_protocol_capability, take_correlation_ids,
try_retire_parked_slot, uncommitted_records_in_reverse, valid_activation_artifact_sha256,
wait_for_parked_run_events_flush, worker_capabilities, ActiveLeaseAuthority,
ActiveLeaseSession, AppendGroupProgress, CompleteJobSender, EntityStateSender,
ParkedSlotEvent, ParkedWorkerSessionRegistration, RunFlushLocks, Worker, WorkerConfig,
WorkerSlotPhaseSnapshot, WorkerSlotPhases,
};
use crate::error::{ErrorCode, SdkError};
use crate::journal_queue::{JournalEventMessage, JournalEventQueue, JournalQueueConfig};
use crate::pb::{
dispatch_component_response, runtime_message, runtime_service_request,
runtime_service_response, service_message, CompleteJobRequest, CompleteJobResponse,
DispatchComponentResponse, EntityStateSaveRequest, GetEntityStateRequest,
GetEntityStateResponse, JobAssignment, PutEntityStateRequest, PutEntityStateResponse,
RuntimeServiceRequest, ServiceMessage, WorkerMode,
};
use std::collections::{HashMap, VecDeque};
use std::time::Duration;
struct ScriptedCompleteJobSender {
outcomes: VecDeque<crate::error::Result<CompleteJobResponse>>,
requests: Vec<CompleteJobRequest>,
}
#[derive(Default)]
struct RecordingEntityStateSender {
get_requests: Vec<GetEntityStateRequest>,
put_requests: Vec<PutEntityStateRequest>,
}
#[async_trait::async_trait]
impl EntityStateSender for RecordingEntityStateSender {
async fn send_get_entity_state(
&mut self,
request: GetEntityStateRequest,
) -> crate::error::Result<GetEntityStateResponse> {
self.get_requests.push(request);
Ok(GetEntityStateResponse {
found: false,
state_json: Vec::new(),
version: 0,
})
}
async fn send_put_entity_state(
&mut self,
request: PutEntityStateRequest,
) -> crate::error::Result<PutEntityStateResponse> {
self.put_requests.push(request);
Ok(PutEntityStateResponse { new_version: 4 })
}
}
#[async_trait::async_trait]
impl CompleteJobSender for ScriptedCompleteJobSender {
async fn send_complete_job(
&mut self,
request: CompleteJobRequest,
) -> crate::error::Result<CompleteJobResponse> {
self.requests.push(request);
self.outcomes
.pop_front()
.unwrap_or_else(|| Err(SdkError::Internal("missing scripted outcome".to_string())))
}
}
#[tokio::test]
async fn checkpoint_ack_timeout_fails_closed_with_unknown_outcome() {
let error = await_checkpoint_ack(
std::future::pending::<()>(),
0,
"Engine.Append",
"run-1",
"workflow.step.completed",
7,
)
.await
.expect_err("checkpoint acknowledgement timeout must fail closed");
match error {
SdkError::Timeout {
message,
operation,
duration_ms,
} => {
assert_eq!(operation, "Engine.Append");
assert_eq!(duration_ms, Some(0));
assert!(message.contains("run_id=run-1"));
assert!(message.contains("event_type=workflow.step.completed"));
assert!(message.contains("seq=7"));
assert!(message.contains("persistence outcome is unknown"));
}
other => panic!("expected typed timeout error, got {other:?}"),
}
}
#[tokio::test]
async fn checkpoint_ack_timeout_wrapper_preserves_completed_and_inner_error_outcomes() {
let result = await_checkpoint_ack(
async { Ok::<_, SdkError>(42) },
100,
"ExecutionEngine.WriteCheckpoint",
"run-1",
"run.completed",
8,
)
.await
.expect("completed future should not time out");
assert_eq!(result.expect("inner result should be unchanged"), 42);
let result = await_checkpoint_ack(
async { Err::<(), _>(SdkError::Internal("append rejected".to_string())) },
100,
"Engine.Append",
"run-1",
"run.completed",
9,
)
.await
.expect("completed future should not time out");
assert!(matches!(
result,
Err(SdkError::Internal(message)) if message == "append rejected"
));
}
#[tokio::test]
async fn active_push_lease_request_carries_complete_authority_tuple() {
let authority = ActiveLeaseAuthority {
worker_id: "worker-1".to_string(),
project_id: "project-1".to_string(),
deployment_id: "deployment-1".to_string(),
run_id: "run-1".to_string(),
lease_id: "lease-1".to_string(),
attempt: 3,
lease_timeout_ms: 120_000,
lease_expires_at_ms: 200_000,
session: ActiveLeaseSession::Push,
};
let request = authority.renewal_request().await;
assert_eq!(request.worker_id, "worker-1");
assert!(request.worker_session_id.is_empty());
assert_eq!(request.project_id, "project-1");
assert_eq!(request.deployment_id, "deployment-1");
assert_eq!(request.run_id, "run-1");
assert_eq!(request.lease_id, "lease-1");
assert_eq!(request.attempt, Some(3));
assert_eq!(request.mode, WorkerMode::Push as i32);
}
#[tokio::test]
async fn revoked_execution_suppresses_late_push_response() {
let config = WorkerConfig::new(
"svc".to_string(),
"1.0.0".to_string(),
"standalone".to_string(),
);
let worker = Worker::new(config, Vec::new(), HashMap::new());
worker
.revoked_executions
.lock()
.unwrap()
.insert("run-1".to_string());
let (tx, rx) = flume::bounded(1);
let message = ServiceMessage {
worker_id: "worker-1".to_string(),
metadata: HashMap::new(),
message_type: Some(service_message::MessageType::FunctionResponse(
DispatchComponentResponse {
invocation_id: "run-1".to_string(),
success: true,
event_type: "run.completed".to_string(),
lease_id: "lease-stale".to_string(),
..Default::default()
},
)),
};
worker
.forward_worker_response(message, false, &tx)
.await
.unwrap();
assert!(rx.try_recv().is_err());
assert!(execution_is_revoked(&worker.revoked_executions, "run-1"));
}
#[test]
fn authority_revocation_invokes_language_cancel_hook() {
let config = WorkerConfig::new(
"svc".to_string(),
"1.0.0".to_string(),
"standalone".to_string(),
);
let worker = Worker::new(config, Vec::new(), HashMap::new());
let cancelled = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let cancelled_by_hook = cancelled.clone();
worker.set_cancel_hook(move |run_id| {
assert_eq!(run_id, "run-1");
cancelled_by_hook.store(true, std::sync::atomic::Ordering::SeqCst);
});
super::revoke_execution_authority(
"run-1",
&worker.revoked_executions,
&worker.cancel_tokens,
&worker.cancel_hook,
);
assert!(cancelled.load(std::sync::atomic::Ordering::SeqCst));
assert!(execution_is_revoked(&worker.revoked_executions, "run-1"));
}
#[test]
fn paused_worker_responses_are_terminal() {
for event_type in ["run.paused", "workflow.paused"] {
assert!(is_terminal_worker_response(event_type));
}
assert!(!is_terminal_worker_response("run.cancelled"));
assert!(!is_terminal_worker_response("output.delta"));
}
#[test]
fn cancelled_worker_response_is_consumed_without_completion() {
let message = ServiceMessage {
message_type: Some(crate::pb::service_message::MessageType::FunctionResponse(
DispatchComponentResponse {
event_type: "run.cancelled".to_string(),
..Default::default()
},
)),
..Default::default()
};
assert!(is_cancelled_worker_response(&message));
assert!(polled_job_completion_from_service_message(
&message,
"run-1",
"lease-1",
1,
&HashMap::new(),
)
.is_none());
}
#[test]
fn push_dispatch_mode_overrides_caller_metadata() {
let mut message = crate::pb::RuntimeMessage {
message_data: Some(runtime_message::MessageData::DispatchComponent(
crate::pb::DispatchComponentRequest {
metadata: HashMap::from([("dispatch_mode".to_string(), "pull".to_string())]),
..Default::default()
},
)),
..Default::default()
};
stamp_dispatch_mode(&mut message, "push");
let Some(runtime_message::MessageData::DispatchComponent(request)) = message.message_data
else {
panic!("dispatch request");
};
assert_eq!(
request.metadata.get("dispatch_mode").map(String::as_str),
Some("push")
);
}
#[test]
fn execution_authority_metadata_overrides_caller_values() {
let mut message = crate::pb::RuntimeMessage {
message_data: Some(runtime_message::MessageData::DispatchComponent(
crate::pb::DispatchComponentRequest {
lease_id: "lease-7".to_string(),
attempt: 7,
metadata: HashMap::from([
("dispatch_mode".to_string(), "push".to_string()),
("worker_id".to_string(), "forged-worker".to_string()),
("lease_id".to_string(), "forged-lease".to_string()),
("lease_attempt".to_string(), "99".to_string()),
]),
..Default::default()
},
)),
..Default::default()
};
stamp_execution_authority_metadata(&mut message, "worker-1", "session-1", "pull");
let Some(runtime_message::MessageData::DispatchComponent(request)) = message.message_data
else {
panic!("dispatch request");
};
assert_eq!(
request.metadata.get("dispatch_mode").map(String::as_str),
Some("pull")
);
assert_eq!(
request.metadata.get("worker_id").map(String::as_str),
Some("worker-1")
);
assert_eq!(
request
.metadata
.get("worker_session_id")
.map(String::as_str),
Some("session-1")
);
assert_eq!(
request.metadata.get("lease_id").map(String::as_str),
Some("lease-7")
);
assert_eq!(
request.metadata.get("lease_attempt").map(String::as_str),
Some("7")
);
}
#[test]
fn activation_component_config_is_canonical_and_sorted() {
assert_eq!(
canonical_activation_component_config(&HashMap::from([
("z".to_string(), "last".to_string()),
("a".to_string(), "first\nline".to_string()),
])),
r#"["object",[["a",["string","first\nline"]],["z",["string","last"]]]]"#
);
}
#[test]
fn activation_artifact_identity_accepts_exactly_32_encoded_bytes() {
assert!(valid_activation_artifact_sha256(
"6161616161616161616161616161616161616161616161616161616161616161"
));
assert!(valid_activation_artifact_sha256(
"YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE="
));
assert!(!valid_activation_artifact_sha256("6161"));
assert!(!valid_activation_artifact_sha256("not-a-digest"));
}
#[test]
fn deployment_artifact_identity_is_domain_separated_and_canonical() {
assert_eq!(
hex::encode(
deployment_artifact_sha256("01234567-89AB-CDEF-0123-456789ABCDEF").unwrap()
),
"c51344c186e74ccb1ce5b5f6122362285d9dd3a4b125d442280de1dcacae8c9f"
);
assert!(deployment_artifact_sha256("not-a-deployment-id").is_none());
}
#[test]
fn negotiated_activation_metadata_uses_typed_dispatch_authority() {
let mut message = crate::pb::RuntimeMessage {
message_data: Some(runtime_message::MessageData::DispatchComponent(
crate::pb::DispatchComponentRequest {
invocation_id: "run-1".to_string(),
component_name: "workflow".to_string(),
lease_id: "lease-1".to_string(),
metadata: HashMap::from([
(
"run_authority".to_string(),
"runtime-run-authority".to_string(),
),
(
"lease_authority".to_string(),
"runtime-lease-authority".to_string(),
),
(
"activation_artifact_sha256".to_string(),
"c51344c186e74ccb1ce5b5f6122362285d9dd3a4b125d442280de1dcacae8c9f"
.to_string(),
),
]),
..Default::default()
},
)),
..Default::default()
};
let components = vec![crate::pb::ComponentInfo {
name: "workflow".to_string(),
config: HashMap::from([("model".to_string(), "gpt-5".to_string())]),
..Default::default()
}];
stamp_activation_dispatch_metadata(
&mut message,
"worker-1",
"session-1",
"1.2.3",
&HashMap::from([
("project_id".to_string(), "project-1".to_string()),
(
"activation_artifact_sha256".to_string(),
"c51344c186e74ccb1ce5b5f6122362285d9dd3a4b125d442280de1dcacae8c9f".to_string(),
),
]),
&activation_definition_configs(&components),
)
.unwrap();
let Some(runtime_message::MessageData::DispatchComponent(request)) = message.message_data
else {
panic!("dispatch request");
};
assert_eq!(
request
.metadata
.get(crate::client::DURABLE_ACTIVATION_V1_CAPABILITY)
.map(String::as_str),
Some("true")
);
assert_eq!(
request
.metadata
.get("worker_session_id")
.map(String::as_str),
Some("session-1")
);
assert_eq!(
request.metadata.get("lease_id").map(String::as_str),
Some("lease-1")
);
assert_eq!(
request.metadata.get("run_authority").map(String::as_str),
Some("runtime-run-authority")
);
assert_eq!(
request.metadata.get("lease_authority").map(String::as_str),
Some("runtime-lease-authority")
);
assert_eq!(
request
.metadata
.get("activation_definition_config")
.map(String::as_str),
Some(r#"["object",[["model",["string","gpt-5"]]]]"#)
);
assert_eq!(
request
.metadata
.get("activation_artifact_sha256")
.map(String::as_str),
Some("c51344c186e74ccb1ce5b5f6122362285d9dd3a4b125d442280de1dcacae8c9f")
);
}
#[test]
fn negotiated_activation_rejects_worker_artifact_mismatch() {
let mut message = crate::pb::RuntimeMessage {
message_data: Some(runtime_message::MessageData::DispatchComponent(
crate::pb::DispatchComponentRequest {
invocation_id: "run-1".to_string(),
component_name: "workflow".to_string(),
lease_id: "lease-1".to_string(),
metadata: HashMap::from([(
"activation_artifact_sha256".to_string(),
"00".repeat(32),
)]),
..Default::default()
},
)),
..Default::default()
};
let error = stamp_activation_dispatch_metadata(
&mut message,
"worker-1",
"session-1",
"1.2.3",
&HashMap::from([("activation_artifact_sha256".to_string(), "11".repeat(32))]),
&HashMap::new(),
)
.expect_err("mismatched worker code must not execute a pinned run");
assert!(matches!(
error,
SdkError::Activation {
code: ErrorCode::NondeterministicReplay,
..
}
));
}
#[test]
fn negotiated_suspension_capability_is_visible_to_language_handlers() {
let mut message = crate::pb::RuntimeMessage {
message_data: Some(runtime_message::MessageData::DispatchComponent(
crate::pb::DispatchComponentRequest::default(),
)),
..Default::default()
};
stamp_protocol_capability(
&mut message,
crate::client::DURABLE_SUSPENSION_V1_CAPABILITY,
);
let Some(runtime_message::MessageData::DispatchComponent(request)) = message.message_data
else {
panic!("dispatch request");
};
assert_eq!(
request
.metadata
.get(crate::client::DURABLE_SUSPENSION_V1_CAPABILITY)
.map(String::as_str),
Some("true")
);
}
#[test]
fn durable_engine_records_are_grouped_by_run_without_reordering_each_run() {
let records = vec![
crate::pb::Record {
run_id: "run-a".into(),
event_type: "a-1".into(),
..Default::default()
},
crate::pb::Record {
run_id: "run-b".into(),
event_type: "b-1".into(),
..Default::default()
},
crate::pb::Record {
run_id: "run-a".into(),
event_type: "a-2".into(),
..Default::default()
},
];
assert_eq!(record_groups_by_run(&records), vec![vec![0, 2], vec![1]]);
}
#[test]
fn durable_engine_partial_group_failure_requeues_only_unacknowledged_records() {
let mut progress = AppendGroupProgress::new(4);
progress.acknowledge(&[0, 2], 2);
let (_error, committed, written) =
progress.failure(SdkError::Internal("run-b failed".to_string()));
assert_eq!(committed, vec![true, false, true, false]);
assert_eq!(written, 2);
assert_eq!(
uncommitted_records_in_reverse(vec![0, 1, 2, 3], &committed),
vec![3, 1]
);
}
#[test]
fn cleanup_run_tracking_removes_per_run_entries() {
let config = WorkerConfig::new(
"svc".to_string(),
"1.0.0".to_string(),
"standalone".to_string(),
);
let worker = Worker::new(config, Vec::new(), HashMap::new());
worker
.pending_lease_ids
.lock()
.unwrap()
.insert("run-1".to_string(), "lease-1".to_string());
worker
.pending_lease_ids
.lock()
.unwrap()
.insert("run-2".to_string(), "lease-2".to_string());
worker
.streaming_runs
.lock()
.unwrap()
.insert("run-1".to_string(), true);
worker.cleanup_run_tracking("run-1");
assert!(!worker
.pending_lease_ids
.lock()
.unwrap()
.contains_key("run-1"));
assert!(worker
.pending_lease_ids
.lock()
.unwrap()
.contains_key("run-2"));
assert!(!worker.streaming_runs.lock().unwrap().contains_key("run-1"));
}
#[test]
fn cleanup_run_tracking_strips_sub_invocation_suffix_for_streaming_flag() {
let config = WorkerConfig::new(
"svc".to_string(),
"1.0.0".to_string(),
"standalone".to_string(),
);
let worker = Worker::new(config, Vec::new(), HashMap::new());
worker
.pending_lease_ids
.lock()
.unwrap()
.insert("run-1:0".to_string(), "lease-1".to_string());
worker
.streaming_runs
.lock()
.unwrap()
.insert("run-1".to_string(), true);
worker.cleanup_run_tracking("run-1:0");
assert!(worker.pending_lease_ids.lock().unwrap().is_empty());
assert!(worker.streaming_runs.lock().unwrap().is_empty());
}
#[test]
fn job_assignment_conversion_preserves_typed_lease() {
let job = JobAssignment {
job_id: "run-1".to_string(),
run_id: "run-1".to_string(),
component_id: String::new(),
component_type: crate::pb::ComponentType::Function as i32,
component_name: "do_work".to_string(),
input_data: br#"{"x":1}"#.to_vec(),
metadata: HashMap::from([
("stream_mode".to_string(), "full".to_string()),
("deployment_id".to_string(), "dep-1".to_string()),
("dispatch_mode".to_string(), "push".to_string()),
("lease_id".to_string(), "forged-lease".to_string()),
("lease_expires_at_ms".to_string(), "999999999".to_string()),
("lease_timeout_ms".to_string(), "1".to_string()),
("max_attempts".to_string(), "5".to_string()),
]),
attempt: 2,
timeout_ms: 0,
trace_id: "trace-1".to_string(),
lease_id: "lease-1".to_string(),
lease_expires_at_ms: 123_456,
};
let (
message,
is_streaming,
run_id,
lease_id,
attempt,
renewal_timeout_ms,
lease_expires_at_ms,
completion_metadata,
) = runtime_message_from_job_assignment(job, 60_000).expect("valid typed assignment");
assert!(is_streaming);
assert_eq!(run_id, "run-1");
assert_eq!(lease_id, "lease-1");
assert_eq!(attempt, 2);
assert_eq!(renewal_timeout_ms, 60_000);
assert_eq!(lease_expires_at_ms, 123_456);
assert_eq!(
completion_metadata.get("attempt").map(String::as_str),
Some("2")
);
assert_eq!(
completion_metadata.get("max_attempts").map(String::as_str),
Some("5")
);
match message.message_data {
Some(runtime_message::MessageData::DispatchComponent(req)) => {
assert_eq!(req.invocation_id, "run-1");
assert_eq!(req.component_name, "do_work");
assert_eq!(req.attempt, 2);
assert_eq!(req.deployment_id, "dep-1");
assert_eq!(req.lease_id, "lease-1");
assert_eq!(
req.metadata.get("lease_id").map(String::as_str),
Some("lease-1")
);
assert_eq!(
req.metadata.get("lease_expires_at_ms").map(String::as_str),
Some("123456")
);
assert_eq!(
req.metadata.get("trace_id").map(String::as_str),
Some("trace-1")
);
assert_eq!(
req.metadata.get("dispatch_mode").map(String::as_str),
Some("pull")
);
}
other => panic!("expected dispatch component, got {other:?}"),
}
}
#[test]
fn negative_polled_job_attempt_is_rejected_before_execution() {
let error = runtime_message_from_job_assignment(
JobAssignment {
job_id: "run-negative-attempt".to_string(),
run_id: "run-negative-attempt".to_string(),
lease_id: "lease-negative-attempt".to_string(),
attempt: -1,
..Default::default()
},
60_000,
)
.expect_err("negative attempt must fail closed");
assert!(error.to_string().contains("negative attempt -1"));
}
#[test]
fn assignment_does_not_fall_back_to_forged_lease_metadata() {
let error = runtime_message_from_job_assignment(
JobAssignment {
job_id: "run-forged-lease".to_string(),
run_id: "run-forged-lease".to_string(),
lease_id: String::new(),
lease_expires_at_ms: 0,
metadata: HashMap::from([
("lease_id".to_string(), "forged-lease".to_string()),
("lease_expires_at_ms".to_string(), "999999999".to_string()),
]),
..Default::default()
},
60_000,
)
.expect_err("typed lease is required before executing user code");
assert!(error.to_string().contains("no typed lease_id"));
}
#[test]
fn parked_completion_preserves_assignment_attempt_and_lease() {
let service_message = ServiceMessage {
message_type: Some(crate::pb::service_message::MessageType::FunctionResponse(
DispatchComponentResponse {
invocation_id: "forged-run:component-0".to_string(),
success: true,
result: Some(dispatch_component_response::Result::OutputData(
br#"{"ok":true}"#.to_vec(),
)),
metadata: HashMap::from([
("attempt".to_string(), "999".to_string()),
("max_attempts".to_string(), "999".to_string()),
("initial_interval_ms".to_string(), "999".to_string()),
("max_interval_ms".to_string(), "999".to_string()),
("backoff_type".to_string(), "forged".to_string()),
("backoff_multiplier".to_string(), "999".to_string()),
("pause_index".to_string(), "2".to_string()),
(
"step_events".to_string(),
r#"{"0":"Ada","1":"blue"}"#.to_string(),
),
]),
event_type: "workflow.paused".to_string(),
lease_id: "forged-lease".to_string(),
..Default::default()
},
)),
..Default::default()
};
let completion = polled_job_completion_from_service_message(
&service_message,
"run-1",
"lease-7",
7,
&HashMap::from([
("attempt".to_string(), "7".to_string()),
("max_attempts".to_string(), "5".to_string()),
("initial_interval_ms".to_string(), "100".to_string()),
("max_interval_ms".to_string(), "1000".to_string()),
("backoff_type".to_string(), "exponential".to_string()),
("backoff_multiplier".to_string(), "2".to_string()),
]),
)
.expect("function response should become a fenced completion");
assert_eq!(completion.job_id, "run-1");
assert_eq!(completion.lease_id, "lease-7");
assert_eq!(completion.attempt, 7);
assert_eq!(completion.event_type, "workflow.paused");
assert_eq!(
completion.metadata.get("attempt").map(String::as_str),
Some("7")
);
assert_eq!(
completion.metadata.get("max_attempts").map(String::as_str),
Some("5")
);
assert_eq!(
completion
.metadata
.get("initial_interval_ms")
.map(String::as_str),
Some("100")
);
assert_eq!(
completion
.metadata
.get("max_interval_ms")
.map(String::as_str),
Some("1000")
);
assert_eq!(
completion.metadata.get("backoff_type").map(String::as_str),
Some("exponential")
);
assert_eq!(
completion
.metadata
.get("backoff_multiplier")
.map(String::as_str),
Some("2")
);
assert_eq!(
completion.metadata.get("pause_index").map(String::as_str),
Some("2")
);
assert_eq!(
completion.metadata.get("step_events").map(String::as_str),
Some(r#"{"0":"Ada","1":"blue"}"#)
);
}
#[test]
fn parked_completion_drops_response_retry_policy_missing_from_assignment() {
let service_message = ServiceMessage {
message_type: Some(crate::pb::service_message::MessageType::FunctionResponse(
DispatchComponentResponse {
success: false,
metadata: HashMap::from([
("attempt".to_string(), "999".to_string()),
("max_attempts".to_string(), "999".to_string()),
("initial_interval_ms".to_string(), "999".to_string()),
("max_interval_ms".to_string(), "999".to_string()),
("backoff_type".to_string(), "forged".to_string()),
("backoff_multiplier".to_string(), "999".to_string()),
]),
event_type: "run.failed".to_string(),
..Default::default()
},
)),
..Default::default()
};
let completion = polled_job_completion_from_service_message(
&service_message,
"run-1",
"lease-7",
7,
&HashMap::from([("attempt".to_string(), "7".to_string())]),
)
.expect("terminal response should become a completion");
assert_eq!(
completion.metadata,
HashMap::from([("attempt".to_string(), "7".to_string())])
);
}
#[test]
fn parked_completion_builds_authoritative_complete_job_request() {
let completion = super::PolledJobCompletion {
job_id: "run-1".to_string(),
success: false,
output_data: Vec::new(),
error_message: "failed".to_string(),
error_code: "TEST_FAILURE".to_string(),
event_type: "run.failed".to_string(),
metadata: HashMap::from([
("attempt".to_string(), "7".to_string()),
("max_attempts".to_string(), "5".to_string()),
("initial_interval_ms".to_string(), "100".to_string()),
("max_interval_ms".to_string(), "1000".to_string()),
("backoff_type".to_string(), "exponential".to_string()),
("backoff_multiplier".to_string(), "2".to_string()),
(
"completion_event_type".to_string(),
"forged.event".to_string(),
),
]),
lease_id: "lease-7".to_string(),
attempt: 7,
};
let request = complete_job_request_from_polled_completion(
"worker-1",
"session-1",
"project-1",
completion,
);
assert_eq!(request.job_id, "run-1");
assert_eq!(request.worker_id, "worker-1");
assert_eq!(request.worker_session_id, "session-1");
assert_eq!(request.project_id, "project-1");
assert_eq!(request.lease_id, "lease-7");
assert_eq!(request.attempt, Some(7));
assert_eq!(
request
.metadata
.get("completion_event_type")
.map(String::as_str),
Some("run.failed")
);
for (key, expected) in [
("attempt", "7"),
("max_attempts", "5"),
("initial_interval_ms", "100"),
("max_interval_ms", "1000"),
("backoff_type", "exponential"),
("backoff_multiplier", "2"),
] {
assert_eq!(
request.metadata.get(key).map(String::as_str),
Some(expected),
"unexpected CompleteJob metadata for {key}"
);
}
}
#[test]
fn parked_suspension_uses_assignment_owned_project_and_run_scope() {
let suspension = crate::pb::WorkerSuspension {
activation_id: "activation-1".into(),
attempt: 2,
fence_token: b"fence-2".to_vec(),
timer_key: "sleep:backoff".into(),
ready_at_ms: 0,
input_digest: vec![1; 32],
definition_digest: vec![2; 32],
continuation: br#"{"step":2}"#.to_vec(),
delay_ms: 5_000,
};
let response = ServiceMessage {
worker_id: "worker-1".into(),
metadata: HashMap::new(),
message_type: Some(service_message::MessageType::FunctionResponse(
DispatchComponentResponse {
invocation_id: "worker-supplied-run".into(),
success: true,
result: Some(dispatch_component_response::Result::WorkerSuspension(
suspension.clone(),
)),
..Default::default()
},
)),
};
let request = polled_job_suspension_request(&response, "project-1", "run-1")
.expect("typed suspension");
assert_eq!(request.project_id, "project-1");
assert_eq!(request.run_id, "run-1");
assert_eq!(request.activation_id, suspension.activation_id);
assert_eq!(request.delay_ms, suspension.delay_ms);
assert_eq!(request.ready_at_ms, 0);
}
#[test]
fn core_timer_error_maps_to_typed_worker_response() {
let suspension = crate::pb::WorkerSuspension {
activation_id: "activation-1".into(),
attempt: 2,
fence_token: b"fence-2".to_vec(),
timer_key: "sleep:sleep_0".into(),
delay_ms: 5_000,
..Default::default()
};
let envelope = super::DurableSuspensionEnvelope {
invocation_id: "run-1".into(),
metadata: HashMap::from([("project_id".into(), "project-1".into())]),
attempt: 2,
lease_id: "lease-2".into(),
};
let message = durable_suspension_service_message("worker-1", &envelope, suspension.clone());
assert_eq!(message.worker_id, "worker-1");
let Some(service_message::MessageType::FunctionResponse(response)) = message.message_type
else {
panic!("expected function response");
};
assert!(response.success);
assert_eq!(response.invocation_id, "run-1");
assert_eq!(response.event_type, "workflow.paused");
assert_eq!(response.attempt, 2);
assert_eq!(response.lease_id, "lease-2");
assert_eq!(
response.result,
Some(dispatch_component_response::Result::WorkerSuspension(
suspension
))
);
}
#[test]
fn parked_nonterminal_response_does_not_complete_assignment() {
let service_message = ServiceMessage {
message_type: Some(crate::pb::service_message::MessageType::FunctionResponse(
DispatchComponentResponse {
invocation_id: "run-1".to_string(),
success: true,
event_type: "output.delta".to_string(),
..Default::default()
},
)),
..Default::default()
};
assert!(polled_job_completion_from_service_message(
&service_message,
"run-1",
"lease-7",
7,
&HashMap::new(),
)
.is_none());
}
#[tokio::test]
async fn parked_entity_state_save_becomes_unary_fenced_engine_request() {
let service_message = ServiceMessage {
message_type: Some(service_message::MessageType::RuntimeService(
RuntimeServiceRequest {
request_id: "state-request-1".to_string(),
session_id: String::new(),
operation: Some(runtime_service_request::Operation::EntityStateSave(
EntityStateSaveRequest {
entity_type: "WorkflowEntity".to_string(),
entity_key: "ks_sequential".to_string(),
state_json: br#"{"total_steps":2}"#.to_vec(),
expected_version: 3,
scope: "run".to_string(),
scope_id: "run-1".to_string(),
},
)),
},
)),
..Default::default()
};
let mut sender = RecordingEntityStateSender::default();
let runtime_response = parked_runtime_service_response(
&mut sender,
&service_message,
"project-1",
"run-1",
"worker-1",
"session-1",
"lease-1",
7,
)
.await
.expect("entity request should be handled inside the parked slot");
assert_eq!(sender.put_requests.len(), 1);
let request = &sender.put_requests[0];
assert_eq!(request.project_id, "project-1");
assert_eq!(request.run_id, "run-1");
assert_eq!(request.worker_id, "worker-1");
assert_eq!(request.worker_session_id, "session-1");
assert_eq!(request.lease_id, "lease-1");
assert_eq!(request.attempt, Some(7));
assert_eq!(request.operation_id, "state-request-1");
assert_eq!(request.expected_version, 3);
let runtime_message::MessageData::RuntimeServiceResponse(response) =
runtime_response.message_data.expect("runtime response")
else {
panic!("expected RuntimeServiceResponse");
};
assert!(response.success);
assert_eq!(response.request_id, "state-request-1");
match response.result {
Some(runtime_service_response::Result::EntityStateSave(result)) => {
assert_eq!(result.new_version, 4);
}
_ => panic!("expected entity-state save result"),
}
}
#[tokio::test]
async fn parked_completion_retries_until_runtime_acknowledges() {
let mut sender = ScriptedCompleteJobSender {
outcomes: VecDeque::from([
Err(SdkError::Connection {
message: "temporary CompleteJob failure".to_string(),
code: ErrorCode::ConnectionFailed,
source: None,
}),
Ok(CompleteJobResponse {
acknowledged: false,
}),
Ok(CompleteJobResponse { acknowledged: true }),
]),
requests: Vec::new(),
};
let request = CompleteJobRequest {
job_id: "run-1".to_string(),
lease_id: "lease-7".to_string(),
attempt: Some(7),
..Default::default()
};
complete_job_with_retry(
&mut sender,
request,
3,
std::time::Duration::from_millis(50),
std::time::Duration::ZERO,
)
.await
.expect("third acknowledged response should succeed");
assert_eq!(sender.requests.len(), 3);
assert!(sender
.requests
.iter()
.all(|request| request.lease_id == "lease-7" && request.attempt == Some(7)));
}
#[tokio::test]
async fn parked_completion_waits_for_run_events_to_finish_flushing() {
let queue = JournalEventQueue::new(JournalQueueConfig {
flush_interval_ms: 1,
..Default::default()
});
queue
.push(JournalEventMessage {
run_id: "run-1".to_string(),
event_type: "function.completed".to_string(),
..Default::default()
})
.unwrap();
let flush_locks = RunFlushLocks::default();
let queue_for_sender = queue.clone();
let locks_for_sender = flush_locks.clone();
let sender = tokio::spawn(async move {
tokio::task::yield_now().await;
let _guard = locks_for_sender.lock_run("run-1").await;
let drained = queue_for_sender.drain_run_events("run-1");
assert_eq!(drained.len(), 1);
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
});
assert!(wait_for_parked_run_events_flush(&queue, &flush_locks, "run-1").await);
sender.await.unwrap();
assert!(!queue.contains_run("run-1"));
}
#[tokio::test]
async fn run_flush_locks_serialize_same_run_without_blocking_other_runs() {
let locks = RunFlushLocks::default();
let run_a_guard = locks.lock_run("run-a").await;
let same_run = tokio::time::timeout(
std::time::Duration::from_millis(10),
locks.lock_run("run-a"),
)
.await;
assert!(
same_run.is_err(),
"same run must wait for its ordering barrier"
);
let other_run = tokio::time::timeout(
std::time::Duration::from_millis(10),
locks.lock_run("run-b"),
)
.await;
assert!(
other_run.is_ok(),
"unrelated run must not share the barrier"
);
drop(run_a_guard);
assert!(tokio::time::timeout(
std::time::Duration::from_millis(10),
locks.lock_run("run-a"),
)
.await
.is_ok());
}
#[test]
fn active_lease_renew_intervals_are_bounded() {
assert_eq!(active_lease_renew_interval_ms(120_000), 60_000);
assert_eq!(active_lease_danger_retry_ms(120_000), 5_000);
assert_eq!(active_lease_renew_interval_ms(2_000), 1_000);
assert_eq!(active_lease_danger_retry_ms(2_000), 200);
assert_eq!(active_lease_renew_interval_ms(6), 3);
assert_eq!(active_lease_danger_retry_ms(6), 1);
for _ in 0..100 {
let jittered = active_lease_renew_interval_with_jitter_ms(120_000);
assert!(
(54_000..=66_000).contains(&jittered),
"jittered interval out of ±10% range: {jittered}"
);
}
}
#[test]
fn worker_session_inactive_errors_are_detected() {
let error = SdkError::Connection {
message: "PollJob failed: code: 'The caller does not have permission to execute the specified operation', message: \"worker session is not active\"".to_string(),
code: ErrorCode::ConnectionFailed,
source: None,
};
assert!(is_worker_session_inactive_error(&error));
}
#[test]
fn parked_worker_session_registration_classifies_rejections() {
let rejected = SdkError::Connection {
message:
"RegisterWorkerSession failed: code: 'Invalid argument', message: \"bad worker\""
.to_string(),
code: ErrorCode::ConnectionFailed,
source: None,
};
let transient = SdkError::Connection {
message: "RegisterWorkerSession failed: code: 'The service is currently unavailable'"
.to_string(),
code: ErrorCode::ConnectionFailed,
source: None,
};
assert!(is_parked_worker_session_registration_rejection(&rejected));
assert!(!is_parked_worker_session_registration_rejection(&transient));
}
#[test]
fn parked_worker_session_refreshed_detects_stale_observed_session() {
assert!(parked_worker_session_was_refreshed(
"new-session",
"old-session"
));
assert!(!parked_worker_session_was_refreshed(
"same-session",
"same-session"
));
}
#[test]
fn parked_ramp_spawn_count_doubles_demand_within_max_slots() {
assert_eq!(parked_ramp_spawn_count(1, 1, 100), 1);
assert_eq!(parked_ramp_spawn_count(4, 4, 100), 4);
assert_eq!(parked_ramp_spawn_count(4, 1, 100), 0);
assert_eq!(parked_ramp_spawn_count(4, 2, 100), 0);
assert_eq!(parked_ramp_spawn_count(4, 3, 100), 2);
assert_eq!(parked_ramp_spawn_count(8, 8, 10), 2);
assert_eq!(parked_ramp_spawn_count(10, 10, 10), 0);
assert_eq!(parked_ramp_spawn_count(12, 10, 10), 0);
assert_eq!(parked_ramp_spawn_count(4, 0, 10), 0);
}
#[tokio::test]
async fn stalled_claim_does_not_signal_slot_ramp_before_language_start() {
let phases = WorkerSlotPhases::default();
let (events_tx, mut events_rx) = tokio::sync::mpsc::unbounded_channel();
phases.set_started_notifier(events_tx);
let guards: Vec<_> = (0..4)
.map(|index| phases.claim(format!("slow-run-{index}")))
.collect();
assert_eq!(
phases.snapshot(),
WorkerSlotPhaseSnapshot {
claimed_not_started: 4,
executing: 0,
terminalizing: 0,
}
);
assert!(
tokio::time::timeout(Duration::from_millis(20), events_rx.recv())
.await
.is_err()
);
phases.mark_started("slow-run-0");
let event = events_rx.recv().await;
assert_eq!(event, Some(ParkedSlotEvent::Started { active_started: 1 }));
let Some(ParkedSlotEvent::Started { active_started }) = event else {
unreachable!();
};
assert_eq!(parked_ramp_spawn_count(4, active_started, 16), 0);
assert_eq!(
phases.snapshot(),
WorkerSlotPhaseSnapshot {
claimed_not_started: 3,
executing: 1,
terminalizing: 0,
}
);
phases.mark_started("slow-run-0");
assert!(events_rx.try_recv().is_err());
phases.mark_terminalizing("slow-run-0");
assert_eq!(
phases.snapshot(),
WorkerSlotPhaseSnapshot {
claimed_not_started: 3,
executing: 0,
terminalizing: 1,
}
);
drop(guards);
assert_eq!(phases.snapshot(), WorkerSlotPhaseSnapshot::default());
}
#[test]
fn try_retire_parked_slot_never_drops_below_min_slots() {
let total = std::sync::atomic::AtomicUsize::new(3);
let busy = std::sync::atomic::AtomicUsize::new(0);
assert!(try_retire_parked_slot(&total, &busy, 1));
assert!(try_retire_parked_slot(&total, &busy, 1));
assert_eq!(total.load(std::sync::atomic::Ordering::Relaxed), 1);
assert!(!try_retire_parked_slot(&total, &busy, 1));
assert_eq!(total.load(std::sync::atomic::Ordering::Relaxed), 1);
}
#[test]
fn try_retire_parked_slot_keeps_min_idle_pollers_while_busy() {
let total = std::sync::atomic::AtomicUsize::new(3);
let busy = std::sync::atomic::AtomicUsize::new(1);
assert!(try_retire_parked_slot(&total, &busy, 1)); assert_eq!(total.load(std::sync::atomic::Ordering::Relaxed), 2);
assert!(!try_retire_parked_slot(&total, &busy, 1));
assert_eq!(total.load(std::sync::atomic::Ordering::Relaxed), 2);
}
#[test]
fn try_retire_parked_slot_respects_floor_under_concurrent_callers() {
let total = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(8));
let busy = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let min_slots = 2;
let retired: usize = (0..8)
.map(|_| {
let total = total.clone();
let busy = busy.clone();
std::thread::spawn(move || try_retire_parked_slot(&total, &busy, min_slots))
})
.collect::<Vec<_>>()
.into_iter()
.map(|h| h.join().unwrap())
.filter(|retired| *retired)
.count();
assert_eq!(retired, 6);
assert_eq!(total.load(std::sync::atomic::Ordering::Relaxed), min_slots);
}
#[test]
fn parked_worker_session_registration_builds_repeatable_request() {
let registration = ParkedWorkerSessionRegistration {
worker_id: "worker-1".into(),
project_id: "project-1".into(),
deployment_id: "deployment-1".into(),
min_slots: 2,
max_slots: 5,
capabilities: vec![crate::pb::WorkerCapability {
component_type: crate::pb::ComponentType::Function as i32,
component_name: "do_work".into(),
}],
components: vec![crate::pb::ComponentInfo {
component_type: crate::pb::ComponentType::Function as i32,
name: "do_work".into(),
..Default::default()
}],
service_name: "svc".into(),
service_version: "1.2.3".into(),
service_type: "worker".into(),
supported_protocol_capabilities: vec![
crate::client::DURABLE_ACTIVATION_V1_CAPABILITY.into()
],
required_protocol_capabilities: Vec::new(),
};
let first = registration.request();
let second = registration.request();
assert_eq!(first.worker_id, "worker-1");
assert_eq!(first.project_id, "project-1");
assert_eq!(first.deployment_id, "deployment-1");
assert_eq!(first.max_slots, 5);
assert_eq!(first.slot_policy.as_ref().unwrap().min_slots, 2);
assert_eq!(first.slot_policy.as_ref().unwrap().max_slots, 5);
assert_eq!(first.capabilities.len(), 1);
assert_eq!(first.components.len(), 1);
assert_eq!(second.service_name, "svc");
assert_eq!(second.service_version, "1.2.3");
assert_eq!(second.service_type, "worker");
}
#[test]
fn worker_capabilities_include_declared_components_and_local_builtin_scorers() {
let capabilities = worker_capabilities(&[crate::pb::ComponentInfo {
component_type: crate::pb::ComponentType::Function as i32,
name: "do_work".into(),
..Default::default()
}]);
assert!(capabilities.iter().any(|cap| {
cap.component_type == crate::pb::ComponentType::Function as i32
&& cap.component_name == "do_work"
}));
for scorer in [
"json_valid",
"step_efficiency",
"plan_quality",
"plan_adherence",
] {
assert!(
capabilities.iter().any(|cap| {
cap.component_type == crate::pb::ComponentType::Scorer as i32
&& cap.component_name == scorer
}),
"missing local built-in scorer capability {scorer}"
);
}
}
#[test]
fn take_correlation_ids_accepts_canonical_keys() {
let mut metadata = HashMap::from([
("correlation_id".to_string(), "span-1".to_string()),
("parent_correlation_id".to_string(), "parent-1".to_string()),
("other".to_string(), "value".to_string()),
]);
let (correlation_id, parent_correlation_id) = take_correlation_ids(&mut metadata);
assert_eq!(correlation_id, "span-1");
assert_eq!(parent_correlation_id, "parent-1");
assert!(!metadata.contains_key("correlation_id"));
assert!(!metadata.contains_key("parent_correlation_id"));
assert_eq!(metadata.get("other").map(String::as_str), Some("value"));
}
#[test]
fn take_correlation_ids_prefers_legacy_short_keys() {
let mut metadata = HashMap::from([
("cid".to_string(), "short-span".to_string()),
("pcid".to_string(), "short-parent".to_string()),
("correlation_id".to_string(), "canonical-span".to_string()),
(
"parent_correlation_id".to_string(),
"canonical-parent".to_string(),
),
]);
let (correlation_id, parent_correlation_id) = take_correlation_ids(&mut metadata);
assert_eq!(correlation_id, "short-span");
assert_eq!(parent_correlation_id, "short-parent");
assert!(metadata.contains_key("correlation_id"));
assert!(metadata.contains_key("parent_correlation_id"));
}
}