pub mod envelope;
pub(crate) mod offset;
pub mod outbox;
pub mod tamper;
pub mod worker;
use std::sync::{
atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering},
Arc,
};
use std::time::Duration;
use tokio::sync::Notify;
use secrecy::SecretString;
#[inline]
pub(crate) fn now_unix_ms() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
#[inline]
pub(crate) fn now_unix_secs() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
pub trait CredentialProvider: Send + Sync {
fn retrieve(&self) -> Option<SecretString>;
fn invalidate(&self) {}
}
#[derive(Debug, Clone)]
pub struct CloudConfig {
pub api_url: String,
pub timeout_connect_ms: u64,
pub timeout_total_ms: u64,
pub retry_delay_ms: u64,
pub channel_size: usize,
pub rate_limit_default_secs: u64,
pub credential_poll_interval_ms: u64,
pub fallback_max_bytes: u64,
pub batch_max_events: usize,
pub batch_max_wait_ms: u64,
pub host_key: Option<String>,
pub host_id: Option<String>,
pub agent_id: Option<String>,
}
impl Default for CloudConfig {
fn default() -> Self {
Self {
api_url: "https://app.openlatch.ai".into(),
timeout_connect_ms: 5000,
timeout_total_ms: 10000,
retry_delay_ms: 2000,
channel_size: 1000,
rate_limit_default_secs: 30,
credential_poll_interval_ms: 60_000,
fallback_max_bytes: 50 * 1024 * 1024,
batch_max_events: 50,
batch_max_wait_ms: 5000,
host_key: None,
host_id: None,
agent_id: None,
}
}
}
#[derive(Debug, Clone)]
pub struct CloudEvent {
pub envelope: serde_json::Value,
pub agent_id: String,
}
#[derive(Debug)]
pub enum CloudError {
AuthError,
RateLimit {
retry_after_secs: u64,
},
LicenseRefused {
code: String,
licensing_url: Option<String>,
retry_after: Option<Duration>,
},
ServerError,
Network,
ClientError(u16),
CompatibilityUnavailable,
}
impl std::fmt::Display for CloudError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CloudError::AuthError => {
write!(f, "cloud auth error (401/403) — API key invalid or revoked")
}
CloudError::RateLimit { retry_after_secs } => {
write!(
f,
"cloud rate limit (429) — retry after {retry_after_secs}s"
)
}
CloudError::LicenseRefused { code, .. } => {
write!(f, "cloud license refusal ({code}) — events are queued")
}
CloudError::ServerError => write!(f, "cloud server error (5xx)"),
CloudError::Network => write!(f, "cloud network error — endpoint unreachable"),
CloudError::ClientError(code) => write!(f, "cloud client error ({code})"),
CloudError::CompatibilityUnavailable => {
write!(f, "cloud protocol compatibility unavailable")
}
}
}
}
pub const MAX_RETRY_AFTER_SECS: u64 = 3_600;
pub const LICENSE_RETRY_SECS: u64 = 900;
pub(crate) fn parse_retry_after(raw: &str) -> Option<u64> {
let value = raw.trim();
if let Ok(secs) = value.parse::<u64>() {
return Some(secs);
}
let when = chrono::DateTime::parse_from_rfc2822(value).ok()?;
let delta = when.timestamp() - chrono::Utc::now().timestamp();
Some(delta.max(0) as u64)
}
pub(crate) const SITE_LICENSE_CODES: [&str; 3] = [
"site_license_missing",
"site_license_expired",
"clock_regression",
];
pub(crate) fn error_code_of(body: &str) -> Option<String> {
serde_json::from_str::<serde_json::Value>(body)
.ok()
.as_ref()
.and_then(|v| v.pointer("/error/code"))
.and_then(serde_json::Value::as_str)
.map(str::to_string)
}
pub(crate) fn body_code_is_site_license(body: &str) -> bool {
error_code_of(body).is_some_and(|code| SITE_LICENSE_CODES.contains(&code.as_str()))
}
#[derive(Debug, Clone)]
pub struct LicenseGate {
pub code: String,
pub licensing_url: Option<String>,
pub at: std::time::SystemTime,
pub until: std::time::Instant,
}
#[derive(Debug, Clone)]
pub struct CloudState {
pub auth_error: Arc<AtomicBool>,
pub forwarded_count: Arc<AtomicU64>,
pub last_sync_secs: Arc<AtomicU64>,
pub drop_count: Arc<AtomicU64>,
pub consecutive_drops: Arc<AtomicU64>,
pub no_credential: Arc<AtomicBool>,
pub consecutive_probe_failures: Arc<AtomicU32>,
pub fallback_drain_notify: Arc<Notify>,
pub outbox_drain_notify: Arc<Notify>,
pub policy_refresh_notify: Arc<Notify>,
pub consecutive_live_drops: Arc<AtomicU64>,
pub live_drops_window_start: Arc<AtomicU64>,
pub emergency_mode: Arc<AtomicBool>,
pub channel_high_water_start_ms: Arc<AtomicU64>,
pub license_gate: Arc<arc_swap::ArcSwapOption<LicenseGate>>,
pub policy_license_refusal: Arc<arc_swap::ArcSwapOption<LicenseGate>>,
}
impl CloudState {
pub fn new() -> Self {
Self {
auth_error: Arc::new(AtomicBool::new(false)),
forwarded_count: Arc::new(AtomicU64::new(0)),
last_sync_secs: Arc::new(AtomicU64::new(0)),
drop_count: Arc::new(AtomicU64::new(0)),
consecutive_drops: Arc::new(AtomicU64::new(0)),
no_credential: Arc::new(AtomicBool::new(true)),
consecutive_probe_failures: Arc::new(AtomicU32::new(0)),
fallback_drain_notify: Arc::new(Notify::new()),
outbox_drain_notify: Arc::new(Notify::new()),
policy_refresh_notify: Arc::new(Notify::new()),
consecutive_live_drops: Arc::new(AtomicU64::new(0)),
live_drops_window_start: Arc::new(AtomicU64::new(0)),
emergency_mode: Arc::new(AtomicBool::new(false)),
channel_high_water_start_ms: Arc::new(AtomicU64::new(0)),
license_gate: Arc::new(arc_swap::ArcSwapOption::empty()),
policy_license_refusal: Arc::new(arc_swap::ArcSwapOption::empty()),
}
}
pub fn notify_drain(&self) {
self.fallback_drain_notify.notify_one();
self.outbox_drain_notify.notify_one();
}
pub fn is_no_credential(&self) -> bool {
self.no_credential.load(Ordering::Relaxed)
}
pub fn set_no_credential(&self, missing: bool) {
self.no_credential.store(missing, Ordering::Relaxed);
}
pub fn is_auth_error(&self) -> bool {
self.auth_error.load(Ordering::Relaxed)
}
pub fn record_successful_forward(&self) {
self.record_successful_forwards(1);
}
pub fn record_successful_forwards(&self, n: u64) {
if n == 0 {
return;
}
self.forwarded_count.fetch_add(n, Ordering::Relaxed);
self.last_sync_secs
.store(now_unix_secs(), Ordering::Relaxed);
self.consecutive_drops.store(0, Ordering::Relaxed);
self.consecutive_live_drops.store(0, Ordering::Relaxed);
self.live_drops_window_start.store(0, Ordering::Relaxed);
}
pub fn record_live_drop(&self) {
let prev = self.consecutive_live_drops.fetch_add(1, Ordering::Relaxed);
if prev == 0 {
self.live_drops_window_start
.store(now_unix_ms(), Ordering::Relaxed);
}
}
pub fn consecutive_live_drops(&self) -> u64 {
self.consecutive_live_drops.load(Ordering::Relaxed)
}
pub fn live_drops_window_start_ms(&self) -> u64 {
self.live_drops_window_start.load(Ordering::Relaxed)
}
pub fn is_emergency_mode(&self) -> bool {
self.emergency_mode.load(Ordering::Relaxed)
}
pub fn note_channel_depth_pct(&self, pct: u64) {
let current = self.channel_high_water_start_ms.load(Ordering::Relaxed);
if pct >= worker::HIGH_WATER_TRIP_PCT {
if current == 0 {
self.channel_high_water_start_ms
.store(now_unix_ms(), Ordering::Relaxed);
}
} else if pct < worker::HIGH_WATER_CLEAR_PCT && current != 0 {
self.channel_high_water_start_ms.store(0, Ordering::Relaxed);
}
}
pub fn channel_high_water_window_ms(&self) -> u64 {
let start = self.channel_high_water_start_ms.load(Ordering::Relaxed);
if start == 0 {
0
} else {
now_unix_ms().saturating_sub(start)
}
}
pub(crate) fn set_emergency_mode(&self, on: bool) {
self.emergency_mode.store(on, Ordering::Relaxed);
}
pub fn clear_auth_error(&self) -> bool {
self.auth_error.swap(false, Ordering::Relaxed)
}
pub fn set_license_gate(
&self,
code: String,
licensing_url: Option<String>,
retry_after: Option<Duration>,
) {
let wait = retry_after.unwrap_or(Duration::from_secs(LICENSE_RETRY_SECS));
self.license_gate.store(Some(Arc::new(LicenseGate {
code,
licensing_url,
at: std::time::SystemTime::now(),
until: std::time::Instant::now() + wait,
})));
}
pub fn license_gate_active(&self) -> bool {
self.license_gate
.load()
.as_ref()
.is_some_and(|gate| gate.until > std::time::Instant::now())
}
pub fn license_gate(&self) -> Option<Arc<LicenseGate>> {
self.license_gate.load_full()
}
pub fn clear_license_gate(&self) -> bool {
self.license_gate.swap(None).is_some()
}
pub fn set_policy_license_refusal(&self, refusal: Option<(String, Option<String>)>) {
self.policy_license_refusal
.store(refusal.map(|(code, licensing_url)| {
Arc::new(LicenseGate {
code,
licensing_url,
at: std::time::SystemTime::now(),
until: std::time::Instant::now(),
})
}));
}
pub fn policy_license_refusal(&self) -> Option<Arc<LicenseGate>> {
self.policy_license_refusal.load_full()
}
pub fn record_health_ok(&self) {
self.consecutive_drops.store(0, Ordering::Relaxed);
self.consecutive_probe_failures.store(0, Ordering::Relaxed);
}
pub fn record_probe_failure(&self) {
self.consecutive_probe_failures
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |n| n.checked_add(1))
.ok();
}
pub fn consecutive_probe_failures(&self) -> u32 {
self.consecutive_probe_failures.load(Ordering::Relaxed)
}
pub fn next_health_delay(&self) -> Duration {
let failures = self.consecutive_probe_failures();
if failures == 0 {
return Duration::from_secs(60);
}
let exp = (failures - 1).min(3);
let base_secs = 5u64.saturating_mul(1u64 << exp);
let capped_secs = base_secs.min(30);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0);
let jitter_pct = 80 + u64::from(nanos % 41); Duration::from_millis(capped_secs * 1000 * jitter_pct / 100)
}
pub fn forwarded_count(&self) -> u64 {
self.forwarded_count.load(Ordering::Relaxed)
}
pub fn last_sync_secs(&self) -> u64 {
self.last_sync_secs.load(Ordering::Relaxed)
}
pub fn record_drop(&self) {
self.record_drops(1);
}
pub fn record_drops(&self, n: u64) {
if n == 0 {
return;
}
self.drop_count.fetch_add(n, Ordering::Relaxed);
self.consecutive_drops.fetch_add(n, Ordering::Relaxed);
}
pub fn drop_count(&self) -> u64 {
self.drop_count.load(Ordering::Relaxed)
}
pub fn consecutive_drops(&self) -> u64 {
self.consecutive_drops.load(Ordering::Relaxed)
}
}
impl Default for CloudState {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn susp_range() -> std::ops::RangeInclusive<u64> {
590..=600
}
#[test]
fn retry_after_parses_delta_seconds() {
assert_eq!(parse_retry_after("120"), Some(120));
assert_eq!(parse_retry_after(" 120 "), Some(120));
}
#[test]
fn retry_after_parses_the_http_date_form() {
let future = chrono::Utc::now() + chrono::Duration::seconds(600);
let header = future.format("%a, %d %b %Y %H:%M:%S GMT").to_string();
let parsed = parse_retry_after(&header).expect("http-date must parse");
assert!(
(susp_range()).contains(&parsed),
"expected ~600s from an http-date, got {parsed}"
);
}
#[test]
fn retry_after_in_the_past_is_zero_not_a_wrapped_negative() {
let past = chrono::Utc::now() - chrono::Duration::seconds(600);
let header = past.format("%a, %d %b %Y %H:%M:%S GMT").to_string();
assert_eq!(parse_retry_after(&header), Some(0));
}
#[test]
fn retry_after_garbage_is_none_not_a_panic() {
assert_eq!(parse_retry_after("soon"), None);
assert_eq!(parse_retry_after(""), None);
assert_eq!(parse_retry_after("-5"), None);
}
#[test]
fn only_the_three_site_codes_make_a_503_a_licence_refusal() {
for code in [
"site_license_missing",
"site_license_expired",
"clock_regression",
] {
let body = format!(r#"{{"error":{{"code":"{code}","message":"nope"}}}}"#);
assert!(body_code_is_site_license(&body), "{code} must be a refusal");
}
assert!(!body_code_is_site_license(
r#"{"error":{"code":"license_expired"}}"#
));
assert!(!body_code_is_site_license(r#"{"error":{}}"#));
assert!(!body_code_is_site_license("{}"));
assert!(!body_code_is_site_license(""));
assert!(!body_code_is_site_license("<html>502 Bad Gateway</html>"));
}
#[test]
fn a_gate_stops_holding_posts_before_it_stops_being_reported() {
let state = CloudState::new();
assert!(!state.license_gate_active());
assert!(state.license_gate().is_none());
state.set_license_gate(
"license_expired".to_string(),
Some("https://app.openlatch.ai/settings/licensing".to_string()),
Some(Duration::from_secs(900)),
);
assert!(state.license_gate_active(), "a fresh gate holds POSTs");
let gate = state.license_gate().expect("the gate is reportable");
assert_eq!(gate.code, "license_expired");
assert_eq!(
gate.licensing_url.as_deref(),
Some("https://app.openlatch.ai/settings/licensing")
);
state.set_license_gate("license_expired".to_string(), None, Some(Duration::ZERO));
assert!(!state.license_gate_active());
assert!(state.license_gate().is_some());
assert!(state.clear_license_gate(), "clearing reports it was set");
assert!(state.license_gate().is_none());
assert!(
!state.clear_license_gate(),
"clearing an absent gate is not a recovery to log"
);
}
#[test]
fn a_gate_with_no_retry_after_waits_the_license_cadence() {
let state = CloudState::new();
state.set_license_gate("license_unknown".to_string(), None, None);
let gate = state.license_gate().expect("gate");
let wait = gate
.until
.saturating_duration_since(std::time::Instant::now());
assert!(
wait > Duration::from_secs(LICENSE_RETRY_SECS - 5)
&& wait <= Duration::from_secs(LICENSE_RETRY_SECS),
"expected ~{LICENSE_RETRY_SECS}s, got {wait:?}"
);
}
#[test]
fn test_cloud_config_default_produces_expected_values() {
let cfg = CloudConfig::default();
assert_eq!(cfg.api_url, "https://app.openlatch.ai");
assert_eq!(cfg.timeout_connect_ms, 5000);
assert_eq!(cfg.timeout_total_ms, 10000);
assert_eq!(cfg.channel_size, 1000);
assert_eq!(cfg.retry_delay_ms, 2000);
assert_eq!(cfg.rate_limit_default_secs, 30);
assert_eq!(cfg.credential_poll_interval_ms, 60_000);
assert_eq!(cfg.fallback_max_bytes, 52_428_800);
}
#[test]
fn test_cloud_error_variants_have_display() {
let e = CloudError::AuthError;
assert!(format!("{e}").contains("auth error"));
let e = CloudError::RateLimit {
retry_after_secs: 42,
};
let s = format!("{e}");
assert!(s.contains("rate limit") && s.contains("42"));
let e = CloudError::ServerError;
assert!(format!("{e}").contains("server error"));
let e = CloudError::Network;
assert!(format!("{e}").contains("network"));
let e = CloudError::ClientError(404);
let s = format!("{e}");
assert!(s.contains("404"));
}
#[test]
fn test_cloud_state_defaults_to_no_auth_error() {
let state = CloudState::new();
assert!(!state.is_auth_error());
}
#[test]
fn test_cloud_state_reflects_atomic_transitions() {
let state = CloudState::new();
state.auth_error.store(true, Ordering::Relaxed);
assert!(state.is_auth_error());
state.auth_error.store(false, Ordering::Relaxed);
assert!(!state.is_auth_error());
}
#[test]
fn test_cloud_state_no_credential_defaults_true_and_toggles() {
let state = CloudState::new();
assert!(
state.is_no_credential(),
"new CloudState must start in no_credential state until the worker polls"
);
state.set_no_credential(false);
assert!(!state.is_no_credential());
state.set_no_credential(true);
assert!(state.is_no_credential());
}
#[tokio::test]
async fn test_notify_drain_wakes_every_consumer() {
let state = CloudState::new();
let fallback_notify = state.fallback_drain_notify.clone();
let outbox_notify = state.outbox_drain_notify.clone();
let fallback_task = tokio::spawn(async move {
fallback_notify.notified().await;
});
let outbox_task = tokio::spawn(async move {
outbox_notify.notified().await;
});
tokio::task::yield_now().await;
tokio::task::yield_now().await;
state.notify_drain();
tokio::time::timeout(Duration::from_secs(2), async {
fallback_task.await.expect("fallback task panicked");
outbox_task.await.expect("outbox task panicked");
})
.await
.expect("one notify_drain() signal must wake both consumers");
}
#[tokio::test]
async fn test_notify_drain_signal_before_wait_is_not_lost() {
let state = CloudState::new();
state.notify_drain();
let fallback_notify = state.fallback_drain_notify.clone();
let outbox_notify = state.outbox_drain_notify.clone();
let fallback_task = tokio::spawn(async move {
fallback_notify.notified().await;
});
let outbox_task = tokio::spawn(async move {
outbox_notify.notified().await;
});
tokio::time::timeout(Duration::from_secs(2), async {
fallback_task.await.expect("fallback task panicked");
outbox_task.await.expect("outbox task panicked");
})
.await
.expect("a signal that landed before either consumer waited must not be dropped");
}
#[test]
fn test_cloud_event_fields_accessible() {
let evt = CloudEvent {
envelope: serde_json::json!({"id": "evt_123"}),
agent_id: "agt_abc".to_string(),
};
assert_eq!(evt.agent_id, "agt_abc");
assert_eq!(evt.envelope["id"], "evt_123");
}
#[test]
fn test_cloud_state_new_initializes_forwarded_count_to_zero() {
let state = CloudState::new();
assert_eq!(state.forwarded_count(), 0);
}
#[test]
fn test_cloud_state_new_initializes_last_sync_secs_to_zero() {
let state = CloudState::new();
assert_eq!(state.last_sync_secs(), 0);
}
#[test]
fn test_cloud_state_record_successful_forward_increments_count() {
let state = CloudState::new();
state.record_successful_forward();
assert_eq!(state.forwarded_count(), 1);
state.record_successful_forward();
assert_eq!(state.forwarded_count(), 2);
}
#[test]
fn test_next_health_delay_healthy_is_sixty_seconds() {
let state = CloudState::new();
assert_eq!(state.next_health_delay(), Duration::from_secs(60));
}
#[test]
fn test_next_health_delay_degraded_in_expected_range() {
let state = CloudState::new();
state.record_probe_failure();
let d = state.next_health_delay();
assert!(d >= Duration::from_millis(4000) && d <= Duration::from_millis(6000));
for _ in 0..3 {
state.record_probe_failure();
}
let d = state.next_health_delay();
assert!(d >= Duration::from_millis(24_000) && d <= Duration::from_millis(36_000));
}
#[test]
fn test_record_live_drop_increments_and_stamps_window_start() {
let state = CloudState::new();
assert_eq!(state.consecutive_live_drops(), 0);
assert_eq!(state.live_drops_window_start_ms(), 0);
state.record_live_drop();
assert_eq!(state.consecutive_live_drops(), 1);
let window_start = state.live_drops_window_start_ms();
assert!(
window_start > 0,
"window start must be stamped on first drop"
);
state.record_live_drop();
state.record_live_drop();
assert_eq!(state.consecutive_live_drops(), 3);
assert_eq!(state.live_drops_window_start_ms(), window_start);
}
#[test]
fn test_record_successful_forward_clears_live_drop_streak() {
let state = CloudState::new();
state.record_live_drop();
state.record_live_drop();
assert_eq!(state.consecutive_live_drops(), 2);
state.record_successful_forward();
assert_eq!(state.consecutive_live_drops(), 0);
assert_eq!(state.live_drops_window_start_ms(), 0);
}
#[test]
fn test_emergency_mode_flag_round_trip() {
let state = CloudState::new();
assert!(!state.is_emergency_mode());
state.set_emergency_mode(true);
assert!(state.is_emergency_mode());
state.set_emergency_mode(false);
assert!(!state.is_emergency_mode());
}
#[test]
fn test_note_channel_depth_pct_hysteresis() {
let state = CloudState::new();
state.note_channel_depth_pct(state_pct_below_trip());
assert_eq!(state.channel_high_water_window_ms(), 0);
state.note_channel_depth_pct(state_pct_above_trip());
let first_start = state.channel_high_water_start_ms.load(Ordering::Relaxed);
assert!(first_start > 0);
state.note_channel_depth_pct(state_pct_in_band());
assert_eq!(
state.channel_high_water_start_ms.load(Ordering::Relaxed),
first_start
);
state.note_channel_depth_pct(state_pct_below_clear());
assert_eq!(state.channel_high_water_start_ms.load(Ordering::Relaxed), 0);
assert_eq!(state.channel_high_water_window_ms(), 0);
}
fn state_pct_below_trip() -> u64 {
worker::HIGH_WATER_TRIP_PCT.saturating_sub(10)
}
fn state_pct_above_trip() -> u64 {
worker::HIGH_WATER_TRIP_PCT + 1
}
fn state_pct_in_band() -> u64 {
(worker::HIGH_WATER_TRIP_PCT + worker::HIGH_WATER_CLEAR_PCT) / 2
}
fn state_pct_below_clear() -> u64 {
worker::HIGH_WATER_CLEAR_PCT.saturating_sub(10)
}
#[test]
fn test_record_health_ok_resets_probe_failures() {
let state = CloudState::new();
state.record_probe_failure();
state.record_probe_failure();
assert_eq!(state.consecutive_probe_failures(), 2);
state.record_health_ok();
assert_eq!(state.consecutive_probe_failures(), 0);
}
#[test]
fn test_cloud_state_record_successful_forward_sets_last_sync_secs() {
let before = now_unix_secs();
let state = CloudState::new();
state.record_successful_forward();
let after = now_unix_secs();
let recorded = state.last_sync_secs();
assert!(
recorded >= before,
"last_sync_secs should be >= before: {recorded} < {before}"
);
assert!(
recorded <= after,
"last_sync_secs should be <= after: {recorded} > {after}"
);
}
}