Files
Aether/crates/aether-usage/runtime/src/runtime.rs
T

14605 lines
539 KiB
Rust

use std::collections::{BTreeMap, HashMap, VecDeque};
use std::future::Future;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex as StdMutex, Weak};
use std::time::{Duration, Instant};
use aether_contracts::ExecutionTelemetry;
use aether_data_contracts::repository::usage::UpsertUsageRecord;
use aether_data_contracts::DataLayerError;
use aether_runtime_state::{RuntimeQueueStats, RuntimeQueueStore};
use async_trait::async_trait;
use futures_util::{FutureExt, StreamExt};
use tokio::sync::mpsc;
use tracing::{info, warn};
use crate::event_capture_budget::{
json_heap_estimate, EventCaptureMemoryBudget, UsageEventCaptureRetention,
};
use crate::executor::spawn_on_usage_background_runtime;
use crate::queue::is_permanent_enqueue_error;
use crate::request_metadata::{
attach_client_request_body_metadata, attach_provider_request_body_metadata,
attach_provider_response_body_metadata, attach_provider_response_model_metadata,
clear_client_request_body_metadata, clear_provider_request_body_metadata,
request_body_derived_facts_action, retain_first_byte_request_metadata,
RequestBodyDerivedFactsAction,
};
use crate::settlement::{
reconcile_usage_policy_cost_for_event_with_result, settle_usage_with_reconciled_cost,
};
use crate::shutdown::{UsageBackgroundTasks, UsageShutdownState};
use crate::worker::{
build_usage_queue_worker_with_record_gate, UsageWorkerControl, UsageWorkerObservation,
};
use crate::{
apply_usage_body_capture_policy_to_event, build_stream_terminal_usage_seed,
build_sync_terminal_usage_seed, build_terminal_usage_event_from_seed,
build_upsert_usage_record_from_event, LifecycleUsageSeed, StreamTerminalUsagePayloadSeed,
SyncTerminalUsagePayloadSeed, TerminalUsageContextSeed, UsageEvent, UsageQueue,
UsageRecordWriter, UsageRuntimeConfig, UsageSettlementWriter,
};
#[async_trait]
pub trait UsageBillingEventEnricher: Send + Sync {
async fn enrich_usage_event(&self, event: &mut UsageEvent) -> Result<(), DataLayerError>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum UsageRequestRecordLevel {
#[default]
Basic,
Full,
}
pub const DEFAULT_USAGE_REQUEST_BODY_CAPTURE_LIMIT_BYTES: usize = usize::MAX;
pub const DEFAULT_USAGE_RESPONSE_BODY_CAPTURE_LIMIT_BYTES: usize = usize::MAX;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UsageBodyCapturePolicy {
pub record_level: UsageRequestRecordLevel,
}
impl Default for UsageBodyCapturePolicy {
fn default() -> Self {
Self {
record_level: UsageRequestRecordLevel::Basic,
}
}
}
#[async_trait]
pub trait UsageRuntimeAccess:
UsageRecordWriter + UsageSettlementWriter + UsageBillingEventEnricher + Send + Sync
{
fn has_usage_writer(&self) -> bool;
fn has_usage_worker_queue(&self) -> bool;
fn usage_worker_queue(&self) -> Option<Arc<dyn RuntimeQueueStore>>;
fn supports_first_byte_usage_fast_path(&self) -> bool {
false
}
fn usage_worker_should_defer_for_database_pressure(&self) -> bool {
false
}
async fn body_capture_policy(&self) -> Result<UsageBodyCapturePolicy, DataLayerError> {
Ok(UsageBodyCapturePolicy::default())
}
async fn request_record_level(&self) -> Result<UsageRequestRecordLevel, DataLayerError> {
Ok(self.body_capture_policy().await?.record_level)
}
}
#[derive(Debug, Clone)]
pub struct UsageRuntime {
config: UsageRuntimeConfig,
shutdown: Arc<UsageShutdownState>,
body_policy_cache: Arc<tokio::sync::Mutex<Option<UsageBodyCapturePolicyCacheEntry>>>,
enqueue_retry: Arc<UsageEnqueueRetryDispatcher>,
worker_supervisor_state: Arc<UsageWorkerSupervisorState>,
worker_record_gate: Option<Arc<UsageWorkerRecordConcurrencyGate>>,
terminal_submission_state: Arc<TerminalSubmissionState>,
terminal_execution: Arc<TerminalExecutionDispatcher>,
terminal_enqueue_state: Arc<LifecycleEnqueueState>,
terminal_direct_fallback_state: Arc<TerminalDirectFallbackState>,
lifecycle_enqueue_state: Arc<LifecycleEnqueueState>,
lifecycle_coalescer: Arc<LifecycleEventCoalescer>,
lifecycle_delay: Arc<LifecycleDelayDispatcher>,
lifecycle_submission: Arc<LifecycleSubmissionDispatcher>,
ordered_lifecycle: Arc<OrderedLifecycleDispatcher>,
pending_persistence: Arc<PendingPersistenceDispatcher>,
first_byte_persistence: Arc<FirstBytePersistenceDispatcher>,
}
#[derive(Debug, Default)]
struct UsageWorkerSupervisorState {
active_count: AtomicUsize,
desired_count: AtomicUsize,
read_batches_total: AtomicU64,
read_entries_total: AtomicU64,
reclaimed_entries_total: AtomicU64,
acked_entries_total: AtomicU64,
dead_lettered_entries_total: AtomicU64,
process_failures_total: AtomicU64,
read_failures_total: AtomicU64,
reclaim_failures_total: AtomicU64,
}
#[derive(Debug)]
pub(crate) struct UsageWorkerRecordConcurrencyGate {
semaphore: tokio::sync::Semaphore,
limit: usize,
in_flight: AtomicUsize,
max_in_flight: AtomicUsize,
wait_total: AtomicU64,
deferred_total: AtomicU64,
}
impl UsageWorkerRecordConcurrencyGate {
pub(crate) fn new(limit: usize) -> Self {
Self {
semaphore: tokio::sync::Semaphore::new(limit.max(1)),
limit: limit.max(1),
in_flight: AtomicUsize::new(0),
max_in_flight: AtomicUsize::new(0),
wait_total: AtomicU64::new(0),
deferred_total: AtomicU64::new(0),
}
}
pub(crate) fn limit(&self) -> usize {
self.limit
}
pub(crate) fn in_flight(&self) -> usize {
self.in_flight.load(Ordering::Acquire)
}
pub(crate) fn max_in_flight(&self) -> usize {
self.max_in_flight.load(Ordering::Acquire)
}
pub(crate) fn wait_total(&self) -> u64 {
self.wait_total.load(Ordering::Acquire)
}
pub(crate) fn deferred_total(&self) -> u64 {
self.deferred_total.load(Ordering::Acquire)
}
pub(crate) fn record_deferred(&self) {
self.deferred_total.fetch_add(1, Ordering::AcqRel);
}
pub(crate) async fn acquire(&self) -> UsageWorkerRecordConcurrencyPermit<'_> {
if self.semaphore.available_permits() == 0 {
self.wait_total.fetch_add(1, Ordering::AcqRel);
}
let permit = self
.semaphore
.acquire()
.await
.expect("usage worker record gate semaphore should not be closed");
let active = self.in_flight.fetch_add(1, Ordering::AcqRel) + 1;
self.max_in_flight.fetch_max(active, Ordering::AcqRel);
UsageWorkerRecordConcurrencyPermit {
gate: self,
_permit: permit,
}
}
fn try_acquire(&self) -> Option<UsageWorkerRecordConcurrencyPermit<'_>> {
let permit = self.semaphore.try_acquire().ok()?;
let active = self.in_flight.fetch_add(1, Ordering::AcqRel) + 1;
self.max_in_flight.fetch_max(active, Ordering::AcqRel);
Some(UsageWorkerRecordConcurrencyPermit {
gate: self,
_permit: permit,
})
}
}
pub(crate) struct UsageWorkerRecordConcurrencyPermit<'a> {
gate: &'a UsageWorkerRecordConcurrencyGate,
_permit: tokio::sync::SemaphorePermit<'a>,
}
impl Drop for UsageWorkerRecordConcurrencyPermit<'_> {
fn drop(&mut self) {
self.gate.in_flight.fetch_sub(1, Ordering::AcqRel);
}
}
#[derive(Debug)]
struct TerminalSubmissionState {
semaphore: Arc<tokio::sync::Semaphore>,
limit: usize,
pending: AtomicUsize,
max_pending: AtomicUsize,
in_flight: AtomicUsize,
max_in_flight: AtomicUsize,
rejected_total: AtomicU64,
}
impl TerminalSubmissionState {
fn new(limit: usize) -> Self {
let limit = limit.clamp(1, TERMINAL_SUBMISSION_MAX_LIMIT);
Self {
semaphore: Arc::new(tokio::sync::Semaphore::new(limit)),
limit,
pending: AtomicUsize::new(0),
max_pending: AtomicUsize::new(0),
in_flight: AtomicUsize::new(0),
max_in_flight: AtomicUsize::new(0),
rejected_total: AtomicU64::new(0),
}
}
fn register_pending(self: &Arc<Self>) -> TerminalSubmissionPendingGuard {
let pending = self.pending.fetch_add(1, Ordering::AcqRel) + 1;
self.max_pending.fetch_max(pending, Ordering::AcqRel);
TerminalSubmissionPendingGuard {
state: Arc::clone(self),
}
}
async fn acquire(self: &Arc<Self>) -> Option<TerminalSubmissionPermit> {
let pending_guard = self.register_pending();
self.acquire_registered(pending_guard).await
}
async fn acquire_registered(
self: &Arc<Self>,
pending_guard: TerminalSubmissionPendingGuard,
) -> Option<TerminalSubmissionPermit> {
let permit = match Arc::clone(&self.semaphore).acquire_owned().await {
Ok(permit) => permit,
Err(_) => {
self.rejected_total.fetch_add(1, Ordering::AcqRel);
return None;
}
};
let active = self.in_flight.fetch_add(1, Ordering::AcqRel) + 1;
self.max_in_flight.fetch_max(active, Ordering::AcqRel);
Some(TerminalSubmissionPermit {
state: Arc::clone(self),
_permit: permit,
_pending_guard: pending_guard,
})
}
fn limit(&self) -> usize {
self.limit
}
fn pending(&self) -> usize {
self.pending.load(Ordering::Acquire)
}
fn max_pending(&self) -> usize {
self.max_pending.load(Ordering::Acquire)
}
fn in_flight(&self) -> usize {
self.in_flight.load(Ordering::Acquire)
}
fn max_in_flight(&self) -> usize {
self.max_in_flight.load(Ordering::Acquire)
}
fn rejected_total(&self) -> u64 {
self.rejected_total.load(Ordering::Acquire)
}
}
struct TerminalSubmissionPendingGuard {
state: Arc<TerminalSubmissionState>,
}
impl Drop for TerminalSubmissionPendingGuard {
fn drop(&mut self) {
self.state.pending.fetch_sub(1, Ordering::AcqRel);
}
}
struct TerminalSubmissionPermit {
state: Arc<TerminalSubmissionState>,
_permit: tokio::sync::OwnedSemaphorePermit,
_pending_guard: TerminalSubmissionPendingGuard,
}
impl Drop for TerminalSubmissionPermit {
fn drop(&mut self) {
self.state.in_flight.fetch_sub(1, Ordering::AcqRel);
// `_pending_guard` is dropped after this method, keeping pending
// non-zero until the active submission has released its permit.
}
}
fn terminal_submission_limit(config: &UsageRuntimeConfig) -> usize {
usize::try_from(config.terminal_submission_max_in_flight)
.unwrap_or(TERMINAL_SUBMISSION_MAX_LIMIT)
.clamp(1, TERMINAL_SUBMISSION_MAX_LIMIT)
}
#[derive(Debug)]
struct TerminalDirectFallbackState {
semaphore: tokio::sync::Semaphore,
limit: usize,
in_flight: AtomicUsize,
max_in_flight: AtomicUsize,
succeeded_total: AtomicU64,
failed_total: AtomicU64,
rejected_total: AtomicU64,
}
impl TerminalDirectFallbackState {
fn new(limit: usize) -> Self {
let limit = limit.max(1);
Self {
semaphore: tokio::sync::Semaphore::new(limit),
limit,
in_flight: AtomicUsize::new(0),
max_in_flight: AtomicUsize::new(0),
succeeded_total: AtomicU64::new(0),
failed_total: AtomicU64::new(0),
rejected_total: AtomicU64::new(0),
}
}
fn try_acquire(&self) -> Option<TerminalDirectFallbackPermit<'_>> {
let permit = match self.semaphore.try_acquire() {
Ok(permit) => permit,
Err(_) => {
self.record_rejected();
return None;
}
};
let active = self.in_flight.fetch_add(1, Ordering::AcqRel) + 1;
self.max_in_flight.fetch_max(active, Ordering::AcqRel);
Some(TerminalDirectFallbackPermit {
state: self,
_permit: permit,
})
}
fn record_succeeded(&self) -> u64 {
self.succeeded_total.fetch_add(1, Ordering::AcqRel) + 1
}
fn record_failed(&self) -> u64 {
self.failed_total.fetch_add(1, Ordering::AcqRel) + 1
}
fn record_rejected(&self) -> u64 {
self.rejected_total.fetch_add(1, Ordering::AcqRel) + 1
}
fn limit(&self) -> usize {
self.limit
}
fn in_flight(&self) -> usize {
self.in_flight.load(Ordering::Acquire)
}
fn max_in_flight(&self) -> usize {
self.max_in_flight.load(Ordering::Acquire)
}
fn succeeded_total(&self) -> u64 {
self.succeeded_total.load(Ordering::Acquire)
}
fn failed_total(&self) -> u64 {
self.failed_total.load(Ordering::Acquire)
}
fn rejected_total(&self) -> u64 {
self.rejected_total.load(Ordering::Acquire)
}
}
struct TerminalDirectFallbackPermit<'a> {
state: &'a TerminalDirectFallbackState,
_permit: tokio::sync::SemaphorePermit<'a>,
}
impl Drop for TerminalDirectFallbackPermit<'_> {
fn drop(&mut self) {
self.state.in_flight.fetch_sub(1, Ordering::AcqRel);
}
}
fn terminal_direct_fallback_limit(config: &UsageRuntimeConfig) -> usize {
config
.worker_record_concurrency_limit
.unwrap_or(TERMINAL_DIRECT_FALLBACK_DEFAULT_MAX_IN_FLIGHT)
.max(1)
}
impl UsageWorkerSupervisorState {
fn record_observation(&self, observation: UsageWorkerObservation) {
if observation.entries_read > 0 || observation.batch_size > 0 {
self.read_batches_total.fetch_add(1, Ordering::AcqRel);
self.read_entries_total.fetch_add(
u64::try_from(observation.entries_read).unwrap_or(u64::MAX),
Ordering::AcqRel,
);
}
self.reclaimed_entries_total.fetch_add(
u64::try_from(observation.reclaimed_entries).unwrap_or(u64::MAX),
Ordering::AcqRel,
);
self.acked_entries_total.fetch_add(
u64::try_from(observation.acked_entries).unwrap_or(u64::MAX),
Ordering::AcqRel,
);
self.dead_lettered_entries_total.fetch_add(
u64::try_from(observation.dead_lettered_entries).unwrap_or(u64::MAX),
Ordering::AcqRel,
);
self.process_failures_total.fetch_add(
u64::try_from(observation.process_failures).unwrap_or(u64::MAX),
Ordering::AcqRel,
);
self.read_failures_total.fetch_add(
u64::try_from(observation.read_failures).unwrap_or(u64::MAX),
Ordering::AcqRel,
);
self.reclaim_failures_total.fetch_add(
u64::try_from(observation.reclaim_failures).unwrap_or(u64::MAX),
Ordering::AcqRel,
);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct UsageRuntimeMetricsSnapshot {
pub enabled: bool,
pub shutdown_started: bool,
pub producers_in_flight: usize,
pub delayed_lifecycle_pending: usize,
pub queue_payload_max_bytes: usize,
pub queue_payload_downgraded_total: u64,
pub queue_payload_rejected_total: u64,
pub queue_read_payload_budget_bytes: usize,
pub queue_read_batch_payload_bytes: usize,
pub queue_read_payload_reserved_bytes: usize,
pub queue_read_payload_waiters: usize,
pub queue_read_payload_wait_total: u64,
pub queue_read_actual_field_bytes_total: u64,
pub queue_read_oversized_entries_total: u64,
pub queue_read_oversized_batches_total: u64,
pub dlq_encoding_budget_bytes: usize,
pub dlq_encoding_max_jobs: usize,
pub dlq_encoding_reserved_bytes: usize,
pub dlq_encoding_active_jobs: usize,
pub dlq_encoding_capacity_rejected_total: u64,
pub dlq_encoding_oversized_rejected_total: u64,
pub dlq_encoding_encoded_total: u64,
pub enqueue_retry_permanent_failure_total: u64,
pub queue_terminal_events: bool,
pub queue_lifecycle_events: bool,
pub worker_count: usize,
pub worker_autoscale_enabled: bool,
pub worker_max_count: usize,
pub worker_record_concurrency_limit: Option<usize>,
pub worker_record_concurrency_in_flight: usize,
pub worker_record_concurrency_max_in_flight: usize,
pub worker_record_concurrency_wait_total: u64,
pub worker_record_deferred_total: u64,
pub worker_active_count: usize,
pub worker_desired_count: usize,
pub worker_read_batches_total: u64,
pub worker_read_entries_total: u64,
pub worker_reclaimed_entries_total: u64,
pub worker_acked_entries_total: u64,
pub worker_dead_lettered_entries_total: u64,
pub worker_process_failures_total: u64,
pub worker_read_failures_total: u64,
pub worker_reclaim_failures_total: u64,
pub retry_deferred_lifecycle_events: bool,
pub terminal_submission_limit: usize,
pub terminal_submission_pending: usize,
pub terminal_submission_max_pending: usize,
pub terminal_submission_in_flight: usize,
pub terminal_submission_max_in_flight: usize,
pub terminal_submission_rejected_total: u64,
pub terminal_enqueue_in_flight: u64,
pub terminal_enqueue_deferred_total: u64,
pub terminal_enqueue_deferred_direct_write_total: u64,
pub terminal_enqueue_deferred_dropped_total: u64,
pub terminal_enqueue_deferred_retry_total: u64,
pub terminal_enqueue_failed_total: u64,
pub terminal_direct_fallback_limit: usize,
pub terminal_direct_fallback_in_flight: usize,
pub terminal_direct_fallback_max_in_flight: usize,
pub terminal_direct_fallback_succeeded_total: u64,
pub terminal_direct_fallback_failed_total: u64,
pub terminal_direct_fallback_rejected_total: u64,
pub lifecycle_enqueue_in_flight: u64,
pub lifecycle_enqueue_deferred_total: u64,
pub lifecycle_enqueue_deferred_dropped_total: u64,
pub lifecycle_enqueue_deferred_retry_total: u64,
pub lifecycle_enqueue_failed_total: u64,
pub lifecycle_submission_capacity: usize,
pub lifecycle_submission_workers: usize,
pub lifecycle_submission_pending: usize,
pub lifecycle_submission_max_pending: usize,
pub lifecycle_submission_enqueued_total: u64,
pub lifecycle_submission_coalesced_total: u64,
pub lifecycle_submission_overflow_total: u64,
pub lifecycle_submission_processed_total: u64,
pub ordered_lifecycle_pending: usize,
pub ordered_lifecycle_max_pending: usize,
pub pending_persistence_capacity: usize,
pub pending_persistence_pending: usize,
pub pending_persistence_max_pending: usize,
pub pending_persistence_batch_flush_total: u64,
pub pending_persistence_batch_records_total: u64,
pub pending_persistence_max_batch_size: usize,
pub pending_persistence_batch_failed_total: u64,
pub pending_persistence_retried_total: u64,
pub pending_persistence_overflow_total: u64,
pub lifecycle_coalescer_entries: usize,
pub lifecycle_coalescer_compact_total: u64,
pub lifecycle_coalescer_compact_entries_scanned_total: u64,
pub first_byte_persistence_capacity: usize,
pub first_byte_persistence_pending: usize,
pub first_byte_persistence_max_pending: usize,
pub first_byte_persistence_dispatched_total: u64,
pub first_byte_persistence_overflow_total: u64,
pub first_byte_persistence_cancelled_total: u64,
pub first_byte_persistence_direct_succeeded_total: u64,
pub first_byte_persistence_direct_failed_total: u64,
pub first_byte_persistence_batch_flush_total: u64,
pub first_byte_persistence_batch_records_total: u64,
pub first_byte_persistence_max_batch_size: usize,
pub first_byte_persistence_batch_failed_total: u64,
pub first_byte_persistence_fallback_accepted_total: u64,
pub first_byte_persistence_fallback_failed_total: u64,
pub enqueue_retry_scheduled_total: u64,
pub enqueue_retry_recovered_total: u64,
pub enqueue_retry_pending: u64,
pub enqueue_retry_failed_total: u64,
pub enqueue_retry_closed_or_unavailable_total: u64,
pub event_capture_memory_budget_bytes: usize,
pub event_capture_memory_retained_bytes: usize,
pub event_capture_memory_downgraded_total: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UsageQueueHealthSnapshot {
pub enabled: bool,
pub configured: bool,
pub stream_key: String,
pub consumer_group: String,
pub dlq_stream_key: String,
pub stream_length: u64,
pub group_pending: u64,
pub group_lag: Option<u64>,
pub oldest_pending_idle_ms: Option<u64>,
pub dlq_length: u64,
}
const USAGE_BODY_CAPTURE_POLICY_CACHE_TTL: Duration = Duration::from_secs(30);
const USAGE_BODY_CAPTURE_POLICY_ERROR_CACHE_TTL: Duration = Duration::from_secs(1);
const LIFECYCLE_ENQUEUE_CIRCUIT_OPEN_MS: u64 = 1_000;
const LIFECYCLE_COALESCER_CLOSE_TTL: Duration = Duration::from_secs(30);
const LIFECYCLE_COALESCER_COMPACT_INTERVAL: Duration = Duration::from_secs(1);
const TERMINAL_SUBMISSION_MAX_LIMIT: usize = 1_048_576;
const TERMINAL_DIRECT_FALLBACK_DEFAULT_MAX_IN_FLIGHT: usize = 32;
const LIFECYCLE_SUBMISSION_MAX_BUFFER: usize = 1_048_576;
const LIFECYCLE_SUBMISSION_MAX_WORKERS: usize = 32;
const PENDING_PERSISTENCE_MAX_BUFFER: usize = 65_536;
const PENDING_PERSISTENCE_BATCH_SIZE: usize = 512;
const PENDING_PERSISTENCE_BATCH_FLUSH_INTERVAL: Duration = Duration::from_millis(1);
const PENDING_PERSISTENCE_MAX_BATCH_CONCURRENCY: usize = 4;
const PENDING_PERSISTENCE_SINGLE_WRITE_TARGET_CONCURRENCY: usize = 32;
// A single-write retry future keeps its slot across backoff. Combined with the
// four outer batch tasks, this bounds non-native writer retries at 32 globally.
const PENDING_PERSISTENCE_SINGLE_WRITE_CONCURRENCY_PER_BATCH: usize =
PENDING_PERSISTENCE_SINGLE_WRITE_TARGET_CONCURRENCY / PENDING_PERSISTENCE_MAX_BATCH_CONCURRENCY;
const PENDING_PERSISTENCE_BATCH_RETRIES_BEFORE_ISOLATION: u64 = 2;
const PENDING_PERSISTENCE_SINGLE_RETRIES_BEFORE_DEGRADE: u64 = 3;
const ORDERED_INTERMEDIATE_RETRIES_BEFORE_DEGRADE: u64 = 3;
const FIRST_BYTE_PERSISTENCE_DEFAULT_CONCURRENCY: usize = 32;
const FIRST_BYTE_PERSISTENCE_MAX_BUFFER: usize = 32_768;
// Keep the first-byte path off the request task while amortizing the single
// transaction used by the PostgreSQL batch writer. The adapter still chunks
// the statement at 128 rows, so this increases rows per commit without
// exceeding PostgreSQL's bind/statement limits.
const FIRST_BYTE_PERSISTENCE_BATCH_SIZE: usize = 512;
const FIRST_BYTE_PERSISTENCE_BATCH_FLUSH_INTERVAL: Duration = Duration::from_millis(1);
const FIRST_BYTE_PERSISTENCE_MAX_BATCH_CONCURRENCY: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TerminalPersistenceOutcome {
Queued,
PersistedDirectly,
BufferedForRetry,
Failed,
}
const LIFECYCLE_COALESCER_SHARD_COUNT: usize = 64;
#[derive(Debug)]
struct LifecycleEventCoalescerShard {
entries: tokio::sync::Mutex<HashMap<String, LifecycleEventCoalescerEntry>>,
next_compaction_at: StdMutex<Option<Instant>>,
}
impl Default for LifecycleEventCoalescerShard {
fn default() -> Self {
Self {
entries: tokio::sync::Mutex::new(HashMap::new()),
next_compaction_at: StdMutex::new(None),
}
}
}
#[derive(Debug)]
struct LifecycleEventCoalescer {
shards: Vec<LifecycleEventCoalescerShard>,
admission: Arc<tokio::sync::Semaphore>,
generation_counter: AtomicU64,
entry_count: AtomicUsize,
rejected_total: AtomicU64,
compact_total: AtomicU64,
compact_entries_scanned_total: AtomicU64,
}
impl Default for LifecycleEventCoalescer {
fn default() -> Self {
Self::new(UsageRuntimeConfig::default().enqueue_retry_buffer_capacity)
}
}
impl LifecycleEventCoalescer {
fn new(capacity: usize) -> Self {
let capacity = capacity.clamp(1, LIFECYCLE_SUBMISSION_MAX_BUFFER);
Self {
shards: (0..LIFECYCLE_COALESCER_SHARD_COUNT)
.map(|_| LifecycleEventCoalescerShard::default())
.collect(),
admission: Arc::new(tokio::sync::Semaphore::new(capacity)),
generation_counter: AtomicU64::new(0),
entry_count: AtomicUsize::new(0),
rejected_total: AtomicU64::new(0),
compact_total: AtomicU64::new(0),
compact_entries_scanned_total: AtomicU64::new(0),
}
}
}
#[derive(Debug)]
struct LifecycleEventCoalescerEntry {
generation: u64,
terminal_seen_at: Option<Instant>,
terminal_cancels_first_byte: bool,
first_byte_seen_at: Option<Instant>,
first_byte_persistence_pending: bool,
_admission: tokio::sync::OwnedSemaphorePermit,
}
impl LifecycleEventCoalescer {
fn try_new_entry(&self) -> Option<LifecycleEventCoalescerEntry> {
let admission = Arc::clone(&self.admission).try_acquire_owned().ok()?;
Some(LifecycleEventCoalescerEntry {
generation: 0,
terminal_seen_at: None,
terminal_cancels_first_byte: false,
first_byte_seen_at: None,
first_byte_persistence_pending: false,
_admission: admission,
})
}
fn record_rejected(&self) {
self.rejected_total.fetch_add(1, Ordering::AcqRel);
}
async fn register(&self, request_id: String) -> Option<u64> {
loop {
let now = Instant::now();
let shard_index = self.shard_index(&request_id);
let shard = &self.shards[shard_index];
let mut entries = shard.entries.lock().await;
self.compact_if_due(shard_index, &mut entries, now);
let before = entries.len();
if !entries.contains_key(&request_id) {
let Some(entry) = self.try_new_entry() else {
drop(entries);
if self.evict_completed_first_byte_entry(&request_id).await {
continue;
}
self.record_rejected();
return None;
};
entries.insert(request_id.clone(), entry);
}
let entry = entries
.get_mut(&request_id)
.expect("coalescer entry should exist after admission");
if lifecycle_event_is_closed(entry, now) {
self.adjust_entry_count(before, entries.len());
return None;
}
entry.generation = self.next_generation();
entry.terminal_seen_at = None;
entry.terminal_cancels_first_byte = false;
entry.first_byte_seen_at = None;
entry.first_byte_persistence_pending = false;
let generation = entry.generation;
self.adjust_entry_count(before, entries.len());
return Some(generation);
}
}
#[cfg(test)]
async fn cancel(&self, request_id: &str) {
loop {
if !self.ensure_terminal_entry(request_id).await {
return;
}
let now = Instant::now();
let shard = &self.shards[self.shard_index(request_id)];
let mut entries = shard.entries.lock().await;
let Some(entry) = entries.get_mut(request_id) else {
continue;
};
entry.generation = self.next_generation();
entry.terminal_seen_at = Some(now);
entry.terminal_cancels_first_byte = true;
entry.first_byte_persistence_pending = false;
return;
}
}
async fn cancel_delayed_for_queued_terminal(&self, request_id: &str) {
loop {
if !self.ensure_terminal_entry(request_id).await {
return;
}
let now = Instant::now();
let shard = &self.shards[self.shard_index(request_id)];
let mut entries = shard.entries.lock().await;
let Some(entry) = entries.get_mut(request_id) else {
continue;
};
if hard_terminal_cancel_is_active(entry, now) {
return;
}
if !entry.first_byte_persistence_pending {
entry.generation = self.next_generation();
}
entry.terminal_seen_at = Some(now);
entry.terminal_cancels_first_byte = false;
return;
}
}
async fn mark_first_byte(&self, request_id: &str) -> Option<u64> {
loop {
let now = Instant::now();
let shard_index = self.shard_index(request_id);
let shard = &self.shards[shard_index];
let mut entries = shard.entries.lock().await;
self.compact_if_due(shard_index, &mut entries, now);
let before = entries.len();
if !entries.contains_key(request_id) {
let Some(entry) = self.try_new_entry() else {
drop(entries);
if self.evict_completed_first_byte_entry(request_id).await {
continue;
}
self.record_rejected();
return None;
};
entries.insert(request_id.to_string(), entry);
}
let entry = entries
.get_mut(request_id)
.expect("coalescer first-byte entry should exist after admission");
if hard_terminal_cancel_is_active(entry, now) || first_byte_marker_is_active(entry, now)
{
self.adjust_entry_count(before, entries.len());
return None;
}
entry.generation = self.next_generation();
entry.first_byte_seen_at = Some(now);
entry.first_byte_persistence_pending = true;
let generation = entry.generation;
self.adjust_entry_count(before, entries.len());
return Some(generation);
}
}
async fn complete_first_byte(&self, request_id: &str, generation: u64) {
let shard = &self.shards[self.shard_index(request_id)];
let mut entries = shard.entries.lock().await;
if let Some(entry) = entries.get_mut(request_id).filter(|entry| {
entry.generation == generation
&& entry.first_byte_persistence_pending
&& !entry.terminal_cancels_first_byte
}) {
entry.first_byte_seen_at = Some(Instant::now());
entry.first_byte_persistence_pending = false;
}
}
async fn rollback_first_byte(&self, request_id: &str, generation: u64) {
let now = Instant::now();
let shard = &self.shards[self.shard_index(request_id)];
let mut entries = shard.entries.lock().await;
let before = entries.len();
if let Some(entry) = entries
.get_mut(request_id)
.filter(|entry| entry.generation == generation && entry.first_byte_persistence_pending)
{
if terminal_cancel_is_active(entry, now) {
entry.generation = self.next_generation();
entry.first_byte_seen_at = None;
entry.first_byte_persistence_pending = false;
} else {
entries.remove(request_id);
}
}
self.adjust_entry_count(before, entries.len());
}
async fn first_byte_is_current(&self, request_id: &str, generation: u64) -> bool {
let now = Instant::now();
self.shards[self.shard_index(request_id)]
.entries
.lock()
.await
.get(request_id)
.is_some_and(|entry| {
entry.generation == generation
&& entry.first_byte_persistence_pending
&& !hard_terminal_cancel_is_active(entry, now)
})
}
async fn abandon(&self, request_id: &str, generation: u64) {
let shard = &self.shards[self.shard_index(request_id)];
let mut entries = shard.entries.lock().await;
let before = entries.len();
if entries
.get(request_id)
.is_some_and(|entry| entry.generation == generation)
{
entries.remove(request_id);
}
self.adjust_entry_count(before, entries.len());
}
async fn should_emit(&self, request_id: &str, generation: u64) -> bool {
let now = Instant::now();
let shard_index = self.shard_index(request_id);
let shard = &self.shards[shard_index];
let mut entries = shard.entries.lock().await;
let before = entries.len();
let Some(entry) = entries.get(request_id) else {
return false;
};
let closed = lifecycle_event_is_closed(entry, now);
let should_emit = entry.generation == generation && !closed;
if should_emit {
entries.remove(request_id);
}
self.adjust_entry_count(before, entries.len());
self.compact_if_due(shard_index, &mut entries, now);
should_emit
}
async fn ensure_terminal_entry(&self, request_id: &str) -> bool {
loop {
let shard_index = self.shard_index(request_id);
let shard = &self.shards[shard_index];
let mut entries = shard.entries.lock().await;
self.compact_if_due(shard_index, &mut entries, Instant::now());
if entries.contains_key(request_id) {
return true;
}
if let Some(entry) = self.try_new_entry() {
let before = entries.len();
entries.insert(request_id.to_string(), entry);
self.adjust_entry_count(before, entries.len());
return true;
}
drop(entries);
if !self.evict_for_terminal_entry(request_id).await {
self.record_rejected();
return false;
}
}
}
async fn evict_for_terminal_entry(&self, excluded_request_id: &str) -> bool {
let now = Instant::now();
let start = self.shard_index(excluded_request_id);
for offset in 0..self.shards.len() {
let shard = &self.shards[(start + offset) % self.shards.len()];
let mut entries = shard.entries.lock().await;
let candidate = entries
.iter()
.find(|(request_id, entry)| {
// A terminal supersedes a delayed intermediate, but never another active
// terminal marker or an in-flight first-byte persistence operation.
request_id.as_str() != excluded_request_id
&& !entry.first_byte_persistence_pending
&& !terminal_cancel_is_active(entry, now)
})
.map(|(request_id, _)| request_id.clone());
if let Some(request_id) = candidate {
let before = entries.len();
entries.remove(&request_id);
self.adjust_entry_count(before, entries.len());
return true;
}
}
false
}
async fn evict_completed_first_byte_entry(&self, excluded_request_id: &str) -> bool {
let now = Instant::now();
let start = self.shard_index(excluded_request_id);
for offset in 0..self.shards.len() {
let shard = &self.shards[(start + offset) % self.shards.len()];
let mut entries = shard.entries.lock().await;
let candidate = entries
.iter()
.find(|(request_id, entry)| {
request_id.as_str() != excluded_request_id
&& !entry.first_byte_persistence_pending
&& entry.first_byte_seen_at.is_some()
&& !terminal_cancel_is_active(entry, now)
})
.map(|(request_id, _)| request_id.clone());
if let Some(request_id) = candidate {
let before = entries.len();
entries.remove(&request_id);
self.adjust_entry_count(before, entries.len());
return true;
}
}
false
}
fn next_generation(&self) -> u64 {
self.generation_counter
.fetch_add(1, Ordering::Relaxed)
.wrapping_add(1)
}
fn shard_index(&self, request_id: &str) -> usize {
retry_worker_index(request_id, self.shards.len())
}
fn adjust_entry_count(&self, before: usize, after: usize) {
match after.cmp(&before) {
std::cmp::Ordering::Greater => {
self.entry_count
.fetch_add(after.saturating_sub(before), Ordering::AcqRel);
}
std::cmp::Ordering::Less => {
self.entry_count
.fetch_sub(before.saturating_sub(after), Ordering::AcqRel);
}
std::cmp::Ordering::Equal => {}
}
}
fn compact_if_due(
&self,
shard_index: usize,
entries: &mut HashMap<String, LifecycleEventCoalescerEntry>,
now: Instant,
) {
let shard = &self.shards[shard_index];
let Ok(mut next_compaction_at) = shard.next_compaction_at.lock() else {
return;
};
if next_compaction_at.is_some_and(|deadline| now < deadline) {
return;
}
*next_compaction_at = Some(now + LIFECYCLE_COALESCER_COMPACT_INTERVAL);
if entries.is_empty() {
return;
}
let before = entries.len();
self.compact_total.fetch_add(1, Ordering::AcqRel);
self.compact_entries_scanned_total.fetch_add(
u64::try_from(entries.len()).unwrap_or(u64::MAX),
Ordering::AcqRel,
);
Self::compact_locked(entries, now);
self.adjust_entry_count(before, entries.len());
}
fn compact_locked(entries: &mut HashMap<String, LifecycleEventCoalescerEntry>, now: Instant) {
entries.retain(|_, entry| {
let has_close_marker =
entry.terminal_seen_at.is_some() || entry.first_byte_seen_at.is_some();
!has_close_marker || lifecycle_event_is_closed(entry, now)
});
}
}
async fn run_lifecycle_coalescer_compactor(coalescer: Weak<LifecycleEventCoalescer>) {
let mut interval = tokio::time::interval(LIFECYCLE_COALESCER_COMPACT_INTERVAL);
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
interval.tick().await;
loop {
interval.tick().await;
let Some(coalescer) = coalescer.upgrade() else {
break;
};
let now = Instant::now();
for shard_index in 0..coalescer.shards.len() {
let shard = &coalescer.shards[shard_index];
let mut entries = shard.entries.lock().await;
coalescer.compact_if_due(shard_index, &mut entries, now);
}
}
}
fn terminal_cancel_is_active(entry: &LifecycleEventCoalescerEntry, now: Instant) -> bool {
entry
.terminal_seen_at
.is_some_and(|seen_at| now.duration_since(seen_at) <= LIFECYCLE_COALESCER_CLOSE_TTL)
}
fn hard_terminal_cancel_is_active(entry: &LifecycleEventCoalescerEntry, now: Instant) -> bool {
entry.terminal_cancels_first_byte && terminal_cancel_is_active(entry, now)
}
fn first_byte_marker_is_active(entry: &LifecycleEventCoalescerEntry, now: Instant) -> bool {
entry.first_byte_persistence_pending
|| entry
.first_byte_seen_at
.is_some_and(|seen_at| now.duration_since(seen_at) <= LIFECYCLE_COALESCER_CLOSE_TTL)
}
fn lifecycle_event_is_closed(entry: &LifecycleEventCoalescerEntry, now: Instant) -> bool {
terminal_cancel_is_active(entry, now) || first_byte_marker_is_active(entry, now)
}
fn make_first_byte_event_lightweight(mut event: UsageEvent) -> UsageEvent {
let data = &mut event.data;
data.request_headers = None;
data.request_body = None;
data.provider_request_headers = None;
data.provider_request_body = None;
data.response_headers = None;
data.response_body = None;
data.client_response_headers = None;
data.client_response_body = None;
data.request_metadata = retain_first_byte_request_metadata(data.request_metadata.take());
event
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum LifecycleSubmissionPriority {
Pending,
Streaming,
FirstByte,
Terminal,
}
type LifecycleAdmissionPermit = Arc<tokio::sync::OwnedSemaphorePermit>;
#[derive(Debug)]
struct LifecycleSubmissionState {
capacity: usize,
workers: usize,
admission: Arc<tokio::sync::Semaphore>,
pending: AtomicUsize,
max_pending: AtomicUsize,
enqueued_total: AtomicU64,
coalesced_total: AtomicU64,
overflow_total: AtomicU64,
processed_total: AtomicU64,
}
impl LifecycleSubmissionState {
fn new(capacity: usize, workers: usize) -> Self {
Self {
capacity,
workers,
admission: Arc::new(tokio::sync::Semaphore::new(capacity)),
..Self::default()
}
}
fn try_admit(&self) -> Option<LifecycleAdmissionPermit> {
Arc::clone(&self.admission)
.try_acquire_owned()
.ok()
.map(Arc::new)
}
async fn admit(&self) -> Option<LifecycleAdmissionPermit> {
Arc::clone(&self.admission)
.acquire_owned()
.await
.ok()
.map(Arc::new)
}
fn record_enqueued(&self) -> usize {
self.enqueued_total.fetch_add(1, Ordering::AcqRel);
let pending = self.pending.fetch_add(1, Ordering::AcqRel) + 1;
self.max_pending.fetch_max(pending, Ordering::AcqRel);
pending
}
fn record_processed(&self) {
self.pending.fetch_sub(1, Ordering::AcqRel);
self.processed_total.fetch_add(1, Ordering::AcqRel);
}
fn record_coalesced(&self) {
self.coalesced_total.fetch_add(1, Ordering::AcqRel);
}
}
impl Default for LifecycleSubmissionState {
fn default() -> Self {
Self {
capacity: 0,
workers: 0,
admission: Arc::new(tokio::sync::Semaphore::new(0)),
pending: AtomicUsize::new(0),
max_pending: AtomicUsize::new(0),
enqueued_total: AtomicU64::new(0),
coalesced_total: AtomicU64::new(0),
overflow_total: AtomicU64::new(0),
processed_total: AtomicU64::new(0),
}
}
}
struct LifecycleSubmissionEnvelope {
item: Box<dyn LifecycleSubmissionItem>,
admission: LifecycleAdmissionPermit,
}
#[async_trait]
trait LifecycleSubmissionItem: Send {
fn request_id(&self) -> &str;
fn priority(&self) -> LifecycleSubmissionPriority;
async fn execute(self: Box<Self>, admission: LifecycleAdmissionPermit);
}
struct LifecycleSubmissionBarrierItem {
request_id: String,
ordered_lifecycle: Arc<OrderedLifecycleDispatcher>,
completion: tokio::sync::oneshot::Sender<OrderedLifecycleCompletion>,
}
#[async_trait]
impl LifecycleSubmissionItem for LifecycleSubmissionBarrierItem {
fn request_id(&self) -> &str {
&self.request_id
}
fn priority(&self) -> LifecycleSubmissionPriority {
LifecycleSubmissionPriority::Terminal
}
async fn execute(self: Box<Self>, admission: LifecycleAdmissionPermit) {
self.ordered_lifecycle.dispatch(
Box::new(OrderedLifecycleBarrierItem {
request_id: self.request_id,
completion: self.completion,
}),
admission,
);
}
}
enum LifecycleSubmissionPayload {
Pending {
seed: LifecycleUsageSeed,
observed_at_unix_secs: u64,
},
Streaming {
seed: LifecycleUsageSeed,
status_code: u16,
telemetry: Option<ExecutionTelemetry>,
observed_at_unix_secs: u64,
},
Active {
seed: LifecycleUsageSeed,
observed_at_unix_secs: u64,
},
TerminalSeed {
seed: LifecycleTerminalUsageSeed,
observed_at_unix_ms: u64,
},
Terminal {
event: UsageEvent,
direct: bool,
completion: Option<tokio::sync::oneshot::Sender<()>>,
},
}
enum LifecycleTerminalUsageSeed {
Sync {
context: TerminalUsageContextSeed,
payload: SyncTerminalUsagePayloadSeed,
capture: Option<TerminalSeedCaptureRetention>,
},
Stream {
context: TerminalUsageContextSeed,
payload: StreamTerminalUsagePayloadSeed,
cancelled: bool,
capture: Option<TerminalSeedCaptureRetention>,
},
Prepared {
kind: TerminalSeedKind,
result: Result<UsageEvent, DataLayerError>,
},
#[cfg(test)]
BlockedBuild {
event: UsageEvent,
started: Arc<tokio::sync::Notify>,
release: Arc<tokio::sync::Notify>,
},
}
#[derive(Clone, Copy)]
enum TerminalSeedKind {
Sync,
Stream,
}
struct TerminalSeedCaptureRetention {
budget: Arc<EventCaptureMemoryBudget>,
retention: UsageEventCaptureRetention,
}
impl TerminalSeedCaptureRetention {
fn try_reserve(
bodies: [Option<&serde_json::Value>; 4],
budget: Arc<EventCaptureMemoryBudget>,
) -> Option<Self> {
let bytes = bodies.into_iter().flatten().fold(0usize, |bytes, body| {
bytes
.saturating_add(std::mem::size_of::<serde_json::Value>())
.saturating_add(json_heap_estimate(body))
});
let mut retention = UsageEventCaptureRetention::default();
retention
.reserve(Arc::clone(&budget), bytes)
.then_some(Self { budget, retention })
}
fn attach(self, event: &mut UsageEvent) {
// The builder moves seed bodies into an unmanaged event. Transfer its existing
// reservation before resizing so concurrent seeds cannot claim the same bytes.
event.data.capture_retention = self.retention;
prepare_event_capture_memory(event, self.budget);
}
}
impl LifecycleTerminalUsageSeed {
fn prepare_capture_memory(self, budget: Arc<EventCaptureMemoryBudget>) -> Self {
let (kind, result) = match self {
Self::Sync {
context,
payload,
capture: None,
} => {
let capture = TerminalSeedCaptureRetention::try_reserve(
[
context.request_body.as_ref(),
context.provider_request.as_ref(),
payload.provider_response_full.as_ref(),
payload.client_response.as_ref(),
],
Arc::clone(&budget),
);
if capture.is_some() {
return Self::Sync {
context,
payload,
capture,
};
}
(
TerminalSeedKind::Sync,
catch_unwind(AssertUnwindSafe(|| {
build_terminal_usage_event_from_seed(build_sync_terminal_usage_seed(
context, payload,
))
})),
)
}
Self::Stream {
context,
payload,
cancelled,
capture: None,
} => {
let capture = TerminalSeedCaptureRetention::try_reserve(
[
context.request_body.as_ref(),
context.provider_request.as_ref(),
payload.provider_response_full.as_ref(),
payload.client_response.as_ref(),
],
Arc::clone(&budget),
);
if capture.is_some() {
return Self::Stream {
context,
payload,
cancelled,
capture,
};
}
(
TerminalSeedKind::Stream,
catch_unwind(AssertUnwindSafe(|| {
build_terminal_usage_event_from_seed(build_stream_terminal_usage_seed(
context, payload, cancelled,
))
})),
)
}
prepared => return prepared,
};
// These seeds still contain token fallbacks, image estimates and terminal
// evidence. Resolve the existing pure builder before discarding diagnostics.
// Keep failures queued so their ordering and error handling remain unchanged.
let result = result
.unwrap_or_else(|_| {
Err(DataLayerError::UnexpectedValue(
"usage builder panicked while preparing capture budget".to_string(),
))
})
.map(|mut event| {
prepare_event_capture_memory(&mut event, budget);
event
});
Self::Prepared { kind, result }
}
async fn build(self, request_id: &str) -> Result<UsageEvent, DataLayerError> {
let (kind, result) = match self {
Self::Sync {
context,
payload,
capture,
} => (
TerminalSeedKind::Sync,
build_sync_terminal_usage_event_offthread(context, payload, capture).await,
),
Self::Stream {
context,
payload,
cancelled,
capture,
} => (
TerminalSeedKind::Stream,
build_stream_terminal_usage_event_offthread(context, payload, cancelled, capture)
.await,
),
Self::Prepared { kind, result } => (kind, result),
#[cfg(test)]
Self::BlockedBuild {
event,
started,
release,
} => {
started.notify_one();
release.notified().await;
return Ok(event);
}
};
if let Err(err) = &result {
match kind {
TerminalSeedKind::Sync => {
warn!(
event_name = "usage_sync_terminal_build_failed",
log_type = "event",
request_id,
error = %err,
"usage runtime failed to build sync terminal usage event"
);
}
TerminalSeedKind::Stream => {
warn!(
event_name = "usage_stream_terminal_build_failed",
log_type = "event",
request_id,
error = %err,
"usage runtime failed to build stream terminal usage event"
);
}
}
}
result
}
}
enum TerminalExecutionPayload {
Seed {
seed: LifecycleTerminalUsageSeed,
observed_at_unix_ms: u64,
},
Event {
event: UsageEvent,
direct: bool,
completion: Option<tokio::sync::oneshot::Sender<()>>,
},
}
#[async_trait]
trait TerminalExecutionItem: Send {
fn request_id(&self) -> &str;
async fn execute(self: Box<Self>);
}
struct TerminalExecutionBarrierItem {
request_id: String,
completion: tokio::sync::oneshot::Sender<OrderedLifecycleCompletion>,
ordered_completion: OrderedLifecycleCompletion,
}
#[async_trait]
impl TerminalExecutionItem for TerminalExecutionBarrierItem {
fn request_id(&self) -> &str {
&self.request_id
}
async fn execute(self: Box<Self>) {
let _ = self.completion.send(self.ordered_completion);
}
}
struct TerminalExecutionItemImpl<T> {
runtime: UsageRuntime,
data: T,
request_id: String,
payload: TerminalExecutionPayload,
pending_guard: TerminalSubmissionPendingGuard,
ordered_completion: OrderedLifecycleCompletion,
}
#[async_trait]
impl<T> TerminalExecutionItem for TerminalExecutionItemImpl<T>
where
T: UsageRuntimeAccess + Clone + 'static,
{
fn request_id(&self) -> &str {
&self.request_id
}
async fn execute(self: Box<Self>) {
let Self {
runtime,
data,
request_id,
payload,
pending_guard,
ordered_completion,
} = *self;
let Some(submission_permit) = runtime
.begin_registered_terminal_submission(&request_id, pending_guard)
.await
else {
return;
};
let (event, direct, completion) = match payload {
TerminalExecutionPayload::Seed {
seed,
observed_at_unix_ms,
} => {
let Ok(mut event) = seed.build(&request_id).await else {
return;
};
event.timestamp_ms = observed_at_unix_ms;
(event, false, None)
}
TerminalExecutionPayload::Event {
event,
direct,
completion,
} => (event, direct, completion),
};
let mut event = event;
runtime
.apply_body_capture_policy_from_data(&data, &mut event)
.await;
let persistence_outcome = runtime
.persist_ordered_terminal_event(&data, event, direct)
.await;
drop(submission_permit);
if persistence_outcome == TerminalPersistenceOutcome::Failed {
warn!(
event_name = "usage_ordered_terminal_persistence_failed",
log_type = "ops",
request_id,
fallback = "advance_after_failed_terminal",
"ordered terminal persistence failed; releasing its admission so later phases can advance"
);
return;
}
ordered_completion.complete();
if let Some(completion) = completion {
let _ = completion.send(());
}
}
}
struct TerminalExecutionDispatcher {
enabled: bool,
}
impl std::fmt::Debug for TerminalExecutionDispatcher {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("TerminalExecutionDispatcher")
.field("enabled", &self.enabled)
.finish()
}
}
impl TerminalExecutionDispatcher {
fn disabled() -> Arc<Self> {
Arc::new(Self { enabled: false })
}
fn spawn(config: &UsageRuntimeConfig) -> Arc<Self> {
Arc::new(Self {
enabled: config.enabled,
})
}
/// Per-request order is already enforced by `OrderedLifecycleDispatcher`. Dispatch each
/// admitted item independently so unrelated request IDs can use the configured terminal
/// semaphore concurrently without hash-shard head-of-line blocking.
fn dispatch_ordered_now(
&self,
item: Box<dyn TerminalExecutionItem>,
) -> Result<(), Box<dyn TerminalExecutionItem>> {
if !self.enabled {
return Err(item);
}
spawn_on_usage_background_runtime(async move {
execute_terminal_item_isolated(item, "ordered_task").await;
});
Ok(())
}
}
#[cfg(test)]
async fn run_terminal_execution_worker(
mut receiver: mpsc::UnboundedReceiver<Box<dyn TerminalExecutionItem>>,
) {
while let Some(item) = receiver.recv().await {
execute_terminal_item_isolated(item, "shard_worker").await;
}
}
async fn execute_terminal_item_isolated(
item: Box<dyn TerminalExecutionItem>,
execution_path: &'static str,
) {
let request_id = item.request_id().to_string();
if AssertUnwindSafe(item.execute())
.catch_unwind()
.await
.is_err()
{
warn!(
event_name = "usage_terminal_execution_panicked",
log_type = "ops",
request_id,
execution_path,
fallback = "advance_after_panicked_terminal",
"usage terminal execution panicked; releasing its admission while the worker continues"
);
}
}
struct LifecycleSubmissionItemImpl<T> {
runtime: UsageRuntime,
data: T,
request_id: String,
payload: LifecycleSubmissionPayload,
}
#[async_trait]
impl<T> LifecycleSubmissionItem for LifecycleSubmissionItemImpl<T>
where
T: UsageRuntimeAccess + Clone + 'static,
{
fn request_id(&self) -> &str {
&self.request_id
}
fn priority(&self) -> LifecycleSubmissionPriority {
match &self.payload {
LifecycleSubmissionPayload::Pending { .. } => LifecycleSubmissionPriority::Pending,
LifecycleSubmissionPayload::Streaming { telemetry, .. }
if telemetry.as_ref().and_then(|value| value.ttfb_ms).is_some() =>
{
LifecycleSubmissionPriority::FirstByte
}
LifecycleSubmissionPayload::Streaming { .. } => LifecycleSubmissionPriority::Streaming,
LifecycleSubmissionPayload::Active { .. } => LifecycleSubmissionPriority::Streaming,
LifecycleSubmissionPayload::TerminalSeed { .. } => {
LifecycleSubmissionPriority::Terminal
}
LifecycleSubmissionPayload::Terminal { .. } => LifecycleSubmissionPriority::Terminal,
}
}
async fn execute(self: Box<Self>, admission: LifecycleAdmissionPermit) {
let Self {
runtime,
data,
request_id,
payload,
} = *self;
let (phase, result) = match payload {
LifecycleSubmissionPayload::Pending {
seed,
observed_at_unix_secs,
} => (
"pending",
crate::write::build_pending_usage_event_from_owned_seed(
seed,
observed_at_unix_secs,
),
),
LifecycleSubmissionPayload::Streaming {
seed,
status_code,
telemetry,
observed_at_unix_secs,
} => (
"streaming",
crate::write::build_streaming_usage_event_from_owned_seed(
seed,
status_code,
telemetry,
observed_at_unix_secs,
),
),
LifecycleSubmissionPayload::Active {
seed,
observed_at_unix_secs,
} => (
"streaming",
crate::write::build_active_usage_event_from_owned_seed(seed, observed_at_unix_secs),
),
LifecycleSubmissionPayload::TerminalSeed {
seed,
observed_at_unix_ms,
} => {
let ordered_lifecycle = Arc::clone(&runtime.ordered_lifecycle);
ordered_lifecycle.dispatch(
Box::new(OrderedLifecycleItemImpl {
runtime,
data,
request_id,
payload: OrderedLifecyclePayload::TerminalSeed {
seed,
observed_at_unix_ms,
},
}),
admission,
);
return;
}
LifecycleSubmissionPayload::Terminal {
event,
direct,
completion,
} => {
let ordered_lifecycle = Arc::clone(&runtime.ordered_lifecycle);
ordered_lifecycle.dispatch(
Box::new(OrderedLifecycleItemImpl {
runtime,
data,
request_id,
payload: OrderedLifecyclePayload::Terminal {
event,
direct,
completion,
},
}),
admission,
);
return;
}
};
match result {
Ok(mut event) => {
runtime
.apply_body_capture_policy_from_data(&data, &mut event)
.await;
let payload = if event.event_type == crate::UsageEventType::Pending {
OrderedLifecyclePayload::Pending { event }
} else {
OrderedLifecyclePayload::Streaming { event }
};
let ordered_lifecycle = Arc::clone(&runtime.ordered_lifecycle);
ordered_lifecycle.dispatch(
Box::new(OrderedLifecycleItemImpl {
runtime,
data,
request_id,
payload,
}),
admission,
);
}
Err(err) => {
warn!(
event_name = "usage_lifecycle_submission_build_failed",
log_type = "event",
request_id = %request_id,
lifecycle_phase = phase,
error = %err,
"usage runtime failed to build a lifecycle event"
);
// Preserve ordering through the dispatcher, then degrade this malformed phase so
// it cannot retain admission or poison the request permanently.
runtime.ordered_lifecycle.dispatch(
Box::new(OrderedLifecycleDegradedItem { request_id }),
admission,
);
}
}
}
}
struct LifecycleSubmissionShard {
sender: mpsc::UnboundedSender<String>,
slots: Arc<StdMutex<HashMap<String, LifecycleSubmissionSlot>>>,
fallback_running: Arc<AtomicBool>,
}
#[derive(Default)]
struct LifecycleSubmissionSlot {
pending: Option<LifecycleSubmissionEnvelope>,
streaming: Option<(LifecycleSubmissionPriority, LifecycleSubmissionEnvelope)>,
terminal: Vec<LifecycleSubmissionEnvelope>,
}
impl LifecycleSubmissionSlot {
fn insert(&mut self, envelope: LifecycleSubmissionEnvelope) -> bool {
let priority = envelope.item.priority();
match priority {
LifecycleSubmissionPriority::Pending => self.pending.replace(envelope).is_some(),
LifecycleSubmissionPriority::Streaming | LifecycleSubmissionPriority::FirstByte => {
if self
.streaming
.as_ref()
.is_some_and(|(current, _)| *current > priority)
{
return true;
}
self.streaming.replace((priority, envelope)).is_some()
}
LifecycleSubmissionPriority::Terminal => {
self.terminal.push(envelope);
false
}
}
}
async fn execute(self) {
if let Some(envelope) = self.pending {
envelope.item.execute(envelope.admission).await;
}
if let Some((_, envelope)) = self.streaming {
envelope.item.execute(envelope.admission).await;
}
for envelope in self.terminal {
envelope.item.execute(envelope.admission).await;
}
}
}
struct LifecycleSubmissionDispatcher {
shards: Vec<LifecycleSubmissionShard>,
state: Arc<LifecycleSubmissionState>,
}
impl std::fmt::Debug for LifecycleSubmissionDispatcher {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("LifecycleSubmissionDispatcher")
.field("capacity", &self.state.capacity)
.field("workers", &self.state.workers)
.field("pending", &self.state.pending.load(Ordering::Acquire))
.finish_non_exhaustive()
}
}
impl LifecycleSubmissionDispatcher {
fn disabled() -> Arc<Self> {
Arc::new(Self {
shards: Vec::new(),
state: Arc::new(LifecycleSubmissionState::default()),
})
}
fn spawn(config: &UsageRuntimeConfig, tasks: &UsageBackgroundTasks) -> Arc<Self> {
if !config.enabled {
return Self::disabled();
}
let capacity = config
.enqueue_retry_buffer_capacity
.clamp(1, LIFECYCLE_SUBMISSION_MAX_BUFFER);
let workers = config
.worker_record_concurrency_limit
.unwrap_or(config.enqueue_retry_workers)
.clamp(1, LIFECYCLE_SUBMISSION_MAX_WORKERS)
.min(capacity);
let state = Arc::new(LifecycleSubmissionState::new(capacity, workers));
let mut shards = Vec::with_capacity(workers);
for _ in 0..workers {
let (sender, receiver) = mpsc::unbounded_channel();
let slots = Arc::new(StdMutex::new(HashMap::new()));
tasks.spawn(run_lifecycle_submission_worker(
receiver,
Arc::clone(&slots),
Arc::clone(&state),
));
shards.push(LifecycleSubmissionShard {
sender,
slots,
fallback_running: Arc::new(AtomicBool::new(false)),
});
}
Arc::new(Self { shards, state })
}
fn dispatch(&self, item: Box<dyn LifecycleSubmissionItem>) -> bool {
if self.shards.is_empty() {
return false;
}
let Some(admission) = self.state.try_admit() else {
let overflow = self.state.overflow_total.fetch_add(1, Ordering::AcqRel) + 1;
if should_log_usage_retry_counter(overflow) {
warn!(
event_name = "usage_lifecycle_submission_overflow",
log_type = "event",
request_id = item.request_id(),
dispatcher_capacity = self.state.capacity,
dispatcher_workers = self.state.workers,
overflow_total = overflow,
fallback = "drop_intermediate_only",
"usage lifecycle handoff reached its hard capacity"
);
}
return false;
};
self.dispatch_admitted(item, admission);
true
}
async fn dispatch_terminal(&self, item: Box<dyn LifecycleSubmissionItem>) -> bool {
if self.shards.is_empty() {
return false;
}
let Some(admission) = self.state.admit().await else {
return false;
};
self.dispatch_admitted(item, admission);
true
}
fn dispatch_admitted(
&self,
item: Box<dyn LifecycleSubmissionItem>,
admission: LifecycleAdmissionPermit,
) {
let request_id = item.request_id().to_string();
let shard_index = retry_worker_index(&request_id, self.shards.len());
let shard = &self.shards[shard_index];
let envelope = LifecycleSubmissionEnvelope { item, admission };
let mut slots = shard
.slots
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if let Some(slot) = slots.get_mut(&request_id) {
if slot.insert(envelope) {
self.state.record_coalesced();
}
return;
}
let mut slot = LifecycleSubmissionSlot::default();
slot.insert(envelope);
slots.insert(request_id.clone(), slot);
drop(slots);
self.state.record_enqueued();
if shard.sender.send(request_id.clone()).is_err() {
let overflow = self.state.overflow_total.fetch_add(1, Ordering::AcqRel) + 1;
warn!(
event_name = "usage_lifecycle_submission_worker_unavailable",
log_type = "event",
request_id,
dispatcher_workers = self.state.workers,
overflow_total = overflow,
fallback = "single_ordered_shard_drainer",
"usage lifecycle ordered worker is unavailable; draining preserved events serially"
);
schedule_lifecycle_submission_fallback(
Arc::clone(&shard.slots),
Arc::clone(&shard.fallback_running),
Arc::clone(&self.state),
);
}
}
}
async fn run_lifecycle_submission_worker(
mut receiver: mpsc::UnboundedReceiver<String>,
slots: Arc<StdMutex<HashMap<String, LifecycleSubmissionSlot>>>,
state: Arc<LifecycleSubmissionState>,
) {
while let Some(request_id) = receiver.recv().await {
let slot = slots
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.remove(&request_id);
if let Some(slot) = slot {
let result = spawn_on_usage_background_runtime(slot.execute()).await;
if let Err(err) = result {
warn!(
event_name = "usage_lifecycle_submission_item_panicked",
log_type = "ops",
request_id,
error = %err,
fallback = "drop_current_slot",
"usage lifecycle submission item panicked; dropping the current slot"
);
state.record_processed();
continue;
}
}
state.record_processed();
}
}
fn schedule_lifecycle_submission_fallback(
slots: Arc<StdMutex<HashMap<String, LifecycleSubmissionSlot>>>,
running: Arc<AtomicBool>,
state: Arc<LifecycleSubmissionState>,
) {
if running.swap(true, Ordering::AcqRel) {
return;
}
spawn_on_usage_background_runtime(async move {
loop {
let next = {
let mut slots = slots
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let request_id = slots.keys().next().cloned();
request_id
.and_then(|request_id| slots.remove(&request_id).map(|slot| (request_id, slot)))
};
let Some((request_id, slot)) = next else {
running.store(false, Ordering::Release);
let has_more = !slots
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.is_empty();
if has_more
&& running
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
continue;
}
return;
};
match spawn_on_usage_background_runtime(slot.execute()).await {
Ok(()) => state.record_processed(),
Err(err) => {
warn!(
event_name = "usage_lifecycle_submission_fallback_item_panicked",
log_type = "ops",
request_id,
error = %err,
fallback = "drop_current_slot",
"usage lifecycle fallback item panicked; dropping the current slot"
);
state.record_processed();
}
}
}
});
}
enum OrderedLifecyclePayload {
Pending {
event: UsageEvent,
},
Streaming {
event: UsageEvent,
},
TerminalSeed {
seed: LifecycleTerminalUsageSeed,
observed_at_unix_ms: u64,
},
Terminal {
event: UsageEvent,
direct: bool,
completion: Option<tokio::sync::oneshot::Sender<()>>,
},
}
trait OrderedLifecycleItem: Send {
fn request_id(&self) -> &str;
fn start(self: Box<Self>, completion: OrderedLifecycleCompletion);
}
struct OrderedLifecycleItemImpl<T> {
runtime: UsageRuntime,
data: T,
request_id: String,
payload: OrderedLifecyclePayload,
}
impl<T> OrderedLifecycleItem for OrderedLifecycleItemImpl<T>
where
T: UsageRuntimeAccess + Clone + 'static,
{
fn request_id(&self) -> &str {
&self.request_id
}
fn start(self: Box<Self>, ordered_completion: OrderedLifecycleCompletion) {
let Self {
runtime,
data,
request_id,
payload,
} = *self;
match payload {
OrderedLifecyclePayload::Pending { event } => {
if data.has_usage_writer() {
let pending_persistence = Arc::clone(&runtime.pending_persistence);
pending_persistence.dispatch(Box::new(PendingPersistenceItemImpl {
runtime,
data,
event,
request_id,
ordered_completion,
}));
} else {
spawn_on_usage_background_runtime(async move {
runtime.enqueue_or_write_lifecycle(&data, event).await;
ordered_completion.complete();
});
}
}
OrderedLifecyclePayload::Streaming { event }
if event.data.first_byte_time_ms.is_some() =>
{
spawn_on_usage_background_runtime(async move {
runtime
.enqueue_first_byte_lifecycle_event_inner(
&data,
event,
false,
Some(ordered_completion),
)
.await;
});
}
OrderedLifecyclePayload::Streaming { event } => {
spawn_on_usage_background_runtime(async move {
if data.has_usage_writer() {
let _ = runtime
.persist_ordered_lifecycle_event(&data, &event, "streaming")
.await;
} else {
runtime.enqueue_or_write_lifecycle(&data, event).await;
}
ordered_completion.complete();
});
}
OrderedLifecyclePayload::TerminalSeed {
seed,
observed_at_unix_ms,
} => {
let pending_guard = runtime.terminal_submission_state.register_pending();
let terminal_execution = Arc::clone(&runtime.terminal_execution);
let item = Box::new(TerminalExecutionItemImpl {
runtime,
data,
request_id,
payload: TerminalExecutionPayload::Seed {
seed,
observed_at_unix_ms,
},
pending_guard,
ordered_completion,
});
match terminal_execution.dispatch_ordered_now(item) {
Ok(()) => {}
Err(item) => {
spawn_on_usage_background_runtime(async move {
execute_terminal_item_isolated(item, "ordered_direct_fallback").await;
});
}
}
}
OrderedLifecyclePayload::Terminal {
event,
direct,
completion,
} => {
let pending_guard = runtime.terminal_submission_state.register_pending();
let terminal_execution = Arc::clone(&runtime.terminal_execution);
let item = Box::new(TerminalExecutionItemImpl {
runtime,
data,
request_id,
payload: TerminalExecutionPayload::Event {
event,
direct,
completion,
},
pending_guard,
ordered_completion,
});
match terminal_execution.dispatch_ordered_now(item) {
Ok(()) => {}
Err(item) => {
spawn_on_usage_background_runtime(async move {
execute_terminal_item_isolated(item, "ordered_direct_fallback").await;
});
}
}
}
}
}
}
struct OrderedLifecycleBarrierItem {
request_id: String,
completion: tokio::sync::oneshot::Sender<OrderedLifecycleCompletion>,
}
impl OrderedLifecycleItem for OrderedLifecycleBarrierItem {
fn request_id(&self) -> &str {
&self.request_id
}
fn start(self: Box<Self>, ordered_completion: OrderedLifecycleCompletion) {
let Self {
request_id,
completion,
} = *self;
let dispatcher = ordered_completion.terminal_execution();
let item = Box::new(TerminalExecutionBarrierItem {
request_id,
completion,
ordered_completion,
});
match dispatcher.dispatch_ordered_now(item) {
Ok(()) => {}
Err(item) => {
spawn_on_usage_background_runtime(async move {
execute_terminal_item_isolated(item, "barrier_direct_fallback").await;
});
}
}
}
}
/// A deterministic build failure still enters the ordered dispatcher, then releases its
/// admission through the completion guard without persisting a malformed phase.
struct OrderedLifecycleDegradedItem {
request_id: String,
}
impl OrderedLifecycleItem for OrderedLifecycleDegradedItem {
fn request_id(&self) -> &str {
&self.request_id
}
fn start(self: Box<Self>, _completion: OrderedLifecycleCompletion) {}
}
#[derive(Debug, Default)]
struct OrderedLifecycleState {
pending: AtomicUsize,
max_pending: AtomicUsize,
}
impl OrderedLifecycleState {
fn record_dispatched(&self) {
let pending = self.pending.fetch_add(1, Ordering::AcqRel) + 1;
self.max_pending.fetch_max(pending, Ordering::AcqRel);
}
fn record_finished(&self) {
self.pending.fetch_sub(1, Ordering::AcqRel);
}
}
#[derive(Default)]
struct OrderedLifecycleQueues {
active: HashMap<String, LifecycleAdmissionPermit>,
queued: HashMap<String, VecDeque<OrderedLifecycleEnvelope>>,
ready: VecDeque<OrderedLifecycleEnvelope>,
}
struct OrderedLifecycleEnvelope {
item: Box<dyn OrderedLifecycleItem>,
admission: LifecycleAdmissionPermit,
}
struct OrderedLifecycleCore {
queues: StdMutex<OrderedLifecycleQueues>,
notify: tokio::sync::Notify,
state: Arc<OrderedLifecycleState>,
terminal_execution: Arc<TerminalExecutionDispatcher>,
}
struct OrderedLifecycleCompletion {
request_id: String,
core: Arc<OrderedLifecycleCore>,
completed: bool,
}
impl OrderedLifecycleCompletion {
fn terminal_execution(&self) -> Arc<TerminalExecutionDispatcher> {
Arc::clone(&self.core.terminal_execution)
}
fn complete(mut self) {
self.finish();
self.completed = true;
}
fn finish(&mut self) {
let mut queues = self
.core
.queues
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if queues.active.remove(&self.request_id).is_none() {
warn!(
event_name = "usage_ordered_lifecycle_duplicate_completion",
log_type = "ops",
request_id = %self.request_id,
"usage ordered lifecycle received an unexpected duplicate completion"
);
return;
}
self.core.state.record_finished();
let next = queues
.queued
.get_mut(&self.request_id)
.and_then(VecDeque::pop_front);
if let Some(next) = next {
queues
.active
.insert(self.request_id.clone(), Arc::clone(&next.admission));
queues.ready.push_back(next);
} else {
queues.queued.remove(&self.request_id);
}
drop(queues);
self.core.notify.notify_one();
}
}
impl Drop for OrderedLifecycleCompletion {
fn drop(&mut self) {
if self.completed {
return;
}
warn!(
event_name = "usage_ordered_lifecycle_degraded_completion",
log_type = "ops",
request_id = %self.request_id,
fallback = "advance_after_failed_intermediate",
"usage ordered lifecycle item ended without explicit completion"
);
self.finish();
}
}
struct OrderedLifecycleDispatcher {
core: Arc<OrderedLifecycleCore>,
}
impl std::fmt::Debug for OrderedLifecycleDispatcher {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("OrderedLifecycleDispatcher")
.field("pending", &self.core.state.pending.load(Ordering::Acquire))
.finish_non_exhaustive()
}
}
impl OrderedLifecycleDispatcher {
fn disabled(terminal_execution: Arc<TerminalExecutionDispatcher>) -> Arc<Self> {
Arc::new(Self {
core: Arc::new(OrderedLifecycleCore {
queues: StdMutex::new(OrderedLifecycleQueues::default()),
notify: tokio::sync::Notify::new(),
state: Arc::new(OrderedLifecycleState::default()),
terminal_execution,
}),
})
}
fn spawn(
config: &UsageRuntimeConfig,
terminal_execution: Arc<TerminalExecutionDispatcher>,
tasks: &UsageBackgroundTasks,
) -> Arc<Self> {
let dispatcher = Self::disabled(terminal_execution);
if config.enabled {
tasks.spawn(run_ordered_lifecycle_dispatcher(Arc::downgrade(
&dispatcher.core,
)));
}
dispatcher
}
fn dispatch(&self, item: Box<dyn OrderedLifecycleItem>, admission: LifecycleAdmissionPermit) {
let request_id = item.request_id().to_string();
let envelope = OrderedLifecycleEnvelope { item, admission };
self.core.state.record_dispatched();
let mut queues = self
.core
.queues
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if !queues.active.contains_key(&request_id) {
queues
.active
.insert(request_id.clone(), Arc::clone(&envelope.admission));
queues.ready.push_back(envelope);
} else {
queues
.queued
.entry(request_id)
.or_default()
.push_back(envelope);
}
drop(queues);
self.core.notify.notify_one();
}
}
async fn run_ordered_lifecycle_dispatcher(core: Weak<OrderedLifecycleCore>) {
loop {
let Some(core) = core.upgrade() else {
return;
};
let ready = {
let mut queues = core
.queues
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
queues.ready.drain(..).collect::<Vec<_>>()
};
if ready.is_empty() {
if Arc::strong_count(&core) == 1 {
return;
}
let _ = tokio::time::timeout(Duration::from_secs(1), core.notify.notified()).await;
continue;
}
for envelope in ready {
let request_id = envelope.item.request_id().to_string();
let completion = OrderedLifecycleCompletion {
request_id: request_id.clone(),
core: Arc::clone(&core),
completed: false,
};
if catch_unwind(AssertUnwindSafe(|| envelope.item.start(completion))).is_err() {
warn!(
event_name = "usage_ordered_lifecycle_start_panicked",
log_type = "ops",
request_id,
fallback = "advance_after_panicked_handoff",
"usage ordered lifecycle item panicked during handoff; releasing its admission"
);
}
}
}
}
#[derive(Debug, Default)]
struct PendingPersistenceState {
capacity: usize,
pending: AtomicUsize,
max_pending: AtomicUsize,
batch_flush_total: AtomicU64,
batch_records_total: AtomicU64,
max_batch_size: AtomicUsize,
batch_failed_total: AtomicU64,
retried_total: AtomicU64,
overflow_total: AtomicU64,
}
impl PendingPersistenceState {
fn new(capacity: usize) -> Self {
Self {
capacity,
..Self::default()
}
}
fn record_dispatched(&self) -> usize {
let pending = self.pending.fetch_add(1, Ordering::AcqRel) + 1;
self.max_pending.fetch_max(pending, Ordering::AcqRel);
pending
}
fn record_finished(&self) {
self.pending.fetch_sub(1, Ordering::AcqRel);
}
}
struct PendingPersistencePendingGuard {
state: Arc<PendingPersistenceState>,
}
impl Drop for PendingPersistencePendingGuard {
fn drop(&mut self) {
self.state.record_finished();
}
}
#[async_trait]
trait PendingPersistenceItem: Send + Sync {
fn request_id(&self) -> &str;
fn batch_identity(&self) -> Option<usize>;
fn build_record(&self) -> Result<UpsertUsageRecord, DataLayerError>;
fn retry_delay(&self, attempts: u64) -> Duration;
async fn write_batch(&self, records: Vec<UpsertUsageRecord>) -> Result<(), DataLayerError>;
async fn complete_success(self: Box<Self>);
fn complete_degraded(self: Box<Self>);
async fn persist_reliably(
self: Box<Self>,
mut record: Option<UpsertUsageRecord>,
state: Arc<PendingPersistenceState>,
mut attempts: u64,
) {
loop {
if record.is_none() {
match self.build_record() {
Ok(built) => record = Some(built),
Err(err) => {
attempts = attempts.saturating_add(1);
state.retried_total.fetch_add(1, Ordering::AcqRel);
if attempts >= PENDING_PERSISTENCE_SINGLE_RETRIES_BEFORE_DEGRADE {
state.overflow_total.fetch_add(1, Ordering::AcqRel);
warn!(
event_name = "usage_pending_record_build_degraded",
log_type = "ops",
request_id = %self.request_id(),
retry_attempts = attempts,
error = %err,
fallback = "drop_pending_only",
"usage pending record build exhausted its bounded retry budget"
);
self.complete_degraded();
return;
}
let delay = self.retry_delay(attempts);
warn!(
event_name = "usage_pending_record_build_retry",
log_type = "ops",
request_id = %self.request_id(),
retry_attempt = attempts,
retry_delay_ms = delay.as_millis() as u64,
error = %err,
fallback = "fail_closed_retry",
"usage pending record build failed; keeping later phases blocked"
);
tokio::time::sleep(delay).await;
continue;
}
}
}
let current = record
.as_ref()
.expect("pending retry record should be available")
.clone();
match catch_usage_writer_panic(
"pending single usage write",
self.write_batch(vec![current]),
)
.await
{
Ok(()) => {
self.complete_success().await;
return;
}
Err(err) => {
attempts = attempts.saturating_add(1);
state.batch_failed_total.fetch_add(1, Ordering::AcqRel);
state.retried_total.fetch_add(1, Ordering::AcqRel);
if attempts >= PENDING_PERSISTENCE_SINGLE_RETRIES_BEFORE_DEGRADE {
state.overflow_total.fetch_add(1, Ordering::AcqRel);
warn!(
event_name = "usage_pending_single_degraded",
log_type = "ops",
request_id = %self.request_id(),
retry_attempts = attempts,
error = %err,
fallback = "drop_pending_only",
"usage pending persistence exhausted its bounded retry budget"
);
self.complete_degraded();
return;
}
let delay = self.retry_delay(attempts);
if should_log_usage_retry_counter(attempts) {
warn!(
event_name = "usage_pending_single_retry",
log_type = "ops",
request_id = %self.request_id(),
retry_attempt = attempts,
retry_delay_ms = delay.as_millis() as u64,
error = %err,
fallback = "isolated_reliable_retry",
"usage pending write failed; retrying without releasing later phases"
);
}
tokio::time::sleep(delay).await;
}
}
}
}
}
struct PendingPersistenceItemImpl<T> {
runtime: UsageRuntime,
data: T,
event: UsageEvent,
request_id: String,
ordered_completion: OrderedLifecycleCompletion,
}
#[async_trait]
impl<T> PendingPersistenceItem for PendingPersistenceItemImpl<T>
where
T: UsageRuntimeAccess + Clone + 'static,
{
fn request_id(&self) -> &str {
&self.request_id
}
fn batch_identity(&self) -> Option<usize> {
(self.data.has_usage_writer()
&& UsageRecordWriter::supports_pending_usage_batch(&self.data))
.then(|| UsageRecordWriter::pending_usage_writer_identity(&self.data))
.flatten()
}
fn build_record(&self) -> Result<UpsertUsageRecord, DataLayerError> {
build_upsert_usage_record_from_event(&self.event)
}
fn retry_delay(&self, attempts: u64) -> Duration {
usage_enqueue_retry_delay(&self.runtime.config, attempts)
}
async fn write_batch(&self, records: Vec<UpsertUsageRecord>) -> Result<(), DataLayerError> {
if let Some(gate) = &self.runtime.worker_record_gate {
let _permit = gate.acquire().await;
self.data.upsert_pending_usage_records(records).await
} else {
self.data.upsert_pending_usage_records(records).await
}
}
async fn complete_success(self: Box<Self>) {
self.ordered_completion.complete();
}
fn complete_degraded(self: Box<Self>) {
self.ordered_completion.complete();
}
}
struct PendingPersistenceEnvelope {
item: Box<dyn PendingPersistenceItem>,
_pending_guard: PendingPersistencePendingGuard,
}
#[derive(Debug)]
struct PendingPersistenceDispatcher {
sender: Option<mpsc::Sender<PendingPersistenceEnvelope>>,
state: Arc<PendingPersistenceState>,
}
impl PendingPersistenceDispatcher {
fn disabled() -> Arc<Self> {
Arc::new(Self {
sender: None,
state: Arc::new(PendingPersistenceState::default()),
})
}
fn spawn(config: &UsageRuntimeConfig, tasks: &UsageBackgroundTasks) -> Arc<Self> {
if !config.enabled {
return Self::disabled();
}
let capacity = config
.enqueue_retry_buffer_capacity
.clamp(1_024, PENDING_PERSISTENCE_MAX_BUFFER);
let state = Arc::new(PendingPersistenceState::new(capacity));
let (sender, receiver) = mpsc::channel(capacity);
tasks.spawn(run_pending_persistence_dispatcher(
receiver,
Arc::clone(&state),
));
Arc::new(Self {
sender: Some(sender),
state,
})
}
fn dispatch(&self, item: Box<dyn PendingPersistenceItem>) {
let pending = self.state.record_dispatched();
let envelope = PendingPersistenceEnvelope {
item,
_pending_guard: PendingPersistencePendingGuard {
state: Arc::clone(&self.state),
},
};
let Some(sender) = &self.sender else {
complete_degraded_pending_envelope(envelope);
return;
};
match sender.try_send(envelope) {
Ok(()) => {}
Err(mpsc::error::TrySendError::Full(envelope)) => {
let overflow = self.state.overflow_total.fetch_add(1, Ordering::AcqRel) + 1;
if should_log_usage_retry_counter(overflow) {
warn!(
event_name = "usage_pending_persistence_overflow",
log_type = "ops",
pending,
capacity = self.state.capacity,
overflow_total = overflow,
fallback = "drop_pending_only",
"usage pending persistence reached capacity; allowing later lifecycle phases to create the row"
);
}
complete_degraded_pending_envelope(envelope);
}
Err(mpsc::error::TrySendError::Closed(envelope)) => {
self.state.overflow_total.fetch_add(1, Ordering::AcqRel);
complete_degraded_pending_envelope(envelope);
}
}
}
}
fn complete_degraded_pending_envelope(envelope: PendingPersistenceEnvelope) {
let PendingPersistenceEnvelope {
item,
_pending_guard,
} = envelope;
item.complete_degraded();
drop(_pending_guard);
}
async fn run_pending_persistence_dispatcher(
mut receiver: mpsc::Receiver<PendingPersistenceEnvelope>,
state: Arc<PendingPersistenceState>,
) {
let mut tasks = tokio::task::JoinSet::new();
loop {
while tasks.len() >= PENDING_PERSISTENCE_MAX_BATCH_CONCURRENCY {
report_pending_persistence_join(tasks.join_next().await);
}
let Some(first) = receiver.recv().await else {
break;
};
let mut batch = vec![first];
while batch.len() < PENDING_PERSISTENCE_BATCH_SIZE {
while batch.len() < PENDING_PERSISTENCE_BATCH_SIZE {
match receiver.try_recv() {
Ok(item) => batch.push(item),
Err(mpsc::error::TryRecvError::Empty) => break,
Err(mpsc::error::TryRecvError::Disconnected) => break,
}
}
if batch.len() >= PENDING_PERSISTENCE_BATCH_SIZE {
break;
}
match tokio::time::timeout(PENDING_PERSISTENCE_BATCH_FLUSH_INTERVAL, receiver.recv())
.await
{
Ok(Some(item)) => batch.push(item),
Ok(None) | Err(_) => break,
}
}
state
.max_batch_size
.fetch_max(batch.len(), Ordering::AcqRel);
let task_state = Arc::clone(&state);
tasks.spawn(async move {
process_pending_persistence_batch(batch, task_state).await;
});
while let Some(result) = tasks.try_join_next() {
report_pending_persistence_join(Some(result));
}
}
while let Some(result) = tasks.join_next().await {
report_pending_persistence_join(Some(result));
}
}
async fn process_pending_persistence_batch(
batch: Vec<PendingPersistenceEnvelope>,
state: Arc<PendingPersistenceState>,
) {
if batch.is_empty() {
return;
}
state.batch_flush_total.fetch_add(1, Ordering::AcqRel);
state.batch_records_total.fetch_add(
u64::try_from(batch.len()).unwrap_or(u64::MAX),
Ordering::AcqRel,
);
let mut items = batch.into_iter().map(Some).collect::<Vec<_>>();
let mut groups = HashMap::<usize, Vec<(usize, UpsertUsageRecord)>>::new();
let mut single_writes = Vec::<(PendingPersistenceEnvelope, Option<UpsertUsageRecord>)>::new();
for (index, item) in items.iter_mut().enumerate() {
let Some(envelope) = item.as_ref() else {
continue;
};
let batch_identity = envelope.item.batch_identity();
let record = match envelope.item.build_record() {
Ok(record) => record,
Err(err) => {
warn!(
event_name = "usage_pending_upsert_build_failed",
log_type = "event",
request_id = %envelope.item.request_id(),
error = %err,
fallback = "isolated_reliable_retry",
"usage runtime failed to build a pending batch record"
);
if let Some(envelope) = item.take() {
single_writes.push((envelope, None));
}
continue;
}
};
if let Some(identity) = batch_identity {
groups.entry(identity).or_default().push((index, record));
} else if let Some(envelope) = item.take() {
single_writes.push((envelope, Some(record)));
}
}
for (_, grouped) in groups {
let Some(&(leader_index, _)) = grouped.first() else {
continue;
};
let record_count = grouped.len();
let records = grouped
.iter()
.map(|(_, record)| record.clone())
.collect::<Vec<_>>();
let mut attempts = 0_u64;
let write_result = loop {
let result = catch_usage_writer_panic(
"pending batch usage write",
items[leader_index]
.as_ref()
.expect("pending batch leader should remain available")
.item
.write_batch(records.clone()),
)
.await;
match result {
Ok(()) => break Ok(()),
Err(err) => {
attempts = attempts.saturating_add(1);
state.batch_failed_total.fetch_add(1, Ordering::AcqRel);
state.retried_total.fetch_add(
u64::try_from(record_count).unwrap_or(u64::MAX),
Ordering::AcqRel,
);
if attempts >= PENDING_PERSISTENCE_BATCH_RETRIES_BEFORE_ISOLATION {
break Err(err);
}
let delay = items[leader_index]
.as_ref()
.expect("pending batch leader should remain available")
.item
.retry_delay(attempts);
warn!(
event_name = "usage_pending_upsert_batch_retry",
log_type = "ops",
records = record_count,
retry_attempt = attempts,
retry_delay_ms = delay.as_millis() as u64,
error = %err,
"usage pending batch failed; retrying without releasing later phases"
);
tokio::time::sleep(delay).await;
}
}
};
match write_result {
Ok(()) => {
for (index, _) in grouped {
if let Some(envelope) = items[index].take() {
envelope.item.complete_success().await;
drop(envelope._pending_guard);
}
}
}
Err(err) => {
warn!(
event_name = "usage_pending_upsert_batch_isolated",
log_type = "ops",
records = record_count,
retry_attempts = attempts,
error = %err,
fallback = "bounded_per_request_reliable_retry",
"usage pending batch repeatedly failed; isolating requests for reliable retry"
);
for (index, record) in grouped {
if let Some(envelope) = items[index].take() {
single_writes.push((envelope, Some(record)));
}
}
}
}
}
futures_util::stream::iter(single_writes)
.for_each_concurrent(
PENDING_PERSISTENCE_SINGLE_WRITE_CONCURRENCY_PER_BATCH,
|(envelope, record)| {
let retry_state = Arc::clone(&state);
async move {
let PendingPersistenceEnvelope {
item,
_pending_guard,
} = envelope;
item.persist_reliably(record, retry_state, 0).await;
drop(_pending_guard);
}
},
)
.await;
}
fn report_pending_persistence_join(result: Option<Result<(), tokio::task::JoinError>>) {
if let Some(Err(err)) = result {
warn!(
event_name = "usage_pending_persistence_task_failed",
log_type = "ops",
error = %err,
fallback = "advance_after_panicked_pending",
"usage pending persistence task failed; releasing affected ordered admissions"
);
}
}
#[derive(Debug, Default)]
struct FirstBytePersistenceState {
capacity: usize,
pending: AtomicUsize,
max_pending: AtomicUsize,
dispatched_total: AtomicU64,
overflow_total: AtomicU64,
cancelled_total: AtomicU64,
direct_succeeded_total: AtomicU64,
direct_failed_total: AtomicU64,
batch_flush_total: AtomicU64,
batch_records_total: AtomicU64,
max_batch_size: AtomicUsize,
batch_failed_total: AtomicU64,
fallback_accepted_total: AtomicU64,
fallback_failed_total: AtomicU64,
}
impl FirstBytePersistenceState {
fn new(capacity: usize) -> Self {
Self {
capacity,
..Self::default()
}
}
fn record_dispatched(&self) {
self.dispatched_total.fetch_add(1, Ordering::AcqRel);
let pending = self.pending.fetch_add(1, Ordering::AcqRel) + 1;
self.max_pending.fetch_max(pending, Ordering::AcqRel);
}
fn record_finished(&self) {
self.pending.fetch_sub(1, Ordering::AcqRel);
}
}
struct FirstBytePersistencePendingGuard {
state: Arc<FirstBytePersistenceState>,
}
impl Drop for FirstBytePersistencePendingGuard {
fn drop(&mut self) {
self.state.record_finished();
}
}
struct FirstByteMarkerCleanup {
coalescer: Arc<LifecycleEventCoalescer>,
request_id: String,
generation: u64,
}
impl FirstByteMarkerCleanup {
async fn rollback(self) {
self.coalescer
.rollback_first_byte(&self.request_id, self.generation)
.await;
}
}
struct FirstByteMarkerGuard {
cleanup: Option<FirstByteMarkerCleanup>,
}
impl FirstByteMarkerGuard {
fn new(coalescer: Arc<LifecycleEventCoalescer>, request_id: String, generation: u64) -> Self {
Self {
cleanup: Some(FirstByteMarkerCleanup {
coalescer,
request_id,
generation,
}),
}
}
fn disarm(&mut self) {
self.cleanup.take();
}
}
impl Drop for FirstByteMarkerGuard {
fn drop(&mut self) {
let Some(cleanup) = self.cleanup.take() else {
return;
};
spawn_on_usage_background_runtime(async move {
cleanup.rollback().await;
});
}
}
#[async_trait]
trait FirstBytePersistenceItem: Send + Sync {
fn batch_identity(&self) -> Option<usize>;
async fn is_current(&self) -> bool;
fn build_record(&self) -> Result<UpsertUsageRecord, DataLayerError>;
async fn write_batch(&self, records: Vec<UpsertUsageRecord>) -> Result<(), DataLayerError>;
async fn complete_success(self: Box<Self>);
async fn complete_cancelled(self: Box<Self>);
async fn enqueue_fallback(self: Box<Self>);
}
struct FirstBytePersistenceItemImpl<T> {
runtime: UsageRuntime,
data: T,
direct_event: UsageEvent,
fallback_event: UsageEvent,
request_id: String,
generation: u64,
marker_guard: FirstByteMarkerGuard,
completion: Option<tokio::sync::oneshot::Sender<()>>,
ordered_completion: Option<OrderedLifecycleCompletion>,
}
#[async_trait]
impl<T> FirstBytePersistenceItem for FirstBytePersistenceItemImpl<T>
where
T: UsageRuntimeAccess + Clone + 'static,
{
fn batch_identity(&self) -> Option<usize> {
(self.data.has_usage_writer()
&& self.data.supports_first_byte_usage_fast_path()
&& UsageRecordWriter::supports_first_byte_usage_batch(&self.data))
.then(|| UsageRecordWriter::first_byte_usage_writer_identity(&self.data))
.flatten()
}
async fn is_current(&self) -> bool {
self.runtime
.lifecycle_coalescer
.first_byte_is_current(&self.request_id, self.generation)
.await
}
fn build_record(&self) -> Result<UpsertUsageRecord, DataLayerError> {
build_upsert_usage_record_from_event(&self.direct_event)
}
async fn write_batch(&self, records: Vec<UpsertUsageRecord>) -> Result<(), DataLayerError> {
if let Some(gate) = &self.runtime.worker_record_gate {
let _permit = gate.acquire().await;
self.data.upsert_first_byte_usage_records(records).await
} else {
self.data.upsert_first_byte_usage_records(records).await
}
}
async fn complete_success(self: Box<Self>) {
let Self {
runtime,
request_id,
generation,
mut marker_guard,
completion,
ordered_completion,
..
} = *self;
runtime
.first_byte_persistence
.state
.direct_succeeded_total
.fetch_add(1, Ordering::AcqRel);
runtime
.lifecycle_coalescer
.complete_first_byte(&request_id, generation)
.await;
marker_guard.disarm();
if let Some(completion) = completion {
let _ = completion.send(());
}
if let Some(completion) = ordered_completion {
completion.complete();
}
}
async fn complete_cancelled(self: Box<Self>) {
let Self {
runtime,
request_id,
generation,
mut marker_guard,
completion,
ordered_completion,
..
} = *self;
runtime
.first_byte_persistence
.state
.cancelled_total
.fetch_add(1, Ordering::AcqRel);
// Keep the coalescer marker intact when a terminal event won the race. A later first-byte
// event is allowed to retry only after the existing generation is explicitly rolled back.
let _ = generation;
marker_guard.disarm();
if let Some(completion) = completion {
let _ = completion.send(());
}
if let Some(ordered_completion) = ordered_completion {
warn!(
event_name = "usage_ordered_first_byte_cancelled",
log_type = "ops",
request_id,
fallback = "drop_first_byte_only",
"an ordered first-byte transition was cancelled; allowing its terminal barrier to advance"
);
ordered_completion.complete();
}
}
async fn enqueue_fallback(self: Box<Self>) {
let Self {
runtime,
data,
direct_event: _,
fallback_event,
request_id,
generation,
mut marker_guard,
completion,
ordered_completion,
..
} = *self;
if let Some(ordered_completion) = ordered_completion {
let degraded = runtime
.first_byte_persistence
.state
.fallback_failed_total
.fetch_add(1, Ordering::AcqRel)
+ 1;
if should_log_usage_retry_counter(degraded) {
warn!(
event_name = "usage_ordered_first_byte_degraded",
log_type = "ops",
request_id,
degraded_total = degraded,
fallback = "drop_first_byte_only",
"ordered first-byte persistence degraded so terminal usage can continue"
);
}
runtime
.lifecycle_coalescer
.complete_first_byte(&request_id, generation)
.await;
marker_guard.disarm();
if let Some(completion) = completion {
let _ = completion.send(());
}
ordered_completion.complete();
return;
}
enqueue_first_byte_fallback(runtime, data, fallback_event, request_id, generation).await;
marker_guard.disarm();
if let Some(completion) = completion {
let _ = completion.send(());
}
}
}
async fn enqueue_first_byte_fallback<T>(
runtime: UsageRuntime,
data: T,
event: UsageEvent,
request_id: String,
generation: u64,
) where
T: UsageRuntimeAccess,
{
if !runtime
.lifecycle_coalescer
.first_byte_is_current(&request_id, generation)
.await
{
runtime
.first_byte_persistence
.state
.cancelled_total
.fetch_add(1, Ordering::AcqRel);
return;
}
if runtime.enqueue_lifecycle_event(&data, event).await {
runtime
.first_byte_persistence
.state
.fallback_accepted_total
.fetch_add(1, Ordering::AcqRel);
runtime
.lifecycle_coalescer
.complete_first_byte(&request_id, generation)
.await;
} else {
runtime
.first_byte_persistence
.state
.fallback_failed_total
.fetch_add(1, Ordering::AcqRel);
runtime
.lifecycle_coalescer
.rollback_first_byte(&request_id, generation)
.await;
}
}
#[derive(Debug)]
struct FirstBytePersistenceDispatcher {
sender: Option<mpsc::Sender<Box<dyn FirstBytePersistenceItem>>>,
state: Arc<FirstBytePersistenceState>,
}
impl FirstBytePersistenceDispatcher {
fn disabled() -> Arc<Self> {
Arc::new(Self {
sender: None,
state: Arc::new(FirstBytePersistenceState::default()),
})
}
fn spawn(config: &UsageRuntimeConfig, tasks: &UsageBackgroundTasks) -> Arc<Self> {
if !config.enabled || !config.queue_lifecycle_events {
return Self::disabled();
}
let capacity = config
.enqueue_retry_buffer_capacity
.clamp(1_024, FIRST_BYTE_PERSISTENCE_MAX_BUFFER);
let concurrency = config
.worker_record_concurrency_limit
.unwrap_or(FIRST_BYTE_PERSISTENCE_DEFAULT_CONCURRENCY)
.clamp(1, 256);
let state = Arc::new(FirstBytePersistenceState::new(capacity));
let (sender, receiver) = mpsc::channel(capacity);
tasks.spawn(run_first_byte_persistence_dispatcher(
receiver,
concurrency,
Arc::clone(&state),
));
Arc::new(Self {
sender: Some(sender),
state,
})
}
async fn dispatch(&self, item: Box<dyn FirstBytePersistenceItem>) {
let Some(sender) = &self.sender else {
item.enqueue_fallback().await;
return;
};
self.state.record_dispatched();
if let Err(err) = sender.try_send(item) {
self.state.record_finished();
self.state.overflow_total.fetch_add(1, Ordering::AcqRel);
err.into_inner().enqueue_fallback().await;
}
}
}
async fn run_first_byte_persistence_dispatcher(
mut receiver: mpsc::Receiver<Box<dyn FirstBytePersistenceItem>>,
concurrency: usize,
state: Arc<FirstBytePersistenceState>,
) {
let mut tasks = tokio::task::JoinSet::new();
let batch_concurrency = concurrency.clamp(1, FIRST_BYTE_PERSISTENCE_MAX_BATCH_CONCURRENCY);
let write_admission = Arc::new(tokio::sync::Semaphore::new(concurrency));
while let Some(first) = receiver.recv().await {
let mut batch = vec![first];
while batch.len() < FIRST_BYTE_PERSISTENCE_BATCH_SIZE {
while batch.len() < FIRST_BYTE_PERSISTENCE_BATCH_SIZE {
match receiver.try_recv() {
Ok(item) => batch.push(item),
Err(mpsc::error::TryRecvError::Empty) => break,
Err(mpsc::error::TryRecvError::Disconnected) => break,
}
}
if batch.len() >= FIRST_BYTE_PERSISTENCE_BATCH_SIZE {
break;
}
match tokio::time::timeout(FIRST_BYTE_PERSISTENCE_BATCH_FLUSH_INTERVAL, receiver.recv())
.await
{
Ok(Some(item)) => batch.push(item),
Ok(None) | Err(_) => break,
}
}
while tasks.len() >= batch_concurrency {
report_first_byte_persistence_join(tasks.join_next().await);
}
state
.max_batch_size
.fetch_max(batch.len(), Ordering::AcqRel);
let task_state = Arc::clone(&state);
let batch_write_admission = Arc::clone(&write_admission);
tasks.spawn(async move {
process_first_byte_persistence_batch(
batch,
task_state,
concurrency,
batch_write_admission,
)
.await;
});
while let Some(result) = tasks.try_join_next() {
report_first_byte_persistence_join(Some(result));
}
}
while let Some(result) = tasks.join_next().await {
report_first_byte_persistence_join(Some(result));
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum FirstByteBatchKey {
Shared(usize),
Single(usize),
}
async fn process_first_byte_persistence_batch(
batch: Vec<Box<dyn FirstBytePersistenceItem>>,
state: Arc<FirstBytePersistenceState>,
single_write_concurrency: usize,
write_admission: Arc<tokio::sync::Semaphore>,
) {
if batch.is_empty() {
return;
}
state.batch_flush_total.fetch_add(1, Ordering::AcqRel);
state.batch_records_total.fetch_add(
u64::try_from(batch.len()).unwrap_or(u64::MAX),
Ordering::AcqRel,
);
let _pending_guards = batch
.iter()
.map(|_| FirstBytePersistencePendingGuard {
state: Arc::clone(&state),
})
.collect::<Vec<_>>();
let mut groups = HashMap::<
FirstByteBatchKey,
Vec<(Box<dyn FirstBytePersistenceItem>, UpsertUsageRecord)>,
>::new();
for (index, item) in batch.into_iter().enumerate() {
if !item.is_current().await {
item.complete_cancelled().await;
continue;
}
let record = match item.build_record() {
Ok(record) => record,
Err(err) => {
warn!(
event_name = "usage_first_byte_upsert_build_failed",
log_type = "event",
error = %err,
"usage runtime failed to build a first-byte batch record"
);
state.direct_failed_total.fetch_add(1, Ordering::AcqRel);
item.enqueue_fallback().await;
continue;
}
};
let key = item
.batch_identity()
.map(FirstByteBatchKey::Shared)
.unwrap_or(FirstByteBatchKey::Single(index));
groups.entry(key).or_default().push((item, record));
}
futures_util::stream::iter(groups.into_values())
.for_each_concurrent(single_write_concurrency.max(1), |grouped| {
let group_state = Arc::clone(&state);
let group_write_admission = Arc::clone(&write_admission);
async move {
process_first_byte_persistence_group(grouped, group_state, group_write_admission)
.await;
}
})
.await;
}
async fn process_first_byte_persistence_group(
grouped: Vec<(Box<dyn FirstBytePersistenceItem>, UpsertUsageRecord)>,
state: Arc<FirstBytePersistenceState>,
write_admission: Arc<tokio::sync::Semaphore>,
) {
let Some((leader, _)) = grouped.first() else {
return;
};
// Repository batch contracts preserve input order for duplicate request IDs, matching
// repeated single-row first-byte upserts and allowing later records to merge other fields.
let records = grouped
.iter()
.map(|(_, record)| record.clone())
.collect::<Vec<_>>();
let record_count = records.len();
let write_permit = write_admission
.acquire_owned()
.await
.expect("first-byte write admission should remain open");
let write_result =
catch_usage_writer_panic("first-byte batch usage write", leader.write_batch(records)).await;
drop(write_permit);
match write_result {
Ok(()) => {
for (item, _) in grouped {
item.complete_success().await;
}
}
Err(err) => {
state.batch_failed_total.fetch_add(1, Ordering::AcqRel);
warn!(
event_name = "usage_first_byte_upsert_batch_failed",
log_type = "ops",
records = record_count,
error = %err,
"usage runtime failed to persist a first-byte batch; falling back per item"
);
for (item, _) in grouped {
state.direct_failed_total.fetch_add(1, Ordering::AcqRel);
item.enqueue_fallback().await;
}
}
}
}
fn report_first_byte_persistence_join(result: Option<Result<(), tokio::task::JoinError>>) {
if let Some(Err(err)) = result {
warn!(
event_name = "usage_first_byte_persistence_task_failed",
log_type = "ops",
error = %err,
"usage first-byte persistence task failed"
);
}
}
impl Default for UsageRuntime {
fn default() -> Self {
Self::disabled()
}
}
impl UsageRuntime {
pub fn disabled() -> Self {
let terminal_execution = TerminalExecutionDispatcher::disabled();
Self {
config: UsageRuntimeConfig::disabled(),
shutdown: Arc::new(UsageShutdownState::default()),
body_policy_cache: Arc::new(tokio::sync::Mutex::new(None)),
enqueue_retry: UsageEnqueueRetryDispatcher::disabled(),
worker_supervisor_state: Arc::new(UsageWorkerSupervisorState::default()),
worker_record_gate: None,
terminal_submission_state: Arc::new(TerminalSubmissionState::new(1)),
terminal_execution: Arc::clone(&terminal_execution),
terminal_enqueue_state: Arc::new(LifecycleEnqueueState::default()),
terminal_direct_fallback_state: Arc::new(TerminalDirectFallbackState::new(
TERMINAL_DIRECT_FALLBACK_DEFAULT_MAX_IN_FLIGHT,
)),
lifecycle_enqueue_state: Arc::new(LifecycleEnqueueState::default()),
lifecycle_coalescer: Arc::new(LifecycleEventCoalescer::default()),
lifecycle_delay: LifecycleDelayDispatcher::disabled(),
lifecycle_submission: LifecycleSubmissionDispatcher::disabled(),
ordered_lifecycle: OrderedLifecycleDispatcher::disabled(terminal_execution),
pending_persistence: PendingPersistenceDispatcher::disabled(),
first_byte_persistence: FirstBytePersistenceDispatcher::disabled(),
}
}
pub fn new(config: UsageRuntimeConfig) -> Result<Self, DataLayerError> {
config.validate()?;
let shutdown = Arc::new(UsageShutdownState::default());
let enqueue_retry = UsageEnqueueRetryDispatcher::spawn(config.clone(), &shutdown);
let worker_record_gate = config
.worker_record_concurrency_limit
.map(UsageWorkerRecordConcurrencyGate::new)
.map(Arc::new);
let lifecycle_enqueue_state = Arc::new(LifecycleEnqueueState::default());
let lifecycle_coalescer = Arc::new(LifecycleEventCoalescer::new(
config.enqueue_retry_buffer_capacity,
));
if config.enabled {
shutdown
.tasks
.spawn(run_lifecycle_coalescer_compactor(Arc::downgrade(
&lifecycle_coalescer,
)));
}
let terminal_submission_state = Arc::new(TerminalSubmissionState::new(
terminal_submission_limit(&config),
));
let terminal_execution = TerminalExecutionDispatcher::spawn(&config);
let ordered_lifecycle = OrderedLifecycleDispatcher::spawn(
&config,
Arc::clone(&terminal_execution),
&shutdown.tasks,
);
let pending_persistence = PendingPersistenceDispatcher::spawn(&config, &shutdown.tasks);
let terminal_direct_fallback_state = Arc::new(TerminalDirectFallbackState::new(
terminal_direct_fallback_limit(&config),
));
let lifecycle_delay = LifecycleDelayDispatcher::spawn(
config.clone(),
Arc::clone(&lifecycle_coalescer),
Arc::clone(&lifecycle_enqueue_state),
Arc::clone(&enqueue_retry),
&shutdown,
);
let lifecycle_submission = LifecycleSubmissionDispatcher::spawn(&config, &shutdown.tasks);
let first_byte_persistence =
FirstBytePersistenceDispatcher::spawn(&config, &shutdown.tasks);
Ok(Self {
config,
shutdown,
body_policy_cache: Arc::new(tokio::sync::Mutex::new(None)),
enqueue_retry,
worker_supervisor_state: Arc::new(UsageWorkerSupervisorState::default()),
worker_record_gate,
terminal_submission_state,
terminal_execution,
terminal_enqueue_state: Arc::new(LifecycleEnqueueState::default()),
terminal_direct_fallback_state,
lifecycle_enqueue_state,
lifecycle_coalescer,
lifecycle_delay,
lifecycle_submission,
ordered_lifecycle,
pending_persistence,
first_byte_persistence,
})
}
pub fn is_enabled(&self) -> bool {
self.config.enabled
}
/// Call before spawning a request finalizer that can outlive its HTTP body.
pub fn track_producer(&self) -> crate::UsageProducerGuard {
self.shutdown.producers.fetch_add(1, Ordering::AcqRel);
crate::UsageProducerGuard(Arc::clone(&self.shutdown.producers))
}
/// Stop request producers before calling. Queued Redis records remain durable
/// for the next consumer; local retry buffers must reach Redis or the database.
/// A timeout leaves the remaining work running so shutdown can be retried.
pub async fn shutdown(&self, timeout: Duration) -> Result<(), DataLayerError> {
self.shutdown_with_local_queue(timeout, None).await
}
/// A process-local queue must also be consumed before stopping its workers.
pub async fn shutdown_with_local_queue(
&self,
timeout: Duration,
local_queue: Option<Arc<dyn RuntimeQueueStore>>,
) -> Result<(), DataLayerError> {
let drain = async {
let _lock = self.shutdown.lock.lock().await;
while self.shutdown.producers.load(Ordering::Acquire) != 0 {
tokio::time::sleep(Duration::from_millis(10)).await;
}
self.lifecycle_submission.state.admission.close();
self.shutdown.drain.send_replace(true);
while self.local_work_pending() != 0 {
tokio::time::sleep(Duration::from_millis(10)).await;
}
if self.config.enabled {
if let Some(queue) = &local_queue {
loop {
let stats = queue
.stats(&self.config.stream_key, Some(&self.config.consumer_group))
.await?;
if stats.stream_length == 0
|| (stats.group_pending == 0 && stats.group_lag == Some(0))
{
break;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
if queue
.stats(&self.config.dlq_stream_key, None)
.await?
.stream_length
!= 0
{
return Err(DataLayerError::InvalidInput(
"local usage dead-letter queue must be recovered before shutdown"
.into(),
));
}
}
}
self.terminal_submission_state.semaphore.close();
self.shutdown.worker_control.request_shutdown();
while self.shutdown.supervisors.load(Ordering::Acquire) != 0 {
tokio::time::sleep(Duration::from_millis(10)).await;
}
self.shutdown.tasks.stop_idle().await;
Ok(())
};
tokio::time::timeout(timeout, drain).await.map_err(|_| {
DataLayerError::TimedOut(format!(
"usage shutdown incomplete: producers={}, local_work={}, retry_pending={}, workers={}",
self.shutdown.producers.load(Ordering::Acquire),
self.local_work_pending(),
self.enqueue_retry.pending(),
self.shutdown.supervisors.load(Ordering::Acquire),
))
})?
}
fn local_work_pending(&self) -> u64 {
let snapshot = self.metrics_snapshot();
// Admission lives through every ordered handoff, including gaps between
// per-stage gauges. Delayed events and retry buffers retain separate permits.
let admitted = self.lifecycle_submission.state.capacity.saturating_sub(
self.lifecycle_submission
.state
.admission
.available_permits(),
);
[
admitted as u64,
snapshot.delayed_lifecycle_pending as u64,
snapshot.lifecycle_submission_pending as u64,
snapshot.ordered_lifecycle_pending as u64,
snapshot.pending_persistence_pending as u64,
snapshot.first_byte_persistence_pending as u64,
snapshot.terminal_submission_pending as u64,
snapshot.terminal_submission_in_flight as u64,
snapshot.terminal_enqueue_in_flight,
snapshot.terminal_direct_fallback_in_flight as u64,
snapshot.lifecycle_enqueue_in_flight,
snapshot.enqueue_retry_pending,
]
.into_iter()
.fold(0_u64, u64::saturating_add)
}
fn track_worker(&self) -> crate::UsageProducerGuard {
self.shutdown.supervisors.fetch_add(1, Ordering::AcqRel);
crate::UsageProducerGuard(Arc::clone(&self.shutdown.supervisors))
}
pub fn metrics_snapshot(&self) -> UsageRuntimeMetricsSnapshot {
let (
event_capture_memory_budget_bytes,
event_capture_memory_retained_bytes,
event_capture_memory_downgraded_total,
) = crate::event_capture_budget::capture_memory_metrics();
let (queue_payload_downgraded_total, queue_payload_rejected_total) =
crate::queue::payload_encoding_totals();
let queue_read = crate::queue_read_budget::queue_read_budget_metrics();
let dlq_encoding = crate::dead_letter_encoding::dead_letter_encoding_metrics();
UsageRuntimeMetricsSnapshot {
queue_payload_max_bytes: self.config.queue_payload_max_bytes,
queue_payload_downgraded_total,
queue_payload_rejected_total,
queue_read_payload_budget_bytes: queue_read.limit_bytes,
queue_read_batch_payload_bytes: queue_read.batch_limit_bytes,
queue_read_payload_reserved_bytes: queue_read.reserved_bytes,
queue_read_payload_waiters: queue_read.waiters,
queue_read_payload_wait_total: queue_read.wait_total,
queue_read_actual_field_bytes_total: queue_read.actual_field_bytes_total,
queue_read_oversized_entries_total: queue_read.oversized_entries_total,
queue_read_oversized_batches_total: queue_read.oversized_batches_total,
dlq_encoding_budget_bytes: dlq_encoding.limit_bytes,
dlq_encoding_max_jobs: dlq_encoding.job_limit,
dlq_encoding_reserved_bytes: dlq_encoding.reserved_bytes,
dlq_encoding_active_jobs: dlq_encoding.active_jobs,
dlq_encoding_capacity_rejected_total: dlq_encoding.capacity_rejected_total,
dlq_encoding_oversized_rejected_total: dlq_encoding.oversized_rejected_total,
dlq_encoding_encoded_total: dlq_encoding.encoded_total,
enqueue_retry_permanent_failure_total: self.enqueue_retry.permanent_failure_total(),
event_capture_memory_budget_bytes,
event_capture_memory_retained_bytes,
event_capture_memory_downgraded_total,
enabled: self.config.enabled,
queue_terminal_events: self.config.queue_terminal_events,
shutdown_started: *self.shutdown.drain.borrow(),
producers_in_flight: self.shutdown.producers.load(Ordering::Acquire),
delayed_lifecycle_pending: self.lifecycle_delay.sender.as_ref().map_or(0, |sender| {
sender
.max_capacity()
.saturating_sub(self.lifecycle_delay.admission.available_permits())
}),
queue_lifecycle_events: self.config.queue_lifecycle_events,
worker_count: self.config.worker_count,
worker_autoscale_enabled: self.config.worker_autoscale_enabled,
worker_max_count: self.config.worker_max_count,
worker_record_concurrency_limit: self
.worker_record_gate
.as_ref()
.map(|gate| gate.limit()),
worker_record_concurrency_in_flight: self
.worker_record_gate
.as_ref()
.map(|gate| gate.in_flight())
.unwrap_or_default(),
worker_record_concurrency_max_in_flight: self
.worker_record_gate
.as_ref()
.map(|gate| gate.max_in_flight())
.unwrap_or_default(),
worker_record_concurrency_wait_total: self
.worker_record_gate
.as_ref()
.map(|gate| gate.wait_total())
.unwrap_or_default(),
worker_record_deferred_total: self
.worker_record_gate
.as_ref()
.map(|gate| gate.deferred_total())
.unwrap_or_default(),
worker_active_count: self
.worker_supervisor_state
.active_count
.load(Ordering::Acquire),
worker_desired_count: self
.worker_supervisor_state
.desired_count
.load(Ordering::Acquire),
worker_read_batches_total: self
.worker_supervisor_state
.read_batches_total
.load(Ordering::Acquire),
worker_read_entries_total: self
.worker_supervisor_state
.read_entries_total
.load(Ordering::Acquire),
worker_reclaimed_entries_total: self
.worker_supervisor_state
.reclaimed_entries_total
.load(Ordering::Acquire),
worker_acked_entries_total: self
.worker_supervisor_state
.acked_entries_total
.load(Ordering::Acquire),
worker_dead_lettered_entries_total: self
.worker_supervisor_state
.dead_lettered_entries_total
.load(Ordering::Acquire),
worker_process_failures_total: self
.worker_supervisor_state
.process_failures_total
.load(Ordering::Acquire),
worker_read_failures_total: self
.worker_supervisor_state
.read_failures_total
.load(Ordering::Acquire),
worker_reclaim_failures_total: self
.worker_supervisor_state
.reclaim_failures_total
.load(Ordering::Acquire),
retry_deferred_lifecycle_events: self.config.retry_deferred_lifecycle_events,
terminal_submission_limit: self.terminal_submission_state.limit(),
terminal_submission_pending: self.terminal_submission_state.pending(),
terminal_submission_max_pending: self.terminal_submission_state.max_pending(),
terminal_submission_in_flight: self.terminal_submission_state.in_flight(),
terminal_submission_max_in_flight: self.terminal_submission_state.max_in_flight(),
terminal_submission_rejected_total: self.terminal_submission_state.rejected_total(),
terminal_enqueue_in_flight: self.terminal_enqueue_state.in_flight(),
terminal_enqueue_deferred_total: self.terminal_enqueue_state.deferred_total(),
terminal_enqueue_deferred_direct_write_total: self
.terminal_enqueue_state
.deferred_direct_write_total(),
terminal_enqueue_deferred_dropped_total: self
.terminal_enqueue_state
.deferred_dropped_total(),
terminal_enqueue_deferred_retry_total: self
.terminal_enqueue_state
.deferred_retry_total(),
terminal_enqueue_failed_total: self.terminal_enqueue_state.failed_total(),
terminal_direct_fallback_limit: self.terminal_direct_fallback_state.limit(),
terminal_direct_fallback_in_flight: self.terminal_direct_fallback_state.in_flight(),
terminal_direct_fallback_max_in_flight: self
.terminal_direct_fallback_state
.max_in_flight(),
terminal_direct_fallback_succeeded_total: self
.terminal_direct_fallback_state
.succeeded_total(),
terminal_direct_fallback_failed_total: self
.terminal_direct_fallback_state
.failed_total(),
terminal_direct_fallback_rejected_total: self
.terminal_direct_fallback_state
.rejected_total(),
lifecycle_enqueue_in_flight: self.lifecycle_enqueue_state.in_flight(),
lifecycle_enqueue_deferred_total: self.lifecycle_enqueue_state.deferred_total(),
lifecycle_enqueue_deferred_dropped_total: self
.lifecycle_enqueue_state
.deferred_dropped_total(),
lifecycle_enqueue_deferred_retry_total: self
.lifecycle_enqueue_state
.deferred_retry_total(),
lifecycle_enqueue_failed_total: self.lifecycle_enqueue_state.failed_total(),
lifecycle_submission_capacity: self.lifecycle_submission.state.capacity,
lifecycle_submission_workers: self.lifecycle_submission.state.workers,
lifecycle_submission_pending: self
.lifecycle_submission
.state
.pending
.load(Ordering::Acquire),
lifecycle_submission_max_pending: self
.lifecycle_submission
.state
.max_pending
.load(Ordering::Acquire),
lifecycle_submission_enqueued_total: self
.lifecycle_submission
.state
.enqueued_total
.load(Ordering::Acquire),
lifecycle_submission_coalesced_total: self
.lifecycle_submission
.state
.coalesced_total
.load(Ordering::Acquire),
lifecycle_submission_overflow_total: self
.lifecycle_submission
.state
.overflow_total
.load(Ordering::Acquire),
lifecycle_submission_processed_total: self
.lifecycle_submission
.state
.processed_total
.load(Ordering::Acquire),
ordered_lifecycle_pending: self
.ordered_lifecycle
.core
.state
.pending
.load(Ordering::Acquire),
ordered_lifecycle_max_pending: self
.ordered_lifecycle
.core
.state
.max_pending
.load(Ordering::Acquire),
pending_persistence_capacity: self.pending_persistence.state.capacity,
pending_persistence_pending: self
.pending_persistence
.state
.pending
.load(Ordering::Acquire),
pending_persistence_max_pending: self
.pending_persistence
.state
.max_pending
.load(Ordering::Acquire),
pending_persistence_batch_flush_total: self
.pending_persistence
.state
.batch_flush_total
.load(Ordering::Acquire),
pending_persistence_batch_records_total: self
.pending_persistence
.state
.batch_records_total
.load(Ordering::Acquire),
pending_persistence_max_batch_size: self
.pending_persistence
.state
.max_batch_size
.load(Ordering::Acquire),
pending_persistence_batch_failed_total: self
.pending_persistence
.state
.batch_failed_total
.load(Ordering::Acquire),
pending_persistence_retried_total: self
.pending_persistence
.state
.retried_total
.load(Ordering::Acquire),
pending_persistence_overflow_total: self
.pending_persistence
.state
.overflow_total
.load(Ordering::Acquire),
lifecycle_coalescer_entries: self
.lifecycle_coalescer
.entry_count
.load(Ordering::Acquire),
lifecycle_coalescer_compact_total: self
.lifecycle_coalescer
.compact_total
.load(Ordering::Acquire),
lifecycle_coalescer_compact_entries_scanned_total: self
.lifecycle_coalescer
.compact_entries_scanned_total
.load(Ordering::Acquire),
first_byte_persistence_capacity: self.first_byte_persistence.state.capacity,
first_byte_persistence_pending: self
.first_byte_persistence
.state
.pending
.load(Ordering::Acquire),
first_byte_persistence_max_pending: self
.first_byte_persistence
.state
.max_pending
.load(Ordering::Acquire),
first_byte_persistence_dispatched_total: self
.first_byte_persistence
.state
.dispatched_total
.load(Ordering::Acquire),
first_byte_persistence_overflow_total: self
.first_byte_persistence
.state
.overflow_total
.load(Ordering::Acquire),
first_byte_persistence_cancelled_total: self
.first_byte_persistence
.state
.cancelled_total
.load(Ordering::Acquire),
first_byte_persistence_direct_succeeded_total: self
.first_byte_persistence
.state
.direct_succeeded_total
.load(Ordering::Acquire),
first_byte_persistence_direct_failed_total: self
.first_byte_persistence
.state
.direct_failed_total
.load(Ordering::Acquire),
first_byte_persistence_batch_flush_total: self
.first_byte_persistence
.state
.batch_flush_total
.load(Ordering::Acquire),
first_byte_persistence_batch_records_total: self
.first_byte_persistence
.state
.batch_records_total
.load(Ordering::Acquire),
first_byte_persistence_max_batch_size: self
.first_byte_persistence
.state
.max_batch_size
.load(Ordering::Acquire),
first_byte_persistence_batch_failed_total: self
.first_byte_persistence
.state
.batch_failed_total
.load(Ordering::Acquire),
first_byte_persistence_fallback_accepted_total: self
.first_byte_persistence
.state
.fallback_accepted_total
.load(Ordering::Acquire),
first_byte_persistence_fallback_failed_total: self
.first_byte_persistence
.state
.fallback_failed_total
.load(Ordering::Acquire),
enqueue_retry_scheduled_total: self.enqueue_retry.scheduled_total(),
enqueue_retry_recovered_total: self.enqueue_retry.recovered_total(),
enqueue_retry_pending: self.enqueue_retry.pending(),
enqueue_retry_failed_total: self.enqueue_retry.retry_failed_total(),
enqueue_retry_closed_or_unavailable_total: self
.enqueue_retry
.closed_or_unavailable_total(),
}
}
pub async fn queue_health_snapshot<T>(
&self,
data: &T,
) -> Result<UsageQueueHealthSnapshot, DataLayerError>
where
T: UsageRuntimeAccess,
{
let mut snapshot = UsageQueueHealthSnapshot {
enabled: self.config.enabled,
configured: false,
stream_key: self.config.stream_key.clone(),
consumer_group: self.config.consumer_group.clone(),
dlq_stream_key: self.config.dlq_stream_key.clone(),
stream_length: 0,
group_pending: 0,
group_lag: None,
oldest_pending_idle_ms: None,
dlq_length: 0,
};
if !self.config.enabled {
return Ok(snapshot);
}
let Some(runner) = data.usage_worker_queue() else {
return Ok(snapshot);
};
snapshot.configured = true;
let stream_stats = runner
.stats(&self.config.stream_key, Some(&self.config.consumer_group))
.await?;
let dlq_stats = runner.stats(&self.config.dlq_stream_key, None).await?;
snapshot.apply_stream_stats(stream_stats);
snapshot.dlq_length = dlq_stats.stream_length;
Ok(snapshot)
}
pub fn can_spawn_worker<T>(&self, data: &T) -> bool
where
T: UsageRuntimeAccess,
{
self.is_enabled()
&& (self.config.queue_terminal_events || self.config.queue_lifecycle_events)
&& data.has_usage_writer()
&& data.has_usage_worker_queue()
}
pub fn spawn_worker<T>(&self, data: Arc<T>) -> Option<tokio::task::JoinHandle<()>>
where
T: UsageRuntimeAccess + 'static,
{
if !self.can_spawn_worker(data.as_ref()) {
return None;
}
let runner = data.usage_worker_queue()?;
let worker = build_usage_queue_worker_with_record_gate(
runner,
data,
self.config.clone(),
self.worker_record_gate.clone(),
None,
)
.ok()?;
let worker = worker.with_shutdown(self.shutdown.worker_control.clone());
let guard = self.track_worker();
Some(spawn_on_usage_background_runtime(async move {
let _guard = guard;
worker.run().await;
}))
}
pub fn spawn_workers<T>(&self, data: Arc<T>) -> Vec<tokio::task::JoinHandle<()>>
where
T: UsageRuntimeAccess + 'static,
{
if !self.can_spawn_worker(data.as_ref()) {
return Vec::new();
}
let Some(runner) = data.usage_worker_queue() else {
return Vec::new();
};
let worker_count = self.config.worker_count.max(1);
let mut handles = Vec::with_capacity(worker_count);
for worker_index in 0..worker_count {
let Ok(worker) = build_usage_queue_worker_with_record_gate(
Arc::clone(&runner),
Arc::clone(&data),
self.config.clone(),
self.worker_record_gate.clone(),
Some(worker_index),
) else {
warn!(
event_name = "usage_worker_build_failed",
log_type = "ops",
worker_index,
"usage runtime failed to build usage queue worker"
);
continue;
};
let worker = worker.with_shutdown(self.shutdown.worker_control.clone());
let guard = self.track_worker();
handles.push(spawn_on_usage_background_runtime(async move {
let _guard = guard;
worker.run().await;
}));
}
handles
}
pub fn spawn_worker_supervisor<T>(&self, data: Arc<T>) -> Option<tokio::task::JoinHandle<()>>
where
T: UsageRuntimeAccess + 'static,
{
if !self.can_spawn_worker(data.as_ref()) {
return None;
}
let runner = data.usage_worker_queue()?;
let runtime = self.clone();
let guard = self.track_worker();
Some(spawn_on_usage_background_runtime(async move {
let _guard = guard;
run_usage_worker_supervisor(
runner,
data,
runtime.config.clone(),
runtime.worker_record_gate.clone(),
Arc::clone(&runtime.worker_supervisor_state),
runtime.shutdown.worker_control.clone(),
)
.await;
}))
}
pub fn record_pending<T>(&self, data: &T, seed: LifecycleUsageSeed)
where
T: UsageRuntimeAccess + Clone + 'static,
{
if !self.is_enabled() {
return;
}
let request_id = seed.request_id.clone();
self.lifecycle_submission
.dispatch(Box::new(LifecycleSubmissionItemImpl {
runtime: self.clone(),
data: T::clone(data),
request_id,
payload: LifecycleSubmissionPayload::Pending {
seed,
observed_at_unix_secs: now_unix_secs(),
},
}));
}
pub async fn record_pending_direct<T>(&self, data: &T, seed: LifecycleUsageSeed)
where
T: UsageRuntimeAccess + Clone + 'static,
{
if !self.is_enabled() {
return;
}
// Keep the direct API non-blocking as well. The ordered dispatcher builds the lightweight
// pending event off the response task and commits it before this request's streaming item.
self.record_pending(data, seed);
}
pub fn record_stream_started<T>(
&self,
data: &T,
seed: &LifecycleUsageSeed,
status_code: u16,
telemetry: Option<&ExecutionTelemetry>,
) where
T: UsageRuntimeAccess + Clone + 'static,
{
if !self.is_enabled() {
return;
}
let seed = seed.clone();
let telemetry = telemetry.cloned();
let request_id = seed.request_id.clone();
self.lifecycle_submission
.dispatch(Box::new(LifecycleSubmissionItemImpl {
runtime: self.clone(),
data: T::clone(data),
request_id,
payload: LifecycleSubmissionPayload::Streaming {
seed,
status_code,
telemetry,
observed_at_unix_secs: now_unix_secs(),
},
}));
}
async fn dispatch_terminal<T>(
&self,
data: &T,
mut event: UsageEvent,
direct: bool,
completion: Option<tokio::sync::oneshot::Sender<()>>,
) where
T: UsageRuntimeAccess + Clone + 'static,
{
prepare_event_capture_memory(
&mut event,
crate::event_capture_budget::shared_capture_memory_budget(),
);
let request_id = event.request_id.clone();
self.lifecycle_submission
.dispatch_terminal(Box::new(LifecycleSubmissionItemImpl {
runtime: self.clone(),
data: T::clone(data),
request_id,
payload: LifecycleSubmissionPayload::Terminal {
event,
direct,
completion,
},
}))
.await;
}
async fn dispatch_terminal_seed<T>(
&self,
data: &T,
request_id: String,
seed: LifecycleTerminalUsageSeed,
) where
T: UsageRuntimeAccess + Clone + 'static,
{
let observed_at_unix_ms = now_unix_ms();
let seed = seed
.prepare_capture_memory(crate::event_capture_budget::shared_capture_memory_budget());
self.lifecycle_submission
.dispatch_terminal(Box::new(LifecycleSubmissionItemImpl {
runtime: self.clone(),
data: T::clone(data),
request_id,
payload: LifecycleSubmissionPayload::TerminalSeed {
seed,
observed_at_unix_ms,
},
}))
.await;
}
async fn await_lifecycle_submission_turn(
&self,
request_id: &str,
) -> Option<OrderedLifecycleCompletion> {
let (completion, completed) = tokio::sync::oneshot::channel();
let accepted = self
.lifecycle_submission
.dispatch_terminal(Box::new(LifecycleSubmissionBarrierItem {
request_id: request_id.to_string(),
ordered_lifecycle: Arc::clone(&self.ordered_lifecycle),
completion,
}))
.await;
if !accepted {
return None;
}
match completed.await {
Ok(ordered_completion) => Some(ordered_completion),
Err(_) => {
warn!(
event_name = "usage_lifecycle_barrier_completion_dropped",
log_type = "ops",
request_id,
fallback = "continue_direct_terminal",
"usage lifecycle barrier completion was dropped before success"
);
None
}
}
}
pub async fn record_stream_started_direct<T>(
&self,
data: &T,
seed: &LifecycleUsageSeed,
status_code: u16,
telemetry: Option<&ExecutionTelemetry>,
) where
T: UsageRuntimeAccess + Clone + 'static,
{
if !self.is_enabled() {
return;
}
// Match `record_stream_started`: the caller must not wait for event construction or DB I/O.
self.record_stream_started(data, seed, status_code, telemetry);
}
pub fn record_sync_active_immediate_async<T>(&self, data: &T, seed: LifecycleUsageSeed)
where
T: UsageRuntimeAccess + Clone + 'static,
{
if !self.is_enabled() {
return;
}
let request_id = seed.request_id.clone();
self.lifecycle_submission
.dispatch(Box::new(LifecycleSubmissionItemImpl {
runtime: self.clone(),
data: T::clone(data),
request_id,
payload: LifecycleSubmissionPayload::Active {
seed,
observed_at_unix_secs: now_unix_secs(),
},
}));
}
pub fn record_stream_started_immediate_async<T>(
&self,
data: &T,
seed: LifecycleUsageSeed,
status_code: u16,
telemetry: Option<ExecutionTelemetry>,
) where
T: UsageRuntimeAccess + Clone + 'static,
{
if !self.is_enabled() {
return;
}
self.record_stream_started(data, &seed, status_code, telemetry.as_ref());
}
async fn begin_terminal_submission(
&self,
request_id: &str,
) -> Option<TerminalSubmissionPermit> {
let permit = self.terminal_submission_state.acquire().await;
self.report_terminal_submission_rejection(request_id, permit.is_none());
permit
}
async fn begin_registered_terminal_submission(
&self,
request_id: &str,
pending_guard: TerminalSubmissionPendingGuard,
) -> Option<TerminalSubmissionPermit> {
let permit = self
.terminal_submission_state
.acquire_registered(pending_guard)
.await;
self.report_terminal_submission_rejection(request_id, permit.is_none());
permit
}
fn report_terminal_submission_rejection(&self, request_id: &str, rejected: bool) {
if !rejected {
return;
}
let rejected_total = self.terminal_submission_state.rejected_total();
if should_log_usage_retry_counter(rejected_total) {
warn!(
event_name = "usage_terminal_submission_rejected",
log_type = "event",
request_id,
submission_limit = self.terminal_submission_state.limit(),
rejected_total,
fallback = "drop",
"usage runtime terminal submission admission closed"
);
}
}
pub async fn record_sync_terminal<T>(
&self,
data: &T,
context_seed: TerminalUsageContextSeed,
payload_seed: SyncTerminalUsagePayloadSeed,
) where
T: UsageRuntimeAccess + Clone + 'static,
{
if !self.is_enabled() {
return;
}
let request_id = context_seed.request_id.clone();
self.dispatch_terminal_seed(
data,
request_id,
LifecycleTerminalUsageSeed::Sync {
context: context_seed,
payload: payload_seed,
capture: None,
},
)
.await;
}
pub async fn record_stream_terminal<T>(
&self,
data: &T,
context_seed: TerminalUsageContextSeed,
payload_seed: StreamTerminalUsagePayloadSeed,
cancelled: bool,
) where
T: UsageRuntimeAccess + Clone + 'static,
{
if !self.is_enabled() {
return;
}
let request_id = context_seed.request_id.clone();
self.dispatch_terminal_seed(
data,
request_id,
LifecycleTerminalUsageSeed::Stream {
context: context_seed,
payload: payload_seed,
cancelled,
capture: None,
},
)
.await;
}
pub async fn submit_terminal_event<T>(&self, data: &T, event: UsageEvent)
where
T: UsageRuntimeAccess + Clone + 'static,
{
if !self.is_enabled() {
return;
}
self.dispatch_terminal(data, event, false, None).await;
}
pub async fn record_terminal_event<T>(&self, data: &T, mut event: UsageEvent)
where
T: UsageRuntimeAccess,
{
if !self.is_enabled() || self.lifecycle_submission.state.admission.is_closed() {
return;
}
prepare_event_capture_memory(
&mut event,
crate::event_capture_budget::shared_capture_memory_budget(),
);
let ordered_completion = self
.await_lifecycle_submission_turn(&event.request_id)
.await;
let Some(_submission_permit) = self.begin_terminal_submission(&event.request_id).await
else {
return;
};
self.apply_body_capture_policy_from_data(data, &mut event)
.await;
let persistence_outcome = self.enqueue_or_write_terminal(data, event).await;
if persistence_outcome != TerminalPersistenceOutcome::Failed {
if let Some(completion) = ordered_completion {
completion.complete();
}
}
}
pub async fn record_terminal_event_direct<T>(&self, data: &T, mut event: UsageEvent)
where
T: UsageRuntimeAccess,
{
if !self.is_enabled() || self.lifecycle_submission.state.admission.is_closed() {
return;
}
prepare_event_capture_memory(
&mut event,
crate::event_capture_budget::shared_capture_memory_budget(),
);
let ordered_completion = self
.await_lifecycle_submission_turn(&event.request_id)
.await;
let Some(_submission_permit) = self.begin_terminal_submission(&event.request_id).await
else {
return;
};
self.apply_body_capture_policy_from_data(data, &mut event)
.await;
if enrich_terminal_event(data, &mut event).await.is_err() {
return;
}
let request_id = event.request_id.clone();
if self.write_event_direct(data, &event).await {
self.lifecycle_coalescer
.cancel_delayed_for_queued_terminal(&request_id)
.await;
if let Some(completion) = ordered_completion {
completion.complete();
}
}
}
async fn apply_body_capture_policy_from_data<T>(&self, data: &T, event: &mut UsageEvent)
where
T: UsageRuntimeAccess,
{
self.apply_body_capture_policy_with_budget(
data,
event,
crate::event_capture_budget::shared_capture_memory_budget(),
)
.await;
}
async fn apply_body_capture_policy_with_budget<T>(
&self,
data: &T,
event: &mut UsageEvent,
budget: Arc<crate::event_capture_budget::EventCaptureMemoryBudget>,
) where
T: UsageRuntimeAccess,
{
// A slow policy read must not retain unbudgeted JSON in mutex waiters.
// Denied captures count even when the eventual Basic policy disables them.
prepare_event_capture_memory(event, Arc::clone(&budget));
match self.cached_body_capture_policy(data).await {
Ok(policy) => apply_usage_body_capture_policy_to_event(policy, event),
Err(err) => {
warn!(
event_name = "usage_body_capture_policy_read_failed",
log_type = "event",
request_id = %event.request_id,
fallback = "basic",
error = %err,
"usage runtime failed to read body capture policy; disabling body capture"
);
apply_usage_body_capture_policy_to_event(UsageBodyCapturePolicy::default(), event);
}
}
event.data.apply_capture_memory_budget(budget);
}
pub async fn body_capture_policy_for<T>(
&self,
data: &T,
) -> Result<UsageBodyCapturePolicy, DataLayerError>
where
T: UsageRuntimeAccess,
{
self.cached_body_capture_policy(data).await
}
async fn cached_body_capture_policy<T>(
&self,
data: &T,
) -> Result<UsageBodyCapturePolicy, DataLayerError>
where
T: UsageRuntimeAccess,
{
let mut cache = self.body_policy_cache.lock().await;
if let Some(entry) = cache.as_ref() {
if entry.cached_at.elapsed() <= entry.ttl {
return match entry.source {
UsageBodyCapturePolicyCacheSource::Loaded => Ok(entry.policy),
UsageBodyCapturePolicyCacheSource::FallbackAfterError => {
Ok(UsageBodyCapturePolicy::default())
}
};
}
}
match data.body_capture_policy().await {
Ok(policy) => {
*cache = Some(UsageBodyCapturePolicyCacheEntry::loaded(policy));
Ok(policy)
}
Err(err) => {
*cache = Some(UsageBodyCapturePolicyCacheEntry::fallback_after_error());
Err(err)
}
}
}
async fn enqueue_or_write_terminal<T>(
&self,
data: &T,
event: UsageEvent,
) -> TerminalPersistenceOutcome
where
T: UsageRuntimeAccess,
{
let request_id = event.request_id.clone();
let outcome = self
.enqueue_or_write_event(data, event, "terminal", self.config.queue_terminal_events)
.await;
// A direct terminal fallback can race the already-dispatched first-byte
// write. Keep the delayed-terminal marker used by the queue path so an
// in-flight `pending -> streaming` transition is not cancelled before it
// reaches the database.
if matches!(
outcome,
TerminalPersistenceOutcome::PersistedDirectly | TerminalPersistenceOutcome::Queued
) {
self.lifecycle_coalescer
.cancel_delayed_for_queued_terminal(&request_id)
.await;
}
outcome
}
async fn enqueue_or_write_lifecycle<T>(&self, data: &T, event: UsageEvent)
where
T: UsageRuntimeAccess + Clone + 'static,
{
if self.config.queue_lifecycle_events {
// Keep pre-first-byte churn debounced, but publish the observed first byte as a single
// background queue event so the live usage row can enter `streaming` without
// putting Redis or database I/O back on the response-body critical path.
if event.event_type == crate::UsageEventType::Streaming
&& event.data.first_byte_time_ms.is_some()
{
self.enqueue_first_byte_lifecycle_event(data, event).await;
} else {
self.enqueue_lifecycle_event_with_config_delay(data, event)
.await;
}
} else {
self.write_event_direct(data, &event).await;
}
}
/// Persist a lifecycle phase before this request's later phase is executed.
///
/// A Redis append is not a persistence barrier: a first-byte or terminal write can otherwise
/// reach the database before the queued pending event is consumed. This ordered handoff runs
/// on the usage runtime, so callers only enqueue a small seed and never wait on the database.
async fn persist_ordered_lifecycle_event<T>(
&self,
data: &T,
event: &UsageEvent,
phase: &'static str,
) -> bool
where
T: UsageRuntimeAccess,
{
let mut attempts = 0_u64;
loop {
let write_succeeded = if let Some(gate) = &self.worker_record_gate {
let _permit = gate.acquire().await;
self.write_event_direct(data, event).await
} else {
self.write_event_direct(data, event).await
};
if write_succeeded {
if attempts > 0 {
info!(
event_name = "usage_pending_ordered_persistence_recovered",
log_type = "ops",
request_id = %event.request_id,
retry_attempts = attempts,
lifecycle_phase = phase,
"ordered lifecycle persistence recovered before the next transition"
);
}
return true;
}
attempts = attempts.saturating_add(1);
if attempts >= ORDERED_INTERMEDIATE_RETRIES_BEFORE_DEGRADE {
warn!(
event_name = "usage_ordered_lifecycle_persistence_degraded",
log_type = "ops",
request_id = %event.request_id,
lifecycle_phase = phase,
retry_attempts = attempts,
fallback = "drop_intermediate_only",
"ordered intermediate persistence exhausted its bounded retry budget"
);
return false;
}
let delay = usage_enqueue_retry_delay(&self.config, attempts);
warn!(
event_name = "usage_ordered_lifecycle_persistence_retry",
log_type = "ops",
request_id = %event.request_id,
lifecycle_phase = phase,
retry_attempt = attempts,
retry_delay_ms = delay.as_millis() as u64,
"ordered lifecycle persistence failed; keeping the request transition behind a retry"
);
tokio::time::sleep(delay).await;
}
}
async fn persist_ordered_terminal_event<T>(
&self,
data: &T,
mut event: UsageEvent,
direct: bool,
) -> TerminalPersistenceOutcome
where
T: UsageRuntimeAccess,
{
if !direct {
// The normal queue path remains available for queue-only nodes and preserves the
// existing terminal fallback policy. It executes only after earlier ordered phases.
return self.enqueue_or_write_terminal(data, event).await;
}
if enrich_terminal_event(data, &mut event).await.is_err() {
return TerminalPersistenceOutcome::Failed;
}
let request_id = event.request_id.clone();
if self.write_event_direct(data, &event).await {
self.lifecycle_coalescer
.cancel_delayed_for_queued_terminal(&request_id)
.await;
TerminalPersistenceOutcome::PersistedDirectly
} else {
TerminalPersistenceOutcome::Failed
}
}
async fn enqueue_first_byte_lifecycle_event<T>(&self, data: &T, event: UsageEvent)
where
T: UsageRuntimeAccess + Clone + 'static,
{
self.enqueue_first_byte_lifecycle_event_inner(data, event, false, None)
.await;
}
async fn enqueue_first_byte_lifecycle_event_inner<T>(
&self,
data: &T,
event: UsageEvent,
wait_for_completion: bool,
ordered_completion: Option<OrderedLifecycleCompletion>,
) where
T: UsageRuntimeAccess + Clone + 'static,
{
let request_id = event.request_id.clone();
let Some(generation) = self.lifecycle_coalescer.mark_first_byte(&request_id).await else {
if let Some(ordered_completion) = ordered_completion {
warn!(
event_name = "usage_ordered_first_byte_not_current",
log_type = "ops",
request_id,
fallback = "drop_duplicate_first_byte",
"ordered first-byte transition was already superseded; allowing later phases to advance"
);
ordered_completion.complete();
}
return;
};
let mut marker_guard = FirstByteMarkerGuard::new(
Arc::clone(&self.lifecycle_coalescer),
request_id.clone(),
generation,
);
let runtime = self.clone();
let data = T::clone(data);
if !data.has_usage_writer() || !data.supports_first_byte_usage_fast_path() {
if let Some(ordered_completion) = ordered_completion {
if data.has_usage_writer() {
let direct_event = make_first_byte_event_lightweight(event);
let _ = runtime
.persist_ordered_lifecycle_event(&data, &direct_event, "streaming")
.await;
runtime
.lifecycle_coalescer
.complete_first_byte(&request_id, generation)
.await;
} else {
enqueue_first_byte_fallback(runtime, data, event, request_id, generation).await;
}
marker_guard.disarm();
ordered_completion.complete();
return;
}
enqueue_first_byte_fallback(runtime, data, event, request_id, generation).await;
marker_guard.disarm();
return;
}
// Keep the complete event for a queue/worker retry. Only the direct database write
// receives the slim payload so a transient direct-write failure cannot overwrite the
// pending row's body-capture and audit metadata with a partial snapshot.
let direct_event = make_first_byte_event_lightweight(event.clone());
let dispatcher = Arc::clone(&runtime.first_byte_persistence);
let (completion, completed) = if wait_for_completion {
let (completion, completed) = tokio::sync::oneshot::channel();
(Some(completion), Some(completed))
} else {
(None, None)
};
dispatcher
.dispatch(Box::new(FirstBytePersistenceItemImpl {
runtime,
data,
direct_event,
fallback_event: event,
request_id,
generation,
marker_guard,
completion,
ordered_completion,
}))
.await;
if let Some(completed) = completed {
let _ = completed.await;
}
}
async fn enqueue_lifecycle_event_with_config_delay<T>(&self, data: &T, event: UsageEvent)
where
T: UsageRuntimeAccess + Clone + 'static,
{
let delay = Duration::from_millis(self.config.lifecycle_enqueue_delay_ms);
if delay.is_zero() {
self.enqueue_lifecycle_event(data, event).await;
return;
}
let request_id = event.request_id.clone();
let Some(generation) = self.lifecycle_coalescer.register(request_id.clone()).await else {
return;
};
let data = T::clone(data);
if let Err(event) = self.lifecycle_delay.schedule(data, event, generation).await {
self.lifecycle_coalescer
.abandon(&request_id, generation)
.await;
self.lifecycle_enqueue_state.record_deferred(
"usage_lifecycle_delay_buffer_deferred",
"delay_buffer_unavailable",
event.event_type,
&event.request_id,
DeferredEnqueueFallback::Drop,
);
}
}
async fn enqueue_lifecycle_event<T>(&self, data: &T, event: UsageEvent) -> bool
where
T: UsageRuntimeAccess,
{
if !self.config.queue_lifecycle_events {
warn!(
event_name = "usage_lifecycle_event_not_queued",
log_type = "event",
usage_event_type = ?event.event_type,
request_id = %event.request_id,
fallback = "none",
"usage runtime lifecycle queue is disabled; lifecycle event will not be written directly"
);
return false;
}
enqueue_lifecycle_event_now(
data,
event,
self.config.clone(),
Arc::clone(&self.lifecycle_enqueue_state),
Arc::clone(&self.enqueue_retry),
)
.await
}
async fn enqueue_or_write_event<T>(
&self,
data: &T,
mut event: UsageEvent,
event_phase: &'static str,
queue_enabled: bool,
) -> TerminalPersistenceOutcome
where
T: UsageRuntimeAccess,
{
if queue_enabled {
if let Some(runner) = data.usage_worker_queue() {
match UsageQueue::new(runner, self.config.clone()) {
Ok(queue) => {
if event_phase == "terminal" {
return self
.enqueue_terminal_event_or_fallback(data, queue, event)
.await;
}
match queue.enqueue(&event).await {
Ok(_) => return TerminalPersistenceOutcome::Queued,
Err(err) => {
return if self.enqueue_retry.schedule(
queue,
event,
event_phase,
err,
) {
TerminalPersistenceOutcome::BufferedForRetry
} else {
TerminalPersistenceOutcome::Failed
};
}
}
}
Err(err) => {
warn!(
event_name = "usage_event_queue_init_failed",
log_type = "event",
event_phase,
usage_event_type = ?event.event_type,
request_id = %event.request_id,
fallback = "direct_write",
error = %err,
"usage runtime failed to build queue; falling back to direct write"
)
}
}
}
}
if event_phase == "terminal" && queue_enabled {
let usage_event_type = event.event_type;
let request_id = event.request_id.clone();
let direct_write_succeeded = self
.try_write_terminal_direct_fallback(data, &mut event, "bounded_local_enqueue_retry")
.await;
let deferred_fallback = if direct_write_succeeded {
DeferredEnqueueFallback::DirectWrite
} else {
DeferredEnqueueFallback::Drop
};
self.terminal_enqueue_state.record_deferred(
"usage_terminal_event_enqueue_deferred",
"queue_unavailable",
usage_event_type,
&request_id,
deferred_fallback,
);
return if direct_write_succeeded {
TerminalPersistenceOutcome::PersistedDirectly
} else {
TerminalPersistenceOutcome::Failed
};
}
if event_phase == "terminal" && enrich_terminal_event(data, &mut event).await.is_err() {
return TerminalPersistenceOutcome::Failed;
}
if self.write_event_direct(data, &event).await {
TerminalPersistenceOutcome::PersistedDirectly
} else {
TerminalPersistenceOutcome::Failed
}
}
async fn enqueue_terminal_event_or_fallback<T>(
&self,
data: &T,
queue: UsageQueue,
mut event: UsageEvent,
) -> TerminalPersistenceOutcome
where
T: UsageRuntimeAccess,
{
let now_ms = now_unix_ms();
if self.terminal_enqueue_state.is_circuit_open(now_ms) {
return self
.defer_terminal_event(
data,
queue,
event,
"circuit_open",
DataLayerError::TimedOut("terminal enqueue circuit is open".to_string()),
)
.await;
}
let Some(_guard) = self
.terminal_enqueue_state
.try_acquire_in_flight(self.config.terminal_enqueue_max_in_flight)
else {
return self
.defer_terminal_event(
data,
queue,
event,
"in_flight_limit",
DataLayerError::TimedOut("terminal enqueue in-flight limit".to_string()),
)
.await;
};
let enqueue_result = match queue.encode_event(&event) {
Ok(encoded) => {
if encoded.diagnostics_omitted
&& self
.try_write_terminal_direct_fallback(data, &mut event, "queue_wire_limit")
.await
{
return TerminalPersistenceOutcome::PersistedDirectly;
}
queue.enqueue_encoded(encoded).await
}
Err(err) => Err(err),
};
if let Err(err) = enqueue_result {
drop(_guard);
if is_permanent_enqueue_error(&err) {
return self
.defer_terminal_event(data, queue, event, "invalid_input", err)
.await;
}
self.terminal_enqueue_state
.open_circuit(now_unix_ms().saturating_add(LIFECYCLE_ENQUEUE_CIRCUIT_OPEN_MS));
let failures = self.terminal_enqueue_state.increment_failed_total();
if should_log_usage_retry_counter(failures) {
warn!(
event_name = "usage_terminal_event_enqueue_failed",
log_type = "event",
usage_event_type = ?event.event_type,
request_id = %event.request_id,
fallback = "direct_write_or_bounded_retry",
failure_total = failures,
circuit_open_ms = LIFECYCLE_ENQUEUE_CIRCUIT_OPEN_MS,
error = %err,
"usage runtime failed to enqueue terminal event; terminal enqueue circuit opened"
);
}
return self
.defer_terminal_event(data, queue, event, "primary_enqueue_failed", err)
.await;
}
TerminalPersistenceOutcome::Queued
}
async fn defer_terminal_event<T>(
&self,
data: &T,
queue: UsageQueue,
mut event: UsageEvent,
reason: &'static str,
cause: DataLayerError,
) -> TerminalPersistenceOutcome
where
T: UsageRuntimeAccess,
{
let usage_event_type = event.event_type;
let request_id = event.request_id.clone();
let fallback = if is_permanent_enqueue_error(&cause) {
"report_failure"
} else {
"bounded_local_enqueue_retry"
};
let direct_write_succeeded = self
.try_write_terminal_direct_fallback(data, &mut event, fallback)
.await;
let (deferred_fallback, outcome) = if direct_write_succeeded {
(
DeferredEnqueueFallback::DirectWrite,
TerminalPersistenceOutcome::PersistedDirectly,
)
} else if self.enqueue_retry.schedule(queue, event, "terminal", cause) {
(
DeferredEnqueueFallback::LocalRetry,
TerminalPersistenceOutcome::BufferedForRetry,
)
} else {
(
DeferredEnqueueFallback::Drop,
TerminalPersistenceOutcome::Failed,
)
};
self.terminal_enqueue_state.record_deferred(
"usage_terminal_event_enqueue_deferred",
reason,
usage_event_type,
&request_id,
deferred_fallback,
);
outcome
}
async fn try_write_terminal_direct_fallback<T>(
&self,
data: &T,
event: &mut UsageEvent,
fallback: &'static str,
) -> bool
where
T: UsageRuntimeAccess,
{
if !data.has_usage_writer() || data.usage_worker_should_defer_for_database_pressure() {
let rejected = self.terminal_direct_fallback_state.record_rejected();
if should_log_usage_retry_counter(rejected) {
warn!(
event_name = "usage_terminal_direct_fallback_rejected",
log_type = "event",
usage_event_type = ?event.event_type,
request_id = %event.request_id,
rejected_total = rejected,
fallback,
"usage runtime skipped terminal direct fallback because the writer is unavailable or under pressure"
);
}
return false;
}
let _worker_record_permit = if let Some(gate) = self.worker_record_gate.as_ref() {
let Some(permit) = gate.try_acquire() else {
gate.record_deferred();
let rejected = self.terminal_direct_fallback_state.record_rejected();
if should_log_usage_retry_counter(rejected) {
warn!(
event_name = "usage_terminal_direct_fallback_worker_gate_saturated",
log_type = "event",
usage_event_type = ?event.event_type,
request_id = %event.request_id,
worker_record_limit = gate.limit(),
rejected_total = rejected,
fallback,
"usage runtime terminal direct fallback was rejected by the shared database concurrency gate"
);
}
return false;
};
Some(permit)
} else {
None
};
let Some(_permit) = self.terminal_direct_fallback_state.try_acquire() else {
let rejected = self.terminal_direct_fallback_state.rejected_total();
if should_log_usage_retry_counter(rejected) {
warn!(
event_name = "usage_terminal_direct_fallback_saturated",
log_type = "event",
usage_event_type = ?event.event_type,
request_id = %event.request_id,
fallback_limit = self.terminal_direct_fallback_state.limit(),
rejected_total = rejected,
fallback,
"usage runtime terminal direct fallback is saturated"
);
}
return false;
};
let write_succeeded = if let Err(err) = data.enrich_usage_event(event).await {
warn!(
event_name = "usage_terminal_direct_fallback_enrichment_failed",
log_type = "event",
usage_event_type = ?event.event_type,
request_id = %event.request_id,
error = %err,
fallback,
"usage runtime could not enrich terminal event for direct fallback"
);
false
} else {
self.write_event_direct(data, event).await
};
if write_succeeded {
let succeeded = self.terminal_direct_fallback_state.record_succeeded();
if should_log_usage_retry_counter(succeeded) {
info!(
event_name = "usage_terminal_direct_fallback_succeeded",
log_type = "event",
usage_event_type = ?event.event_type,
request_id = %event.request_id,
succeeded_total = succeeded,
"usage runtime persisted terminal event through bounded direct fallback"
);
}
true
} else {
let failed = self.terminal_direct_fallback_state.record_failed();
if should_log_usage_retry_counter(failed) {
warn!(
event_name = "usage_terminal_direct_fallback_failed",
log_type = "event",
usage_event_type = ?event.event_type,
request_id = %event.request_id,
failed_total = failed,
fallback,
"usage runtime terminal direct fallback failed"
);
}
false
}
}
async fn write_event_direct<T>(&self, data: &T, event: &UsageEvent) -> bool
where
T: UsageRuntimeAccess,
{
let reconciled = match reconcile_usage_policy_cost_for_event_with_result(data, event).await
{
Ok(reconciled) => reconciled,
Err(err) => {
warn!(
event_name = "usage_event_cost_reconciliation_failed",
log_type = "event",
usage_event_type = ?event.event_type,
request_id = %event.request_id,
error = %err,
"usage runtime failed to reconcile plan cost before direct usage upsert"
);
return false;
}
};
match build_upsert_usage_record_from_event(event) {
Ok(record) => match catch_usage_writer_panic(
"direct usage upsert",
data.upsert_usage_record(record),
)
.await
{
Ok(Some(stored)) => {
if let Err(err) =
settle_usage_with_reconciled_cost(data, &stored, reconciled).await
{
warn!(
event_name = "usage_terminal_settlement_failed",
log_type = "event",
request_id = %event.request_id,
error = %err,
"usage runtime failed to settle terminal usage directly"
);
return false;
}
true
}
Ok(None) => true,
Err(err) => {
warn!(
event_name = "usage_event_upsert_failed",
log_type = "event",
usage_event_type = ?event.event_type,
request_id = %event.request_id,
error = %err,
"usage runtime failed to upsert usage event directly"
);
false
}
},
Err(err) => {
warn!(
event_name = "usage_event_upsert_build_failed",
log_type = "event",
usage_event_type = ?event.event_type,
request_id = %event.request_id,
error = %err,
"usage runtime failed to build usage event upsert"
);
false
}
}
}
}
pub(crate) fn prepare_event_capture_memory(
event: &mut UsageEvent,
budget: Arc<crate::event_capture_budget::EventCaptureMemoryBudget>,
) {
preserve_request_facts(event);
preserve_provider_response_facts(event);
event.data.apply_capture_memory_budget(budget);
}
pub(crate) fn prepare_decoded_event_capture_memory(
event: &mut UsageEvent,
budget: Arc<crate::event_capture_budget::EventCaptureMemoryBudget>,
) {
preserve_request_facts_with_legacy_missing(event, true);
preserve_provider_response_facts(event);
event.data.apply_capture_memory_budget(budget);
}
fn preserve_request_facts(event: &mut UsageEvent) {
preserve_request_facts_with_legacy_missing(event, false);
}
fn preserve_request_facts_with_legacy_missing(
event: &mut UsageEvent,
preserve_implicit_missing: bool,
) {
let data = &mut event.data;
match request_body_derived_facts_action(data.request_body.as_ref(), data.request_body_state) {
RequestBodyDerivedFactsAction::Refresh => {
data.request_metadata = attach_client_request_body_metadata(
data.request_metadata.take(),
data.request_body.as_ref(),
);
}
RequestBodyDerivedFactsAction::Clear => {
// Legacy wire events can carry derived facts without either capture field.
// An explicit typed `none` still authoritatively clears those facts.
if !preserve_implicit_missing
|| data.request_body.is_some()
|| data.request_body_state.is_some()
{
data.request_metadata =
clear_client_request_body_metadata(data.request_metadata.take());
}
}
RequestBodyDerivedFactsAction::Preserve => {}
}
match request_body_derived_facts_action(
data.provider_request_body.as_ref(),
data.provider_request_body_state,
) {
RequestBodyDerivedFactsAction::Refresh => {
data.request_metadata = attach_provider_request_body_metadata(
data.request_metadata.take(),
data.endpoint_api_format
.as_deref()
.or(data.api_format.as_deref()),
data.target_model.as_deref().or(Some(data.model.as_str())),
Some(data.model.as_str()),
data.provider_request_body.as_ref(),
);
}
RequestBodyDerivedFactsAction::Clear => {
if !preserve_implicit_missing
|| data.provider_request_body.is_some()
|| data.provider_request_body_state.is_some()
{
data.request_metadata =
clear_provider_request_body_metadata(data.request_metadata.take());
}
}
RequestBodyDerivedFactsAction::Preserve => {}
}
}
fn preserve_provider_response_facts(event: &mut UsageEvent) {
let metadata = event.data.request_metadata.take();
let metadata =
attach_provider_response_body_metadata(metadata, event.data.response_body.as_ref());
event.data.request_metadata = attach_provider_response_model_metadata(
metadata,
event.data.request_body.as_ref(),
event.data.request_body_state,
event.data.api_format.as_deref(),
event.data.response_body.as_ref(),
event.data.response_body_state,
event.data.endpoint_api_format.as_deref(),
);
}
impl UsageQueueHealthSnapshot {
fn apply_stream_stats(&mut self, stats: RuntimeQueueStats) {
self.stream_length = stats.stream_length;
self.group_pending = stats.group_pending;
self.group_lag = stats.group_lag;
self.oldest_pending_idle_ms = stats.oldest_pending_idle_ms;
}
}
async fn enrich_terminal_event<T>(data: &T, event: &mut UsageEvent) -> Result<(), DataLayerError>
where
T: UsageBillingEventEnricher + Send + Sync,
{
if let Err(err) = data.enrich_usage_event(event).await {
warn!(
event_name = "usage_terminal_billing_enrichment_failed",
log_type = "event",
request_id = %event.request_id,
error = %err,
"usage runtime failed to enrich terminal usage event with billing"
);
return Err(err);
}
Ok(())
}
struct ManagedUsageWorker {
control: UsageWorkerControl,
stopping: bool,
}
struct UsageWorkerReconcileInputs<'a, T> {
runner: &'a Arc<dyn RuntimeQueueStore>,
data: &'a Arc<T>,
config: &'a UsageRuntimeConfig,
worker_record_gate: &'a Option<Arc<UsageWorkerRecordConcurrencyGate>>,
telemetry_tx: &'a mpsc::Sender<UsageWorkerObservation>,
}
struct UsageWorkerReconcileState<'a> {
join_set: &'a mut tokio::task::JoinSet<usize>,
worker_task_indexes: &'a mut BTreeMap<tokio::task::Id, usize>,
workers: &'a mut BTreeMap<usize, ManagedUsageWorker>,
next_worker_index: &'a mut usize,
}
async fn run_usage_worker_supervisor<T>(
runner: Arc<dyn RuntimeQueueStore>,
data: Arc<T>,
config: UsageRuntimeConfig,
worker_record_gate: Option<Arc<UsageWorkerRecordConcurrencyGate>>,
state: Arc<UsageWorkerSupervisorState>,
control: UsageWorkerControl,
) where
T: UsageRuntimeAccess + 'static,
{
let min_workers = config.worker_count.max(1);
let max_workers = if config.worker_autoscale_enabled {
config.worker_max_count.max(min_workers)
} else {
min_workers
};
let mut desired_workers = min_workers;
let mut next_worker_index = 0usize;
let mut workers = BTreeMap::<usize, ManagedUsageWorker>::new();
let mut worker_task_indexes = BTreeMap::<tokio::task::Id, usize>::new();
let mut join_set = tokio::task::JoinSet::<usize>::new();
let (telemetry_tx, mut telemetry_rx) =
mpsc::channel::<UsageWorkerObservation>(max_workers.saturating_mul(4).clamp(16, 1024));
let mut scale_interval = tokio::time::interval(Duration::from_millis(
config.worker_scale_interval_ms.max(1),
));
scale_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
let mut full_reads = 0usize;
let mut busy_reads = 0usize;
let mut idle_reads = 0usize;
let mut idle_ticks = 0u64;
state
.desired_count
.store(desired_workers, Ordering::Release);
let reconcile_inputs = UsageWorkerReconcileInputs {
runner: &runner,
data: &data,
config: &config,
worker_record_gate: &worker_record_gate,
telemetry_tx: &telemetry_tx,
};
reconcile_usage_workers(
&reconcile_inputs,
UsageWorkerReconcileState {
join_set: &mut join_set,
worker_task_indexes: &mut worker_task_indexes,
workers: &mut workers,
next_worker_index: &mut next_worker_index,
},
desired_workers,
);
state.active_count.store(workers.len(), Ordering::Release);
loop {
tokio::select! {
biased;
_ = control.wait_for_shutdown() => {
state.desired_count.store(0, Ordering::Release);
for worker in workers.values() {
worker.control.request_shutdown();
}
while join_set.join_next().await.is_some() {}
state.active_count.store(0, Ordering::Release);
break;
}
Some(observation) = telemetry_rx.recv() => {
state.record_observation(observation);
if observation.entries_read == 0 {
idle_reads = idle_reads.saturating_add(1);
} else {
busy_reads = busy_reads.saturating_add(1);
if observation.entries_read >= observation.batch_size {
full_reads = full_reads.saturating_add(1);
}
}
}
_ = scale_interval.tick() => {
drain_finished_usage_workers(
&mut join_set,
&mut worker_task_indexes,
&mut workers,
);
if config.worker_autoscale_enabled {
let active_workers = workers.len().max(1);
let high_pressure = full_reads > 0
|| (busy_reads >= active_workers.saturating_mul(2) && idle_reads == 0);
if high_pressure && desired_workers < max_workers {
let grow_by = active_workers.div_ceil(2);
let next = desired_workers
.saturating_add(grow_by.max(1))
.clamp(min_workers, max_workers);
if next > desired_workers {
info!(
event_name = "usage_worker_autoscale_up",
log_type = "ops",
desired_workers = next,
previous_desired_workers = desired_workers,
active_workers = workers.len(),
max_workers,
full_reads,
busy_reads,
idle_reads,
"usage worker supervisor scaled up"
);
desired_workers = next;
idle_ticks = 0;
}
} else if desired_workers > min_workers
&& busy_reads == 0
&& full_reads == 0
&& idle_reads >= active_workers
{
idle_ticks = idle_ticks.saturating_add(1);
if idle_ticks >= config.worker_idle_scale_down_ticks {
let next = desired_workers
.saturating_sub(desired_workers.div_ceil(2))
.max(min_workers);
if next < desired_workers {
info!(
event_name = "usage_worker_autoscale_down",
log_type = "ops",
desired_workers = next,
previous_desired_workers = desired_workers,
active_workers = workers.len(),
min_workers,
idle_ticks,
"usage worker supervisor scaled down"
);
desired_workers = next;
}
idle_ticks = 0;
}
} else if busy_reads > 0 || full_reads > 0 {
idle_ticks = 0;
}
}
state.desired_count.store(desired_workers, Ordering::Release);
reconcile_usage_workers(
&reconcile_inputs,
UsageWorkerReconcileState {
join_set: &mut join_set,
worker_task_indexes: &mut worker_task_indexes,
workers: &mut workers,
next_worker_index: &mut next_worker_index,
},
desired_workers,
);
state
.active_count
.store(workers.len(), Ordering::Release);
full_reads = 0;
busy_reads = 0;
idle_reads = 0;
}
}
}
}
fn reconcile_usage_workers<T>(
inputs: &UsageWorkerReconcileInputs<'_, T>,
state: UsageWorkerReconcileState<'_>,
desired_workers: usize,
) where
T: UsageRuntimeAccess + 'static,
{
let UsageWorkerReconcileState {
join_set,
worker_task_indexes,
workers,
next_worker_index,
} = state;
while workers.len() < desired_workers {
let worker_index = *next_worker_index;
*next_worker_index = (*next_worker_index).saturating_add(1);
let control = UsageWorkerControl::default();
let Ok(worker) = build_usage_queue_worker_with_record_gate(
Arc::clone(inputs.runner),
Arc::clone(inputs.data),
inputs.config.clone(),
inputs.worker_record_gate.clone(),
Some(worker_index),
) else {
warn!(
event_name = "usage_worker_build_failed",
log_type = "ops",
worker_index,
"usage runtime failed to build elastic usage queue worker"
);
break;
};
let worker = worker.with_supervisor(control.clone(), inputs.telemetry_tx.clone());
let handle = join_set.spawn(async move {
worker.run().await;
worker_index
});
worker_task_indexes.insert(handle.id(), worker_index);
workers.insert(
worker_index,
ManagedUsageWorker {
control,
stopping: false,
},
);
}
let mut excess = workers.len().saturating_sub(desired_workers);
for worker in workers.values_mut().rev() {
if excess == 0 {
break;
}
if worker.stopping {
continue;
}
worker.control.request_shutdown();
worker.stopping = true;
excess -= 1;
}
}
fn drain_finished_usage_workers(
join_set: &mut tokio::task::JoinSet<usize>,
worker_task_indexes: &mut BTreeMap<tokio::task::Id, usize>,
workers: &mut BTreeMap<usize, ManagedUsageWorker>,
) {
while let Some(result) = join_set.try_join_next_with_id() {
match result {
Ok((task_id, worker_index)) => {
worker_task_indexes.remove(&task_id);
let stopping = workers
.remove(&worker_index)
.is_some_and(|worker| worker.stopping);
if !stopping {
warn!(
event_name = "usage_worker_unexpected_exit",
log_type = "ops",
worker_index,
"usage worker exited before supervisor requested shutdown"
);
}
}
Err(err) => {
let worker_index = worker_task_indexes.remove(&err.id());
if let Some(worker_index) = worker_index {
workers.remove(&worker_index);
warn!(
event_name = "usage_worker_join_failed",
log_type = "ops",
worker_index,
error = %err,
"usage worker task failed"
);
continue;
}
warn!(
event_name = "usage_worker_join_failed",
log_type = "ops",
error = %err,
"usage worker task failed"
);
}
}
}
}
#[derive(Debug, Clone, Copy)]
struct UsageBodyCapturePolicyCacheEntry {
cached_at: Instant,
ttl: Duration,
policy: UsageBodyCapturePolicy,
source: UsageBodyCapturePolicyCacheSource,
}
impl UsageBodyCapturePolicyCacheEntry {
fn loaded(policy: UsageBodyCapturePolicy) -> Self {
Self {
cached_at: Instant::now(),
ttl: USAGE_BODY_CAPTURE_POLICY_CACHE_TTL,
policy,
source: UsageBodyCapturePolicyCacheSource::Loaded,
}
}
fn fallback_after_error() -> Self {
Self {
cached_at: Instant::now(),
ttl: USAGE_BODY_CAPTURE_POLICY_ERROR_CACHE_TTL,
policy: UsageBodyCapturePolicy::default(),
source: UsageBodyCapturePolicyCacheSource::FallbackAfterError,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum UsageBodyCapturePolicyCacheSource {
Loaded,
FallbackAfterError,
}
#[derive(Debug, Default)]
struct LifecycleEnqueueState {
in_flight: AtomicU64,
circuit_open_until_unix_ms: AtomicU64,
skipped_total: AtomicU64,
direct_write_total: AtomicU64,
dropped_total: AtomicU64,
retry_total: AtomicU64,
failed_total: AtomicU64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DeferredEnqueueFallback {
DirectWrite,
LocalRetry,
Drop,
}
impl LifecycleEnqueueState {
fn in_flight(&self) -> u64 {
self.in_flight.load(Ordering::Acquire)
}
fn deferred_total(&self) -> u64 {
self.skipped_total.load(Ordering::Acquire)
}
fn deferred_dropped_total(&self) -> u64 {
self.dropped_total.load(Ordering::Acquire)
}
fn deferred_direct_write_total(&self) -> u64 {
self.direct_write_total.load(Ordering::Acquire)
}
fn deferred_retry_total(&self) -> u64 {
self.retry_total.load(Ordering::Acquire)
}
fn failed_total(&self) -> u64 {
self.failed_total.load(Ordering::Acquire)
}
fn increment_failed_total(&self) -> u64 {
self.failed_total.fetch_add(1, Ordering::AcqRel) + 1
}
fn is_circuit_open(&self, now_unix_ms: u64) -> bool {
self.circuit_open_until_unix_ms.load(Ordering::Acquire) > now_unix_ms
}
fn open_circuit(&self, open_until_unix_ms: u64) {
let mut current = self.circuit_open_until_unix_ms.load(Ordering::Acquire);
while open_until_unix_ms > current {
match self.circuit_open_until_unix_ms.compare_exchange(
current,
open_until_unix_ms,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => break,
Err(next) => current = next,
}
}
}
fn try_acquire_in_flight<'a>(
&'a self,
max_in_flight: u64,
) -> Option<LifecycleEnqueueInFlightGuard<'a>> {
let mut current = self.in_flight.load(Ordering::Acquire);
loop {
if current >= max_in_flight {
return None;
}
match self.in_flight.compare_exchange_weak(
current,
current + 1,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => {
return Some(LifecycleEnqueueInFlightGuard { state: self });
}
Err(next) => current = next,
}
}
}
fn record_deferred(
&self,
event_name: &'static str,
reason: &'static str,
usage_event_type: crate::UsageEventType,
request_id: &str,
deferred_fallback: DeferredEnqueueFallback,
) {
let skipped = self.skipped_total.fetch_add(1, Ordering::AcqRel) + 1;
let fallback = match deferred_fallback {
DeferredEnqueueFallback::DirectWrite => {
self.direct_write_total.fetch_add(1, Ordering::AcqRel);
"direct_write"
}
DeferredEnqueueFallback::LocalRetry => {
self.retry_total.fetch_add(1, Ordering::AcqRel);
"local_enqueue_retry"
}
DeferredEnqueueFallback::Drop => {
self.dropped_total.fetch_add(1, Ordering::AcqRel);
"drop"
}
};
if should_log_usage_retry_counter(skipped) {
warn!(
event_name,
log_type = "event",
usage_event_type = ?usage_event_type,
request_id,
reason,
deferred_total = skipped,
fallback,
"usage runtime deferred usage enqueue"
);
}
}
}
struct LifecycleEnqueueInFlightGuard<'a> {
state: &'a LifecycleEnqueueState,
}
impl Drop for LifecycleEnqueueInFlightGuard<'_> {
fn drop(&mut self) {
self.state.in_flight.fetch_sub(1, Ordering::AcqRel);
}
}
#[async_trait]
trait DelayedLifecycleEvent: Send {
async fn enqueue(
self: Box<Self>,
config: UsageRuntimeConfig,
coalescer: Arc<LifecycleEventCoalescer>,
enqueue_state: Arc<LifecycleEnqueueState>,
enqueue_retry: Arc<UsageEnqueueRetryDispatcher>,
);
}
struct DelayedLifecycleEventItem<T> {
data: T,
event: UsageEvent,
generation: u64,
}
#[async_trait]
impl<T> DelayedLifecycleEvent for DelayedLifecycleEventItem<T>
where
T: UsageRuntimeAccess + Clone + 'static,
{
async fn enqueue(
self: Box<Self>,
config: UsageRuntimeConfig,
coalescer: Arc<LifecycleEventCoalescer>,
enqueue_state: Arc<LifecycleEnqueueState>,
enqueue_retry: Arc<UsageEnqueueRetryDispatcher>,
) {
if !coalescer
.should_emit(&self.event.request_id, self.generation)
.await
{
return;
}
enqueue_lifecycle_event_now(&self.data, self.event, config, enqueue_state, enqueue_retry)
.await;
}
}
struct DelayedLifecycleQueueItem {
due_at: tokio::time::Instant,
item: Box<dyn DelayedLifecycleEvent>,
_admission: tokio::sync::OwnedSemaphorePermit,
}
#[derive(Debug)]
struct LifecycleDelayDispatcher {
delay: Duration,
admission: Arc<tokio::sync::Semaphore>,
sender: Option<mpsc::Sender<DelayedLifecycleQueueItem>>,
}
impl LifecycleDelayDispatcher {
fn disabled() -> Arc<Self> {
Arc::new(Self {
delay: Duration::ZERO,
admission: Arc::new(tokio::sync::Semaphore::new(0)),
sender: None,
})
}
fn spawn(
config: UsageRuntimeConfig,
coalescer: Arc<LifecycleEventCoalescer>,
enqueue_state: Arc<LifecycleEnqueueState>,
enqueue_retry: Arc<UsageEnqueueRetryDispatcher>,
shutdown: &UsageShutdownState,
) -> Arc<Self> {
if !config.enabled
|| !config.queue_lifecycle_events
|| config.lifecycle_enqueue_delay_ms == 0
{
return Self::disabled();
}
let capacity = config.enqueue_retry_buffer_capacity.clamp(1, 1_048_576);
let delay = Duration::from_millis(config.lifecycle_enqueue_delay_ms.max(1));
let admission = Arc::new(tokio::sync::Semaphore::new(capacity));
let (sender, receiver) = mpsc::channel(capacity);
shutdown.tasks.spawn(run_lifecycle_delay_worker(
config,
coalescer,
enqueue_state,
enqueue_retry,
receiver,
shutdown.drain.subscribe(),
));
Arc::new(Self {
delay,
admission,
sender: Some(sender),
})
}
async fn schedule<T>(
&self,
data: T,
event: UsageEvent,
generation: u64,
) -> Result<(), UsageEvent>
where
T: UsageRuntimeAccess + Clone + 'static,
{
let Some(sender) = &self.sender else {
return Err(event);
};
let Ok(admission) = Arc::clone(&self.admission).try_acquire_owned() else {
return Err(event);
};
let Ok(permit) = sender.try_reserve() else {
return Err(event);
};
permit.send(DelayedLifecycleQueueItem {
due_at: tokio::time::Instant::now() + self.delay,
item: Box::new(DelayedLifecycleEventItem {
data,
event,
generation,
}),
_admission: admission,
});
Ok(())
}
}
async fn run_lifecycle_delay_worker(
config: UsageRuntimeConfig,
coalescer: Arc<LifecycleEventCoalescer>,
enqueue_state: Arc<LifecycleEnqueueState>,
enqueue_retry: Arc<UsageEnqueueRetryDispatcher>,
mut receiver: mpsc::Receiver<DelayedLifecycleQueueItem>,
mut drain: tokio::sync::watch::Receiver<bool>,
) {
let mut pending = BTreeMap::<tokio::time::Instant, Vec<DelayedLifecycleQueueItem>>::new();
let mut receiver_open = true;
loop {
if *drain.borrow_and_update() {
receiver.close();
while let Ok(item) = receiver.try_recv() {
pending.entry(item.due_at).or_default().push(item);
}
for (_, items) in std::mem::take(&mut pending) {
for item in items {
item.item
.enqueue(
config.clone(),
Arc::clone(&coalescer),
Arc::clone(&enqueue_state),
Arc::clone(&enqueue_retry),
)
.await;
}
}
break;
}
if !pending.is_empty() {
enqueue_due_lifecycle_items(
&mut pending,
tokio::time::Instant::now(),
&config,
&coalescer,
&enqueue_state,
&enqueue_retry,
)
.await;
}
if pending.is_empty() {
if !receiver_open {
break;
}
let next = tokio::select! {
next = receiver.recv() => next,
_ = crate::shutdown::wait_for_drain(&mut drain) => continue,
};
match next {
Some(item) => {
pending.entry(item.due_at).or_default().push(item);
continue;
}
None => break,
}
}
let next_due_at = *pending
.first_key_value()
.map(|(due_at, _)| due_at)
.expect("pending lifecycle delay item should exist");
if receiver_open {
tokio::select! {
_ = crate::shutdown::wait_for_drain(&mut drain) => continue,
maybe_item = receiver.recv() => {
match maybe_item {
Some(item) => {
pending.entry(item.due_at).or_default().push(item);
}
None => {
receiver_open = false;
}
}
}
_ = tokio::time::sleep_until(next_due_at) => {
enqueue_due_lifecycle_items(
&mut pending,
tokio::time::Instant::now(),
&config,
&coalescer,
&enqueue_state,
&enqueue_retry,
).await;
}
}
} else {
tokio::time::sleep_until(next_due_at).await;
enqueue_due_lifecycle_items(
&mut pending,
tokio::time::Instant::now(),
&config,
&coalescer,
&enqueue_state,
&enqueue_retry,
)
.await;
}
}
}
async fn enqueue_due_lifecycle_items(
pending: &mut BTreeMap<tokio::time::Instant, Vec<DelayedLifecycleQueueItem>>,
now: tokio::time::Instant,
config: &UsageRuntimeConfig,
coalescer: &Arc<LifecycleEventCoalescer>,
enqueue_state: &Arc<LifecycleEnqueueState>,
enqueue_retry: &Arc<UsageEnqueueRetryDispatcher>,
) {
let mut ready = Vec::new();
while let Some((&due_at, _)) = pending.first_key_value() {
if due_at > now {
break;
}
if let Some(mut items) = pending.remove(&due_at) {
ready.append(&mut items);
}
}
for item in ready {
item.item
.enqueue(
config.clone(),
Arc::clone(coalescer),
Arc::clone(enqueue_state),
Arc::clone(enqueue_retry),
)
.await;
}
}
async fn enqueue_lifecycle_event_now<T>(
data: &T,
event: UsageEvent,
config: UsageRuntimeConfig,
enqueue_state: Arc<LifecycleEnqueueState>,
enqueue_retry: Arc<UsageEnqueueRetryDispatcher>,
) -> bool
where
T: UsageRuntimeAccess,
{
let Some(runner) = data.usage_worker_queue() else {
warn!(
event_name = "usage_lifecycle_event_queue_unavailable",
log_type = "event",
usage_event_type = ?event.event_type,
request_id = %event.request_id,
fallback = "none",
"usage runtime lifecycle queue is unavailable; lifecycle event will not be written directly"
);
return false;
};
let queue = match UsageQueue::new(runner, config.clone()) {
Ok(queue) => queue,
Err(err) => {
warn!(
event_name = "usage_lifecycle_event_queue_init_failed",
log_type = "event",
usage_event_type = ?event.event_type,
request_id = %event.request_id,
fallback = "none",
error = %err,
"usage runtime failed to build lifecycle queue; lifecycle event will not be written directly"
);
return false;
}
};
let now_ms = now_unix_ms();
if enqueue_state.is_circuit_open(now_ms) {
let retry_enabled = config.retry_deferred_lifecycle_events;
if retry_enabled {
let usage_event_type = event.event_type;
let request_id = event.request_id.clone();
let accepted = enqueue_retry.schedule(
queue,
event,
"lifecycle",
DataLayerError::TimedOut("lifecycle enqueue circuit is open".to_string()),
);
enqueue_state.record_deferred(
"usage_lifecycle_event_enqueue_deferred",
"circuit_open",
usage_event_type,
&request_id,
if accepted {
DeferredEnqueueFallback::LocalRetry
} else {
DeferredEnqueueFallback::Drop
},
);
return accepted;
}
enqueue_state.record_deferred(
"usage_lifecycle_event_enqueue_deferred",
"circuit_open",
event.event_type,
&event.request_id,
DeferredEnqueueFallback::Drop,
);
return false;
}
let Some(_guard) = enqueue_state.try_acquire_in_flight(config.lifecycle_enqueue_max_in_flight)
else {
let retry_enabled = config.retry_deferred_lifecycle_events;
if retry_enabled {
let usage_event_type = event.event_type;
let request_id = event.request_id.clone();
let accepted = enqueue_retry.schedule(
queue,
event,
"lifecycle",
DataLayerError::TimedOut("lifecycle enqueue in-flight limit".to_string()),
);
enqueue_state.record_deferred(
"usage_lifecycle_event_enqueue_deferred",
"in_flight_limit",
usage_event_type,
&request_id,
if accepted {
DeferredEnqueueFallback::LocalRetry
} else {
DeferredEnqueueFallback::Drop
},
);
return accepted;
}
enqueue_state.record_deferred(
"usage_lifecycle_event_enqueue_deferred",
"in_flight_limit",
event.event_type,
&event.request_id,
DeferredEnqueueFallback::Drop,
);
return false;
};
let Err(err) = queue.enqueue(&event).await else {
return true;
};
if is_permanent_enqueue_error(&err) {
enqueue_state.record_deferred(
"usage_lifecycle_event_enqueue_deferred",
"invalid_input",
event.event_type,
&event.request_id,
DeferredEnqueueFallback::Drop,
);
return enqueue_retry.schedule(queue, event, "lifecycle", err);
}
enqueue_state.open_circuit(now_unix_ms().saturating_add(LIFECYCLE_ENQUEUE_CIRCUIT_OPEN_MS));
let failures = enqueue_state.increment_failed_total();
let retry_enabled = config.retry_deferred_lifecycle_events;
if should_log_usage_retry_counter(failures) {
warn!(
event_name = "usage_lifecycle_event_enqueue_failed",
log_type = "event",
usage_event_type = ?event.event_type,
request_id = %event.request_id,
fallback = if retry_enabled { "local_enqueue_retry" } else { "none" },
failure_total = failures,
circuit_open_ms = LIFECYCLE_ENQUEUE_CIRCUIT_OPEN_MS,
error = %err,
"usage runtime failed to enqueue lifecycle event; lifecycle enqueue circuit opened"
);
}
let usage_event_type = event.event_type;
let request_id = event.request_id.clone();
let accepted = retry_enabled && enqueue_retry.schedule(queue, event, "lifecycle", err);
enqueue_state.record_deferred(
"usage_lifecycle_event_enqueue_deferred",
"primary_enqueue_failed",
usage_event_type,
&request_id,
if accepted {
DeferredEnqueueFallback::LocalRetry
} else {
DeferredEnqueueFallback::Drop
},
);
accepted
}
#[derive(Debug)]
struct UsageEnqueueRetryDispatcher {
senders: Vec<mpsc::Sender<UsageEnqueueRetryItem>>,
metrics: Arc<UsageEnqueueDispatcherMetrics>,
}
#[derive(Debug, Default)]
struct UsageEnqueueDispatcherMetrics {
scheduled_total: AtomicU64,
permanent_failure_total: AtomicU64,
recovered_total: AtomicU64,
pending: AtomicU64,
retry_failed_total: AtomicU64,
closed_or_unavailable_total: AtomicU64,
}
struct UsageEnqueueRetryItem {
queue: UsageQueue,
event: UsageEvent,
event_phase: &'static str,
attempts: u64,
delay_before_first_attempt: bool,
}
impl UsageEnqueueRetryDispatcher {
fn disabled() -> Arc<Self> {
Arc::new(Self {
senders: Vec::new(),
metrics: Arc::new(UsageEnqueueDispatcherMetrics::default()),
})
}
fn spawn(config: UsageRuntimeConfig, shutdown: &UsageShutdownState) -> Arc<Self> {
let lifecycle_retry_enabled =
config.queue_lifecycle_events && config.retry_deferred_lifecycle_events;
if !config.enabled || !(config.queue_terminal_events || lifecycle_retry_enabled) {
return Self::disabled();
}
let workers = config
.enqueue_retry_workers
.min(config.enqueue_retry_buffer_capacity)
.max(1);
let mut senders = Vec::with_capacity(workers);
let metrics = Arc::new(UsageEnqueueDispatcherMetrics::default());
for worker_index in 0..workers {
let capacity =
retry_worker_capacity(config.enqueue_retry_buffer_capacity, workers, worker_index);
let (sender, receiver) = mpsc::channel(capacity);
senders.push(sender);
let worker_config = config.clone();
let worker_metrics = Arc::clone(&metrics);
let drain = shutdown.drain.subscribe();
shutdown.tasks.spawn(async move {
run_usage_enqueue_retry_worker_with_drain(
worker_index,
worker_config,
receiver,
worker_metrics,
drain,
)
.await;
});
}
Arc::new(Self { senders, metrics })
}
fn schedule(
&self,
queue: UsageQueue,
event: UsageEvent,
event_phase: &'static str,
mut cause: DataLayerError,
) -> bool {
// Circuit and admission failures can reach this path without an encoding attempt.
if !is_permanent_enqueue_error(&cause) {
if let Err(error) = queue.validate_event(&event) {
if is_permanent_enqueue_error(&error) {
cause = error;
}
}
}
if is_permanent_enqueue_error(&cause) {
let rejected = self.metrics.record_permanent_failure();
if should_log_usage_retry_counter(rejected) {
warn!(
event_name = "usage_event_enqueue_invalid_input",
log_type = "ops",
event_phase,
usage_event_type = ?event.event_type,
request_id = %event.request_id,
rejected_total = rejected,
error = %cause,
"usage event could not be persisted; invalid queue input cannot be retried"
);
}
return false;
}
let event_type = event.event_type;
let request_id = event.request_id.clone();
let cause_message = cause.to_string();
let scheduled = self.schedule_item(queue, event, event_phase, Some(cause_message.as_str()));
if let Some(scheduled) = scheduled {
if should_log_usage_retry_counter(scheduled) {
warn!(
event_name = "usage_event_enqueue_failed_retry_scheduled",
log_type = "event",
event_phase,
usage_event_type = ?event_type,
request_id,
retry_scheduled_total = scheduled,
fallback = "local_enqueue_retry",
error = %cause_message,
"usage runtime failed to enqueue usage event; scheduled local retry"
);
}
true
} else {
false
}
}
fn schedule_item(
&self,
queue: UsageQueue,
event: UsageEvent,
event_phase: &'static str,
cause: Option<&str>,
) -> Option<u64> {
let worker_index = retry_worker_index(&event.request_id, self.senders.len());
let Some(sender) = self.senders.get(worker_index) else {
self.metrics.record_closed_or_unavailable();
warn!(
event_name = "usage_event_enqueue_retry_unavailable",
log_type = "event",
event_phase,
usage_event_type = ?event.event_type,
request_id = %event.request_id,
error = ?cause,
fallback = "drop",
"usage runtime local enqueue retry dispatcher is unavailable"
);
return None;
};
match sender.try_reserve() {
Ok(permit) => {
let scheduled = self.metrics.record_scheduled();
permit.send(UsageEnqueueRetryItem {
queue,
event,
event_phase,
attempts: 0,
delay_before_first_attempt: true,
});
Some(scheduled)
}
Err(mpsc::error::TrySendError::Full(_)) => {
let dropped = self.metrics.record_closed_or_unavailable();
if should_log_usage_retry_counter(dropped) {
warn!(
event_name = "usage_event_enqueue_retry_buffer_full",
log_type = "event",
event_phase,
usage_event_type = ?event.event_type,
request_id = %event.request_id,
worker_index,
retry_dropped_total = dropped,
fallback = "drop",
"usage runtime local enqueue retry buffer is full; dropped usage event"
);
}
None
}
Err(mpsc::error::TrySendError::Closed(_)) => {
self.metrics.record_closed_or_unavailable();
warn!(
event_name = "usage_event_enqueue_retry_closed",
log_type = "event",
event_phase,
usage_event_type = ?event.event_type,
request_id = %event.request_id,
worker_index,
fallback = "drop",
"usage runtime local enqueue retry dispatcher is closed"
);
None
}
}
}
fn scheduled_total(&self) -> u64 {
self.metrics.scheduled_total.load(Ordering::Acquire)
}
fn permanent_failure_total(&self) -> u64 {
self.metrics.permanent_failure_total.load(Ordering::Acquire)
}
fn recovered_total(&self) -> u64 {
self.metrics.recovered_total.load(Ordering::Acquire)
}
fn pending(&self) -> u64 {
self.metrics.pending.load(Ordering::Acquire)
}
fn retry_failed_total(&self) -> u64 {
self.metrics.retry_failed_total.load(Ordering::Acquire)
}
fn closed_or_unavailable_total(&self) -> u64 {
self.metrics
.closed_or_unavailable_total
.load(Ordering::Acquire)
}
}
impl UsageEnqueueDispatcherMetrics {
fn record_permanent_failure(&self) -> u64 {
self.permanent_failure_total.fetch_add(1, Ordering::AcqRel) + 1
}
fn record_scheduled(&self) -> u64 {
self.pending.fetch_add(1, Ordering::AcqRel);
self.scheduled_total.fetch_add(1, Ordering::AcqRel) + 1
}
fn record_recovered(&self) -> u64 {
let recovered = self.recovered_total.fetch_add(1, Ordering::AcqRel) + 1;
self.pending.fetch_sub(1, Ordering::AcqRel);
recovered
}
fn record_retry_failed(&self) {
self.retry_failed_total.fetch_add(1, Ordering::AcqRel);
}
fn record_closed_or_unavailable(&self) -> u64 {
self.closed_or_unavailable_total
.fetch_add(1, Ordering::AcqRel)
+ 1
}
}
#[cfg(test)]
async fn run_usage_enqueue_retry_worker(
worker_index: usize,
config: UsageRuntimeConfig,
receiver: mpsc::Receiver<UsageEnqueueRetryItem>,
metrics: Arc<UsageEnqueueDispatcherMetrics>,
) {
let (_sender, drain) = tokio::sync::watch::channel(false);
run_usage_enqueue_retry_worker_with_drain(worker_index, config, receiver, metrics, drain).await;
}
async fn run_usage_enqueue_retry_worker_with_drain(
worker_index: usize,
config: UsageRuntimeConfig,
mut receiver: mpsc::Receiver<UsageEnqueueRetryItem>,
metrics: Arc<UsageEnqueueDispatcherMetrics>,
mut drain: tokio::sync::watch::Receiver<bool>,
) {
let mut initial_retry_delay_applied = false;
while let Some(mut item) = receiver.recv().await {
if item.delay_before_first_attempt && !initial_retry_delay_applied {
initial_retry_delay_applied = true;
if !*drain.borrow() {
crate::shutdown::retry_delay(usage_enqueue_retry_delay(&config, 1), &mut drain)
.await;
}
}
loop {
match item.queue.enqueue(&item.event).await {
Ok(_) => {
let recovered = metrics.record_recovered();
if item.attempts > 0 && should_log_usage_retry_counter(recovered) {
warn!(
event_name = "usage_event_enqueue_retry_recovered",
log_type = "event",
event_phase = item.event_phase,
usage_event_type = ?item.event.event_type,
request_id = %item.event.request_id,
worker_index,
retry_attempts = item.attempts,
retry_recovered_total = recovered,
"usage runtime local enqueue retry recovered"
);
}
break;
}
Err(err) if is_permanent_enqueue_error(&err) => {
let rejected = metrics.record_permanent_failure();
metrics.pending.fetch_sub(1, Ordering::AcqRel);
if should_log_usage_retry_counter(rejected) {
warn!(
event_name = "usage_event_enqueue_retry_invalid_input",
log_type = "ops",
event_phase = item.event_phase,
usage_event_type = ?item.event.event_type,
request_id = %item.event.request_id,
worker_index,
rejected_total = rejected,
error = %err,
"usage enqueue retry ended for invalid input; advancing to the next event"
);
}
break;
}
Err(err) => {
item.attempts = item.attempts.saturating_add(1);
metrics.record_retry_failed();
let delay = usage_enqueue_retry_delay(&config, item.attempts);
if should_log_usage_retry_counter(item.attempts) {
warn!(
event_name = "usage_event_enqueue_retry_failed",
log_type = "event",
event_phase = item.event_phase,
usage_event_type = ?item.event.event_type,
request_id = %item.event.request_id,
worker_index,
retry_attempt = item.attempts,
retry_delay_ms = delay.as_millis() as u64,
error = %err,
"usage runtime local enqueue retry failed; will retry"
);
}
crate::shutdown::retry_delay(delay, &mut drain).await;
}
}
}
}
}
fn usage_enqueue_retry_delay(config: &UsageRuntimeConfig, attempts: u64) -> Duration {
let exponent = attempts.saturating_sub(1).min(16);
let multiplier = 1_u64.checked_shl(exponent as u32).unwrap_or(u64::MAX);
let delay_ms = config
.enqueue_retry_initial_backoff_ms
.saturating_mul(multiplier)
.min(config.enqueue_retry_max_backoff_ms);
Duration::from_millis(delay_ms.max(1))
}
async fn catch_usage_writer_panic<T, F>(
operation: &'static str,
future: F,
) -> Result<T, DataLayerError>
where
F: Future<Output = Result<T, DataLayerError>>,
{
match AssertUnwindSafe(future).catch_unwind().await {
Ok(result) => result,
Err(_) => Err(DataLayerError::UnexpectedValue(format!(
"{operation} panicked"
))),
}
}
fn retry_worker_capacity(total_capacity: usize, workers: usize, worker_index: usize) -> usize {
let workers = workers.max(1);
let base = total_capacity / workers;
let remainder = total_capacity % workers;
(base + usize::from(worker_index < remainder)).max(1)
}
fn retry_worker_index(request_id: &str, worker_count: usize) -> usize {
if worker_count <= 1 {
return 0;
}
(fnv_hash(request_id.as_bytes()) % worker_count as u64) as usize
}
fn fnv_hash(bytes: &[u8]) -> u64 {
const FNV_OFFSET: u64 = 14_695_981_039_346_656_037;
const FNV_PRIME: u64 = 1_099_511_628_211;
let mut hash = FNV_OFFSET;
for byte in bytes {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
fn should_log_usage_retry_counter(value: u64) -> bool {
value <= 8 || value.is_power_of_two() || value.is_multiple_of(1_000)
}
async fn build_sync_terminal_usage_event_offthread(
context_seed: TerminalUsageContextSeed,
payload_seed: SyncTerminalUsagePayloadSeed,
capture: Option<TerminalSeedCaptureRetention>,
) -> Result<UsageEvent, DataLayerError> {
build_terminal_usage_event_offthread(capture, move || {
build_terminal_usage_event_from_seed(build_sync_terminal_usage_seed(
context_seed,
payload_seed,
))
})
.await
}
async fn build_stream_terminal_usage_event_offthread(
context_seed: TerminalUsageContextSeed,
payload_seed: StreamTerminalUsagePayloadSeed,
cancelled: bool,
capture: Option<TerminalSeedCaptureRetention>,
) -> Result<UsageEvent, DataLayerError> {
build_terminal_usage_event_offthread(capture, move || {
build_terminal_usage_event_from_seed(build_stream_terminal_usage_seed(
context_seed,
payload_seed,
cancelled,
))
})
.await
}
async fn build_terminal_usage_event_offthread(
capture: Option<TerminalSeedCaptureRetention>,
build: impl FnOnce() -> Result<UsageEvent, DataLayerError> + Send + 'static,
) -> Result<UsageEvent, DataLayerError> {
tokio::task::spawn_blocking(move || {
let mut event = build()?;
if let Some(capture) = capture {
capture.attach(&mut event);
}
Ok(event)
})
.await
.map_err(join_error_to_data_layer)?
}
fn join_error_to_data_layer(err: tokio::task::JoinError) -> DataLayerError {
DataLayerError::UnexpectedValue(format!("usage builder task join failed: {err}"))
}
fn now_unix_secs() -> u64 {
now_unix_ms() / 1_000
}
fn now_unix_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
#[cfg(test)]
mod tests {
mod shutdown {
include!("runtime_shutdown_tests.rs");
}
mod queue_payload {
include!("runtime_queue_payload_tests.rs");
}
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Instant;
use aether_contracts::{ExecutionPlan, ExecutionTelemetry, RequestBody};
use aether_data_contracts::repository::settlement::{
StoredUsageSettlement, UsageSettlementInput,
};
use aether_data_contracts::repository::usage::{
StoredRequestUsageAudit, UpsertUsageRecord, UsageBodyCaptureState,
};
use aether_data_contracts::DataLayerError;
use aether_runtime_state::{MemoryRuntimeStateConfig, RuntimeQueueStore, RuntimeState};
use async_trait::async_trait;
use serde_json::json;
use tokio::sync::mpsc;
use tokio::time::{sleep, timeout, Duration};
use super::{
preserve_provider_response_facts, preserve_request_facts, LifecycleAdmissionPermit,
LifecycleEventCoalescer, LifecycleSubmissionDispatcher, LifecycleSubmissionItem,
LifecycleSubmissionPriority, LifecycleTerminalUsageSeed, UsageBillingEventEnricher,
UsageBodyCapturePolicy, UsageEnqueueRetryDispatcher, UsageRequestRecordLevel,
UsageRuntimeAccess, UsageWorkerObservation, UsageWorkerSupervisorState,
};
use crate::worker::ManualProxyNodeCounter;
use crate::{
apply_usage_body_capture_policy_to_event, build_lifecycle_usage_seed,
build_terminal_usage_context_seed, SyncTerminalUsagePayloadSeed, TerminalUsageContextSeed,
UsageEvent, UsageEventData, UsageEventType, UsageQueue, UsageRecordWriter, UsageRuntime,
UsageRuntimeConfig, UsageSettlementWriter,
};
fn terminal_test_plan(request_id: &str) -> ExecutionPlan {
ExecutionPlan {
request_id: request_id.to_string(),
candidate_id: Some(format!("candidate-{request_id}")),
provider_name: Some("openai".to_string()),
provider_id: "provider-terminal-test".to_string(),
endpoint_id: "endpoint-terminal-test".to_string(),
key_id: "key-terminal-test".to_string(),
method: "POST".to_string(),
url: "https://example.com/v1/responses".to_string(),
headers: BTreeMap::new(),
content_type: Some("application/json".to_string()),
content_encoding: None,
body: RequestBody::from_json(json!({"model": "gpt-5"})),
stream: false,
client_api_format: "openai:responses".to_string(),
provider_api_format: "openai:responses".to_string(),
model_name: Some("gpt-5".to_string()),
proxy: None,
transport_profile: None,
timeouts: None,
}
}
fn sync_terminal_test_seeds(
request_id: &str,
) -> (TerminalUsageContextSeed, SyncTerminalUsagePayloadSeed) {
let plan = terminal_test_plan(request_id);
(
build_terminal_usage_context_seed(&plan, None),
SyncTerminalUsagePayloadSeed {
report_kind: "sync_completed".to_string(),
status_code: 200,
response_time_ms: Some(12),
first_byte_time_ms: None,
provider_response_headers: None,
client_response_headers: None,
provider_response_full: Some(json!({"id": request_id})),
provider_response_body_state: None,
client_response: Some(json!({"id": request_id})),
client_response_body_state: None,
standardized_usage: None,
capture_metadata: None,
},
)
}
fn terminal_seed_capture_test_seeds(
request_id: &str,
padding_bytes: usize,
) -> (TerminalUsageContextSeed, SyncTerminalUsagePayloadSeed) {
let (_, mut payload) = sync_terminal_test_seeds(request_id);
let provider_request = json!({
"model": "gpt-5.6-sol", "reasoning": {"effort": "medium"},
"service_tier": "priority", "prompt": "retained request facts"
});
let mut plan = terminal_test_plan(request_id);
plan.model_name = Some("gpt-5.6-sol".to_string());
plan.body = RequestBody::from_json(provider_request.clone());
let report_context = json!({
"original_request_body": {"reasoning": {"effort": "high"}},
"provider_request_body": provider_request,
});
let context = build_terminal_usage_context_seed(&plan, Some(&report_context));
payload.provider_response_full = Some(json!({
"id": request_id,
"output": "x".repeat(padding_bytes),
"service_tier": "default",
"usage": {"input_tokens": 100, "output_tokens": 500, "total_tokens": 600}
}));
(context, payload)
}
#[tokio::test]
async fn terminal_seed_capture_budget_zero_preserves_sync_facts_and_explicit_zero() {
for tokens in [0, 100] {
let (context, mut payload) = terminal_seed_capture_test_seeds("seed-budget-sync", 4096);
payload.provider_response_full.as_mut().unwrap()["usage"] = json!({
"input_tokens": tokens, "output_tokens": tokens, "total_tokens": 2 * tokens
});
let mut expected = crate::build_terminal_usage_event_from_seed(
crate::build_sync_terminal_usage_seed(context.clone(), payload.clone()),
)
.expect("original terminal event");
let budget = Arc::new(super::EventCaptureMemoryBudget::new(0));
let prepared = LifecycleTerminalUsageSeed::Sync {
context,
payload,
capture: None,
}
.prepare_capture_memory(Arc::clone(&budget));
assert!(matches!(
prepared,
LifecycleTerminalUsageSeed::Prepared { .. }
));
let event = prepared
.build("seed-budget-sync")
.await
.expect("prepared event");
super::prepare_event_capture_memory(&mut expected, Arc::clone(&budget));
assert_eq!(event.event_type, expected.event_type);
assert_eq!(event.data, expected.data);
assert_eq!(event.data.input_tokens, Some(tokens));
assert_eq!(event.data.output_tokens, Some(tokens));
assert_eq!(event.data.total_tokens, Some(2 * tokens));
assert!(event.data.request_body.is_none());
assert!(event.data.provider_request_body.is_none());
assert!(event.data.response_body.is_none());
assert!(event.data.client_response_body.is_none());
assert_eq!(
event.data.response_body_state,
Some(UsageBodyCaptureState::Truncated)
);
let metadata = event.data.request_metadata.as_ref().expect("billing facts");
assert_eq!(metadata["requested_reasoning_effort"], "high");
assert_eq!(metadata["provider_reasoning_effort"], "medium");
assert_eq!(metadata["provider_service_tier"], "priority");
assert_eq!(metadata["provider_actual_service_tier"], "default");
assert_eq!(metadata["provider_cache_ttl_minutes"], 30);
assert_eq!(budget.retained_bytes(), 0);
}
}
#[tokio::test]
async fn terminal_seed_capture_budget_zero_preserves_image_estimates_and_dimensions() {
let (mut context, mut payload) = terminal_seed_capture_test_seeds("seed-budget-image", 0);
context.client_contract = "openai:image".to_string();
context.provider_contract = "openai:image".to_string();
context.request_type = "image".to_string();
context.provider_request = Some(json!({
"model": "gpt-image-2", "prompt": "Draw a red kite", "n": 1,
"size": "1024x1024", "quality": "high", "output_format": "png"
}));
payload.provider_response_full = Some(json!({
"data": [{"b64_json": "x".repeat(4096)}, {"b64_json": "y".repeat(4096)}]
}));
let expected = crate::build_terminal_usage_event_from_seed(
crate::build_sync_terminal_usage_seed(context.clone(), payload.clone()),
)
.expect("original image event");
let budget = Arc::new(super::EventCaptureMemoryBudget::new(0));
let event = LifecycleTerminalUsageSeed::Sync {
context,
payload,
capture: None,
}
.prepare_capture_memory(Arc::clone(&budget))
.build("seed-budget-image")
.await
.expect("image event");
assert_eq!(event.event_type, UsageEventType::Completed);
assert_eq!(event.data.input_tokens, expected.data.input_tokens);
assert!(event.data.input_tokens.unwrap_or_default() > 0);
assert_eq!(event.data.total_tokens, expected.data.total_tokens);
let metadata = event
.data
.request_metadata
.as_ref()
.expect("image dimensions");
assert_eq!(metadata["dimensions"]["image_count"], 2);
assert_eq!(metadata["dimensions"]["image_size"], "1024x1024");
assert_eq!(metadata["dimensions"]["image_quality"], "high");
assert_eq!(metadata["dimensions"]["image_output_format"], "png");
assert!(event.data.response_body.is_none());
assert!(event.data.provider_request_body.is_none());
assert_eq!(budget.retained_bytes(), 0);
}
#[tokio::test]
async fn terminal_seed_capture_budget_zero_preserves_stream_terminal_evidence() {
for (failed, cancelled) in [(false, false), (true, false), (false, true)] {
let (mut context, _) = terminal_seed_capture_test_seeds("seed-budget-stream", 0);
context.is_stream = true;
let provider_response_full = Some(json!({
"chunks": [{
"type": if failed { "response.failed" } else { "response.completed" },
"response": {
"status": if failed { "failed" } else { "completed" },
"service_tier": "default",
"usage": {"input_tokens": 100, "output_tokens": 500, "total_tokens": 600},
"error": if failed { json!({"message": "provider refused"}) } else { json!(null) }
}
}]
}));
let payload = crate::StreamTerminalUsagePayloadSeed {
report_kind: "openai_responses_stream_success".to_string(),
status_code: if cancelled { 499 } else { 200 },
response_time_ms: Some(12),
first_byte_time_ms: Some(3),
provider_response_headers: None,
client_response_headers: None,
provider_response_full,
provider_response_body_state: Some(UsageBodyCaptureState::Inline),
client_response: None,
client_response_body_state: Some(UsageBodyCaptureState::None),
standardized_usage: None,
provider_actual_service_tier: Some("default".to_string()),
observed_stream_finish: Some(true),
terminal_error_message: None,
capture_metadata: None,
};
let mut expected = crate::build_terminal_usage_event_from_seed(
crate::build_stream_terminal_usage_seed(
context.clone(),
payload.clone(),
cancelled,
),
)
.expect("original stream event");
let budget = Arc::new(super::EventCaptureMemoryBudget::new(0));
let event = LifecycleTerminalUsageSeed::Stream {
context,
payload,
cancelled,
capture: None,
}
.prepare_capture_memory(Arc::clone(&budget))
.build("seed-budget-stream")
.await
.expect("stream event");
super::prepare_event_capture_memory(&mut expected, Arc::clone(&budget));
assert_eq!(event.event_type, expected.event_type);
assert_eq!(event.data, expected.data);
assert_eq!(event.data.input_tokens, Some(100));
assert_eq!(event.data.output_tokens, Some(500));
assert_eq!(event.data.first_byte_time_ms, Some(3));
assert_eq!(
event.event_type,
if cancelled {
UsageEventType::Cancelled
} else if failed {
UsageEventType::Failed
} else {
UsageEventType::Completed
}
);
if failed {
assert_eq!(
event.data.error_message.as_deref(),
Some("provider refused")
);
}
assert!(event.data.response_body.is_none());
assert_eq!(budget.retained_bytes(), 0);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn terminal_seed_capture_budget_bounds_concurrent_seeds_and_transfers_to_events() {
const COUNT: usize = 12;
const LIMIT: usize = 96 * 1024;
let budget = Arc::new(super::EventCaptureMemoryBudget::new(LIMIT));
let barrier = Arc::new(tokio::sync::Barrier::new(COUNT));
let mut tasks = Vec::new();
for index in 0..COUNT {
let (context, payload) =
terminal_seed_capture_test_seeds(&format!("seed-budget-{index}"), 32 * 1024);
let budget = Arc::clone(&budget);
let barrier = Arc::clone(&barrier);
tasks.push(tokio::spawn(async move {
barrier.wait().await;
LifecycleTerminalUsageSeed::Sync {
context,
payload,
capture: None,
}
.prepare_capture_memory(budget)
}));
}
let mut seeds = Vec::new();
for task in tasks {
seeds.push(task.await.expect("concurrent seed preparation"));
}
let retained = budget.retained_bytes();
assert!(retained > 0 && retained <= LIMIT);
assert!(seeds
.iter()
.any(|seed| matches!(seed, LifecycleTerminalUsageSeed::Prepared { .. })));
let mut events = Vec::new();
for seed in seeds {
let event = seed.build("seed-budget").await.expect("terminal event");
assert_eq!(event.data.input_tokens, Some(100));
assert_eq!(event.data.output_tokens, Some(500));
events.push(event);
}
assert_eq!(
budget.retained_bytes(),
retained,
"ownership transfer must not drop or duplicate reservations"
);
drop(events);
assert_eq!(budget.retained_bytes(), 0);
let (context, payload) = terminal_seed_capture_test_seeds("seed-budget-dropped", 4096);
let queued = LifecycleTerminalUsageSeed::Sync {
context,
payload,
capture: None,
}
.prepare_capture_memory(Arc::clone(&budget));
assert!(budget.retained_bytes() > 0);
drop(queued);
assert_eq!(
budget.retained_bytes(),
0,
"dropping an unbuilt seed releases its bodies"
);
}
#[tokio::test]
async fn terminal_seed_capture_budget_cancellation_keeps_running_blocking_build_reserved() {
let budget = Arc::new(super::EventCaptureMemoryBudget::new(64 * 1024));
let body = json!({"diagnostic": "x".repeat(4096)});
let capture = super::TerminalSeedCaptureRetention::try_reserve(
[Some(&body), None, None, None],
Arc::clone(&budget),
);
let retained = budget.retained_bytes();
assert!(retained > 0);
let (started, started_rx) = tokio::sync::oneshot::channel();
let (release, release_rx) = std::sync::mpsc::channel();
let building = tokio::spawn(super::build_terminal_usage_event_offthread(
capture,
move || {
let _ = started.send(());
release_rx
.recv_timeout(std::time::Duration::from_secs(2))
.expect("release blocking builder");
Ok(UsageEvent::new(
UsageEventType::Completed,
"seed-budget-cancel",
UsageEventData {
response_body: Some(body),
..UsageEventData::default()
},
))
},
));
timeout(Duration::from_secs(1), started_rx)
.await
.expect("builder started")
.expect("start signal");
building.abort();
assert!(building
.await
.expect_err("cancelled wrapper")
.is_cancelled());
assert_eq!(
budget.retained_bytes(),
retained,
"spawn_blocking survives cancellation of the caller"
);
release.send(()).expect("release owned builder");
timeout(Duration::from_secs(2), async {
while budget.retained_bytes() != 0 {
tokio::task::yield_now().await;
}
})
.await
.expect("detached blocking result drops its body and reservation");
let body = json!({"diagnostic": "x".repeat(4096)});
let capture = super::TerminalSeedCaptureRetention::try_reserve(
[Some(&body), None, None, None],
Arc::clone(&budget),
);
let result = super::build_terminal_usage_event_offthread(capture, move || {
drop(body);
panic!("forced terminal seed builder panic")
})
.await;
assert!(result.is_err());
assert_eq!(budget.retained_bytes(), 0);
}
#[tokio::test]
async fn terminal_seed_capture_budget_bounds_admission_backlog_and_preserves_failed_build_progress(
) {
const COUNT: usize = 12;
for limit in [0, 96 * 1024] {
let config = UsageRuntimeConfig {
enabled: true,
terminal_submission_max_in_flight: 1,
..UsageRuntimeConfig::default()
};
let runtime = UsageRuntime::new(config).expect("usage runtime");
let store = CloneQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default())),
};
let budget = Arc::new(super::EventCaptureMemoryBudget::new(limit));
let held = runtime
.terminal_submission_state
.acquire()
.await
.expect("hold terminal admission");
runtime
.dispatch_terminal_seed(
&store,
"seed-budget-build-error".to_string(),
LifecycleTerminalUsageSeed::Prepared {
kind: super::TerminalSeedKind::Sync,
result: Err(DataLayerError::UnexpectedValue(
"forced prepared builder failure".to_string(),
)),
},
)
.await;
for index in 0..COUNT {
let request_id = format!("seed-budget-backlog-{index}");
let (context, payload) = terminal_seed_capture_test_seeds(&request_id, 32 * 1024);
let seed = LifecycleTerminalUsageSeed::Sync {
context,
payload,
capture: None,
}
.prepare_capture_memory(Arc::clone(&budget));
runtime
.dispatch_terminal_seed(&store, request_id, seed)
.await;
}
timeout(Duration::from_secs(2), async {
while runtime.metrics_snapshot().terminal_submission_pending < COUNT + 1 {
tokio::task::yield_now().await;
}
})
.await
.expect("all seeds wait behind terminal admission");
assert!(budget.retained_bytes() <= limit);
assert_eq!(budget.retained_bytes() > 0, limit > 0);
assert!(budget.downgraded_total() > 0);
assert!(
store.records.lock().unwrap().is_empty(),
"no persistence before admission"
);
drop(held);
timeout(Duration::from_secs(5), async {
loop {
let snapshot = runtime.metrics_snapshot();
if store.records.lock().unwrap().len() == COUNT
&& snapshot.terminal_submission_pending == 0
&& snapshot.lifecycle_submission_pending == 0
{
break;
}
tokio::task::yield_now().await;
}
})
.await
.expect("failed build releases its permit and every later terminal persists");
let records = store.records.lock().unwrap();
for record in records.iter() {
assert_eq!(record.status, "completed");
assert_eq!(record.input_tokens, Some(100));
assert_eq!(record.output_tokens, Some(500));
assert_eq!(record.total_tokens, Some(600));
assert!(
record.response_body.is_none(),
"default Basic policy still applies at its original position"
);
}
assert_eq!(budget.retained_bytes(), 0);
assert_eq!(runtime.metrics_snapshot().terminal_submission_in_flight, 0);
}
}
struct TestLifecycleSubmissionItem {
request_id: String,
priority: LifecycleSubmissionPriority,
started: Option<Arc<tokio::sync::Notify>>,
release: Option<Arc<tokio::sync::Notify>>,
seen: Arc<Mutex<Vec<LifecycleSubmissionPriority>>>,
}
struct PanickingLifecycleSubmissionItem {
request_id: String,
}
#[async_trait]
impl LifecycleSubmissionItem for PanickingLifecycleSubmissionItem {
fn request_id(&self) -> &str {
&self.request_id
}
fn priority(&self) -> LifecycleSubmissionPriority {
LifecycleSubmissionPriority::Pending
}
async fn execute(self: Box<Self>, _admission: LifecycleAdmissionPermit) {
panic!("forced lifecycle submission panic");
}
}
struct PanickingTerminalExecutionItem {
request_id: String,
}
#[async_trait]
impl super::TerminalExecutionItem for PanickingTerminalExecutionItem {
fn request_id(&self) -> &str {
&self.request_id
}
async fn execute(self: Box<Self>) {
panic!("forced terminal execution panic");
}
}
struct CompletingTerminalExecutionItem {
request_id: String,
completion: tokio::sync::oneshot::Sender<()>,
}
#[async_trait]
impl super::TerminalExecutionItem for CompletingTerminalExecutionItem {
fn request_id(&self) -> &str {
&self.request_id
}
async fn execute(self: Box<Self>) {
let _ = self.completion.send(());
}
}
struct PanickingFirstBytePersistenceItem {
_marker_guard: super::FirstByteMarkerGuard,
record: UpsertUsageRecord,
}
#[async_trait]
impl super::FirstBytePersistenceItem for PanickingFirstBytePersistenceItem {
fn batch_identity(&self) -> Option<usize> {
panic!("forced first-byte identity panic");
}
async fn is_current(&self) -> bool {
true
}
fn build_record(&self) -> Result<UpsertUsageRecord, DataLayerError> {
Ok(self.record.clone())
}
async fn write_batch(
&self,
_records: Vec<UpsertUsageRecord>,
) -> Result<(), DataLayerError> {
unreachable!("identity panic should precede the write")
}
async fn complete_success(self: Box<Self>) {
unreachable!("identity panic should precede completion")
}
async fn complete_cancelled(self: Box<Self>) {
unreachable!("test item is always current")
}
async fn enqueue_fallback(self: Box<Self>) {
unreachable!("identity panic should precede fallback")
}
}
#[async_trait]
impl LifecycleSubmissionItem for TestLifecycleSubmissionItem {
fn request_id(&self) -> &str {
&self.request_id
}
fn priority(&self) -> LifecycleSubmissionPriority {
self.priority
}
async fn execute(self: Box<Self>, _admission: LifecycleAdmissionPermit) {
if let Some(started) = self.started {
started.notify_one();
}
if let Some(release) = self.release {
release.notified().await;
}
self.seen
.lock()
.expect("lifecycle test seen lock")
.push(self.priority);
}
}
#[derive(Default)]
struct NoRedisUsageStore {
records: Mutex<Vec<UpsertUsageRecord>>,
enrichment_failures: AtomicUsize,
enrichment_calls: AtomicUsize,
enriched_costs: Option<(f64, f64)>,
}
struct QueueConfiguredUsageStore {
inner: NoRedisUsageStore,
queue: Arc<dyn RuntimeQueueStore>,
}
#[derive(Clone)]
struct CloneQueueConfiguredUsageStore {
records: Arc<Mutex<Vec<UpsertUsageRecord>>>,
queue: Arc<dyn RuntimeQueueStore>,
}
#[derive(Clone)]
struct BatchingQueueUsageStore {
records: Arc<Mutex<Vec<UpsertUsageRecord>>>,
queue: Arc<dyn RuntimeQueueStore>,
}
#[derive(Clone)]
struct OrderedBatchUsageStore {
records: Arc<Mutex<Vec<UpsertUsageRecord>>>,
pending_batch_sizes: Arc<Mutex<Vec<usize>>>,
pending_attempts: Arc<Mutex<BTreeMap<String, usize>>>,
permanent_pending_failure_request_id: Option<Arc<str>>,
remaining_pending_failures: Arc<AtomicUsize>,
remaining_pending_panics: Arc<AtomicUsize>,
remaining_first_byte_panics: Arc<AtomicUsize>,
queue: Arc<dyn RuntimeQueueStore>,
}
#[derive(Clone)]
struct BlockingNonBatchPendingStore {
release_writes: Arc<tokio::sync::Semaphore>,
writes_in_flight: Arc<AtomicUsize>,
max_writes_in_flight: Arc<AtomicUsize>,
writes_completed: Arc<AtomicUsize>,
}
#[derive(Clone)]
struct AdmissionBlockedUsageStore {
release_writes: Arc<tokio::sync::Semaphore>,
writes_in_flight: Arc<AtomicUsize>,
max_writes_in_flight: Arc<AtomicUsize>,
writes_completed: Arc<AtomicUsize>,
}
struct BlockingNonBatchPendingItem {
request_id: String,
record: UpsertUsageRecord,
store: BlockingNonBatchPendingStore,
record_gate: Arc<super::UsageWorkerRecordConcurrencyGate>,
completed: Arc<AtomicUsize>,
degraded: Arc<AtomicUsize>,
}
struct PanicOnceQueueConfiguredUsageStore {
inner: CloneQueueConfiguredUsageStore,
remaining_panics: AtomicUsize,
}
#[derive(Clone)]
struct PanicOncePolicyQueueConfiguredUsageStore {
inner: CloneQueueConfiguredUsageStore,
remaining_policy_panics: Arc<AtomicUsize>,
policy_reads: Arc<AtomicUsize>,
}
struct EnrichmentCountingQueueStore {
records: Mutex<Vec<UpsertUsageRecord>>,
queue: Arc<dyn RuntimeQueueStore>,
enrich_calls: AtomicUsize,
}
#[derive(Clone)]
struct FailingWriteQueueConfiguredUsageStore {
queue: Arc<dyn RuntimeQueueStore>,
upsert_attempts: Arc<AtomicUsize>,
}
#[derive(Clone)]
struct QueueOnlyUsageStore {
queue: Arc<dyn RuntimeQueueStore>,
upsert_attempts: Arc<AtomicUsize>,
}
#[derive(Clone)]
struct BlockingWriteQueueConfiguredUsageStore {
records: Arc<Mutex<Vec<UpsertUsageRecord>>>,
queue: Arc<dyn RuntimeQueueStore>,
write_started: Arc<tokio::sync::Notify>,
release_writes: Arc<tokio::sync::Notify>,
writes_completed: Arc<AtomicUsize>,
}
#[derive(Clone)]
struct BlockingPolicyQueueConfiguredUsageStore {
queue: Arc<dyn RuntimeQueueStore>,
policy_started: Arc<tokio::sync::Notify>,
release_policy: Arc<tokio::sync::Notify>,
policy_released: Arc<AtomicBool>,
policy_reads: Arc<AtomicUsize>,
}
impl BlockingPolicyQueueConfiguredUsageStore {
fn new(queue: Arc<dyn RuntimeQueueStore>) -> Self {
Self {
queue,
policy_started: Arc::new(tokio::sync::Notify::new()),
release_policy: Arc::new(tokio::sync::Notify::new()),
policy_released: Arc::new(AtomicBool::new(false)),
policy_reads: Arc::new(AtomicUsize::new(0)),
}
}
fn release_blocked_policy(&self) {
self.policy_released.store(true, Ordering::Release);
self.release_policy.notify_waiters();
}
}
#[derive(Default)]
struct FailingPolicyUsageStore {
inner: NoRedisUsageStore,
policy_reads: AtomicUsize,
}
struct FlakyAppendQueueStore {
inner: Arc<dyn RuntimeQueueStore>,
remaining_failures: AtomicUsize,
append_attempts: AtomicUsize,
successful_appends: AtomicUsize,
active_appends: AtomicUsize,
max_active_appends: AtomicUsize,
append_delay_ms: u64,
}
impl FlakyAppendQueueStore {
fn new(inner: Arc<dyn RuntimeQueueStore>, remaining_failures: usize) -> Self {
Self {
inner,
remaining_failures: AtomicUsize::new(remaining_failures),
append_attempts: AtomicUsize::new(0),
successful_appends: AtomicUsize::new(0),
active_appends: AtomicUsize::new(0),
max_active_appends: AtomicUsize::new(0),
append_delay_ms: 0,
}
}
fn with_append_delay_ms(mut self, append_delay_ms: u64) -> Self {
self.append_delay_ms = append_delay_ms;
self
}
}
async fn wait_for_enqueue_dispatcher_to_drain(runtime: &UsageRuntime, expected_recovered: u64) {
timeout(Duration::from_secs(5), async {
loop {
let snapshot = runtime.metrics_snapshot();
if snapshot.enqueue_retry_recovered_total >= expected_recovered
&& snapshot.enqueue_retry_pending == 0
{
return;
}
sleep(Duration::from_millis(1)).await;
}
})
.await
.expect("local usage enqueue dispatcher should drain");
}
#[async_trait]
impl UsageRecordWriter for NoRedisUsageStore {
async fn upsert_usage_record(
&self,
record: UpsertUsageRecord,
) -> Result<Option<StoredRequestUsageAudit>, DataLayerError> {
self.records.lock().expect("records lock").push(record);
Ok(None)
}
}
#[async_trait]
impl UsageSettlementWriter for NoRedisUsageStore {
fn has_usage_settlement_writer(&self) -> bool {
false
}
async fn settle_usage(
&self,
_input: UsageSettlementInput,
) -> Result<Option<StoredUsageSettlement>, DataLayerError> {
Ok(None)
}
}
#[async_trait]
impl UsageBillingEventEnricher for NoRedisUsageStore {
async fn enrich_usage_event(&self, event: &mut UsageEvent) -> Result<(), DataLayerError> {
self.enrichment_calls.fetch_add(1, Ordering::AcqRel);
if self
.enrichment_failures
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |remaining| {
remaining.checked_sub(1)
})
.is_ok()
{
// Real enrichers may update part of the event before a lookup fails.
event.data.total_cost_usd = Some(999.0);
return Err(DataLayerError::TimedOut("test billing lookup".to_string()));
}
if let Some((listed, actual)) = self.enriched_costs {
event.data.total_cost_usd = Some(listed);
event.data.actual_total_cost_usd = Some(actual);
}
Ok(())
}
}
#[async_trait]
impl ManualProxyNodeCounter for NoRedisUsageStore {
async fn increment_manual_proxy_node_requests(
&self,
_node_id: &str,
_total_delta: i64,
_failed_delta: i64,
_latency_ms: Option<i64>,
) -> Result<(), DataLayerError> {
Ok(())
}
}
impl UsageRuntimeAccess for NoRedisUsageStore {
fn has_usage_writer(&self) -> bool {
true
}
fn has_usage_worker_queue(&self) -> bool {
false
}
fn usage_worker_queue(&self) -> Option<Arc<dyn RuntimeQueueStore>> {
None
}
}
#[async_trait]
impl UsageRecordWriter for QueueConfiguredUsageStore {
async fn upsert_usage_record(
&self,
record: UpsertUsageRecord,
) -> Result<Option<StoredRequestUsageAudit>, DataLayerError> {
self.inner.upsert_usage_record(record).await
}
}
#[async_trait]
impl UsageSettlementWriter for QueueConfiguredUsageStore {
fn has_usage_settlement_writer(&self) -> bool {
false
}
async fn settle_usage(
&self,
_input: UsageSettlementInput,
) -> Result<Option<StoredUsageSettlement>, DataLayerError> {
Ok(None)
}
}
#[async_trait]
impl UsageBillingEventEnricher for QueueConfiguredUsageStore {
async fn enrich_usage_event(&self, _event: &mut UsageEvent) -> Result<(), DataLayerError> {
Ok(())
}
}
#[async_trait]
impl ManualProxyNodeCounter for QueueConfiguredUsageStore {
async fn increment_manual_proxy_node_requests(
&self,
_node_id: &str,
_total_delta: i64,
_failed_delta: i64,
_latency_ms: Option<i64>,
) -> Result<(), DataLayerError> {
Ok(())
}
}
impl UsageRuntimeAccess for QueueConfiguredUsageStore {
fn has_usage_writer(&self) -> bool {
true
}
fn has_usage_worker_queue(&self) -> bool {
true
}
fn usage_worker_queue(&self) -> Option<Arc<dyn RuntimeQueueStore>> {
Some(Arc::clone(&self.queue))
}
}
#[async_trait]
impl UsageRecordWriter for CloneQueueConfiguredUsageStore {
async fn upsert_usage_record(
&self,
record: UpsertUsageRecord,
) -> Result<Option<StoredRequestUsageAudit>, DataLayerError> {
self.records.lock().expect("records lock").push(record);
Ok(None)
}
}
#[async_trait]
impl UsageRecordWriter for BlockingNonBatchPendingStore {
async fn upsert_usage_record(
&self,
_record: UpsertUsageRecord,
) -> Result<Option<StoredRequestUsageAudit>, DataLayerError> {
let active = self.writes_in_flight.fetch_add(1, Ordering::AcqRel) + 1;
self.max_writes_in_flight
.fetch_max(active, Ordering::AcqRel);
let permit = self
.release_writes
.acquire()
.await
.expect("blocking pending store semaphore should remain open");
permit.forget();
self.writes_in_flight.fetch_sub(1, Ordering::AcqRel);
self.writes_completed.fetch_add(1, Ordering::AcqRel);
Ok(None)
}
}
#[async_trait]
impl UsageRecordWriter for AdmissionBlockedUsageStore {
async fn upsert_usage_record(
&self,
_record: UpsertUsageRecord,
) -> Result<Option<StoredRequestUsageAudit>, DataLayerError> {
let active = self.writes_in_flight.fetch_add(1, Ordering::AcqRel) + 1;
self.max_writes_in_flight
.fetch_max(active, Ordering::AcqRel);
let permit = self
.release_writes
.acquire()
.await
.expect("admission test semaphore should remain open");
permit.forget();
self.writes_in_flight.fetch_sub(1, Ordering::AcqRel);
self.writes_completed.fetch_add(1, Ordering::AcqRel);
Ok(None)
}
}
#[async_trait]
impl UsageSettlementWriter for AdmissionBlockedUsageStore {
fn has_usage_settlement_writer(&self) -> bool {
false
}
async fn settle_usage(
&self,
_input: UsageSettlementInput,
) -> Result<Option<StoredUsageSettlement>, DataLayerError> {
Ok(None)
}
}
#[async_trait]
impl UsageBillingEventEnricher for AdmissionBlockedUsageStore {
async fn enrich_usage_event(&self, _event: &mut UsageEvent) -> Result<(), DataLayerError> {
Ok(())
}
}
impl UsageRuntimeAccess for AdmissionBlockedUsageStore {
fn has_usage_writer(&self) -> bool {
true
}
fn supports_first_byte_usage_fast_path(&self) -> bool {
true
}
fn has_usage_worker_queue(&self) -> bool {
false
}
fn usage_worker_queue(&self) -> Option<Arc<dyn RuntimeQueueStore>> {
None
}
}
#[async_trait]
impl super::PendingPersistenceItem for BlockingNonBatchPendingItem {
fn request_id(&self) -> &str {
&self.request_id
}
fn batch_identity(&self) -> Option<usize> {
None
}
fn build_record(&self) -> Result<UpsertUsageRecord, DataLayerError> {
Ok(self.record.clone())
}
fn retry_delay(&self, _attempts: u64) -> Duration {
Duration::from_millis(1)
}
async fn write_batch(&self, records: Vec<UpsertUsageRecord>) -> Result<(), DataLayerError> {
let _permit = self.record_gate.acquire().await;
self.store.upsert_pending_usage_records(records).await
}
async fn complete_success(self: Box<Self>) {
self.completed.fetch_add(1, Ordering::AcqRel);
}
fn complete_degraded(self: Box<Self>) {
self.degraded.fetch_add(1, Ordering::AcqRel);
}
}
#[async_trait]
impl UsageRecordWriter for BatchingQueueUsageStore {
fn supports_first_byte_usage_batch(&self) -> bool {
true
}
fn first_byte_usage_writer_identity(&self) -> Option<usize> {
Some(Arc::as_ptr(&self.records) as usize)
}
fn supports_pending_usage_batch(&self) -> bool {
true
}
fn pending_usage_writer_identity(&self) -> Option<usize> {
Some(Arc::as_ptr(&self.records) as usize)
}
async fn upsert_usage_record(
&self,
record: UpsertUsageRecord,
) -> Result<Option<StoredRequestUsageAudit>, DataLayerError> {
self.records.lock().expect("records lock").push(record);
Ok(None)
}
async fn upsert_first_byte_usage_records(
&self,
records: Vec<UpsertUsageRecord>,
) -> Result<(), DataLayerError> {
self.records.lock().expect("records lock").extend(records);
Ok(())
}
async fn upsert_pending_usage_records(
&self,
records: Vec<UpsertUsageRecord>,
) -> Result<(), DataLayerError> {
self.records.lock().expect("records lock").extend(records);
Ok(())
}
}
#[async_trait]
impl UsageRecordWriter for OrderedBatchUsageStore {
fn supports_first_byte_usage_batch(&self) -> bool {
true
}
fn first_byte_usage_writer_identity(&self) -> Option<usize> {
Some(Arc::as_ptr(&self.records) as usize)
}
fn supports_pending_usage_batch(&self) -> bool {
true
}
fn pending_usage_writer_identity(&self) -> Option<usize> {
Some(Arc::as_ptr(&self.records) as usize)
}
async fn upsert_usage_record(
&self,
record: UpsertUsageRecord,
) -> Result<Option<StoredRequestUsageAudit>, DataLayerError> {
self.records.lock().expect("records lock").push(record);
Ok(None)
}
async fn upsert_first_byte_usage_records(
&self,
records: Vec<UpsertUsageRecord>,
) -> Result<(), DataLayerError> {
if self
.remaining_first_byte_panics
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |remaining| {
(remaining > 0).then(|| remaining - 1)
})
.is_ok()
{
panic!("forced first-byte batch panic");
}
self.records.lock().expect("records lock").extend(records);
Ok(())
}
async fn upsert_pending_usage_records(
&self,
records: Vec<UpsertUsageRecord>,
) -> Result<(), DataLayerError> {
self.pending_batch_sizes
.lock()
.expect("pending batch sizes lock")
.push(records.len());
{
let mut attempts = self.pending_attempts.lock().expect("pending attempts lock");
for record in &records {
*attempts.entry(record.request_id.clone()).or_default() += 1;
}
}
if self
.permanent_pending_failure_request_id
.as_deref()
.is_some_and(|request_id| {
records.iter().any(|record| record.request_id == request_id)
})
{
return Err(DataLayerError::Postgres(
"forced permanent pending batch failure".to_string(),
));
}
if self
.remaining_pending_panics
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |remaining| {
(remaining > 0).then(|| remaining - 1)
})
.is_ok()
{
panic!("forced pending batch panic");
}
if self
.remaining_pending_failures
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |remaining| {
(remaining > 0).then(|| remaining - 1)
})
.is_ok()
{
return Err(DataLayerError::Postgres(
"forced pending batch failure".to_string(),
));
}
self.records.lock().expect("records lock").extend(records);
Ok(())
}
}
#[async_trait]
impl UsageSettlementWriter for OrderedBatchUsageStore {
fn has_usage_settlement_writer(&self) -> bool {
false
}
async fn settle_usage(
&self,
_input: UsageSettlementInput,
) -> Result<Option<StoredUsageSettlement>, DataLayerError> {
Ok(None)
}
}
#[async_trait]
impl UsageBillingEventEnricher for OrderedBatchUsageStore {
async fn enrich_usage_event(&self, _event: &mut UsageEvent) -> Result<(), DataLayerError> {
Ok(())
}
}
impl UsageRuntimeAccess for OrderedBatchUsageStore {
fn has_usage_writer(&self) -> bool {
true
}
fn supports_first_byte_usage_fast_path(&self) -> bool {
true
}
fn has_usage_worker_queue(&self) -> bool {
true
}
fn usage_worker_queue(&self) -> Option<Arc<dyn RuntimeQueueStore>> {
Some(Arc::clone(&self.queue))
}
}
#[async_trait]
impl UsageSettlementWriter for BatchingQueueUsageStore {
fn has_usage_settlement_writer(&self) -> bool {
false
}
async fn settle_usage(
&self,
_input: UsageSettlementInput,
) -> Result<Option<StoredUsageSettlement>, DataLayerError> {
Ok(None)
}
}
#[async_trait]
impl UsageBillingEventEnricher for BatchingQueueUsageStore {
async fn enrich_usage_event(&self, _event: &mut UsageEvent) -> Result<(), DataLayerError> {
Ok(())
}
}
impl UsageRuntimeAccess for BatchingQueueUsageStore {
fn has_usage_writer(&self) -> bool {
true
}
fn supports_first_byte_usage_fast_path(&self) -> bool {
true
}
fn has_usage_worker_queue(&self) -> bool {
true
}
fn usage_worker_queue(&self) -> Option<Arc<dyn RuntimeQueueStore>> {
Some(Arc::clone(&self.queue))
}
}
#[async_trait]
impl UsageSettlementWriter for CloneQueueConfiguredUsageStore {
fn has_usage_settlement_writer(&self) -> bool {
false
}
async fn settle_usage(
&self,
_input: UsageSettlementInput,
) -> Result<Option<StoredUsageSettlement>, DataLayerError> {
Ok(None)
}
}
#[async_trait]
impl UsageBillingEventEnricher for CloneQueueConfiguredUsageStore {
async fn enrich_usage_event(&self, _event: &mut UsageEvent) -> Result<(), DataLayerError> {
Ok(())
}
}
#[async_trait]
impl ManualProxyNodeCounter for CloneQueueConfiguredUsageStore {
async fn increment_manual_proxy_node_requests(
&self,
_node_id: &str,
_total_delta: i64,
_failed_delta: i64,
_latency_ms: Option<i64>,
) -> Result<(), DataLayerError> {
Ok(())
}
}
impl UsageRuntimeAccess for CloneQueueConfiguredUsageStore {
fn has_usage_writer(&self) -> bool {
true
}
fn supports_first_byte_usage_fast_path(&self) -> bool {
true
}
fn has_usage_worker_queue(&self) -> bool {
true
}
fn usage_worker_queue(&self) -> Option<Arc<dyn RuntimeQueueStore>> {
Some(Arc::clone(&self.queue))
}
}
#[async_trait]
impl UsageRecordWriter for PanicOncePolicyQueueConfiguredUsageStore {
async fn upsert_usage_record(
&self,
record: UpsertUsageRecord,
) -> Result<Option<StoredRequestUsageAudit>, DataLayerError> {
self.inner.upsert_usage_record(record).await
}
}
#[async_trait]
impl UsageSettlementWriter for PanicOncePolicyQueueConfiguredUsageStore {
fn has_usage_settlement_writer(&self) -> bool {
false
}
async fn settle_usage(
&self,
_input: UsageSettlementInput,
) -> Result<Option<StoredUsageSettlement>, DataLayerError> {
Ok(None)
}
}
#[async_trait]
impl UsageBillingEventEnricher for PanicOncePolicyQueueConfiguredUsageStore {
async fn enrich_usage_event(&self, _event: &mut UsageEvent) -> Result<(), DataLayerError> {
Ok(())
}
}
#[async_trait]
impl UsageRuntimeAccess for PanicOncePolicyQueueConfiguredUsageStore {
fn has_usage_writer(&self) -> bool {
true
}
fn has_usage_worker_queue(&self) -> bool {
true
}
fn usage_worker_queue(&self) -> Option<Arc<dyn RuntimeQueueStore>> {
Some(Arc::clone(&self.inner.queue))
}
async fn body_capture_policy(&self) -> Result<UsageBodyCapturePolicy, DataLayerError> {
self.policy_reads.fetch_add(1, Ordering::AcqRel);
if self
.remaining_policy_panics
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |remaining| {
(remaining > 0).then(|| remaining - 1)
})
.is_ok()
{
panic!("forced body capture policy panic");
}
Ok(UsageBodyCapturePolicy::default())
}
}
#[async_trait]
impl UsageRecordWriter for PanicOnceQueueConfiguredUsageStore {
async fn upsert_usage_record(
&self,
record: UpsertUsageRecord,
) -> Result<Option<StoredRequestUsageAudit>, DataLayerError> {
let should_panic = self
.remaining_panics
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| {
(current > 0).then(|| current - 1)
})
.is_ok();
if should_panic {
panic!("forced usage writer panic");
}
self.inner.upsert_usage_record(record).await
}
}
#[async_trait]
impl UsageSettlementWriter for PanicOnceQueueConfiguredUsageStore {
fn has_usage_settlement_writer(&self) -> bool {
false
}
async fn settle_usage(
&self,
_input: UsageSettlementInput,
) -> Result<Option<StoredUsageSettlement>, DataLayerError> {
Ok(None)
}
}
#[async_trait]
impl UsageBillingEventEnricher for PanicOnceQueueConfiguredUsageStore {
async fn enrich_usage_event(&self, _event: &mut UsageEvent) -> Result<(), DataLayerError> {
Ok(())
}
}
#[async_trait]
impl ManualProxyNodeCounter for PanicOnceQueueConfiguredUsageStore {
async fn increment_manual_proxy_node_requests(
&self,
_node_id: &str,
_total_delta: i64,
_failed_delta: i64,
_latency_ms: Option<i64>,
) -> Result<(), DataLayerError> {
Ok(())
}
}
impl UsageRuntimeAccess for PanicOnceQueueConfiguredUsageStore {
fn has_usage_writer(&self) -> bool {
true
}
fn has_usage_worker_queue(&self) -> bool {
true
}
fn usage_worker_queue(&self) -> Option<Arc<dyn RuntimeQueueStore>> {
Some(Arc::clone(&self.inner.queue))
}
}
#[async_trait]
impl UsageRecordWriter for EnrichmentCountingQueueStore {
async fn upsert_usage_record(
&self,
record: UpsertUsageRecord,
) -> Result<Option<StoredRequestUsageAudit>, DataLayerError> {
self.records.lock().expect("records lock").push(record);
Ok(None)
}
}
#[async_trait]
impl UsageSettlementWriter for EnrichmentCountingQueueStore {
fn has_usage_settlement_writer(&self) -> bool {
false
}
async fn settle_usage(
&self,
_input: UsageSettlementInput,
) -> Result<Option<StoredUsageSettlement>, DataLayerError> {
Ok(None)
}
}
#[async_trait]
impl UsageBillingEventEnricher for EnrichmentCountingQueueStore {
async fn enrich_usage_event(&self, event: &mut UsageEvent) -> Result<(), DataLayerError> {
self.enrich_calls.fetch_add(1, Ordering::AcqRel);
event.data.total_cost_usd = Some(0.123);
Ok(())
}
}
#[async_trait]
impl ManualProxyNodeCounter for EnrichmentCountingQueueStore {
async fn increment_manual_proxy_node_requests(
&self,
_node_id: &str,
_total_delta: i64,
_failed_delta: i64,
_latency_ms: Option<i64>,
) -> Result<(), DataLayerError> {
Ok(())
}
}
impl UsageRuntimeAccess for EnrichmentCountingQueueStore {
fn has_usage_writer(&self) -> bool {
true
}
fn has_usage_worker_queue(&self) -> bool {
true
}
fn usage_worker_queue(&self) -> Option<Arc<dyn RuntimeQueueStore>> {
Some(Arc::clone(&self.queue))
}
}
#[async_trait]
impl UsageRecordWriter for FailingWriteQueueConfiguredUsageStore {
fn supports_first_byte_usage_batch(&self) -> bool {
true
}
fn first_byte_usage_writer_identity(&self) -> Option<usize> {
Some(Arc::as_ptr(&self.upsert_attempts) as usize)
}
async fn upsert_usage_record(
&self,
_record: UpsertUsageRecord,
) -> Result<Option<StoredRequestUsageAudit>, DataLayerError> {
self.upsert_attempts.fetch_add(1, Ordering::AcqRel);
Err(DataLayerError::Postgres(
"forced direct fallback write failure".to_string(),
))
}
async fn upsert_first_byte_usage_records(
&self,
_records: Vec<UpsertUsageRecord>,
) -> Result<(), DataLayerError> {
self.upsert_attempts.fetch_add(1, Ordering::AcqRel);
Err(DataLayerError::Postgres(
"forced batch first-byte write failure".to_string(),
))
}
}
#[async_trait]
impl UsageSettlementWriter for FailingWriteQueueConfiguredUsageStore {
fn has_usage_settlement_writer(&self) -> bool {
false
}
async fn settle_usage(
&self,
_input: UsageSettlementInput,
) -> Result<Option<StoredUsageSettlement>, DataLayerError> {
Ok(None)
}
}
#[async_trait]
impl UsageBillingEventEnricher for FailingWriteQueueConfiguredUsageStore {
async fn enrich_usage_event(&self, _event: &mut UsageEvent) -> Result<(), DataLayerError> {
Ok(())
}
}
#[async_trait]
impl ManualProxyNodeCounter for FailingWriteQueueConfiguredUsageStore {
async fn increment_manual_proxy_node_requests(
&self,
_node_id: &str,
_total_delta: i64,
_failed_delta: i64,
_latency_ms: Option<i64>,
) -> Result<(), DataLayerError> {
Ok(())
}
}
impl UsageRuntimeAccess for FailingWriteQueueConfiguredUsageStore {
fn has_usage_writer(&self) -> bool {
true
}
fn supports_first_byte_usage_fast_path(&self) -> bool {
true
}
fn has_usage_worker_queue(&self) -> bool {
true
}
fn usage_worker_queue(&self) -> Option<Arc<dyn RuntimeQueueStore>> {
Some(Arc::clone(&self.queue))
}
}
#[async_trait]
impl UsageRecordWriter for QueueOnlyUsageStore {
async fn upsert_usage_record(
&self,
_record: UpsertUsageRecord,
) -> Result<Option<StoredRequestUsageAudit>, DataLayerError> {
self.upsert_attempts.fetch_add(1, Ordering::AcqRel);
Ok(None)
}
}
#[async_trait]
impl UsageSettlementWriter for QueueOnlyUsageStore {
fn has_usage_settlement_writer(&self) -> bool {
false
}
async fn settle_usage(
&self,
_input: UsageSettlementInput,
) -> Result<Option<StoredUsageSettlement>, DataLayerError> {
Ok(None)
}
}
#[async_trait]
impl UsageBillingEventEnricher for QueueOnlyUsageStore {
async fn enrich_usage_event(&self, _event: &mut UsageEvent) -> Result<(), DataLayerError> {
Ok(())
}
}
#[async_trait]
impl ManualProxyNodeCounter for QueueOnlyUsageStore {
async fn increment_manual_proxy_node_requests(
&self,
_node_id: &str,
_total_delta: i64,
_failed_delta: i64,
_latency_ms: Option<i64>,
) -> Result<(), DataLayerError> {
Ok(())
}
}
impl UsageRuntimeAccess for QueueOnlyUsageStore {
fn has_usage_writer(&self) -> bool {
false
}
fn has_usage_worker_queue(&self) -> bool {
true
}
fn usage_worker_queue(&self) -> Option<Arc<dyn RuntimeQueueStore>> {
Some(Arc::clone(&self.queue))
}
}
#[async_trait]
impl UsageRecordWriter for BlockingWriteQueueConfiguredUsageStore {
async fn upsert_usage_record(
&self,
record: UpsertUsageRecord,
) -> Result<Option<StoredRequestUsageAudit>, DataLayerError> {
self.write_started.notify_one();
self.release_writes.notified().await;
self.records.lock().expect("records lock").push(record);
self.writes_completed.fetch_add(1, Ordering::AcqRel);
Ok(None)
}
}
#[async_trait]
impl UsageSettlementWriter for BlockingWriteQueueConfiguredUsageStore {
fn has_usage_settlement_writer(&self) -> bool {
false
}
async fn settle_usage(
&self,
_input: UsageSettlementInput,
) -> Result<Option<StoredUsageSettlement>, DataLayerError> {
Ok(None)
}
}
#[async_trait]
impl UsageBillingEventEnricher for BlockingWriteQueueConfiguredUsageStore {
async fn enrich_usage_event(&self, _event: &mut UsageEvent) -> Result<(), DataLayerError> {
Ok(())
}
}
#[async_trait]
impl ManualProxyNodeCounter for BlockingWriteQueueConfiguredUsageStore {
async fn increment_manual_proxy_node_requests(
&self,
_node_id: &str,
_total_delta: i64,
_failed_delta: i64,
_latency_ms: Option<i64>,
) -> Result<(), DataLayerError> {
Ok(())
}
}
impl UsageRuntimeAccess for BlockingWriteQueueConfiguredUsageStore {
fn has_usage_writer(&self) -> bool {
true
}
fn supports_first_byte_usage_fast_path(&self) -> bool {
true
}
fn has_usage_worker_queue(&self) -> bool {
true
}
fn usage_worker_queue(&self) -> Option<Arc<dyn RuntimeQueueStore>> {
Some(Arc::clone(&self.queue))
}
}
#[async_trait]
impl UsageRecordWriter for BlockingPolicyQueueConfiguredUsageStore {
async fn upsert_usage_record(
&self,
_record: UpsertUsageRecord,
) -> Result<Option<StoredRequestUsageAudit>, DataLayerError> {
Ok(None)
}
}
#[async_trait]
impl UsageSettlementWriter for BlockingPolicyQueueConfiguredUsageStore {
fn has_usage_settlement_writer(&self) -> bool {
false
}
async fn settle_usage(
&self,
_input: UsageSettlementInput,
) -> Result<Option<StoredUsageSettlement>, DataLayerError> {
Ok(None)
}
}
#[async_trait]
impl UsageBillingEventEnricher for BlockingPolicyQueueConfiguredUsageStore {
async fn enrich_usage_event(&self, _event: &mut UsageEvent) -> Result<(), DataLayerError> {
Ok(())
}
}
#[async_trait]
impl ManualProxyNodeCounter for BlockingPolicyQueueConfiguredUsageStore {
async fn increment_manual_proxy_node_requests(
&self,
_node_id: &str,
_total_delta: i64,
_failed_delta: i64,
_latency_ms: Option<i64>,
) -> Result<(), DataLayerError> {
Ok(())
}
}
#[async_trait]
impl UsageRuntimeAccess for BlockingPolicyQueueConfiguredUsageStore {
fn has_usage_writer(&self) -> bool {
true
}
fn has_usage_worker_queue(&self) -> bool {
true
}
fn usage_worker_queue(&self) -> Option<Arc<dyn RuntimeQueueStore>> {
Some(Arc::clone(&self.queue))
}
async fn body_capture_policy(&self) -> Result<UsageBodyCapturePolicy, DataLayerError> {
self.policy_reads.fetch_add(1, Ordering::AcqRel);
self.policy_started.notify_one();
// Latch the gate: Notify is edge-triggered, and later policy reads
// (or a waiter that subscribed after a single notify) must not hang.
loop {
if self.policy_released.load(Ordering::Acquire) {
break;
}
let notified = self.release_policy.notified();
if self.policy_released.load(Ordering::Acquire) {
break;
}
notified.await;
}
Ok(UsageBodyCapturePolicy::default())
}
}
#[async_trait]
impl UsageRecordWriter for FailingPolicyUsageStore {
async fn upsert_usage_record(
&self,
record: UpsertUsageRecord,
) -> Result<Option<StoredRequestUsageAudit>, DataLayerError> {
self.inner.upsert_usage_record(record).await
}
}
#[async_trait]
impl UsageSettlementWriter for FailingPolicyUsageStore {
fn has_usage_settlement_writer(&self) -> bool {
false
}
async fn settle_usage(
&self,
_input: UsageSettlementInput,
) -> Result<Option<StoredUsageSettlement>, DataLayerError> {
Ok(None)
}
}
#[async_trait]
impl UsageBillingEventEnricher for FailingPolicyUsageStore {
async fn enrich_usage_event(&self, _event: &mut UsageEvent) -> Result<(), DataLayerError> {
Ok(())
}
}
#[async_trait]
impl ManualProxyNodeCounter for FailingPolicyUsageStore {
async fn increment_manual_proxy_node_requests(
&self,
_node_id: &str,
_total_delta: i64,
_failed_delta: i64,
_latency_ms: Option<i64>,
) -> Result<(), DataLayerError> {
Ok(())
}
}
#[async_trait]
impl UsageRuntimeAccess for FailingPolicyUsageStore {
fn has_usage_writer(&self) -> bool {
true
}
fn has_usage_worker_queue(&self) -> bool {
false
}
fn usage_worker_queue(&self) -> Option<Arc<dyn RuntimeQueueStore>> {
None
}
async fn body_capture_policy(&self) -> Result<UsageBodyCapturePolicy, DataLayerError> {
self.policy_reads.fetch_add(1, Ordering::AcqRel);
Err(DataLayerError::Postgres(
"forced policy read failure".to_string(),
))
}
}
#[async_trait]
impl RuntimeQueueStore for FlakyAppendQueueStore {
async fn ensure_consumer_group(
&self,
stream: &str,
group: &str,
start_id: &str,
) -> Result<(), DataLayerError> {
self.inner
.ensure_consumer_group(stream, group, start_id)
.await
}
async fn append_fields_with_maxlen(
&self,
stream: &str,
fields: &BTreeMap<String, String>,
maxlen: Option<usize>,
) -> Result<String, DataLayerError> {
self.append_attempts.fetch_add(1, Ordering::AcqRel);
let active = self.active_appends.fetch_add(1, Ordering::AcqRel) + 1;
self.max_active_appends.fetch_max(active, Ordering::AcqRel);
if self.append_delay_ms > 0 {
sleep(Duration::from_millis(self.append_delay_ms)).await;
}
let failed = self
.remaining_failures
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| {
(current > 0).then(|| current - 1)
})
.is_ok();
let result = if failed {
Err(DataLayerError::Redis("forced append failure".to_string()))
} else {
self.inner
.append_fields_with_maxlen(stream, fields, maxlen)
.await
};
if result.is_ok() {
self.successful_appends.fetch_add(1, Ordering::AcqRel);
}
self.active_appends.fetch_sub(1, Ordering::AcqRel);
result
}
async fn read_group(
&self,
stream: &str,
group: &str,
consumer: &str,
count: usize,
block_ms: Option<u64>,
) -> Result<Vec<aether_runtime_state::RuntimeQueueEntry>, DataLayerError> {
self.inner
.read_group(stream, group, consumer, count, block_ms)
.await
}
async fn claim_stale(
&self,
stream: &str,
group: &str,
consumer: &str,
start_id: &str,
config: aether_runtime_state::RuntimeQueueReclaimConfig,
) -> Result<Vec<aether_runtime_state::RuntimeQueueEntry>, DataLayerError> {
self.inner
.claim_stale(stream, group, consumer, start_id, config)
.await
}
async fn ack(
&self,
stream: &str,
group: &str,
ids: &[String],
) -> Result<usize, DataLayerError> {
self.inner.ack(stream, group, ids).await
}
async fn delete(&self, stream: &str, ids: &[String]) -> Result<usize, DataLayerError> {
self.inner.delete(stream, ids).await
}
async fn stats(
&self,
stream: &str,
group: Option<&str>,
) -> Result<aether_runtime_state::RuntimeQueueStats, DataLayerError> {
self.inner.stats(stream, group).await
}
}
#[derive(Clone, Copy)]
enum DirectTerminalTestEntry {
PublicDirect,
OrderedDirect,
QueueDisabled,
}
async fn invoke_direct_terminal_test_entry(
entry: DirectTerminalTestEntry,
runtime: &UsageRuntime,
store: &NoRedisUsageStore,
event: UsageEvent,
) -> Option<super::TerminalPersistenceOutcome> {
match entry {
DirectTerminalTestEntry::PublicDirect => {
runtime.record_terminal_event_direct(store, event).await;
None
}
DirectTerminalTestEntry::OrderedDirect => Some(
runtime
.persist_ordered_terminal_event(store, event, true)
.await,
),
DirectTerminalTestEntry::QueueDisabled => {
Some(runtime.enqueue_or_write_terminal(store, event).await)
}
}
}
async fn assert_direct_terminal_enrichment_failure_is_not_persisted(
entry: DirectTerminalTestEntry,
request_id: &str,
) {
let runtime = UsageRuntime::new(UsageRuntimeConfig {
enabled: true,
queue_terminal_events: false,
..UsageRuntimeConfig::default()
})
.expect("usage runtime");
let store = NoRedisUsageStore {
enrichment_failures: AtomicUsize::new(1),
enriched_costs: Some((0.456, 0.123)),
..NoRedisUsageStore::default()
};
let generation = runtime
.lifecycle_coalescer
.register(request_id.to_string())
.await
.expect("delayed lifecycle generation");
let event = UsageEvent::new(
UsageEventType::Completed,
request_id,
UsageEventData {
user_id: Some("user-direct-pricing-retry".to_string()),
provider_name: "openai".to_string(),
provider_id: Some("provider-direct-pricing-retry".to_string()),
model: "gpt-5".to_string(),
input_tokens: Some(4),
output_tokens: Some(8),
total_tokens: Some(12),
total_cost_usd: Some(0.9),
actual_total_cost_usd: Some(0.8),
status_code: Some(200),
..UsageEventData::default()
},
);
let outcome = timeout(
Duration::from_secs(2),
invoke_direct_terminal_test_entry(entry, &runtime, &store, event.clone()),
)
.await
.expect("failed enrichment should release the terminal turn");
if let Some(outcome) = outcome {
assert_eq!(outcome, super::TerminalPersistenceOutcome::Failed);
}
assert_eq!(store.enrichment_calls.load(Ordering::Acquire), 1);
assert!(store.records.lock().expect("records lock").is_empty());
{
let coalescer = &runtime.lifecycle_coalescer;
let entries = coalescer.shards[coalescer.shard_index(request_id)]
.entries
.lock()
.await;
let marker = entries
.get(request_id)
.expect("delayed marker is preserved");
assert_eq!(marker.generation, generation);
assert!(marker.terminal_seen_at.is_none());
}
let snapshot = runtime.metrics_snapshot();
assert_eq!(snapshot.ordered_lifecycle_pending, 0);
assert_eq!(snapshot.terminal_submission_in_flight, 0);
assert_eq!(snapshot.enqueue_retry_scheduled_total, 0);
let outcome = timeout(
Duration::from_secs(2),
invoke_direct_terminal_test_entry(entry, &runtime, &store, event),
)
.await
.expect("a later terminal attempt should be able to persist");
if let Some(outcome) = outcome {
assert_eq!(
outcome,
super::TerminalPersistenceOutcome::PersistedDirectly
);
}
assert_eq!(store.enrichment_calls.load(Ordering::Acquire), 2);
{
let records = store.records.lock().expect("records lock");
assert_eq!(records.len(), 1);
assert_eq!(records[0].total_cost_usd, Some(0.456));
assert_eq!(records[0].actual_total_cost_usd, Some(0.123));
assert_eq!(records[0].total_tokens, Some(12));
}
{
let coalescer = &runtime.lifecycle_coalescer;
let entries = coalescer.shards[coalescer.shard_index(request_id)]
.entries
.lock()
.await;
let marker = entries.get(request_id).expect("successful terminal marker");
assert!(marker.terminal_seen_at.is_some());
}
assert_eq!(runtime.metrics_snapshot().ordered_lifecycle_pending, 0);
assert_eq!(runtime.metrics_snapshot().terminal_submission_in_flight, 0);
}
#[tokio::test]
async fn direct_terminal_pricing_failure_preserves_lifecycle_and_allows_correct_retry() {
assert_direct_terminal_enrichment_failure_is_not_persisted(
DirectTerminalTestEntry::PublicDirect,
"direct-terminal-pricing-retry",
)
.await;
}
#[tokio::test]
async fn ordered_direct_terminal_pricing_failure_returns_failed_before_correct_retry() {
assert_direct_terminal_enrichment_failure_is_not_persisted(
DirectTerminalTestEntry::OrderedDirect,
"ordered-direct-terminal-pricing-retry",
)
.await;
}
#[tokio::test]
async fn queue_disabled_terminal_pricing_failure_returns_failed_before_correct_retry() {
assert_direct_terminal_enrichment_failure_is_not_persisted(
DirectTerminalTestEntry::QueueDisabled,
"queue-disabled-terminal-pricing-retry",
)
.await;
}
#[tokio::test]
async fn bounded_direct_fallback_pricing_failure_does_not_write_or_report_success() {
let runtime = UsageRuntime::new(UsageRuntimeConfig {
enabled: true,
..UsageRuntimeConfig::default()
})
.expect("usage runtime");
let store = NoRedisUsageStore {
enrichment_failures: AtomicUsize::new(1),
enriched_costs: Some((0.456, 0.123)),
..NoRedisUsageStore::default()
};
let mut event = UsageEvent::new(
UsageEventType::Completed,
"bounded-fallback-pricing-retry",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
total_tokens: Some(12),
..UsageEventData::default()
},
);
assert!(
!runtime
.try_write_terminal_direct_fallback(&store, &mut event, "test_retry")
.await
);
assert!(store.records.lock().expect("records lock").is_empty());
let snapshot = runtime.metrics_snapshot();
assert_eq!(snapshot.terminal_direct_fallback_failed_total, 1);
assert_eq!(snapshot.terminal_direct_fallback_succeeded_total, 0);
assert_eq!(snapshot.terminal_direct_fallback_in_flight, 0);
assert!(
runtime
.try_write_terminal_direct_fallback(&store, &mut event, "test_retry")
.await
);
let records = store.records.lock().expect("records lock");
assert_eq!(records.len(), 1);
assert_eq!(records[0].total_cost_usd, Some(0.456));
assert_eq!(records[0].actual_total_cost_usd, Some(0.123));
drop(records);
assert_eq!(
runtime
.metrics_snapshot()
.terminal_direct_fallback_succeeded_total,
1
);
}
#[tokio::test]
async fn terminal_usage_without_redis_writes_directly_to_usage_repository() {
let runtime = UsageRuntime::new(UsageRuntimeConfig {
enabled: true,
..UsageRuntimeConfig::default()
})
.expect("usage runtime should build");
let store = NoRedisUsageStore::default();
let event = UsageEvent::new(
UsageEventType::Completed,
"req-no-redis-1",
UsageEventData {
user_id: Some("user-no-redis-1".to_string()),
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
input_tokens: Some(4),
output_tokens: Some(8),
total_tokens: Some(12),
status_code: Some(200),
..UsageEventData::default()
},
);
runtime.record_terminal_event(&store, event).await;
let records = store.records.lock().expect("records lock");
assert_eq!(records.len(), 1);
assert_eq!(records[0].request_id, "req-no-redis-1");
assert_eq!(records[0].status, "completed");
assert_eq!(records[0].total_tokens, Some(12));
}
#[tokio::test]
async fn direct_terminal_usage_bypasses_redis_queue_and_writes_repository() {
let runtime = UsageRuntime::new(UsageRuntimeConfig {
enabled: true,
..UsageRuntimeConfig::default()
})
.expect("usage runtime should build");
let store = QueueConfiguredUsageStore {
inner: NoRedisUsageStore::default(),
queue: Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default())),
};
let event = UsageEvent::new(
UsageEventType::Failed,
"req-direct-terminal-1",
UsageEventData {
user_id: Some("user-direct-terminal-1".to_string()),
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(503),
error_message: Some("upstream failed".to_string()),
..UsageEventData::default()
},
);
runtime.record_terminal_event_direct(&store, event).await;
let records = store.inner.records.lock().expect("records lock");
assert_eq!(records.len(), 1);
assert_eq!(records[0].request_id, "req-direct-terminal-1");
assert_eq!(records[0].status, "failed");
assert_eq!(records[0].billing_status, "void");
assert_eq!(records[0].status_code, Some(503));
}
#[tokio::test]
async fn pending_usage_is_persisted_before_later_lifecycle_events() {
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: true,
stream_key: "usage:events:test:pending".to_string(),
consumer_group: "usage_consumers_test_pending".to_string(),
consumer_block_ms: 1,
..UsageRuntimeConfig::default()
};
let queue_runner: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let queue = UsageQueue::new(Arc::clone(&queue_runner), config.clone())
.expect("usage queue should build");
queue
.ensure_consumer_group()
.await
.expect("consumer group should initialize");
let store = CloneQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: queue_runner,
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let plan = ExecutionPlan {
request_id: "req-lifecycle-queue-pending-1".to_string(),
candidate_id: Some("cand-lifecycle-queue-pending-1".to_string()),
provider_name: Some("openai".to_string()),
provider_id: "provider-1".to_string(),
endpoint_id: "endpoint-1".to_string(),
key_id: "key-1".to_string(),
method: "POST".to_string(),
url: "https://example.com/v1/responses".to_string(),
headers: BTreeMap::new(),
content_type: Some("application/json".to_string()),
content_encoding: None,
body: RequestBody::from_json(json!({"model": "gpt-5"})),
stream: false,
client_api_format: "openai:responses".to_string(),
provider_api_format: "openai:responses".to_string(),
model_name: Some("gpt-5".to_string()),
proxy: None,
transport_profile: None,
timeouts: None,
};
runtime.record_pending(&store, build_lifecycle_usage_seed(&plan, None));
timeout(Duration::from_secs(1), async {
while store.records.lock().expect("records lock").is_empty() {
tokio::task::yield_now().await;
}
})
.await
.expect("pending lifecycle usage should be persisted");
{
let records = store.records.lock().expect("records lock");
assert_eq!(records.len(), 1);
assert_eq!(records[0].request_id, "req-lifecycle-queue-pending-1");
assert_eq!(records[0].status, "pending");
}
assert!(
queue
.read_group("usage-test-consumer")
.await
.expect("queue read should succeed")
.is_empty(),
"ordered pending persistence should not leave a duplicate queue event"
);
}
#[tokio::test]
async fn lifecycle_submission_persists_pending_before_first_byte_streaming() {
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: true,
worker_record_concurrency_limit: Some(1),
stream_key: "usage:events:test:ordered-pending-streaming".to_string(),
consumer_group: "usage_consumers_test_ordered_pending_streaming".to_string(),
consumer_block_ms: 1,
..UsageRuntimeConfig::default()
};
let store = CloneQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default())),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let plan = ExecutionPlan {
request_id: "req-ordered-pending-streaming".to_string(),
candidate_id: Some("cand-ordered-pending-streaming".to_string()),
provider_name: Some("openai".to_string()),
provider_id: "provider-1".to_string(),
endpoint_id: "endpoint-1".to_string(),
key_id: "key-1".to_string(),
method: "POST".to_string(),
url: "https://example.com/v1/chat/completions".to_string(),
headers: BTreeMap::new(),
content_type: Some("application/json".to_string()),
content_encoding: None,
body: RequestBody::from_json(json!({"model": "gpt-5"})),
stream: true,
client_api_format: "openai:chat".to_string(),
provider_api_format: "openai:chat".to_string(),
model_name: Some("gpt-5".to_string()),
proxy: None,
transport_profile: None,
timeouts: None,
};
let seed = build_lifecycle_usage_seed(&plan, None);
runtime.record_pending(&store, seed.clone());
runtime.record_stream_started(
&store,
&seed,
200,
Some(&ExecutionTelemetry {
ttfb_ms: Some(12),
elapsed_ms: Some(34),
upstream_bytes: Some(56),
}),
);
timeout(Duration::from_secs(1), async {
while store.records.lock().expect("records lock").len() < 2 {
tokio::task::yield_now().await;
}
})
.await
.expect("pending and streaming lifecycle writes should complete");
let records = store.records.lock().expect("records lock");
assert_eq!(records.len(), 2);
assert_eq!(records[0].status, "pending");
assert_eq!(records[1].status, "streaming");
assert_eq!(records[1].first_byte_time_ms, Some(12));
}
#[tokio::test]
async fn duplicate_first_byte_releases_ordered_barrier_for_terminal() {
const CAPACITY: usize = 8;
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: true,
worker_record_concurrency_limit: Some(1),
enqueue_retry_buffer_capacity: CAPACITY,
..UsageRuntimeConfig::default()
};
let store = CloneQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default())),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let request_id = "req-duplicate-first-byte-terminal";
let seed = build_lifecycle_usage_seed(&terminal_test_plan(request_id), None);
let telemetry = ExecutionTelemetry {
ttfb_ms: Some(12),
elapsed_ms: Some(20),
upstream_bytes: Some(1),
};
runtime.record_stream_started(&store, &seed, 200, Some(&telemetry));
timeout(Duration::from_secs(1), async {
loop {
let snapshot = runtime.metrics_snapshot();
if store.records.lock().expect("records lock").len() == 1
&& snapshot.lifecycle_submission_pending == 0
&& snapshot.first_byte_persistence_pending == 0
&& snapshot.ordered_lifecycle_pending == 0
{
break;
}
tokio::task::yield_now().await;
}
})
.await
.expect("the first first-byte transition should complete");
runtime.record_stream_started(&store, &seed, 200, Some(&telemetry));
timeout(
Duration::from_secs(1),
runtime.record_terminal_event_direct(
&store,
UsageEvent::new(
UsageEventType::Completed,
request_id,
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
..UsageEventData::default()
},
),
),
)
.await
.expect("a duplicate first-byte marker must release the terminal barrier");
{
let records = store.records.lock().expect("records lock");
assert_eq!(
records.len(),
2,
"the duplicate first byte must be coalesced"
);
assert_eq!(records[0].status, "streaming");
assert_eq!(records[1].status, "completed");
}
// The terminal persistence notification can arrive before the submission
// dispatcher accounts for its completed task and releases admission.
timeout(Duration::from_secs(1), async {
loop {
let snapshot = runtime.metrics_snapshot();
if snapshot.lifecycle_submission_pending == 0
&& snapshot.first_byte_persistence_pending == 0
&& snapshot.ordered_lifecycle_pending == 0
&& runtime
.lifecycle_submission
.state
.admission
.available_permits()
== CAPACITY
{
break;
}
sleep(Duration::from_millis(1)).await;
}
})
.await
.expect("duplicate first-byte submission accounting should drain");
let snapshot = runtime.metrics_snapshot();
assert_eq!(snapshot.lifecycle_submission_pending, 0);
assert_eq!(snapshot.first_byte_persistence_pending, 0);
assert_eq!(snapshot.ordered_lifecycle_pending, 0);
assert_eq!(
runtime
.lifecycle_submission
.state
.admission
.available_permits(),
CAPACITY
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn direct_terminal_holds_ordered_slot_until_persistence_finishes() {
let config = UsageRuntimeConfig {
enabled: true,
worker_record_concurrency_limit: Some(2),
terminal_submission_max_in_flight: 2,
enqueue_retry_buffer_capacity: 8,
..UsageRuntimeConfig::default()
};
let store = AdmissionBlockedUsageStore {
release_writes: Arc::new(tokio::sync::Semaphore::new(0)),
writes_in_flight: Arc::new(AtomicUsize::new(0)),
max_writes_in_flight: Arc::new(AtomicUsize::new(0)),
writes_completed: Arc::new(AtomicUsize::new(0)),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let request_id = "req-direct-terminal-ordering";
let direct_runtime = runtime.clone();
let direct_store = store.clone();
let direct_terminal = tokio::spawn(async move {
direct_runtime
.record_terminal_event_direct(
&direct_store,
UsageEvent::new(
UsageEventType::Completed,
request_id,
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
..UsageEventData::default()
},
),
)
.await;
});
timeout(Duration::from_secs(1), async {
while store.writes_in_flight.load(Ordering::Acquire) != 1 {
tokio::task::yield_now().await;
}
})
.await
.expect("the direct terminal write should start");
runtime.record_pending(
&store,
build_lifecycle_usage_seed(&terminal_test_plan(request_id), None),
);
timeout(Duration::from_secs(1), async {
while runtime.metrics_snapshot().ordered_lifecycle_pending != 2 {
tokio::task::yield_now().await;
}
})
.await
.expect("the later pending phase should queue behind the direct terminal");
sleep(Duration::from_millis(25)).await;
assert_eq!(store.writes_in_flight.load(Ordering::Acquire), 1);
assert_eq!(store.max_writes_in_flight.load(Ordering::Acquire), 1);
assert_eq!(store.writes_completed.load(Ordering::Acquire), 0);
store.release_writes.add_permits(1);
timeout(Duration::from_secs(1), direct_terminal)
.await
.expect("the direct terminal caller should finish after persistence")
.expect("the direct terminal task should not panic");
timeout(Duration::from_secs(1), async {
while store.writes_completed.load(Ordering::Acquire) != 1
|| store.writes_in_flight.load(Ordering::Acquire) != 1
{
tokio::task::yield_now().await;
}
})
.await
.expect("the queued pending write should start only after terminal persistence");
store.release_writes.add_permits(1);
timeout(Duration::from_secs(1), async {
loop {
let snapshot = runtime.metrics_snapshot();
if store.writes_completed.load(Ordering::Acquire) == 2
&& snapshot.ordered_lifecycle_pending == 0
&& snapshot.lifecycle_submission_pending == 0
{
break;
}
tokio::task::yield_now().await;
}
})
.await
.expect("the queued pending phase should drain after its write resumes");
assert_eq!(store.max_writes_in_flight.load(Ordering::Acquire), 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn terminal_lifecycle_admission_bounds_resident_work_and_backpressures_excess() {
const CAPACITY: usize = 40;
const TOTAL: usize = CAPACITY + 8;
let config = UsageRuntimeConfig {
enabled: true,
worker_record_concurrency_limit: Some(CAPACITY),
terminal_submission_max_in_flight: CAPACITY as u64,
enqueue_retry_buffer_capacity: CAPACITY,
..UsageRuntimeConfig::default()
};
let store = AdmissionBlockedUsageStore {
release_writes: Arc::new(tokio::sync::Semaphore::new(0)),
writes_in_flight: Arc::new(AtomicUsize::new(0)),
max_writes_in_flight: Arc::new(AtomicUsize::new(0)),
writes_completed: Arc::new(AtomicUsize::new(0)),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let mut submissions = Vec::with_capacity(TOTAL);
for index in 0..TOTAL {
let runtime = runtime.clone();
let store = store.clone();
submissions.push(tokio::spawn(async move {
runtime
.submit_terminal_event(
&store,
UsageEvent::new(
UsageEventType::Completed,
format!("req-terminal-admission-{index}"),
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
..UsageEventData::default()
},
),
)
.await;
}));
}
timeout(Duration::from_secs(2), async {
loop {
let finished = submissions
.iter()
.filter(|submission| submission.is_finished())
.count();
if finished == CAPACITY
&& store.writes_in_flight.load(Ordering::Acquire) == CAPACITY
&& runtime
.lifecycle_submission
.state
.admission
.available_permits()
== 0
{
break;
}
tokio::task::yield_now().await;
}
})
.await
.expect("the hard lifecycle bound should backpressure excess terminal calls");
let saturated = runtime.metrics_snapshot();
assert_eq!(
submissions
.iter()
.filter(|submission| submission.is_finished())
.count(),
CAPACITY,
"only admitted callers may return while persistence is blocked"
);
assert!(saturated.lifecycle_submission_pending <= CAPACITY);
assert!(saturated.ordered_lifecycle_pending <= CAPACITY);
assert!(saturated.terminal_submission_pending <= CAPACITY);
assert!(saturated.terminal_submission_in_flight <= CAPACITY);
assert_eq!(saturated.ordered_lifecycle_pending, CAPACITY);
assert_eq!(saturated.terminal_submission_pending, CAPACITY);
assert_eq!(
store.max_writes_in_flight.load(Ordering::Acquire),
CAPACITY,
"configured workers should still execute independently up to the hard bound"
);
store.release_writes.add_permits(TOTAL);
timeout(Duration::from_secs(2), async {
for submission in submissions {
submission
.await
.expect("terminal admission task should not panic");
}
})
.await
.expect("all backpressured terminal callers should recover after persistence resumes");
timeout(Duration::from_secs(2), async {
loop {
let snapshot = runtime.metrics_snapshot();
if store.writes_completed.load(Ordering::Acquire) == TOTAL
&& snapshot.lifecycle_submission_pending == 0
&& snapshot.ordered_lifecycle_pending == 0
&& snapshot.terminal_submission_pending == 0
{
break;
}
tokio::task::yield_now().await;
}
})
.await
.expect("all admitted lifecycle work should drain after recovery");
let recovered = runtime.metrics_snapshot();
assert!(recovered.lifecycle_submission_max_pending <= CAPACITY);
assert!(recovered.ordered_lifecycle_max_pending <= CAPACITY);
assert!(recovered.terminal_submission_max_pending <= CAPACITY);
assert_eq!(recovered.terminal_submission_in_flight, 0);
assert_eq!(
runtime
.lifecycle_submission
.state
.admission
.available_permits(),
CAPACITY
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn non_batch_pending_single_writes_are_concurrent_and_globally_bounded() {
const ITEMS_PER_BATCH: usize =
super::PENDING_PERSISTENCE_SINGLE_WRITE_CONCURRENCY_PER_BATCH * 2;
const TOTAL_ITEMS: usize =
ITEMS_PER_BATCH * super::PENDING_PERSISTENCE_MAX_BATCH_CONCURRENCY;
assert_eq!(
super::PENDING_PERSISTENCE_SINGLE_WRITE_CONCURRENCY_PER_BATCH
* super::PENDING_PERSISTENCE_MAX_BATCH_CONCURRENCY,
super::PENDING_PERSISTENCE_SINGLE_WRITE_TARGET_CONCURRENCY
);
let store = BlockingNonBatchPendingStore {
release_writes: Arc::new(tokio::sync::Semaphore::new(0)),
writes_in_flight: Arc::new(AtomicUsize::new(0)),
max_writes_in_flight: Arc::new(AtomicUsize::new(0)),
writes_completed: Arc::new(AtomicUsize::new(0)),
};
assert!(!UsageRecordWriter::supports_pending_usage_batch(&store));
let record_gate = Arc::new(super::UsageWorkerRecordConcurrencyGate::new(
super::PENDING_PERSISTENCE_SINGLE_WRITE_TARGET_CONCURRENCY * 2,
));
let completed = Arc::new(AtomicUsize::new(0));
let degraded = Arc::new(AtomicUsize::new(0));
let state = Arc::new(super::PendingPersistenceState::new(TOTAL_ITEMS));
let mut tasks = tokio::task::JoinSet::new();
for batch_index in 0..super::PENDING_PERSISTENCE_MAX_BATCH_CONCURRENCY {
let mut batch = Vec::with_capacity(ITEMS_PER_BATCH);
for item_index in 0..ITEMS_PER_BATCH {
let request_id = format!("req-blocking-non-batch-{batch_index}-{item_index}");
let record = super::build_upsert_usage_record_from_event(&UsageEvent::new(
UsageEventType::Pending,
request_id.clone(),
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
..UsageEventData::default()
},
))
.expect("pending record should build");
state.record_dispatched();
batch.push(super::PendingPersistenceEnvelope {
item: Box::new(BlockingNonBatchPendingItem {
request_id,
record,
store: store.clone(),
record_gate: Arc::clone(&record_gate),
completed: Arc::clone(&completed),
degraded: Arc::clone(&degraded),
}),
_pending_guard: super::PendingPersistencePendingGuard {
state: Arc::clone(&state),
},
});
}
let task_state = Arc::clone(&state);
tasks.spawn(async move {
super::process_pending_persistence_batch(batch, task_state).await;
});
}
timeout(Duration::from_secs(2), async {
while store.max_writes_in_flight.load(Ordering::Acquire)
< super::PENDING_PERSISTENCE_SINGLE_WRITE_TARGET_CONCURRENCY
{
tokio::task::yield_now().await;
}
})
.await
.expect("non-batch pending writes should fill the bounded concurrency window");
sleep(Duration::from_millis(25)).await;
let max_in_flight = store.max_writes_in_flight.load(Ordering::Acquire);
assert!(max_in_flight > super::PENDING_PERSISTENCE_MAX_BATCH_CONCURRENCY);
assert_eq!(
max_in_flight,
super::PENDING_PERSISTENCE_SINGLE_WRITE_TARGET_CONCURRENCY
);
assert_eq!(
store.writes_in_flight.load(Ordering::Acquire),
super::PENDING_PERSISTENCE_SINGLE_WRITE_TARGET_CONCURRENCY
);
assert_eq!(
record_gate.max_in_flight(),
super::PENDING_PERSISTENCE_SINGLE_WRITE_TARGET_CONCURRENCY
);
store.release_writes.add_permits(TOTAL_ITEMS);
timeout(Duration::from_secs(2), async {
while let Some(result) = tasks.join_next().await {
result.expect("pending batch task should complete");
}
})
.await
.expect("bounded non-batch pending writes should drain after release");
assert_eq!(store.writes_completed.load(Ordering::Acquire), TOTAL_ITEMS);
assert_eq!(completed.load(Ordering::Acquire), TOTAL_ITEMS);
assert_eq!(degraded.load(Ordering::Acquire), 0);
assert_eq!(state.pending.load(Ordering::Acquire), 0);
assert_eq!(store.writes_in_flight.load(Ordering::Acquire), 0);
assert_eq!(record_gate.in_flight(), 0);
}
#[tokio::test]
async fn saturated_pending_persistence_drops_overflow_and_stays_bounded() {
const RESIDENT_BOUND: usize = 1_024
+ super::PENDING_PERSISTENCE_BATCH_SIZE
* super::PENDING_PERSISTENCE_MAX_BATCH_CONCURRENCY
+ 1;
const TOTAL_ITEMS: usize = RESIDENT_BOUND + 128;
let config = UsageRuntimeConfig {
enabled: true,
enqueue_retry_buffer_capacity: 1_024,
..UsageRuntimeConfig::default()
};
let tasks = super::UsageBackgroundTasks::default();
let dispatcher = super::PendingPersistenceDispatcher::spawn(&config, &tasks);
let store = BlockingNonBatchPendingStore {
release_writes: Arc::new(tokio::sync::Semaphore::new(0)),
writes_in_flight: Arc::new(AtomicUsize::new(0)),
max_writes_in_flight: Arc::new(AtomicUsize::new(0)),
writes_completed: Arc::new(AtomicUsize::new(0)),
};
let record_gate = Arc::new(super::UsageWorkerRecordConcurrencyGate::new(
super::PENDING_PERSISTENCE_SINGLE_WRITE_TARGET_CONCURRENCY * 2,
));
let completed = Arc::new(AtomicUsize::new(0));
let degraded = Arc::new(AtomicUsize::new(0));
let dispatch_dispatcher = Arc::clone(&dispatcher);
let dispatch_store = store.clone();
let dispatch_gate = Arc::clone(&record_gate);
let dispatch_completed = Arc::clone(&completed);
let dispatch_degraded = Arc::clone(&degraded);
let dispatch = tokio::spawn(async move {
for index in 0..TOTAL_ITEMS {
let request_id = format!("req-bounded-pending-{index}");
let record = super::build_upsert_usage_record_from_event(&UsageEvent::new(
UsageEventType::Pending,
request_id.clone(),
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
..UsageEventData::default()
},
))
.expect("pending record should build");
dispatch_dispatcher.dispatch(Box::new(BlockingNonBatchPendingItem {
request_id,
record,
store: dispatch_store.clone(),
record_gate: Arc::clone(&dispatch_gate),
completed: Arc::clone(&dispatch_completed),
degraded: Arc::clone(&dispatch_degraded),
}));
}
});
timeout(Duration::from_secs(5), dispatch)
.await
.expect("overflow dispatch must remain non-blocking")
.expect("overflow dispatch task should not panic");
let overflow = dispatcher.state.overflow_total.load(Ordering::Acquire);
assert!(overflow > 0, "the test must overflow the bounded channel");
assert_eq!(degraded.load(Ordering::Acquire) as u64, overflow);
assert!(
dispatcher.state.max_pending.load(Ordering::Acquire) <= RESIDENT_BOUND,
"resident pending records exceeded the fixed channel and worker bound"
);
store.release_writes.add_permits(TOTAL_ITEMS);
timeout(Duration::from_secs(5), async {
while dispatcher.state.pending.load(Ordering::Acquire) != 0 {
tokio::task::yield_now().await;
}
})
.await
.expect("all accepted pending records should drain after recovery");
let writes_completed = store.writes_completed.load(Ordering::Acquire);
assert_eq!(completed.load(Ordering::Acquire), writes_completed);
assert_eq!(writes_completed as u64 + overflow, TOTAL_ITEMS as u64);
assert!(
store.max_writes_in_flight.load(Ordering::Acquire)
<= super::PENDING_PERSISTENCE_SINGLE_WRITE_TARGET_CONCURRENCY,
"pending persistence exceeded its fixed worker window"
);
}
#[tokio::test]
async fn ordered_lifecycle_batches_beyond_submission_worker_count() {
const REQUESTS: usize = 96;
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: true,
worker_record_concurrency_limit: Some(1),
enqueue_retry_workers: 1,
enqueue_retry_initial_backoff_ms: 1,
enqueue_retry_max_backoff_ms: 2,
..UsageRuntimeConfig::default()
};
let store = OrderedBatchUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
pending_batch_sizes: Arc::new(Mutex::new(Vec::new())),
pending_attempts: Arc::new(Mutex::new(BTreeMap::new())),
permanent_pending_failure_request_id: None,
remaining_pending_failures: Arc::new(AtomicUsize::new(0)),
remaining_pending_panics: Arc::new(AtomicUsize::new(0)),
remaining_first_byte_panics: Arc::new(AtomicUsize::new(0)),
queue: Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default())),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let mut request_ids = Vec::with_capacity(REQUESTS);
for index in 0..REQUESTS {
let request_id = format!("req-ordered-batch-{index}");
let seed = build_lifecycle_usage_seed(&terminal_test_plan(&request_id), None);
runtime.record_pending(&store, seed.clone());
runtime.record_stream_started(
&store,
&seed,
200,
Some(&ExecutionTelemetry {
ttfb_ms: Some(10),
elapsed_ms: Some(10),
upstream_bytes: Some(1),
}),
);
request_ids.push(request_id);
}
timeout(Duration::from_secs(5), async {
loop {
let snapshot = runtime.metrics_snapshot();
if store.records.lock().expect("records lock").len() == REQUESTS * 2
&& snapshot.lifecycle_submission_pending == 0
&& snapshot.ordered_lifecycle_pending == 0
&& snapshot.pending_persistence_pending == 0
&& snapshot.first_byte_persistence_pending == 0
{
break;
}
tokio::task::yield_now().await;
}
})
.await
.expect("ordered pending and first-byte batches should drain");
let snapshot = runtime.metrics_snapshot();
assert!(
snapshot.pending_persistence_max_batch_size > 7,
"pending max batch was {}",
snapshot.pending_persistence_max_batch_size
);
assert!(
snapshot.first_byte_persistence_max_batch_size > 7,
"first-byte max batch was {}",
snapshot.first_byte_persistence_max_batch_size
);
assert_eq!(
snapshot.pending_persistence_batch_records_total,
REQUESTS as u64
);
assert_eq!(
snapshot.first_byte_persistence_batch_records_total,
REQUESTS as u64
);
let records = store.records.lock().expect("records lock");
for request_id in request_ids {
let statuses = records
.iter()
.filter(|record| record.request_id == request_id)
.map(|record| record.status.as_str())
.collect::<Vec<_>>();
assert_eq!(statuses, vec!["pending", "streaming"]);
}
}
#[tokio::test]
async fn ordered_lifecycle_retries_pending_and_degrades_first_byte_before_terminal_barrier() {
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: true,
worker_record_concurrency_limit: Some(1),
enqueue_retry_initial_backoff_ms: 1,
enqueue_retry_max_backoff_ms: 2,
..UsageRuntimeConfig::default()
};
let store = OrderedBatchUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
pending_batch_sizes: Arc::new(Mutex::new(Vec::new())),
pending_attempts: Arc::new(Mutex::new(BTreeMap::new())),
permanent_pending_failure_request_id: None,
remaining_pending_failures: Arc::new(AtomicUsize::new(1)),
remaining_pending_panics: Arc::new(AtomicUsize::new(1)),
remaining_first_byte_panics: Arc::new(AtomicUsize::new(1)),
queue: Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default())),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let request_id = "req-ordered-retry-barrier";
let seed = build_lifecycle_usage_seed(&terminal_test_plan(request_id), None);
runtime.record_pending(&store, seed.clone());
runtime.record_stream_started(
&store,
&seed,
200,
Some(&ExecutionTelemetry {
ttfb_ms: Some(9),
elapsed_ms: Some(9),
upstream_bytes: Some(1),
}),
);
runtime
.record_terminal_event_direct(
&store,
UsageEvent::new(
UsageEventType::Completed,
request_id,
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
..UsageEventData::default()
},
),
)
.await;
let records = store.records.lock().expect("records lock");
let statuses = records
.iter()
.filter(|record| record.request_id == request_id)
.map(|record| record.status.as_str())
.collect::<Vec<_>>();
assert_eq!(statuses, vec!["pending", "completed"]);
drop(records);
let snapshot = runtime.metrics_snapshot();
assert!(snapshot.pending_persistence_batch_failed_total >= 2);
assert!(snapshot.pending_persistence_retried_total >= 2);
assert_eq!(snapshot.first_byte_persistence_batch_failed_total, 1);
assert_eq!(snapshot.first_byte_persistence_direct_failed_total, 1);
assert_eq!(snapshot.first_byte_persistence_fallback_failed_total, 1);
assert_eq!(snapshot.first_byte_persistence_pending, 0);
assert_eq!(snapshot.ordered_lifecycle_pending, 0);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn permanent_pending_failure_isolated_without_blocking_healthy_or_terminal_writes() {
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: true,
worker_record_concurrency_limit: Some(4),
enqueue_retry_initial_backoff_ms: 1,
enqueue_retry_max_backoff_ms: 1,
..UsageRuntimeConfig::default()
};
let poison_request_id = "req-permanent-pending-failure";
let healthy_request_id = "req-after-permanent-pending-failure";
let pending_attempts = Arc::new(Mutex::new(BTreeMap::new()));
let pending_batch_sizes = Arc::new(Mutex::new(Vec::new()));
let store = OrderedBatchUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
pending_batch_sizes: Arc::clone(&pending_batch_sizes),
pending_attempts: Arc::clone(&pending_attempts),
permanent_pending_failure_request_id: Some(Arc::from(poison_request_id)),
remaining_pending_failures: Arc::new(AtomicUsize::new(0)),
remaining_pending_panics: Arc::new(AtomicUsize::new(0)),
remaining_first_byte_panics: Arc::new(AtomicUsize::new(0)),
queue: Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default())),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
runtime.record_pending(
&store,
build_lifecycle_usage_seed(&terminal_test_plan(poison_request_id), None),
);
runtime.record_pending(
&store,
build_lifecycle_usage_seed(&terminal_test_plan(healthy_request_id), None),
);
let poison_runtime = runtime.clone();
let poison_store = store.clone();
let poison_terminal = tokio::spawn(async move {
poison_runtime
.record_terminal_event_direct(
&poison_store,
UsageEvent::new(
UsageEventType::Completed,
poison_request_id,
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
..UsageEventData::default()
},
),
)
.await;
});
let healthy_runtime = runtime.clone();
let healthy_store = store.clone();
let healthy_terminal = tokio::spawn(async move {
healthy_runtime
.record_terminal_event_direct(
&healthy_store,
UsageEvent::new(
UsageEventType::Completed,
healthy_request_id,
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
..UsageEventData::default()
},
),
)
.await;
});
timeout(Duration::from_secs(2), async {
poison_terminal
.await
.expect("poison terminal task should not panic");
healthy_terminal
.await
.expect("healthy terminal task should not panic");
})
.await
.expect("bounded pending retries must release both terminal barriers");
timeout(Duration::from_secs(1), async {
loop {
let snapshot = runtime.metrics_snapshot();
if snapshot.lifecycle_submission_pending == 0
&& snapshot.pending_persistence_pending == 0
&& snapshot.ordered_lifecycle_pending == 0
{
break;
}
tokio::task::yield_now().await;
}
})
.await
.expect("pending persistence and ordered lifecycle gauges should drain");
let records = store.records.lock().expect("records lock");
let poison_statuses = records
.iter()
.filter(|record| record.request_id == poison_request_id)
.map(|record| record.status.as_str())
.collect::<Vec<_>>();
let healthy_statuses = records
.iter()
.filter(|record| record.request_id == healthy_request_id)
.map(|record| record.status.as_str())
.collect::<Vec<_>>();
assert_eq!(poison_statuses, vec!["completed"]);
assert_eq!(healthy_statuses, vec!["pending", "completed"]);
drop(records);
let attempts = pending_attempts.lock().expect("pending attempts lock");
assert_eq!(
attempts.get(poison_request_id).copied(),
Some(
(super::PENDING_PERSISTENCE_BATCH_RETRIES_BEFORE_ISOLATION
+ super::PENDING_PERSISTENCE_SINGLE_RETRIES_BEFORE_DEGRADE)
as usize
)
);
assert!(
(1..=3).contains(
&attempts
.get(healthy_request_id)
.copied()
.expect("healthy pending record should be attempted")
),
"a healthy record may share the failed batch but must need only one isolated write"
);
drop(attempts);
assert_eq!(
pending_batch_sizes
.lock()
.expect("pending batch sizes lock")
.len(),
6,
"batch isolation plus bounded single retries must have fixed write cost"
);
let snapshot = runtime.metrics_snapshot();
assert_eq!(snapshot.pending_persistence_batch_failed_total, 5);
assert_eq!(snapshot.pending_persistence_overflow_total, 1);
assert!((5..=7).contains(&snapshot.pending_persistence_retried_total));
assert_eq!(snapshot.pending_persistence_pending, 0);
assert_eq!(snapshot.ordered_lifecycle_pending, 0);
}
#[tokio::test]
async fn permanent_ordered_first_byte_failure_degrades_and_allows_terminal_progress() {
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: true,
worker_record_concurrency_limit: Some(1),
enqueue_retry_initial_backoff_ms: 1,
enqueue_retry_max_backoff_ms: 1,
..UsageRuntimeConfig::default()
};
let request_id = "req-permanent-ordered-first-byte-failure";
let remaining_first_byte_panics = Arc::new(AtomicUsize::new(usize::MAX));
let store = OrderedBatchUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
pending_batch_sizes: Arc::new(Mutex::new(Vec::new())),
pending_attempts: Arc::new(Mutex::new(BTreeMap::new())),
permanent_pending_failure_request_id: None,
remaining_pending_failures: Arc::new(AtomicUsize::new(0)),
remaining_pending_panics: Arc::new(AtomicUsize::new(0)),
remaining_first_byte_panics: Arc::clone(&remaining_first_byte_panics),
queue: Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default())),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let seed = build_lifecycle_usage_seed(&terminal_test_plan(request_id), None);
runtime.record_pending(&store, seed.clone());
runtime.record_stream_started(
&store,
&seed,
200,
Some(&ExecutionTelemetry {
ttfb_ms: Some(9),
elapsed_ms: Some(12),
upstream_bytes: Some(1),
}),
);
timeout(
Duration::from_secs(2),
runtime.record_terminal_event_direct(
&store,
UsageEvent::new(
UsageEventType::Completed,
request_id,
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
..UsageEventData::default()
},
),
),
)
.await
.expect("terminal should pass the degraded first-byte barrier");
timeout(Duration::from_secs(1), async {
loop {
let snapshot = runtime.metrics_snapshot();
if snapshot.first_byte_persistence_pending == 0
&& snapshot.ordered_lifecycle_pending == 0
{
break;
}
tokio::task::yield_now().await;
}
})
.await
.expect("first-byte and ordered lifecycle gauges should drain");
let records = store.records.lock().expect("records lock");
let statuses = records
.iter()
.filter(|record| record.request_id == request_id)
.map(|record| record.status.as_str())
.collect::<Vec<_>>();
assert_eq!(statuses, vec!["pending", "completed"]);
drop(records);
assert_eq!(
remaining_first_byte_panics.load(Ordering::Acquire),
usize::MAX - 1,
"an ordered first-byte failure must not spawn an unbounded fallback task"
);
let snapshot = runtime.metrics_snapshot();
assert_eq!(snapshot.first_byte_persistence_batch_failed_total, 1);
assert_eq!(snapshot.first_byte_persistence_direct_failed_total, 1);
assert_eq!(snapshot.first_byte_persistence_fallback_failed_total, 1);
assert_eq!(snapshot.first_byte_persistence_pending, 0);
assert_eq!(snapshot.ordered_lifecycle_pending, 0);
}
#[tokio::test]
async fn terminal_worker_catches_panic_and_processes_the_next_item() {
let (sender, receiver) = mpsc::unbounded_channel::<Box<dyn super::TerminalExecutionItem>>();
let worker = tokio::spawn(super::run_terminal_execution_worker(receiver));
assert!(
sender
.send(Box::new(PanickingTerminalExecutionItem {
request_id: "req-terminal-worker-direct-panic".to_string(),
}))
.is_ok(),
"the panic item should enter the worker FIFO"
);
let (completion, completed) = tokio::sync::oneshot::channel();
assert!(
sender
.send(Box::new(CompletingTerminalExecutionItem {
request_id: "req-terminal-worker-direct-next".to_string(),
completion,
}))
.is_ok(),
"the healthy item should enter the same worker FIFO"
);
timeout(Duration::from_secs(1), completed)
.await
.expect("the worker should continue after the preceding panic")
.expect("the healthy item should report completion");
drop(sender);
timeout(Duration::from_secs(1), worker)
.await
.expect("the terminal worker should stop after its sender closes")
.expect("the terminal worker should exit normally");
}
#[tokio::test]
async fn failed_terminal_persistence_releases_admission_and_allows_later_attempts() {
let config = UsageRuntimeConfig {
enabled: true,
worker_record_concurrency_limit: Some(1),
enqueue_retry_workers: 1,
..UsageRuntimeConfig::default()
};
let upsert_attempts = Arc::new(AtomicUsize::new(0));
let store = FailingWriteQueueConfiguredUsageStore {
queue: Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default())),
upsert_attempts: Arc::clone(&upsert_attempts),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let request_id = "req-terminal-persistence-fail-closed";
runtime
.submit_terminal_event(
&store,
UsageEvent::new(
UsageEventType::Completed,
request_id,
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
..UsageEventData::default()
},
),
)
.await;
timeout(Duration::from_secs(1), async {
while upsert_attempts.load(Ordering::Acquire) != 1
|| runtime.metrics_snapshot().terminal_submission_pending != 0
|| runtime.metrics_snapshot().ordered_lifecycle_pending != 0
{
tokio::task::yield_now().await;
}
})
.await
.expect("the failed terminal persistence attempt should release admission");
timeout(
Duration::from_secs(1),
runtime.record_terminal_event_direct(
&store,
UsageEvent::new(
UsageEventType::Completed,
request_id,
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
..UsageEventData::default()
},
),
),
)
.await
.expect("a later terminal attempt must not remain behind a failed write");
assert_eq!(
upsert_attempts.load(Ordering::Acquire),
2,
"the later direct caller should receive its own bounded write attempt"
);
// Direct persistence can finish before the submission worker joins the
// barrier handoff and accounts for its completed slot.
timeout(Duration::from_secs(1), async {
loop {
let snapshot = runtime.metrics_snapshot();
let submission = &runtime.lifecycle_submission.state;
if snapshot.terminal_submission_pending == 0
&& snapshot.ordered_lifecycle_pending == 0
&& snapshot.lifecycle_submission_pending == 0
&& submission.admission.available_permits() == submission.capacity
{
break;
}
sleep(Duration::from_millis(1)).await;
}
})
.await
.expect("failed terminal submission accounting and admission should drain");
let snapshot = runtime.metrics_snapshot();
assert_eq!(snapshot.terminal_submission_pending, 0);
assert_eq!(snapshot.ordered_lifecycle_pending, 0);
assert_eq!(snapshot.lifecycle_submission_pending, 0);
}
#[tokio::test]
async fn terminal_worker_panic_releases_admission_without_stopping_the_shard() {
let config = UsageRuntimeConfig {
enabled: true,
worker_record_concurrency_limit: Some(1),
enqueue_retry_workers: 1,
..UsageRuntimeConfig::default()
};
let records = Arc::new(Mutex::new(Vec::new()));
let policy_reads = Arc::new(AtomicUsize::new(0));
let remaining_policy_panics = Arc::new(AtomicUsize::new(1));
let store = PanicOncePolicyQueueConfiguredUsageStore {
inner: CloneQueueConfiguredUsageStore {
records: Arc::clone(&records),
queue: Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default())),
},
remaining_policy_panics: Arc::clone(&remaining_policy_panics),
policy_reads: Arc::clone(&policy_reads),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let failed_request_id = "req-terminal-worker-panic-fail-closed";
runtime
.submit_terminal_event(
&store,
UsageEvent::new(
UsageEventType::Completed,
failed_request_id,
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
..UsageEventData::default()
},
),
)
.await;
timeout(Duration::from_secs(1), async {
while policy_reads.load(Ordering::Acquire) == 0
|| runtime.metrics_snapshot().terminal_submission_pending != 0
|| runtime.metrics_snapshot().ordered_lifecycle_pending != 0
{
tokio::task::yield_now().await;
}
})
.await
.expect("the first terminal item should panic and release admission");
timeout(
Duration::from_secs(1),
runtime.record_terminal_event_direct(
&store,
UsageEvent::new(
UsageEventType::Completed,
failed_request_id,
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
..UsageEventData::default()
},
),
),
)
.await
.expect("a later attempt for the panicked request should make progress");
let healthy_request_id = "req-terminal-worker-panic-healthy";
runtime
.submit_terminal_event(
&store,
UsageEvent::new(
UsageEventType::Completed,
healthy_request_id,
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
..UsageEventData::default()
},
),
)
.await;
runtime
.record_terminal_event_direct(
&store,
UsageEvent::new(
UsageEventType::Completed,
healthy_request_id,
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
..UsageEventData::default()
},
),
)
.await;
assert_eq!(remaining_policy_panics.load(Ordering::Acquire), 0);
{
let records = records.lock().expect("records lock");
assert_eq!(
records
.iter()
.filter(|record| record.request_id == healthy_request_id)
.count(),
2,
"the same terminal shard should continue processing healthy requests"
);
assert!(
records
.iter()
.filter(|record| record.request_id == failed_request_id)
.count()
== 1,
"only the later healthy attempt should persist for the panicked request"
);
}
// The final direct attempt also submits a barrier whose worker may
// account for completion after the persistence call has returned.
timeout(Duration::from_secs(1), async {
loop {
let snapshot = runtime.metrics_snapshot();
let submission = &runtime.lifecycle_submission.state;
if snapshot.terminal_submission_pending == 0
&& snapshot.ordered_lifecycle_pending == 0
&& snapshot.lifecycle_submission_pending == 0
&& submission.admission.available_permits() == submission.capacity
{
break;
}
sleep(Duration::from_millis(1)).await;
}
})
.await
.expect("panicked terminal submission accounting and admission should drain");
let snapshot = runtime.metrics_snapshot();
assert_eq!(snapshot.terminal_submission_pending, 0);
assert_eq!(snapshot.ordered_lifecycle_pending, 0);
assert_eq!(snapshot.lifecycle_submission_pending, 0);
}
#[tokio::test]
async fn stale_lifecycle_generation_keeps_the_current_generation_registered() {
let coalescer = super::LifecycleEventCoalescer::default();
let request_id = "req-lifecycle-generation";
let pending_generation = coalescer
.register(request_id.to_string())
.await
.expect("pending generation should register");
let streaming_generation = coalescer
.register(request_id.to_string())
.await
.expect("streaming generation should register");
assert!(!coalescer.should_emit(request_id, pending_generation).await);
assert!(
coalescer
.should_emit(request_id, streaming_generation)
.await
);
}
#[tokio::test]
async fn lifecycle_submission_panic_releases_admission_and_allows_later_slots() {
let config = UsageRuntimeConfig {
enabled: true,
enqueue_retry_buffer_capacity: 8,
enqueue_retry_workers: 1,
worker_record_concurrency_limit: Some(1),
..UsageRuntimeConfig::default()
};
let tasks = super::UsageBackgroundTasks::default();
let dispatcher = LifecycleSubmissionDispatcher::spawn(&config, &tasks);
let request_id = "req-lifecycle-submission-panic";
dispatcher.dispatch(Box::new(PanickingLifecycleSubmissionItem {
request_id: request_id.to_string(),
}));
timeout(Duration::from_secs(1), async {
while dispatcher.state.pending.load(Ordering::Acquire) != 0 {
tokio::task::yield_now().await;
}
})
.await
.expect("the panicked lifecycle slot should release its pending gauge");
let seen = Arc::new(Mutex::new(Vec::new()));
dispatcher.dispatch(Box::new(TestLifecycleSubmissionItem {
request_id: request_id.to_string(),
priority: LifecycleSubmissionPriority::Terminal,
started: None,
release: None,
seen: Arc::clone(&seen),
}));
timeout(Duration::from_secs(1), async {
while dispatcher.state.processed_total.load(Ordering::Acquire) < 2
|| dispatcher.state.pending.load(Ordering::Acquire) != 0
{
tokio::task::yield_now().await;
}
})
.await
.expect("a later slot should run after the panicked slot releases admission");
assert_eq!(
seen.lock().expect("lifecycle test seen lock").as_slice(),
[LifecycleSubmissionPriority::Terminal]
);
assert_eq!(dispatcher.state.enqueued_total.load(Ordering::Acquire), 2);
assert_eq!(dispatcher.state.processed_total.load(Ordering::Acquire), 2);
}
#[tokio::test]
async fn lifecycle_submission_preserves_pending_before_coalesced_first_byte() {
let config = UsageRuntimeConfig {
enabled: true,
enqueue_retry_buffer_capacity: 8,
enqueue_retry_workers: 1,
worker_record_concurrency_limit: Some(1),
..UsageRuntimeConfig::default()
};
let tasks = super::UsageBackgroundTasks::default();
let dispatcher = LifecycleSubmissionDispatcher::spawn(&config, &tasks);
let started = Arc::new(tokio::sync::Notify::new());
let release = Arc::new(tokio::sync::Notify::new());
let seen = Arc::new(Mutex::new(Vec::new()));
dispatcher.dispatch(Box::new(TestLifecycleSubmissionItem {
request_id: "blocker".to_string(),
priority: LifecycleSubmissionPriority::Pending,
started: Some(Arc::clone(&started)),
release: Some(Arc::clone(&release)),
seen: Arc::clone(&seen),
}));
timeout(Duration::from_secs(1), started.notified())
.await
.expect("lifecycle worker should start the blocker");
dispatcher.dispatch(Box::new(TestLifecycleSubmissionItem {
request_id: "target".to_string(),
priority: LifecycleSubmissionPriority::Pending,
started: None,
release: None,
seen: Arc::clone(&seen),
}));
dispatcher.dispatch(Box::new(TestLifecycleSubmissionItem {
request_id: "target".to_string(),
priority: LifecycleSubmissionPriority::Streaming,
started: None,
release: None,
seen: Arc::clone(&seen),
}));
dispatcher.dispatch(Box::new(TestLifecycleSubmissionItem {
request_id: "target".to_string(),
priority: LifecycleSubmissionPriority::FirstByte,
started: None,
release: None,
seen: Arc::clone(&seen),
}));
assert_eq!(dispatcher.state.pending.load(Ordering::Acquire), 2);
release.notify_one();
timeout(Duration::from_secs(1), async {
loop {
if seen.lock().expect("lifecycle test seen lock").len() == 3 {
break;
}
sleep(Duration::from_millis(1)).await;
}
})
.await
.expect("coalesced lifecycle items should drain");
let seen = seen.lock().expect("lifecycle test seen lock").clone();
assert_eq!(
seen,
vec![
LifecycleSubmissionPriority::Pending,
LifecycleSubmissionPriority::Pending,
LifecycleSubmissionPriority::FirstByte
]
);
assert_eq!(dispatcher.state.coalesced_total.load(Ordering::Acquire), 1);
assert_eq!(dispatcher.state.overflow_total.load(Ordering::Acquire), 0);
}
#[tokio::test]
async fn streaming_lifecycle_event_survives_a_superseded_pending_event() {
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: true,
lifecycle_enqueue_delay_ms: 40,
stream_key: "usage:events:test:pending-streaming-coalescing".to_string(),
consumer_group: "usage_consumers_test_pending_streaming_coalescing".to_string(),
consumer_block_ms: 1,
..UsageRuntimeConfig::default()
};
let queue_runner: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let queue = UsageQueue::new(Arc::clone(&queue_runner), config.clone())
.expect("usage queue should build");
queue
.ensure_consumer_group()
.await
.expect("consumer group should initialize");
let store = CloneQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: queue_runner,
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
runtime
.enqueue_or_write_lifecycle(
&store,
UsageEvent::new(
UsageEventType::Pending,
"req-pending-streaming-coalescing",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5.6-sol".to_string(),
..UsageEventData::default()
},
),
)
.await;
sleep(Duration::from_millis(10)).await;
runtime
.enqueue_or_write_lifecycle(
&store,
UsageEvent::new(
UsageEventType::Streaming,
"req-pending-streaming-coalescing",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5.6-sol".to_string(),
..UsageEventData::default()
},
),
)
.await;
sleep(Duration::from_millis(90)).await;
let entries = queue
.read_group("usage-test-consumer-pending-streaming-coalescing")
.await
.expect("queue read should succeed");
assert_eq!(entries.len(), 1);
let event = UsageEvent::from_stream_fields(&entries[0].fields)
.expect("queued usage event should parse");
assert_eq!(event.event_type, UsageEventType::Streaming);
}
#[tokio::test]
async fn pending_lifecycle_enqueue_can_be_delayed() {
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: true,
lifecycle_enqueue_delay_ms: 30,
stream_key: "usage:events:test:pending-delay".to_string(),
consumer_group: "usage_consumers_test_pending_delay".to_string(),
consumer_block_ms: 1,
..UsageRuntimeConfig::default()
};
let queue_runner: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let queue = UsageQueue::new(Arc::clone(&queue_runner), config.clone())
.expect("usage queue should build");
queue
.ensure_consumer_group()
.await
.expect("consumer group should initialize");
let store = CloneQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: queue_runner,
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let event = UsageEvent::new(
UsageEventType::Pending,
"req-lifecycle-delay",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
..UsageEventData::default()
},
);
runtime.enqueue_or_write_lifecycle(&store, event).await;
let immediate = queue
.read_group("usage-test-consumer-delay")
.await
.expect("queue read should succeed");
assert!(
immediate.is_empty(),
"lifecycle event should not enqueue before the delay"
);
sleep(Duration::from_millis(60)).await;
let entries = queue
.read_group("usage-test-consumer-delay")
.await
.expect("queue read should succeed");
assert_eq!(entries.len(), 1);
let event = UsageEvent::from_stream_fields(&entries[0].fields)
.expect("queued usage event should parse");
assert_eq!(event.request_id, "req-lifecycle-delay");
assert_eq!(event.event_type, UsageEventType::Pending);
}
#[tokio::test]
async fn lifecycle_delay_worker_respects_each_item_due_time() {
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: true,
lifecycle_enqueue_delay_ms: 60,
stream_key: "usage:events:test:per-item-delay".to_string(),
consumer_group: "usage_consumers_test_per_item_delay".to_string(),
consumer_block_ms: 1,
..UsageRuntimeConfig::default()
};
let queue_runner: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let queue = UsageQueue::new(Arc::clone(&queue_runner), config.clone())
.expect("usage queue should build");
queue
.ensure_consumer_group()
.await
.expect("consumer group should initialize");
let store = CloneQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: queue_runner,
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
runtime
.enqueue_or_write_lifecycle(
&store,
UsageEvent::new(
UsageEventType::Pending,
"req-per-item-delay-1",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
..UsageEventData::default()
},
),
)
.await;
sleep(Duration::from_millis(40)).await;
runtime
.enqueue_or_write_lifecycle(
&store,
UsageEvent::new(
UsageEventType::Pending,
"req-per-item-delay-2",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
..UsageEventData::default()
},
),
)
.await;
sleep(Duration::from_millis(50)).await;
runtime
.lifecycle_coalescer
.cancel("req-per-item-delay-2")
.await;
let entries = queue
.read_group("usage-test-consumer-per-item-delay-first")
.await
.expect("queue read should succeed");
assert_eq!(
entries.len(),
1,
"only the first lifecycle event should be due"
);
let event = UsageEvent::from_stream_fields(&entries[0].fields)
.expect("queued usage event should parse");
assert_eq!(event.request_id, "req-per-item-delay-1");
let premature_second = queue
.read_group("usage-test-consumer-per-item-delay-premature-second")
.await
.expect("queue read should succeed");
assert!(
premature_second.is_empty(),
"second lifecycle event should not be emitted before its own delay"
);
sleep(Duration::from_millis(50)).await;
let entries = queue
.read_group("usage-test-consumer-per-item-delay-second")
.await
.expect("queue read should succeed");
assert!(
entries.is_empty(),
"second lifecycle event should still be cancellable until its own due time"
);
}
#[tokio::test]
async fn lifecycle_delay_retains_admission_while_items_reside_in_timer_heap() {
const CAPACITY: usize = 3;
const BURST: usize = CAPACITY * 3;
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: true,
lifecycle_enqueue_delay_ms: 60_000,
enqueue_retry_buffer_capacity: CAPACITY,
..UsageRuntimeConfig::default()
};
let store = CloneQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default())),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let mut accepted = 0;
for index in 0..BURST {
let event = UsageEvent::new(
UsageEventType::Pending,
format!("req-delay-heap-bound-{index}"),
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
..UsageEventData::default()
},
);
if runtime
.lifecycle_delay
.schedule(store.clone(), event, index as u64 + 1)
.await
.is_ok()
{
accepted += 1;
}
}
assert_eq!(accepted, CAPACITY);
timeout(Duration::from_secs(1), async {
while runtime
.lifecycle_delay
.sender
.as_ref()
.expect("delay sender should be enabled")
.capacity()
!= CAPACITY
{
tokio::task::yield_now().await;
}
})
.await
.expect("the delay worker should move accepted items into its timer heap");
assert_eq!(
runtime.lifecycle_delay.admission.available_permits(),
0,
"moving items out of the channel must not release resident-work admission"
);
let overflow = UsageEvent::new(
UsageEventType::Pending,
"req-delay-heap-overflow",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
..UsageEventData::default()
},
);
assert!(
runtime
.lifecycle_delay
.schedule(store, overflow, u64::MAX)
.await
.is_err(),
"timer-heap residents must continue to enforce the hard capacity"
);
}
#[tokio::test]
async fn older_delayed_generation_does_not_discard_latest_lifecycle_event() {
let coalescer = LifecycleEventCoalescer::default();
let request_id = "req-latest-lifecycle-generation";
let older = coalescer.register(request_id.to_string()).await;
let latest = coalescer.register(request_id.to_string()).await;
assert!(
!coalescer
.should_emit(request_id, older.expect("older generation"))
.await,
"the superseded generation must not be emitted"
);
assert!(
coalescer
.should_emit(request_id, latest.expect("latest generation"))
.await,
"checking an older generation must preserve the latest generation"
);
}
#[tokio::test]
async fn lifecycle_cleanup_tokens_preserve_newer_first_byte_and_terminal_markers() {
let coalescer = LifecycleEventCoalescer::default();
let request_id = "req-lifecycle-cleanup-token";
let delayed_generation = coalescer
.register(request_id.to_string())
.await
.expect("delayed generation");
let first_byte_generation = coalescer
.mark_first_byte(request_id)
.await
.expect("first-byte generation");
coalescer.abandon(request_id, delayed_generation).await;
assert!(
coalescer.mark_first_byte(request_id).await.is_none(),
"abandoning an older delayed item must preserve the first-byte marker"
);
coalescer.cancel(request_id).await;
coalescer
.rollback_first_byte(request_id, first_byte_generation)
.await;
assert!(
coalescer.register(request_id.to_string()).await.is_none(),
"rolling back an older first-byte enqueue must preserve the terminal marker"
);
let retry_request_id = "req-first-byte-marker-retry";
let retry_generation = coalescer
.mark_first_byte(retry_request_id)
.await
.expect("retry first-byte generation");
coalescer
.rollback_first_byte(retry_request_id, retry_generation)
.await;
assert!(
coalescer.mark_first_byte(retry_request_id).await.is_some(),
"a rejected first-byte enqueue should be allowed to retry"
);
}
#[tokio::test]
async fn accepted_first_byte_remains_current_after_coalescer_ttl() {
let coalescer = LifecycleEventCoalescer::default();
let request_id = "req-first-byte-persistence-after-ttl";
let generation = coalescer
.mark_first_byte(request_id)
.await
.expect("first-byte generation");
{
let shard = &coalescer.shards[coalescer.shard_index(request_id)];
let mut entries = shard.entries.lock().await;
entries
.get_mut(request_id)
.expect("first-byte marker")
.first_byte_seen_at = Some(Instant::now() - Duration::from_secs(31));
}
*coalescer.shards[coalescer.shard_index(request_id)]
.next_compaction_at
.lock()
.expect("coalescer compaction deadline") =
Some(Instant::now() - Duration::from_secs(1));
coalescer
.register("req-trigger-unrelated-compaction".to_string())
.await
.expect("unrelated lifecycle generation");
assert!(
coalescer
.first_byte_is_current(request_id, generation)
.await,
"the coalescer TTL must not become a persistence deadline"
);
coalescer.cancel(request_id).await;
assert!(
!coalescer
.first_byte_is_current(request_id, generation)
.await,
"a terminal marker must still cancel the accepted first-byte write"
);
}
#[tokio::test]
async fn completed_first_byte_marker_expires_after_coalescer_ttl() {
let coalescer = LifecycleEventCoalescer::default();
let request_id = "req-first-byte-completed-marker-ttl";
let generation = coalescer
.mark_first_byte(request_id)
.await
.expect("first-byte generation");
coalescer.complete_first_byte(request_id, generation).await;
{
let shard = &coalescer.shards[coalescer.shard_index(request_id)];
let mut entries = shard.entries.lock().await;
entries
.get_mut(request_id)
.expect("completed first-byte marker")
.first_byte_seen_at = Some(Instant::now() - Duration::from_secs(31));
}
assert!(
coalescer.mark_first_byte(request_id).await.is_some(),
"completed first-byte markers should only deduplicate for the configured TTL"
);
}
#[tokio::test]
async fn lifecycle_coalescer_hard_bounds_large_burst_without_repeated_compaction() {
const CAPACITY: usize = 32;
const BURST: usize = CAPACITY * 4;
let coalescer = LifecycleEventCoalescer::new(CAPACITY);
for shard in &coalescer.shards {
*shard
.next_compaction_at
.lock()
.expect("coalescer compaction deadline") =
Some(Instant::now() + Duration::from_secs(60));
}
let mut accepted = 0;
for index in 0..BURST {
if coalescer
.mark_first_byte(&format!("req-coalescer-burst-{index}"))
.await
.is_some()
{
accepted += 1;
}
}
assert_eq!(accepted, CAPACITY);
assert_eq!(coalescer.entry_count.load(Ordering::Acquire), CAPACITY);
assert_eq!(coalescer.admission.available_permits(), 0);
assert_eq!(
coalescer.rejected_total.load(Ordering::Acquire),
(BURST - CAPACITY) as u64
);
assert_eq!(coalescer.compact_total.load(Ordering::Acquire), 0);
for shard in &coalescer.shards {
*shard
.next_compaction_at
.lock()
.expect("coalescer compaction deadline") =
Some(Instant::now() - Duration::from_secs(1));
}
let mut compacted_shards = 0_u64;
let now = Instant::now();
for (shard_index, shard) in coalescer.shards.iter().enumerate() {
let mut entries = shard.entries.lock().await;
compacted_shards += u64::from(!entries.is_empty());
coalescer.compact_if_due(shard_index, &mut entries, now);
}
assert_eq!(
coalescer.compact_total.load(Ordering::Acquire),
compacted_shards
);
assert_eq!(
coalescer
.compact_entries_scanned_total
.load(Ordering::Acquire),
CAPACITY as u64
);
assert_eq!(
coalescer.entry_count.load(Ordering::Acquire),
CAPACITY,
"in-flight first-byte markers must not be evicted by TTL compaction"
);
}
#[tokio::test]
async fn terminal_marker_evicts_completed_coalescer_entry_at_capacity() {
const CAPACITY: usize = 2;
let coalescer = LifecycleEventCoalescer::new(CAPACITY);
let first_request_id = "req-coalescer-completed-first";
let second_request_id = "req-coalescer-completed-second";
let first_generation = coalescer
.mark_first_byte(first_request_id)
.await
.expect("first completed marker should be admitted");
coalescer
.complete_first_byte(first_request_id, first_generation)
.await;
let second_generation = coalescer
.mark_first_byte(second_request_id)
.await
.expect("second completed marker should be admitted");
coalescer
.complete_first_byte(second_request_id, second_generation)
.await;
assert_eq!(coalescer.admission.available_permits(), 0);
let terminal_request_id = "req-coalescer-terminal-at-capacity";
coalescer.cancel(terminal_request_id).await;
let entries = coalescer.shards.iter().fold(0, |count, shard| {
count
+ shard
.entries
.try_lock()
.expect("coalescer shards should be unlocked after cancellation")
.len()
});
assert_eq!(entries, CAPACITY);
assert_eq!(coalescer.entry_count.load(Ordering::Acquire), CAPACITY);
assert_eq!(coalescer.admission.available_permits(), 0);
assert!(
coalescer.shards[coalescer.shard_index(terminal_request_id)]
.entries
.lock()
.await
.contains_key(terminal_request_id),
"a terminal marker should replace a completed marker when the cap is full"
);
}
#[tokio::test]
async fn completed_first_byte_markers_do_not_throttle_new_intermediates_at_capacity() {
const CAPACITY: usize = 2;
let coalescer = LifecycleEventCoalescer::new(CAPACITY);
for request_id in ["req-completed-cap-first", "req-completed-cap-second"] {
let generation = coalescer
.mark_first_byte(request_id)
.await
.expect("completed first-byte marker should be admitted");
coalescer.complete_first_byte(request_id, generation).await;
}
assert_eq!(coalescer.admission.available_permits(), 0);
assert!(
coalescer
.mark_first_byte("req-new-first-byte-at-capacity")
.await
.is_some(),
"a new first byte should replace a completed tombstone"
);
assert!(
coalescer
.register("req-new-delayed-at-capacity".to_string())
.await
.is_some(),
"a new delayed intermediate should replace the remaining completed tombstone"
);
assert_eq!(coalescer.entry_count.load(Ordering::Acquire), CAPACITY);
assert_eq!(coalescer.admission.available_permits(), 0);
assert_eq!(coalescer.rejected_total.load(Ordering::Acquire), 0);
}
#[tokio::test]
async fn terminal_marker_evicts_delayed_intermediate_at_capacity() {
const CAPACITY: usize = 2;
let coalescer = LifecycleEventCoalescer::new(CAPACITY);
let delayed_request_ids = ["req-delayed-capacity-first", "req-delayed-capacity-second"];
for request_id in delayed_request_ids {
coalescer
.register(request_id.to_string())
.await
.expect("delayed intermediate should be admitted");
}
assert_eq!(coalescer.admission.available_permits(), 0);
let terminal_request_id = "req-terminal-replaces-delayed";
coalescer.cancel(terminal_request_id).await;
assert!(
coalescer.shards[coalescer.shard_index(terminal_request_id)]
.entries
.lock()
.await
.contains_key(terminal_request_id),
"terminal admission must displace a delayed intermediate when the cap is full"
);
let retained_delayed = async {
let mut retained = 0;
for request_id in delayed_request_ids {
if coalescer.shards[coalescer.shard_index(request_id)]
.entries
.lock()
.await
.contains_key(request_id)
{
retained += 1;
}
}
retained
}
.await;
assert_eq!(retained_delayed, CAPACITY - 1);
assert_eq!(coalescer.entry_count.load(Ordering::Acquire), CAPACITY);
assert_eq!(coalescer.admission.available_permits(), 0);
}
#[tokio::test]
async fn terminal_marker_does_not_evict_active_terminal_marker() {
let coalescer = LifecycleEventCoalescer::new(1);
let active_terminal_request_id = "req-active-terminal-marker";
coalescer.cancel(active_terminal_request_id).await;
let rejected_terminal_request_id = "req-terminal-marker-rejected-at-capacity";
coalescer.cancel(rejected_terminal_request_id).await;
assert!(
coalescer.shards[coalescer.shard_index(active_terminal_request_id)]
.entries
.lock()
.await
.contains_key(active_terminal_request_id),
"an active terminal marker must retain its TTL protection"
);
assert!(
!coalescer.shards[coalescer.shard_index(rejected_terminal_request_id)]
.entries
.lock()
.await
.contains_key(rejected_terminal_request_id),
"a new marker must not replace an active terminal marker"
);
assert_eq!(coalescer.entry_count.load(Ordering::Acquire), 1);
assert_eq!(coalescer.admission.available_permits(), 0);
assert_eq!(coalescer.rejected_total.load(Ordering::Acquire), 1);
}
#[tokio::test]
async fn terminal_event_cancels_delayed_lifecycle_enqueue() {
let config = UsageRuntimeConfig {
enabled: true,
queue_terminal_events: true,
queue_lifecycle_events: true,
lifecycle_enqueue_delay_ms: 50,
stream_key: "usage:events:test:pending-cancel".to_string(),
consumer_group: "usage_consumers_test_pending_cancel".to_string(),
consumer_block_ms: 1,
..UsageRuntimeConfig::default()
};
let queue_runner: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let queue = UsageQueue::new(Arc::clone(&queue_runner), config.clone())
.expect("usage queue should build");
queue
.ensure_consumer_group()
.await
.expect("consumer group should initialize");
let store = CloneQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: queue_runner,
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
runtime
.enqueue_or_write_lifecycle(
&store,
UsageEvent::new(
UsageEventType::Pending,
"req-lifecycle-cancel",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
..UsageEventData::default()
},
),
)
.await;
runtime
.record_terminal_event(
&store,
UsageEvent::new(
UsageEventType::Completed,
"req-lifecycle-cancel",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
total_tokens: Some(12),
status_code: Some(200),
..UsageEventData::default()
},
),
)
.await;
sleep(Duration::from_millis(80)).await;
let entries = queue
.read_group("usage-test-consumer-cancel")
.await
.expect("queue read should succeed");
assert_eq!(entries.len(), 1);
let event = UsageEvent::from_stream_fields(&entries[0].fields)
.expect("queued usage event should parse");
assert_eq!(event.request_id, "req-lifecycle-cancel");
assert_eq!(event.event_type, UsageEventType::Completed);
}
#[tokio::test]
async fn first_byte_batch_panic_rolls_back_marker_admission() {
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: true,
worker_record_concurrency_limit: Some(1),
enqueue_retry_buffer_capacity: 1,
..UsageRuntimeConfig::default()
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let request_id = "req-first-byte-marker-panic";
let generation = runtime
.lifecycle_coalescer
.mark_first_byte(request_id)
.await
.expect("first-byte marker should be admitted");
let record = super::build_upsert_usage_record_from_event(&UsageEvent::new(
UsageEventType::Streaming,
request_id,
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
first_byte_time_ms: Some(12),
..UsageEventData::default()
},
))
.expect("first-byte record should build");
runtime
.first_byte_persistence
.dispatch(Box::new(PanickingFirstBytePersistenceItem {
_marker_guard: super::FirstByteMarkerGuard::new(
Arc::clone(&runtime.lifecycle_coalescer),
request_id.to_string(),
generation,
),
record,
}))
.await;
timeout(Duration::from_secs(1), async {
while runtime
.first_byte_persistence
.state
.pending
.load(Ordering::Acquire)
!= 0
|| runtime.lifecycle_coalescer.admission.available_permits() != 1
{
tokio::task::yield_now().await;
}
})
.await
.expect("a panicked first-byte batch should release marker admission");
assert!(!runtime.lifecycle_coalescer.shards
[runtime.lifecycle_coalescer.shard_index(request_id)]
.entries
.lock()
.await
.contains_key(request_id));
assert!(
runtime
.lifecycle_coalescer
.mark_first_byte("req-first-byte-after-panic")
.await
.is_some(),
"new first-byte work should be admitted after panic cleanup"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn non_batch_first_byte_writes_use_configured_bounded_concurrency() {
const CONCURRENCY: usize = 16;
const REQUESTS: usize = CONCURRENCY;
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: true,
worker_record_concurrency_limit: Some(CONCURRENCY),
enqueue_retry_buffer_capacity: REQUESTS * 2,
..UsageRuntimeConfig::default()
};
let store = AdmissionBlockedUsageStore {
release_writes: Arc::new(tokio::sync::Semaphore::new(0)),
writes_in_flight: Arc::new(AtomicUsize::new(0)),
max_writes_in_flight: Arc::new(AtomicUsize::new(0)),
writes_completed: Arc::new(AtomicUsize::new(0)),
};
assert!(!UsageRecordWriter::supports_first_byte_usage_batch(&store));
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
for index in 0..REQUESTS {
let request_id = format!("req-non-batch-first-byte-{index}");
let seed = build_lifecycle_usage_seed(&terminal_test_plan(&request_id), None);
runtime.record_stream_started(
&store,
&seed,
200,
Some(&ExecutionTelemetry {
ttfb_ms: Some(12),
elapsed_ms: Some(20),
upstream_bytes: Some(1),
}),
);
}
timeout(Duration::from_secs(2), async {
while store.max_writes_in_flight.load(Ordering::Acquire) != CONCURRENCY {
tokio::task::yield_now().await;
}
})
.await
.expect("non-batch first-byte writes should fill configured concurrency");
assert_eq!(store.writes_in_flight.load(Ordering::Acquire), CONCURRENCY);
assert_eq!(
runtime
.worker_record_gate
.as_ref()
.expect("record gate should be configured")
.max_in_flight(),
CONCURRENCY
);
store.release_writes.add_permits(REQUESTS);
timeout(Duration::from_secs(2), async {
loop {
let snapshot = runtime.metrics_snapshot();
if store.writes_completed.load(Ordering::Acquire) == REQUESTS
&& snapshot.lifecycle_submission_pending == 0
&& snapshot.ordered_lifecycle_pending == 0
&& snapshot.first_byte_persistence_pending == 0
{
break;
}
tokio::task::yield_now().await;
}
})
.await
.expect("non-batch first-byte writes should drain after release");
assert_eq!(store.writes_in_flight.load(Ordering::Acquire), 0);
assert_eq!(
store.max_writes_in_flight.load(Ordering::Acquire),
CONCURRENCY
);
}
#[tokio::test]
async fn first_byte_dispatcher_batches_records_for_one_writer() {
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: true,
worker_record_concurrency_limit: Some(4),
stream_key: "usage:events:test:first-byte-batch".to_string(),
consumer_group: "usage_consumers_test_first_byte_batch".to_string(),
consumer_block_ms: 1,
..UsageRuntimeConfig::default()
};
let queue_runner: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let store = BatchingQueueUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: queue_runner,
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
for index in 0..32 {
runtime
.enqueue_or_write_lifecycle(
&store,
UsageEvent::new(
UsageEventType::Streaming,
format!("req-first-byte-batch-{index}"),
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
first_byte_time_ms: Some(12 + index as u64),
..UsageEventData::default()
},
),
)
.await;
}
timeout(Duration::from_secs(2), async {
loop {
let snapshot = runtime.metrics_snapshot();
if snapshot.first_byte_persistence_pending == 0
&& snapshot.first_byte_persistence_direct_succeeded_total == 32
{
break;
}
tokio::task::yield_now().await;
}
})
.await
.expect("first-byte batch dispatcher should drain");
let records = store.records.lock().expect("records lock");
assert_eq!(records.len(), 32);
let snapshot = runtime.metrics_snapshot();
assert!(snapshot.first_byte_persistence_batch_flush_total >= 1);
assert_eq!(snapshot.first_byte_persistence_batch_records_total, 32);
assert!(snapshot.first_byte_persistence_max_batch_size >= 2);
assert_eq!(snapshot.first_byte_persistence_batch_failed_total, 0);
}
#[tokio::test]
async fn stream_started_direct_with_first_byte_bypasses_queue_backlog_and_lifecycle_delay() {
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: true,
lifecycle_enqueue_delay_ms: 30,
stream_key: "usage:events:test:stream-direct-delay".to_string(),
consumer_group: "usage_consumers_test_stream_direct_delay".to_string(),
consumer_block_ms: 1,
..UsageRuntimeConfig::default()
};
let queue_runner: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let queue = UsageQueue::new(Arc::clone(&queue_runner), config.clone())
.expect("usage queue should build");
queue
.ensure_consumer_group()
.await
.expect("consumer group should initialize");
let store = CloneQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: queue_runner,
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let plan = ExecutionPlan {
request_id: "req-stream-direct-delay".to_string(),
candidate_id: Some("cand-stream-direct-delay".to_string()),
provider_name: Some("openai".to_string()),
provider_id: "provider-1".to_string(),
endpoint_id: "endpoint-1".to_string(),
key_id: "key-1".to_string(),
method: "POST".to_string(),
url: "https://example.com/v1/chat/completions".to_string(),
headers: BTreeMap::new(),
content_type: Some("application/json".to_string()),
content_encoding: None,
body: RequestBody::from_json(json!({"model": "gpt-5"})),
stream: true,
client_api_format: "openai:chat".to_string(),
provider_api_format: "openai:chat".to_string(),
model_name: Some("gpt-5".to_string()),
proxy: None,
transport_profile: None,
timeouts: None,
};
let lifecycle_seed = build_lifecycle_usage_seed(&plan, None);
runtime
.record_stream_started_direct(
&store,
&lifecycle_seed,
200,
Some(&ExecutionTelemetry {
ttfb_ms: Some(12),
elapsed_ms: Some(34),
upstream_bytes: Some(56),
}),
)
.await;
timeout(Duration::from_secs(1), async {
while store.records.lock().expect("records lock").is_empty() {
tokio::task::yield_now().await;
}
})
.await
.expect("first-byte transition should be persisted directly");
{
let records = store.records.lock().expect("records lock");
assert_eq!(records.len(), 1);
assert_eq!(records[0].request_id, "req-stream-direct-delay");
assert_eq!(records[0].status, "streaming");
assert_eq!(records[0].first_byte_time_ms, Some(12));
}
sleep(Duration::from_millis(60)).await;
let queued = queue
.read_group("usage-test-consumer-stream-direct-delay-after-wait")
.await
.expect("queue read should succeed");
assert!(
queued.is_empty(),
"a successful first-byte fast-path write must bypass the ordinary queue"
);
let snapshot = runtime.metrics_snapshot();
assert_eq!(snapshot.first_byte_persistence_pending, 0);
assert_eq!(snapshot.first_byte_persistence_direct_succeeded_total, 1);
assert_eq!(snapshot.first_byte_persistence_direct_failed_total, 0);
}
#[tokio::test]
async fn stream_started_without_first_byte_keeps_lifecycle_delay() {
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: true,
lifecycle_enqueue_delay_ms: 30,
stream_key: "usage:events:test:stream-no-first-byte-delay".to_string(),
consumer_group: "usage_consumers_test_stream_no_first_byte_delay".to_string(),
consumer_block_ms: 1,
..UsageRuntimeConfig::default()
};
let queue_runner: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let queue = UsageQueue::new(Arc::clone(&queue_runner), config.clone())
.expect("usage queue should build");
queue
.ensure_consumer_group()
.await
.expect("consumer group should initialize");
let store = CloneQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: queue_runner,
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let event = UsageEvent::new(
UsageEventType::Streaming,
"req-stream-no-first-byte-delay",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
first_byte_time_ms: None,
..UsageEventData::default()
},
);
runtime.enqueue_or_write_lifecycle(&store, event).await;
let immediate = queue
.read_group("usage-test-consumer-stream-no-first-byte-immediate")
.await
.expect("queue read should succeed");
assert!(
immediate.is_empty(),
"pre-first-byte streaming event should still be coalesced"
);
sleep(Duration::from_millis(60)).await;
let entries = queue
.read_group("usage-test-consumer-stream-no-first-byte-delayed")
.await
.expect("queue read should succeed");
assert_eq!(entries.len(), 1);
let event = UsageEvent::from_stream_fields(&entries[0].fields)
.expect("queued usage event should parse");
assert_eq!(event.request_id, "req-stream-no-first-byte-delay");
assert_eq!(event.event_type, UsageEventType::Streaming);
assert_eq!(event.data.first_byte_time_ms, None);
}
#[tokio::test]
async fn first_byte_transition_supersedes_delayed_pending_event() {
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: true,
lifecycle_enqueue_delay_ms: 40,
stream_key: "usage:events:test:first-byte-supersedes-pending".to_string(),
consumer_group: "usage_consumers_test_first_byte_supersedes_pending".to_string(),
consumer_block_ms: 1,
..UsageRuntimeConfig::default()
};
let queue_runner: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let queue = UsageQueue::new(Arc::clone(&queue_runner), config.clone())
.expect("usage queue should build");
queue
.ensure_consumer_group()
.await
.expect("consumer group should initialize");
let store = CloneQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: queue_runner,
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
runtime
.enqueue_or_write_lifecycle(
&store,
UsageEvent::new(
UsageEventType::Pending,
"req-first-byte-supersedes-pending",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
..UsageEventData::default()
},
),
)
.await;
runtime
.enqueue_or_write_lifecycle(
&store,
UsageEvent::new(
UsageEventType::Streaming,
"req-first-byte-supersedes-pending",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
first_byte_time_ms: Some(12),
..UsageEventData::default()
},
),
)
.await;
timeout(Duration::from_secs(1), async {
while store.records.lock().expect("records lock").is_empty() {
tokio::task::yield_now().await;
}
})
.await
.expect("first-byte transition should be persisted directly");
{
let records = store.records.lock().expect("records lock");
assert_eq!(records.len(), 1);
assert_eq!(records[0].status, "streaming");
assert_eq!(records[0].first_byte_time_ms, Some(12));
}
sleep(Duration::from_millis(80)).await;
let delayed = queue
.read_group("usage-test-consumer-first-byte-after-delay")
.await
.expect("queue read should succeed");
assert!(
delayed.is_empty(),
"the delayed pending event must not overwrite the first-byte transition"
);
}
#[tokio::test]
async fn first_byte_transition_uses_queue_when_node_has_no_usage_writer() {
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: true,
lifecycle_enqueue_delay_ms: 1_000,
stream_key: "usage:events:test:first-byte-queue-only-node".to_string(),
consumer_group: "usage_consumers_test_first_byte_queue_only_node".to_string(),
consumer_block_ms: 1,
..UsageRuntimeConfig::default()
};
let queue_runner: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let queue = UsageQueue::new(Arc::clone(&queue_runner), config.clone())
.expect("usage queue should build");
queue
.ensure_consumer_group()
.await
.expect("consumer group should initialize");
let upsert_attempts = Arc::new(AtomicUsize::new(0));
let store = QueueOnlyUsageStore {
queue: queue_runner,
upsert_attempts: Arc::clone(&upsert_attempts),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
runtime
.enqueue_or_write_lifecycle(
&store,
UsageEvent::new(
UsageEventType::Streaming,
"req-first-byte-queue-only-node",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
first_byte_time_ms: Some(12),
request_metadata: Some(json!({
"trace_id": "trace-queue-only-first-byte",
"billing_snapshot": {"source": "original-event"}
})),
..UsageEventData::default()
},
),
)
.await;
let entries = timeout(Duration::from_secs(1), async {
loop {
let entries = queue
.read_group("usage-test-consumer-first-byte-queue-only-node")
.await
.expect("queue read should succeed");
if !entries.is_empty() {
break entries;
}
tokio::task::yield_now().await;
}
})
.await
.expect("first-byte transition should be queued");
assert_eq!(entries.len(), 1);
let event = UsageEvent::from_stream_fields(&entries[0].fields)
.expect("queued usage event should parse");
assert_eq!(event.event_type, UsageEventType::Streaming);
assert_eq!(
event.data.request_metadata,
Some(json!({
"trace_id": "trace-queue-only-first-byte",
"billing_snapshot": {"source": "original-event"}
})),
"queue-only nodes must preserve the original lifecycle event"
);
assert_eq!(upsert_attempts.load(Ordering::Acquire), 0);
let snapshot = runtime.metrics_snapshot();
assert_eq!(snapshot.first_byte_persistence_pending, 0);
assert_eq!(snapshot.first_byte_persistence_fallback_accepted_total, 1);
assert_eq!(snapshot.first_byte_persistence_fallback_failed_total, 0);
}
#[tokio::test]
async fn terminal_cancellation_drops_buffered_first_byte_write() {
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: true,
worker_record_concurrency_limit: Some(1),
stream_key: "usage:events:test:first-byte-terminal-cancel".to_string(),
consumer_group: "usage_consumers_test_first_byte_terminal_cancel".to_string(),
consumer_block_ms: 1,
..UsageRuntimeConfig::default()
};
let queue_runner: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let write_started = Arc::new(tokio::sync::Notify::new());
let release_writes = Arc::new(tokio::sync::Notify::new());
let writes_completed = Arc::new(AtomicUsize::new(0));
let store = BlockingWriteQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: queue_runner,
write_started: Arc::clone(&write_started),
release_writes: Arc::clone(&release_writes),
writes_completed: Arc::clone(&writes_completed),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let first_write_started = write_started.notified();
let streaming_event = |request_id| {
UsageEvent::new(
UsageEventType::Streaming,
request_id,
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
first_byte_time_ms: Some(12),
..UsageEventData::default()
},
)
};
runtime
.enqueue_or_write_lifecycle(&store, streaming_event("req-first-byte-blocking"))
.await;
timeout(Duration::from_secs(1), first_write_started)
.await
.expect("first fast-path write should start");
runtime
.enqueue_or_write_lifecycle(&store, streaming_event("req-first-byte-cancelled"))
.await;
runtime
.lifecycle_coalescer
.cancel("req-first-byte-cancelled")
.await;
release_writes.notify_one();
timeout(Duration::from_secs(1), async {
while writes_completed.load(Ordering::Acquire) != 1 {
tokio::task::yield_now().await;
}
})
.await
.expect("first fast-path write should finish");
sleep(Duration::from_millis(50)).await;
assert_eq!(writes_completed.load(Ordering::Acquire), 1);
let snapshot = runtime.metrics_snapshot();
assert_eq!(snapshot.first_byte_persistence_pending, 0);
assert_eq!(snapshot.first_byte_persistence_cancelled_total, 1);
}
#[tokio::test]
async fn queued_terminal_keeps_inflight_first_byte_persistence_alive() {
let config = UsageRuntimeConfig {
enabled: true,
queue_terminal_events: true,
queue_lifecycle_events: true,
stream_key: "usage:events:test:first-byte-queued-terminal".to_string(),
consumer_group: "usage_consumers_test_first_byte_queued_terminal".to_string(),
consumer_block_ms: 1,
..UsageRuntimeConfig::default()
};
let queue_runner: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let queue = UsageQueue::new(Arc::clone(&queue_runner), config.clone())
.expect("usage queue should build");
queue
.ensure_consumer_group()
.await
.expect("consumer group should initialize");
let write_started = Arc::new(tokio::sync::Notify::new());
let release_writes = Arc::new(tokio::sync::Notify::new());
let writes_completed = Arc::new(AtomicUsize::new(0));
let store = BlockingWriteQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: queue_runner,
write_started: Arc::clone(&write_started),
release_writes: Arc::clone(&release_writes),
writes_completed: Arc::clone(&writes_completed),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let request_id = "req-first-byte-queued-terminal";
runtime
.enqueue_or_write_lifecycle(
&store,
UsageEvent::new(
UsageEventType::Streaming,
request_id,
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
first_byte_time_ms: Some(12),
..UsageEventData::default()
},
),
)
.await;
timeout(Duration::from_secs(1), write_started.notified())
.await
.expect("first-byte write should start");
runtime
.enqueue_or_write_terminal(
&store,
UsageEvent::new(
UsageEventType::Completed,
request_id,
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
..UsageEventData::default()
},
),
)
.await;
release_writes.notify_one();
timeout(Duration::from_secs(1), async {
while writes_completed.load(Ordering::Acquire) != 1
|| runtime
.metrics_snapshot()
.first_byte_persistence_direct_succeeded_total
!= 1
|| runtime.metrics_snapshot().first_byte_persistence_pending != 0
{
tokio::task::yield_now().await;
}
})
.await
.expect("queued terminal must not cancel the in-flight first-byte write");
assert_eq!(
runtime
.metrics_snapshot()
.first_byte_persistence_direct_succeeded_total,
1
);
let entries = queue
.read_group("usage-test-first-byte-queued-terminal")
.await
.expect("queued terminal should be readable");
assert_eq!(entries.len(), 1);
let event = UsageEvent::from_stream_fields(&entries[0].fields)
.expect("queued terminal should parse");
assert_eq!(event.event_type, UsageEventType::Completed);
}
#[tokio::test]
async fn first_byte_append_failure_is_retried_locally() {
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: true,
lifecycle_enqueue_delay_ms: 1_000,
retry_deferred_lifecycle_events: true,
stream_key: "usage:events:test:first-byte-retry".to_string(),
consumer_group: "usage_consumers_test_first_byte_retry".to_string(),
consumer_block_ms: 1,
enqueue_retry_initial_backoff_ms: 1,
enqueue_retry_max_backoff_ms: 5,
enqueue_retry_buffer_capacity: 16,
enqueue_retry_workers: 1,
..UsageRuntimeConfig::default()
};
let inner_queue: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let queue = UsageQueue::new(Arc::clone(&inner_queue), config.clone())
.expect("usage queue should build");
queue
.ensure_consumer_group()
.await
.expect("consumer group should initialize");
let flaky_queue: Arc<dyn RuntimeQueueStore> = Arc::new(
FlakyAppendQueueStore::new(Arc::clone(&inner_queue), 1).with_append_delay_ms(200),
);
let upsert_attempts = Arc::new(AtomicUsize::new(0));
let store = FailingWriteQueueConfiguredUsageStore {
queue: flaky_queue,
upsert_attempts: Arc::clone(&upsert_attempts),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let request_metadata = json!({
"trace_id": "trace-first-byte-retry",
"upstream_is_stream": true,
"billing_snapshot": {"status": "pending", "source": "full-event"},
"stage_timings_ms": {"planning": 7}
});
timeout(
Duration::from_millis(100),
runtime.enqueue_or_write_lifecycle(
&store,
UsageEvent::new(
UsageEventType::Streaming,
"req-first-byte-retry",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
first_byte_time_ms: Some(12),
request_headers: Some(json!({"x-request-id": "full-event"})),
request_body: Some(json!({"messages": [{"content": "hello"}]})),
request_body_state: Some(UsageBodyCaptureState::Inline),
provider_request_headers: Some(json!({"x-provider": "full-event"})),
provider_request_body: Some(json!({"model": "gpt-5"})),
provider_request_body_state: Some(UsageBodyCaptureState::Inline),
response_headers: Some(json!({"content-type": "text/event-stream"})),
response_body_state: Some(UsageBodyCaptureState::Unavailable),
request_metadata: Some(request_metadata.clone()),
..UsageEventData::default()
},
),
),
)
.await
.expect("first-byte submission must not wait for the Redis append");
wait_for_enqueue_dispatcher_to_drain(&runtime, 1).await;
assert_eq!(upsert_attempts.load(Ordering::Acquire), 1);
let snapshot = runtime.metrics_snapshot();
assert_eq!(snapshot.first_byte_persistence_batch_failed_total, 1);
assert_eq!(snapshot.first_byte_persistence_direct_failed_total, 1);
let entries = queue
.read_group("usage-test-consumer-first-byte-retry")
.await
.expect("queue read should succeed");
assert_eq!(entries.len(), 1);
let event = UsageEvent::from_stream_fields(&entries[0].fields)
.expect("queued usage event should parse");
assert_eq!(event.event_type, UsageEventType::Streaming);
assert_eq!(event.data.first_byte_time_ms, Some(12));
assert_eq!(
event.data.request_headers,
Some(json!({"x-request-id": "full-event"}))
);
assert_eq!(
event.data.request_body,
Some(json!({"messages": [{"content": "hello"}]}))
);
assert_eq!(
event.data.request_body_state,
Some(UsageBodyCaptureState::Inline)
);
assert_eq!(
event.data.provider_request_headers,
Some(json!({"x-provider": "full-event"}))
);
assert_eq!(
event.data.provider_request_body,
Some(json!({"model": "gpt-5"}))
);
assert_eq!(
event.data.provider_request_body_state,
Some(UsageBodyCaptureState::Inline)
);
assert_eq!(
event.data.response_headers,
Some(json!({"content-type": "text/event-stream"}))
);
assert_eq!(
event.data.response_body_state,
Some(UsageBodyCaptureState::Unavailable)
);
assert_eq!(event.data.request_metadata, Some(request_metadata));
}
#[tokio::test]
async fn direct_terminal_event_cancels_delayed_lifecycle_enqueue() {
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: true,
lifecycle_enqueue_delay_ms: 50,
stream_key: "usage:events:test:direct-terminal-cancel".to_string(),
consumer_group: "usage_consumers_test_direct_terminal_cancel".to_string(),
consumer_block_ms: 1,
..UsageRuntimeConfig::default()
};
let queue_runner: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let queue = UsageQueue::new(Arc::clone(&queue_runner), config.clone())
.expect("usage queue should build");
queue
.ensure_consumer_group()
.await
.expect("consumer group should initialize");
let store = CloneQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: queue_runner,
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
runtime
.enqueue_or_write_lifecycle(
&store,
UsageEvent::new(
UsageEventType::Pending,
"req-direct-terminal-cancel",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
..UsageEventData::default()
},
),
)
.await;
runtime
.record_terminal_event_direct(
&store,
UsageEvent::new(
UsageEventType::Completed,
"req-direct-terminal-cancel",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
total_tokens: Some(12),
status_code: Some(200),
..UsageEventData::default()
},
),
)
.await;
sleep(Duration::from_millis(80)).await;
let entries = queue
.read_group("usage-test-consumer-direct-terminal-cancel")
.await
.expect("queue read should succeed");
assert!(
entries.is_empty(),
"direct terminal usage should cancel delayed lifecycle queue writes"
);
let records = store.records.lock().expect("records lock");
assert_eq!(records.len(), 1);
assert_eq!(records[0].request_id, "req-direct-terminal-cancel");
assert_eq!(records[0].status, "completed");
}
#[test]
fn spawn_workers_uses_configured_worker_count() {
let config = UsageRuntimeConfig {
enabled: true,
queue_terminal_events: true,
worker_count: 3,
stream_key: "usage:events:test:worker-count".to_string(),
consumer_group: "usage_consumers_test_worker_count".to_string(),
..UsageRuntimeConfig::default()
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let store = CloneQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default())),
};
let handles = runtime.spawn_workers(Arc::new(store));
assert_eq!(handles.len(), 3);
for handle in handles {
handle.abort();
}
}
#[test]
fn worker_supervisor_state_records_worker_observations() {
let state = UsageWorkerSupervisorState::default();
state.record_observation(UsageWorkerObservation {
worker_index: Some(1),
entries_read: 2,
batch_size: 16,
reclaimed_entries: 1,
acked_entries: 2,
dead_lettered_entries: 1,
process_failures: 1,
read_failures: 1,
reclaim_failures: 1,
});
assert_eq!(state.read_batches_total.load(Ordering::Acquire), 1);
assert_eq!(state.read_entries_total.load(Ordering::Acquire), 2);
assert_eq!(state.reclaimed_entries_total.load(Ordering::Acquire), 1);
assert_eq!(state.acked_entries_total.load(Ordering::Acquire), 2);
assert_eq!(state.dead_lettered_entries_total.load(Ordering::Acquire), 1);
assert_eq!(state.process_failures_total.load(Ordering::Acquire), 1);
assert_eq!(state.read_failures_total.load(Ordering::Acquire), 1);
assert_eq!(state.reclaim_failures_total.load(Ordering::Acquire), 1);
}
#[tokio::test]
async fn worker_supervisor_scales_up_when_reads_stay_full() {
let config = UsageRuntimeConfig {
enabled: true,
queue_terminal_events: true,
worker_count: 1,
worker_autoscale_enabled: true,
worker_max_count: 4,
worker_scale_interval_ms: 10,
worker_idle_scale_down_ticks: 100,
stream_key: "usage:events:test:worker-autoscale-up".to_string(),
consumer_group: "usage_consumers_test_worker_autoscale_up".to_string(),
consumer_batch_size: 1,
consumer_block_ms: 1,
..UsageRuntimeConfig::default()
};
let queue_runner: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let queue = UsageQueue::new(Arc::clone(&queue_runner), config.clone())
.expect("usage queue should build");
queue
.ensure_consumer_group()
.await
.expect("consumer group should initialize");
for index in 0..32 {
queue
.enqueue(&UsageEvent::new(
UsageEventType::Completed,
format!("req-worker-autoscale-up-{index}"),
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
total_tokens: Some(12),
status_code: Some(200),
..UsageEventData::default()
},
))
.await
.expect("usage event should enqueue");
}
let store = CloneQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: queue_runner,
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let supervisor = runtime
.spawn_worker_supervisor(Arc::new(store))
.expect("supervisor should spawn");
for _ in 0..100 {
let snapshot = runtime.metrics_snapshot();
if snapshot.worker_desired_count > 1 {
supervisor.abort();
return;
}
sleep(Duration::from_millis(10)).await;
}
supervisor.abort();
let snapshot = runtime.metrics_snapshot();
assert!(
snapshot.worker_desired_count > 1,
"usage worker supervisor should scale up after repeated full reads: {snapshot:?}"
);
}
#[tokio::test]
async fn worker_supervisor_replaces_worker_after_panic() {
let config = UsageRuntimeConfig {
enabled: true,
queue_terminal_events: true,
worker_count: 1,
worker_autoscale_enabled: false,
worker_max_count: 1,
worker_scale_interval_ms: 10,
stream_key: "usage:events:test:worker-panic-recovery".to_string(),
consumer_group: "usage_consumers_test_worker_panic_recovery".to_string(),
consumer_batch_size: 1,
consumer_block_ms: 1,
reclaim_idle_ms: 60_000,
reclaim_interval_ms: 60_000,
..UsageRuntimeConfig::default()
};
let queue_runner: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let queue = UsageQueue::new(Arc::clone(&queue_runner), config.clone())
.expect("usage queue should build");
queue
.ensure_consumer_group()
.await
.expect("consumer group should initialize");
queue
.enqueue(&UsageEvent::new(
UsageEventType::Completed,
"req-worker-panic-first",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
total_tokens: Some(12),
status_code: Some(200),
..UsageEventData::default()
},
))
.await
.expect("first usage event should enqueue");
let records = Arc::new(Mutex::new(Vec::new()));
let store = Arc::new(PanicOnceQueueConfiguredUsageStore {
inner: CloneQueueConfiguredUsageStore {
records: Arc::clone(&records),
queue: queue_runner,
},
remaining_panics: AtomicUsize::new(1),
});
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let supervisor = runtime
.spawn_worker_supervisor(Arc::clone(&store))
.expect("supervisor should spawn");
for _ in 0..100 {
if store.remaining_panics.load(Ordering::Acquire) == 0 {
break;
}
sleep(Duration::from_millis(10)).await;
}
assert_eq!(
store.remaining_panics.load(Ordering::Acquire),
0,
"first worker should panic while processing the first event"
);
queue
.enqueue(&UsageEvent::new(
UsageEventType::Completed,
"req-worker-panic-second",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
total_tokens: Some(24),
status_code: Some(200),
..UsageEventData::default()
},
))
.await
.expect("second usage event should enqueue");
for _ in 0..100 {
let recorded_second = records
.lock()
.expect("records lock")
.iter()
.any(|record| record.request_id == "req-worker-panic-second");
if recorded_second {
supervisor.abort();
return;
}
sleep(Duration::from_millis(10)).await;
}
supervisor.abort();
let records = records.lock().expect("records lock");
assert!(
records
.iter()
.any(|record| record.request_id == "req-worker-panic-second"),
"replacement worker should consume events after the first worker panics: {records:?}"
);
}
#[tokio::test]
async fn ordered_pending_bypasses_lifecycle_queue_append_failure() {
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: true,
lifecycle_enqueue_delay_ms: 0,
retry_deferred_lifecycle_events: false,
stream_key: "usage:events:test:lifecycle-failure".to_string(),
consumer_group: "usage_consumers_test_lifecycle_failure".to_string(),
consumer_block_ms: 1,
enqueue_retry_initial_backoff_ms: 1,
enqueue_retry_max_backoff_ms: 5,
enqueue_retry_buffer_capacity: 16,
enqueue_retry_workers: 1,
..UsageRuntimeConfig::default()
};
let inner_queue: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let flaky_queue: Arc<dyn RuntimeQueueStore> = Arc::new(FlakyAppendQueueStore::new(
Arc::clone(&inner_queue),
usize::MAX,
));
let store = CloneQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: flaky_queue,
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let plan = ExecutionPlan {
request_id: "req-lifecycle-queue-failure-1".to_string(),
candidate_id: Some("cand-lifecycle-queue-failure-1".to_string()),
provider_name: Some("openai".to_string()),
provider_id: "provider-1".to_string(),
endpoint_id: "endpoint-1".to_string(),
key_id: "key-1".to_string(),
method: "POST".to_string(),
url: "https://example.com/v1/responses".to_string(),
headers: BTreeMap::new(),
content_type: Some("application/json".to_string()),
content_encoding: None,
body: RequestBody::from_json(json!({"model": "gpt-5"})),
stream: false,
client_api_format: "openai:responses".to_string(),
provider_api_format: "openai:responses".to_string(),
model_name: Some("gpt-5".to_string()),
proxy: None,
transport_profile: None,
timeouts: None,
};
runtime.record_pending(&store, build_lifecycle_usage_seed(&plan, None));
sleep(Duration::from_millis(50)).await;
let records = store.records.lock().expect("records lock");
assert_eq!(records.len(), 1);
assert_eq!(records[0].status, "pending");
drop(records);
assert_eq!(
runtime.metrics_snapshot().lifecycle_enqueue_failed_total,
0,
"ordered pending persistence should not attempt the lifecycle queue"
);
}
#[tokio::test]
async fn lifecycle_enqueue_failure_opens_short_circuit() {
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: true,
retry_deferred_lifecycle_events: false,
stream_key: "usage:events:test:lifecycle-circuit".to_string(),
consumer_group: "usage_consumers_test_lifecycle_circuit".to_string(),
consumer_block_ms: 1,
..UsageRuntimeConfig::default()
};
let inner_queue: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let flaky_queue = Arc::new(FlakyAppendQueueStore::new(
Arc::clone(&inner_queue),
usize::MAX,
));
let store = CloneQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: flaky_queue.clone(),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
for index in 0..10 {
let event = UsageEvent::new(
UsageEventType::Pending,
format!("req-lifecycle-circuit-{index}"),
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
..UsageEventData::default()
},
);
runtime.enqueue_lifecycle_event(&store, event).await;
}
assert_eq!(
flaky_queue.append_attempts.load(Ordering::Acquire),
1,
"lifecycle enqueue circuit should prevent repeated Redis appends after a failure"
);
let snapshot = runtime.metrics_snapshot();
assert_eq!(
snapshot.lifecycle_enqueue_failed_total, 1,
"first append failure should open the lifecycle enqueue circuit"
);
assert_eq!(
snapshot.lifecycle_enqueue_deferred_dropped_total, 10,
"the failed primary event and circuit-deferred events should be counted as dropped"
);
assert_eq!(
snapshot.enqueue_retry_scheduled_total, 0,
"default lifecycle overload policy must not schedule local enqueue retries"
);
assert!(
store.records.lock().expect("records lock").is_empty(),
"lifecycle enqueue circuit must not fall back to direct DB writes"
);
}
#[tokio::test]
async fn lifecycle_deferred_retry_can_be_enabled() {
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: true,
retry_deferred_lifecycle_events: true,
stream_key: "usage:events:test:lifecycle-deferred-retry".to_string(),
consumer_group: "usage_consumers_test_lifecycle_deferred_retry".to_string(),
consumer_block_ms: 1,
enqueue_retry_initial_backoff_ms: 1,
enqueue_retry_max_backoff_ms: 5,
enqueue_retry_buffer_capacity: 16,
enqueue_retry_workers: 1,
..UsageRuntimeConfig::default()
};
let inner_queue: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let flaky_queue = Arc::new(FlakyAppendQueueStore::new(Arc::clone(&inner_queue), 1));
let store = CloneQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: flaky_queue,
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
runtime
.enqueue_lifecycle_event(
&store,
UsageEvent::new(
UsageEventType::Pending,
"req-lifecycle-deferred-retry-open",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
..UsageEventData::default()
},
),
)
.await;
runtime
.enqueue_lifecycle_event(
&store,
UsageEvent::new(
UsageEventType::Pending,
"req-lifecycle-deferred-retry-scheduled",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
..UsageEventData::default()
},
),
)
.await;
let snapshot = runtime.metrics_snapshot();
assert_eq!(snapshot.lifecycle_enqueue_deferred_retry_total, 2);
assert_eq!(snapshot.lifecycle_enqueue_deferred_dropped_total, 0);
assert_eq!(
snapshot.enqueue_retry_scheduled_total, 2,
"the initial append failure and the circuit-deferred event should both retry"
);
}
#[tokio::test]
async fn lifecycle_retry_buffer_full_drops_without_waiting_for_capacity() {
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: true,
enqueue_retry_buffer_capacity: 1,
enqueue_retry_workers: 1,
..UsageRuntimeConfig::default()
};
let runner: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let queue = UsageQueue::new(runner, config.clone()).expect("usage queue should build");
let (sender, _receiver) = mpsc::channel(1);
let dispatcher = UsageEnqueueRetryDispatcher {
senders: vec![sender],
metrics: Arc::new(Default::default()),
};
let event = |request_id: &str| {
UsageEvent::new(
UsageEventType::Streaming,
request_id,
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
first_byte_time_ms: Some(12),
..UsageEventData::default()
},
)
};
assert!(dispatcher.schedule(
queue.clone(),
event("req-lifecycle-retry-buffer-fill"),
"lifecycle",
DataLayerError::Redis("forced failure".to_string()),
));
let started_at = std::time::Instant::now();
let accepted = dispatcher.schedule(
queue,
event("req-lifecycle-retry-buffer-overflow"),
"lifecycle",
DataLayerError::Redis("forced failure".to_string()),
);
assert!(
started_at.elapsed() < Duration::from_millis(50),
"retry submission must remain non-blocking"
);
assert!(!accepted);
assert_eq!(dispatcher.scheduled_total(), 1);
assert_eq!(dispatcher.pending(), 1);
assert_eq!(dispatcher.closed_or_unavailable_total(), 1);
}
fn event_capture_budget_retry_event(
budget: &Arc<crate::event_capture_budget::EventCaptureMemoryBudget>,
) -> UsageEvent {
let mut event = UsageEvent {
event_type: UsageEventType::Failed,
request_id: "event-capture-budget-retry".to_string(),
timestamp_ms: 123_000,
data: UsageEventData {
provider_name: "provider".to_string(),
model: "model".to_string(),
input_tokens: Some(100),
output_tokens: Some(500),
total_tokens: Some(600),
cache_read_input_tokens: Some(0),
status_code: Some(502),
error_category: Some("upstream_error".to_string()),
error_message: Some("upstream failed".to_string()),
response_body: Some(json!({"diagnostic": "x".repeat(1000)})),
response_body_state: Some(UsageBodyCaptureState::Inline),
request_metadata: Some(json!({
"plan_usage_reservation_token": "550e8400-e29b-41d4-a716-446655440000",
"provider_service_tier": "priority",
"provider_cache_ttl_minutes": 60
})),
..UsageEventData::default()
},
};
event.data.apply_capture_memory_budget(Arc::clone(budget));
event
}
#[tokio::test]
async fn event_capture_budget_bounds_blocked_policy_waiters_and_releases_on_cancel_or_basic() {
for limit in [0, 64 * 1024] {
let runtime = UsageRuntime::new(UsageRuntimeConfig::default()).expect("runtime");
let store = BlockingPolicyQueueConfiguredUsageStore::new(Arc::new(
RuntimeState::memory(MemoryRuntimeStateConfig::default()),
));
let budget = Arc::new(crate::event_capture_budget::EventCaptureMemoryBudget::new(
limit,
));
let start = || {
let runtime = runtime.clone();
let store = store.clone();
let budget = Arc::clone(&budget);
tokio::spawn(async move {
let mut event = UsageEvent::new(
UsageEventType::Completed,
"event-capture-budget-policy",
UsageEventData {
input_tokens: Some(100),
output_tokens: Some(500),
response_body: Some(json!("x".repeat(40 * 1024))),
..UsageEventData::default()
},
);
runtime
.apply_body_capture_policy_with_budget(&store, &mut event, budget)
.await;
event
})
};
let reading = start();
timeout(Duration::from_secs(2), store.policy_started.notified())
.await
.expect("policy read is blocked");
let retained = budget.retained_bytes();
assert_eq!(retained == 0, limit == 0);
assert!(retained <= limit);
let previous_denials = budget.downgraded_total();
let waiting = start();
timeout(Duration::from_secs(2), async {
while budget.downgraded_total() == previous_denials {
sleep(Duration::from_millis(1)).await;
}
})
.await
.expect("waiting event must apply budget before acquiring policy mutex");
assert_eq!(store.policy_reads.load(Ordering::Acquire), 1);
assert_eq!(budget.retained_bytes(), retained);
waiting.abort();
assert!(waiting
.await
.expect_err("waiting task aborted")
.is_cancelled());
assert_eq!(budget.retained_bytes(), retained);
reading.abort();
assert!(reading
.await
.expect_err("reading task aborted")
.is_cancelled());
assert_eq!(budget.retained_bytes(), 0);
let completing = start();
timeout(Duration::from_secs(2), store.policy_started.notified())
.await
.expect("replacement policy read starts");
assert_eq!(budget.retained_bytes(), retained);
store.release_blocked_policy();
let event = timeout(Duration::from_secs(2), completing)
.await
.expect("Basic policy completes")
.expect("policy task completion");
assert!(event.data.response_body.is_none());
assert_eq!(
event.data.response_body_state,
Some(UsageBodyCaptureState::Disabled)
);
assert_eq!(event.data.input_tokens, Some(100));
assert_eq!(event.data.output_tokens, Some(500));
assert_eq!(
budget.retained_bytes(),
0,
"Basic returns the lease while the event is still alive"
);
}
}
#[tokio::test]
async fn event_capture_budget_retry_holds_lease_and_preserves_event_after_recovery() {
for limit in [0, 64 * 1024] {
let config = UsageRuntimeConfig {
enabled: true,
queue_terminal_events: true,
consumer_block_ms: 1,
enqueue_retry_initial_backoff_ms: 1,
enqueue_retry_max_backoff_ms: 2,
..UsageRuntimeConfig::default()
};
let inner: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let flaky = Arc::new(FlakyAppendQueueStore::new(inner, usize::MAX));
let queue = UsageQueue::new(flaky.clone(), config.clone()).expect("retry queue");
queue.ensure_consumer_group().await.expect("consumer group");
let budget = Arc::new(crate::event_capture_budget::EventCaptureMemoryBudget::new(
limit,
));
let event = event_capture_budget_retry_event(&budget);
let expected_fields = event.to_stream_fields().expect("expected wire event");
let retained = budget.retained_bytes();
assert_eq!(retained == 0, limit == 0);
let (sender, receiver) = mpsc::channel(1);
let metrics = Arc::new(super::UsageEnqueueDispatcherMetrics::default());
let dispatcher = UsageEnqueueRetryDispatcher {
senders: vec![sender],
metrics: Arc::clone(&metrics),
};
assert!(dispatcher.schedule(
queue.clone(),
event,
"terminal",
DataLayerError::Redis("initial failure".to_string())
));
let worker = tokio::spawn(super::run_usage_enqueue_retry_worker(
0,
config,
receiver,
Arc::clone(&metrics),
));
timeout(Duration::from_secs(2), async {
while metrics.retry_failed_total.load(Ordering::Acquire) < 2 {
sleep(Duration::from_millis(1)).await;
}
})
.await
.expect("two retry failures");
assert_eq!(budget.retained_bytes(), retained);
assert_eq!(dispatcher.pending(), 1);
flaky.remaining_failures.store(0, Ordering::Release);
drop(dispatcher);
timeout(Duration::from_secs(2), worker)
.await
.expect("retry recovery")
.expect("worker completion");
assert_eq!(budget.retained_bytes(), 0);
assert_eq!(metrics.pending.load(Ordering::Acquire), 0);
let entries = queue
.read_group("capture-budget-consumer")
.await
.expect("persisted queue");
assert_eq!(entries.len(), 1);
assert_eq!(
entries[0].fields, expected_fields,
"retry must preserve identity, terminal failure, billing, and truncation metadata"
);
}
}
#[tokio::test]
async fn event_capture_budget_retry_cancellation_releases_owned_body() {
let config = UsageRuntimeConfig {
enabled: true,
queue_terminal_events: true,
enqueue_retry_initial_backoff_ms: 1,
enqueue_retry_max_backoff_ms: 2,
..UsageRuntimeConfig::default()
};
let inner: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let flaky = Arc::new(FlakyAppendQueueStore::new(inner, usize::MAX));
let queue = UsageQueue::new(flaky, config.clone()).expect("retry queue");
let budget = Arc::new(crate::event_capture_budget::EventCaptureMemoryBudget::new(
64 * 1024,
));
let event = event_capture_budget_retry_event(&budget);
let (sender, receiver) = mpsc::channel(1);
let metrics = Arc::new(super::UsageEnqueueDispatcherMetrics::default());
let dispatcher = UsageEnqueueRetryDispatcher {
senders: vec![sender],
metrics: Arc::clone(&metrics),
};
assert!(dispatcher.schedule(
queue,
event,
"terminal",
DataLayerError::Redis("failure".to_string())
));
let worker = tokio::spawn(super::run_usage_enqueue_retry_worker(
0,
config,
receiver,
Arc::clone(&metrics),
));
timeout(Duration::from_secs(2), async {
while metrics.retry_failed_total.load(Ordering::Acquire) == 0 {
sleep(Duration::from_millis(1)).await;
}
})
.await
.expect("retry starts");
assert!(budget.retained_bytes() > 0);
worker.abort();
assert!(worker.await.expect_err("worker was aborted").is_cancelled());
assert_eq!(budget.retained_bytes(), 0);
}
#[tokio::test]
async fn event_capture_budget_retry_rejection_and_queued_drop_release_owned_bodies() {
let budget = Arc::new(crate::event_capture_budget::EventCaptureMemoryBudget::new(
64 * 1024,
));
let first = event_capture_budget_retry_event(&budget);
let retained_once = budget.retained_bytes();
let second = event_capture_budget_retry_event(&budget);
assert_eq!(budget.retained_bytes(), retained_once * 2);
let config = UsageRuntimeConfig::default();
let runner: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let queue = UsageQueue::new(runner, config).expect("queue");
let (sender, receiver) = mpsc::channel(1);
let dispatcher = UsageEnqueueRetryDispatcher {
senders: vec![sender],
metrics: Arc::new(super::UsageEnqueueDispatcherMetrics::default()),
};
assert!(dispatcher.schedule(
queue.clone(),
first,
"terminal",
DataLayerError::Redis("failure".to_string())
));
assert!(!dispatcher.schedule(
queue,
second,
"terminal",
DataLayerError::Redis("failure".to_string())
));
assert_eq!(budget.retained_bytes(), retained_once);
drop(receiver);
assert_eq!(budget.retained_bytes(), 0);
}
#[tokio::test]
async fn disabled_lifecycle_queue_writes_pending_directly() {
let config = UsageRuntimeConfig {
enabled: true,
queue_lifecycle_events: false,
stream_key: "usage:events:test:lifecycle-disabled".to_string(),
consumer_group: "usage_consumers_test_lifecycle_disabled".to_string(),
consumer_block_ms: 1,
..UsageRuntimeConfig::default()
};
let queue_runner: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let store = CloneQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: queue_runner,
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let plan = ExecutionPlan {
request_id: "req-lifecycle-disabled-1".to_string(),
candidate_id: Some("cand-lifecycle-disabled-1".to_string()),
provider_name: Some("openai".to_string()),
provider_id: "provider-1".to_string(),
endpoint_id: "endpoint-1".to_string(),
key_id: "key-1".to_string(),
method: "POST".to_string(),
url: "https://example.com/v1/responses".to_string(),
headers: BTreeMap::new(),
content_type: Some("application/json".to_string()),
content_encoding: None,
body: RequestBody::from_json(json!({"model": "gpt-5"})),
stream: false,
client_api_format: "openai:responses".to_string(),
provider_api_format: "openai:responses".to_string(),
model_name: Some("gpt-5".to_string()),
proxy: None,
transport_profile: None,
timeouts: None,
};
runtime.record_pending(&store, build_lifecycle_usage_seed(&plan, None));
sleep(Duration::from_millis(50)).await;
let records = store.records.lock().expect("records lock");
let record = records
.first()
.expect("disabled lifecycle queue should direct-write pending usage");
assert_eq!(record.request_id, "req-lifecycle-disabled-1");
assert_eq!(record.status, "pending");
assert_eq!(record.billing_status, "pending");
}
#[tokio::test]
async fn queued_terminal_usage_does_not_enrich_or_write_before_enqueue() {
let config = UsageRuntimeConfig {
enabled: true,
queue_terminal_events: true,
stream_key: "usage:events:test:terminal".to_string(),
consumer_group: "usage_consumers_test_terminal".to_string(),
consumer_block_ms: 1,
..UsageRuntimeConfig::default()
};
let queue_runner: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let queue = UsageQueue::new(Arc::clone(&queue_runner), config.clone())
.expect("usage queue should build");
queue
.ensure_consumer_group()
.await
.expect("consumer group should initialize");
let store = EnrichmentCountingQueueStore {
records: Mutex::new(Vec::new()),
queue: queue_runner,
enrich_calls: AtomicUsize::new(0),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let event = UsageEvent::new(
UsageEventType::Completed,
"req-terminal-queue-1",
UsageEventData {
user_id: Some("user-terminal-queue-1".to_string()),
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
input_tokens: Some(4),
output_tokens: Some(8),
total_tokens: Some(12),
status_code: Some(200),
..UsageEventData::default()
},
);
runtime.record_terminal_event(&store, event).await;
assert_eq!(store.enrich_calls.load(Ordering::Acquire), 0);
assert!(store.records.lock().expect("records lock").is_empty());
assert_eq!(
runtime.metrics_snapshot().enqueue_retry_scheduled_total,
0,
"healthy terminal enqueue should be durable before returning"
);
let entries = queue
.read_group("usage-test-terminal-consumer")
.await
.expect("queue read should succeed");
let entry = entries.first().expect("terminal event should be queued");
let queued = UsageEvent::from_stream_fields(&entry.fields)
.expect("queued terminal event should parse");
assert_eq!(queued.request_id, "req-terminal-queue-1");
assert_eq!(queued.event_type, UsageEventType::Completed);
assert_eq!(queued.data.total_cost_usd, None);
}
fn oversized_full_terminal_event(max_bytes: usize) -> (serde_json::Value, UsageEvent) {
let body = json!({"content": "full-body".repeat(max_bytes / 16)});
let mut event = UsageEvent::new(
UsageEventType::Completed,
"oversized-full-capture",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
total_tokens: Some(12),
request_body: Some(body.clone()),
provider_request_body: Some(body.clone()),
response_body: Some(body.clone()),
client_response_body: Some(body.clone()),
..UsageEventData::default()
},
);
apply_usage_body_capture_policy_to_event(
UsageBodyCapturePolicy {
record_level: UsageRequestRecordLevel::Full,
},
&mut event,
);
(body, event)
}
#[tokio::test]
async fn oversized_full_terminal_capture_is_persisted_without_queue_truncation() {
let config = UsageRuntimeConfig {
enabled: true,
queue_terminal_events: true,
consumer_block_ms: 1,
..UsageRuntimeConfig::default()
};
let queue_runner: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let queue = UsageQueue::new(Arc::clone(&queue_runner), config.clone()).unwrap();
let store = EnrichmentCountingQueueStore {
records: Mutex::new(Vec::new()),
queue: queue_runner,
enrich_calls: AtomicUsize::new(0),
};
let runtime = UsageRuntime::new(config.clone()).unwrap();
let (body, event) = oversized_full_terminal_event(config.queue_payload_max_bytes);
assert!(
event
.to_bounded_stream_fields(config.queue_payload_max_bytes)
.unwrap()
.diagnostics_omitted
);
let outcome = runtime.enqueue_or_write_terminal(&store, event).await;
assert_eq!(
outcome,
super::TerminalPersistenceOutcome::PersistedDirectly
);
assert_eq!(store.enrich_calls.load(Ordering::Acquire), 1);
assert_eq!(queue.stats().await.unwrap().stream_length, 0);
let records = store.records.lock().unwrap();
assert_eq!(records.len(), 1);
for captured in [
&records[0].request_body,
&records[0].provider_request_body,
&records[0].response_body,
&records[0].client_response_body,
] {
assert_eq!(captured.as_ref(), Some(&body));
}
}
#[tokio::test]
async fn oversized_full_terminal_capture_keeps_bounded_queue_fallback() {
for unavailable in ["writer", "write_failure", "worker_gate", "fallback_gate"] {
let config = UsageRuntimeConfig {
enabled: true,
queue_terminal_events: true,
consumer_block_ms: 1,
worker_record_concurrency_limit: Some(1),
..UsageRuntimeConfig::default()
};
let queue_runner: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let queue = UsageQueue::new(Arc::clone(&queue_runner), config.clone()).unwrap();
queue.ensure_consumer_group().await.unwrap();
let store = FailingWriteQueueConfiguredUsageStore {
queue: Arc::clone(&queue_runner),
upsert_attempts: Arc::new(AtomicUsize::new(0)),
};
let queue_only = QueueOnlyUsageStore {
queue: queue_runner,
upsert_attempts: Arc::clone(&store.upsert_attempts),
};
let runtime = UsageRuntime::new(config.clone()).unwrap();
let worker_permit = (unavailable == "worker_gate").then(|| {
runtime
.worker_record_gate
.as_ref()
.unwrap()
.try_acquire()
.unwrap()
});
let fallback_permit = (unavailable == "fallback_gate").then(|| {
runtime
.terminal_direct_fallback_state
.try_acquire()
.unwrap()
});
let (_, event) = oversized_full_terminal_event(config.queue_payload_max_bytes);
let outcome = if unavailable == "writer" {
runtime.enqueue_or_write_terminal(&queue_only, event).await
} else {
runtime.enqueue_or_write_terminal(&store, event).await
};
assert_eq!(
outcome,
super::TerminalPersistenceOutcome::Queued,
"{unavailable}"
);
assert_eq!(
store.upsert_attempts.load(Ordering::Acquire),
usize::from(unavailable == "write_failure")
);
let entries = queue
.read_group("oversized-capture-consumer")
.await
.unwrap();
assert_eq!(entries.len(), 1);
assert!(entries[0].fields["payload"].len() <= config.queue_payload_max_bytes);
let queued = UsageEvent::from_stream_fields(&entries[0].fields).unwrap();
assert_eq!(queued.data.total_tokens, Some(12));
for (body, state) in [
(&queued.data.request_body, queued.data.request_body_state),
(
&queued.data.provider_request_body,
queued.data.provider_request_body_state,
),
(&queued.data.response_body, queued.data.response_body_state),
(
&queued.data.client_response_body,
queued.data.client_response_body_state,
),
] {
assert!(body.is_none());
assert_eq!(state, Some(UsageBodyCaptureState::Truncated));
}
assert_eq!(runtime.metrics_snapshot().terminal_enqueue_failed_total, 0);
drop((worker_permit, fallback_permit));
}
}
#[tokio::test]
async fn terminal_enqueue_failure_uses_bounded_direct_database_fallback() {
let config = UsageRuntimeConfig {
enabled: true,
queue_terminal_events: true,
stream_key: "usage:events:test:terminal-circuit".to_string(),
consumer_group: "usage_consumers_test_terminal_circuit".to_string(),
consumer_block_ms: 1,
enqueue_retry_initial_backoff_ms: 1,
enqueue_retry_max_backoff_ms: 5,
enqueue_retry_buffer_capacity: 16,
enqueue_retry_workers: 1,
..UsageRuntimeConfig::default()
};
let inner_queue: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let flaky_queue = Arc::new(FlakyAppendQueueStore::new(Arc::clone(&inner_queue), 2));
let store = CloneQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: flaky_queue.clone(),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
for index in 0..2 {
runtime
.record_terminal_event(
&store,
UsageEvent::new(
UsageEventType::Completed,
format!("req-terminal-circuit-{index}"),
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
total_tokens: Some(12),
status_code: Some(200),
..UsageEventData::default()
},
),
)
.await;
}
let snapshot = runtime.metrics_snapshot();
assert_eq!(
snapshot.terminal_enqueue_failed_total, 1,
"the first terminal submission should attempt Redis before opening the circuit"
);
assert_eq!(
snapshot.terminal_enqueue_deferred_direct_write_total, 2,
"the failed primary event and circuit-deferred event should use direct fallback"
);
assert_eq!(snapshot.terminal_enqueue_deferred_retry_total, 0);
assert_eq!(snapshot.terminal_enqueue_deferred_dropped_total, 0);
assert_eq!(snapshot.terminal_direct_fallback_succeeded_total, 2);
assert_eq!(snapshot.enqueue_retry_scheduled_total, 0);
assert_eq!(snapshot.enqueue_retry_recovered_total, 0);
assert_eq!(snapshot.enqueue_retry_pending, 0);
assert_eq!(snapshot.enqueue_retry_failed_total, 0);
assert_eq!(snapshot.enqueue_retry_closed_or_unavailable_total, 0);
assert_eq!(
store.records.lock().expect("records lock").len(),
2,
"both terminal events should be persisted directly"
);
assert_eq!(flaky_queue.successful_appends.load(Ordering::Acquire), 0);
}
#[tokio::test]
async fn failed_terminal_persistence_does_not_cancel_buffered_first_byte() {
let config = UsageRuntimeConfig {
enabled: true,
queue_terminal_events: true,
queue_lifecycle_events: true,
stream_key: "usage:events:test:terminal-total-failure".to_string(),
consumer_group: "usage_consumers_test_terminal_total_failure".to_string(),
consumer_block_ms: 1,
..UsageRuntimeConfig::default()
};
let inner_queue: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let queue: Arc<dyn RuntimeQueueStore> =
Arc::new(FlakyAppendQueueStore::new(inner_queue, usize::MAX));
let store = FailingWriteQueueConfiguredUsageStore {
queue,
upsert_attempts: Arc::new(AtomicUsize::new(0)),
};
let mut runtime = UsageRuntime::new(config).expect("usage runtime should build");
runtime.enqueue_retry = UsageEnqueueRetryDispatcher::disabled();
let request_id = "req-terminal-total-failure";
let generation = runtime
.lifecycle_coalescer
.mark_first_byte(request_id)
.await
.expect("buffered first-byte generation");
runtime
.enqueue_or_write_terminal(
&store,
UsageEvent::new(
UsageEventType::Completed,
request_id,
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
..UsageEventData::default()
},
),
)
.await;
assert!(
runtime
.lifecycle_coalescer
.first_byte_is_current(request_id, generation)
.await,
"terminal loss must not discard the only remaining lifecycle transition"
);
assert_eq!(
runtime
.metrics_snapshot()
.terminal_enqueue_deferred_dropped_total,
1
);
let direct_request_id = "req-terminal-direct-total-failure";
let direct_generation = runtime
.lifecycle_coalescer
.mark_first_byte(direct_request_id)
.await
.expect("direct-path buffered first-byte generation");
runtime
.record_terminal_event_direct(
&store,
UsageEvent::new(
UsageEventType::Completed,
direct_request_id,
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
..UsageEventData::default()
},
),
)
.await;
assert!(
runtime
.lifecycle_coalescer
.first_byte_is_current(direct_request_id, direct_generation)
.await,
"failed direct terminal persistence must not cancel buffered first-byte state"
);
}
#[tokio::test]
async fn permanent_redis_and_database_failure_keeps_terminal_retry_tasks_bounded() {
const TOTAL: usize = 32;
const RETRY_CAPACITY: u64 = 1;
const RETRY_WORKERS: u64 = 1;
let config = UsageRuntimeConfig {
enabled: true,
queue_terminal_events: true,
worker_record_concurrency_limit: Some(1),
terminal_submission_max_in_flight: TOTAL as u64,
terminal_enqueue_max_in_flight: 1,
stream_key: "usage:events:test:terminal-permanent-failure".to_string(),
consumer_group: "usage_consumers_test_terminal_permanent_failure".to_string(),
consumer_block_ms: 1,
enqueue_retry_initial_backoff_ms: 1,
enqueue_retry_max_backoff_ms: 5,
enqueue_retry_buffer_capacity: RETRY_CAPACITY as usize,
enqueue_retry_workers: RETRY_WORKERS as usize,
..UsageRuntimeConfig::default()
};
let inner_queue: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let flaky_queue = Arc::new(FlakyAppendQueueStore::new(
Arc::clone(&inner_queue),
usize::MAX,
));
let queue: Arc<dyn RuntimeQueueStore> = flaky_queue.clone();
let store = FailingWriteQueueConfiguredUsageStore {
queue,
upsert_attempts: Arc::new(AtomicUsize::new(0)),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let mut submissions = tokio::task::JoinSet::new();
for index in 0..TOTAL {
let runtime = runtime.clone();
let store = store.clone();
submissions.spawn(async move {
runtime
.record_terminal_event(
&store,
UsageEvent::new(
UsageEventType::Completed,
format!("req-terminal-permanent-failure-{index}"),
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
total_tokens: Some(12),
status_code: Some(200),
..UsageEventData::default()
},
),
)
.await;
});
}
let completed_without_waiters = timeout(Duration::from_secs(1), async {
while let Some(result) = submissions.join_next().await {
result.expect("terminal submission should not panic");
}
})
.await;
let saturated_snapshot = runtime.metrics_snapshot();
flaky_queue.remaining_failures.store(0, Ordering::Release);
if completed_without_waiters.is_err() {
submissions.abort_all();
while submissions.join_next().await.is_some() {}
}
wait_for_enqueue_dispatcher_to_drain(
&runtime,
saturated_snapshot.enqueue_retry_scheduled_total,
)
.await;
assert!(
completed_without_waiters.is_ok(),
"terminal submissions must never wait outside bounded fallback queues"
);
assert_eq!(
saturated_snapshot.terminal_enqueue_deferred_total,
TOTAL as u64
);
assert_eq!(
saturated_snapshot.terminal_direct_fallback_failed_total,
TOTAL as u64
);
assert_eq!(
store.upsert_attempts.load(Ordering::Acquire),
TOTAL,
"each terminal event should receive one bounded direct write attempt"
);
assert_eq!(
saturated_snapshot.terminal_enqueue_deferred_retry_total
+ saturated_snapshot.terminal_enqueue_deferred_dropped_total,
TOTAL as u64
);
assert_eq!(
saturated_snapshot.enqueue_retry_scheduled_total,
saturated_snapshot.terminal_enqueue_deferred_retry_total
);
assert!(
saturated_snapshot.enqueue_retry_pending <= RETRY_CAPACITY + RETRY_WORKERS,
"retry pending events must stay within active workers plus channel capacity"
);
assert!(
saturated_snapshot.terminal_enqueue_deferred_dropped_total > 0,
"the test must saturate the bounded retry buffer"
);
assert_eq!(
saturated_snapshot.enqueue_retry_closed_or_unavailable_total,
saturated_snapshot.terminal_enqueue_deferred_dropped_total
);
assert!(saturated_snapshot.terminal_direct_fallback_max_in_flight <= 1);
assert_eq!(saturated_snapshot.terminal_direct_fallback_in_flight, 0);
assert_eq!(saturated_snapshot.terminal_enqueue_in_flight, 0);
}
#[tokio::test]
async fn slow_database_fallback_backpressures_excess_at_hard_capacity_and_recovers() {
const EXCESS_SUBMISSIONS: usize = 16;
let config = UsageRuntimeConfig {
enabled: true,
queue_terminal_events: true,
worker_record_concurrency_limit: Some(1),
terminal_submission_max_in_flight: (EXCESS_SUBMISSIONS + 1) as u64,
terminal_enqueue_max_in_flight: 1,
stream_key: "usage:events:test:terminal-slow-db-fallback".to_string(),
consumer_group: "usage_consumers_test_terminal_slow_db_fallback".to_string(),
consumer_block_ms: 1,
enqueue_retry_initial_backoff_ms: 1,
enqueue_retry_max_backoff_ms: 5,
enqueue_retry_buffer_capacity: 1,
enqueue_retry_workers: 1,
..UsageRuntimeConfig::default()
};
let inner_queue: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let flaky_queue = Arc::new(FlakyAppendQueueStore::new(
Arc::clone(&inner_queue),
usize::MAX,
));
let queue: Arc<dyn RuntimeQueueStore> = flaky_queue.clone();
let store = BlockingWriteQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue,
write_started: Arc::new(tokio::sync::Notify::new()),
release_writes: Arc::new(tokio::sync::Notify::new()),
writes_completed: Arc::new(AtomicUsize::new(0)),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let write_started = store.write_started.notified();
let first_runtime = runtime.clone();
let first_store = store.clone();
let mut first_submission = tokio::spawn(async move {
first_runtime
.record_terminal_event(
&first_store,
UsageEvent::new(
UsageEventType::Completed,
"req-terminal-slow-db-fallback-first",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
total_tokens: Some(12),
status_code: Some(200),
..UsageEventData::default()
},
),
)
.await;
});
let first_write_started = timeout(Duration::from_secs(1), write_started).await;
let mut excess = tokio::task::JoinSet::new();
for index in 0..EXCESS_SUBMISSIONS {
let runtime = runtime.clone();
let store = store.clone();
excess.spawn(async move {
runtime
.record_terminal_event(
&store,
UsageEvent::new(
UsageEventType::Completed,
format!("req-terminal-slow-db-fallback-excess-{index}"),
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
total_tokens: Some(12),
status_code: Some(200),
..UsageEventData::default()
},
),
)
.await;
});
}
let excess_backpressured = timeout(Duration::from_millis(50), async {
while let Some(result) = excess.join_next().await {
result.expect("excess terminal submission should not panic");
}
})
.await;
let saturated_snapshot = runtime.metrics_snapshot();
flaky_queue.remaining_failures.store(0, Ordering::Release);
sleep(Duration::from_millis(
super::LIFECYCLE_ENQUEUE_CIRCUIT_OPEN_MS + 25,
))
.await;
store.release_writes.notify_waiters();
let first_completed = timeout(Duration::from_secs(1), &mut first_submission).await;
if first_completed.is_err() {
first_submission.abort();
let _ = first_submission.await;
}
let excess_recovered = timeout(Duration::from_secs(2), async {
while let Some(result) = excess.join_next().await {
result.expect("backpressured terminal submission should not panic");
}
})
.await;
wait_for_enqueue_dispatcher_to_drain(
&runtime,
saturated_snapshot.enqueue_retry_scheduled_total,
)
.await;
let recovered_snapshot = runtime.metrics_snapshot();
assert!(
first_write_started.is_ok(),
"first fallback should reach the writer"
);
assert!(
excess_backpressured.is_err(),
"excess terminal calls should wait outside the hard resident-work bound"
);
assert!(first_completed.is_ok(), "released fallback should complete");
if excess_recovered.is_err() {
let snapshot = runtime.metrics_snapshot();
panic!(
"backpressured terminal calls should recover after persistence resumes: \
remaining={} lifecycle_pending={} ordered_pending={} terminal_pending={} \
terminal_in_flight={} writes_completed={} successful_appends={}",
excess.len(),
snapshot.lifecycle_submission_pending,
snapshot.ordered_lifecycle_pending,
snapshot.terminal_submission_pending,
snapshot.terminal_submission_in_flight,
store.writes_completed.load(Ordering::Acquire),
flaky_queue.successful_appends.load(Ordering::Acquire),
);
}
assert_eq!(saturated_snapshot.terminal_direct_fallback_limit, 1);
assert_eq!(saturated_snapshot.terminal_direct_fallback_in_flight, 1);
assert_eq!(saturated_snapshot.terminal_direct_fallback_max_in_flight, 1);
assert_eq!(saturated_snapshot.worker_record_concurrency_in_flight, 1);
assert_eq!(
saturated_snapshot.worker_record_concurrency_max_in_flight,
1
);
assert_eq!(saturated_snapshot.worker_record_deferred_total, 0);
assert_eq!(
saturated_snapshot.terminal_direct_fallback_rejected_total,
0
);
assert_eq!(
saturated_snapshot.terminal_enqueue_deferred_retry_total
+ saturated_snapshot.terminal_enqueue_deferred_dropped_total,
0
);
assert_eq!(saturated_snapshot.enqueue_retry_pending, 0);
assert_eq!(store.writes_completed.load(Ordering::Acquire), 1);
assert_eq!(
flaky_queue.successful_appends.load(Ordering::Acquire),
EXCESS_SUBMISSIONS
);
assert_eq!(recovered_snapshot.terminal_direct_fallback_in_flight, 0);
assert_eq!(recovered_snapshot.worker_record_concurrency_in_flight, 0);
assert_eq!(recovered_snapshot.lifecycle_submission_pending, 0);
assert_eq!(recovered_snapshot.ordered_lifecycle_pending, 0);
assert_eq!(
recovered_snapshot.terminal_direct_fallback_succeeded_total,
1
);
}
#[tokio::test]
async fn terminal_seed_preserves_pending_streaming_terminal_order() {
let config = UsageRuntimeConfig {
enabled: true,
worker_record_concurrency_limit: Some(1),
..UsageRuntimeConfig::default()
};
let queue: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let store = CloneQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue,
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let request_id = "req-terminal-seed-ordered";
let plan = terminal_test_plan(request_id);
let lifecycle_seed = build_lifecycle_usage_seed(&plan, None);
let (context_seed, payload_seed) = sync_terminal_test_seeds(request_id);
runtime.record_pending(&store, lifecycle_seed.clone());
runtime.record_stream_started(&store, &lifecycle_seed, 200, None);
runtime
.record_sync_terminal(&store, context_seed, payload_seed)
.await;
timeout(Duration::from_secs(2), async {
loop {
let records = store.records.lock().expect("records lock").len();
if records == 3 && runtime.metrics_snapshot().lifecycle_submission_pending == 0 {
break;
}
tokio::task::yield_now().await;
}
})
.await
.expect("ordered terminal seed should fully persist");
let records = store.records.lock().expect("records lock");
assert_eq!(records.len(), 3);
assert!(records.iter().all(|record| record.request_id == request_id));
assert_eq!(records[0].status, "pending");
assert_eq!(records[1].status, "streaming");
assert_eq!(records[2].status, "completed");
}
#[tokio::test]
async fn awaited_terminal_does_not_hold_permit_behind_queued_terminal_seed() {
let config = UsageRuntimeConfig {
enabled: true,
worker_record_concurrency_limit: Some(1),
terminal_submission_max_in_flight: 1,
..UsageRuntimeConfig::default()
};
let queue: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let store = BlockingWriteQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue,
write_started: Arc::new(tokio::sync::Notify::new()),
release_writes: Arc::new(tokio::sync::Notify::new()),
writes_completed: Arc::new(AtomicUsize::new(0)),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let blocker_started = Arc::new(tokio::sync::Notify::new());
let release_blocker = Arc::new(tokio::sync::Notify::new());
let seen = Arc::new(Mutex::new(Vec::new()));
runtime
.lifecycle_submission
.dispatch(Box::new(TestLifecycleSubmissionItem {
request_id: "req-terminal-seed-deadlock-blocker".to_string(),
priority: LifecycleSubmissionPriority::Pending,
started: Some(Arc::clone(&blocker_started)),
release: Some(Arc::clone(&release_blocker)),
seen,
}));
timeout(Duration::from_secs(1), blocker_started.notified())
.await
.expect("lifecycle blocker should start");
let (context_seed, payload_seed) =
sync_terminal_test_seeds("req-terminal-seed-before-awaited");
runtime
.record_sync_terminal(&store, context_seed, payload_seed)
.await;
let awaited_runtime = runtime.clone();
let awaited_store = store.clone();
let awaited_terminal = tokio::spawn(async move {
awaited_runtime
.record_terminal_event(
&awaited_store,
UsageEvent::new(
UsageEventType::Completed,
"req-awaited-terminal-after-seed",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
..UsageEventData::default()
},
),
)
.await;
});
timeout(Duration::from_secs(1), async {
while runtime.metrics_snapshot().lifecycle_submission_pending < 3 {
tokio::task::yield_now().await;
}
})
.await
.expect("terminal seed and awaited barrier should queue behind the blocker");
assert_eq!(runtime.metrics_snapshot().terminal_submission_pending, 0);
let first_write_started = store.write_started.notified();
release_blocker.notify_one();
timeout(Duration::from_secs(1), first_write_started)
.await
.expect("terminal seed write should start");
timeout(Duration::from_secs(1), async {
while runtime.metrics_snapshot().terminal_submission_pending != 2 {
tokio::task::yield_now().await;
}
})
.await
.expect("awaited terminal should queue behind the terminal seed permit");
let second_write_started = store.write_started.notified();
store.release_writes.notify_one();
timeout(Duration::from_secs(1), second_write_started)
.await
.expect("awaited terminal write should start after the seed completes");
store.release_writes.notify_one();
timeout(Duration::from_secs(2), awaited_terminal)
.await
.expect("awaited terminal must not deadlock behind the queued seed")
.expect("awaited terminal task should not panic");
timeout(Duration::from_secs(2), async {
while store.records.lock().expect("records lock").len() != 2
|| runtime.metrics_snapshot().lifecycle_submission_pending != 0
|| store.writes_completed.load(Ordering::Acquire) != 2
{
tokio::task::yield_now().await;
}
})
.await
.expect("both terminal records should persist");
let records = store.records.lock().expect("records lock");
assert!(records
.iter()
.any(|record| record.request_id == "req-terminal-seed-before-awaited"));
assert!(records
.iter()
.any(|record| record.request_id == "req-awaited-terminal-after-seed"));
assert_eq!(
runtime.metrics_snapshot().terminal_submission_max_pending,
2
);
}
#[tokio::test]
async fn submitted_terminal_does_not_hold_permit_behind_queued_terminal_seed() {
let config = UsageRuntimeConfig {
enabled: true,
worker_record_concurrency_limit: Some(1),
terminal_submission_max_in_flight: 1,
..UsageRuntimeConfig::default()
};
let queue: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let store = BlockingWriteQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue,
write_started: Arc::new(tokio::sync::Notify::new()),
release_writes: Arc::new(tokio::sync::Notify::new()),
writes_completed: Arc::new(AtomicUsize::new(0)),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let blocker_started = Arc::new(tokio::sync::Notify::new());
let release_blocker = Arc::new(tokio::sync::Notify::new());
runtime
.lifecycle_submission
.dispatch(Box::new(TestLifecycleSubmissionItem {
request_id: "req-submitted-terminal-deadlock-blocker".to_string(),
priority: LifecycleSubmissionPriority::Pending,
started: Some(Arc::clone(&blocker_started)),
release: Some(Arc::clone(&release_blocker)),
seen: Arc::new(Mutex::new(Vec::new())),
}));
timeout(Duration::from_secs(1), blocker_started.notified())
.await
.expect("lifecycle blocker should start");
let (context_seed, payload_seed) =
sync_terminal_test_seeds("req-terminal-seed-before-submitted");
runtime
.record_sync_terminal(&store, context_seed, payload_seed)
.await;
runtime
.submit_terminal_event(
&store,
UsageEvent::new(
UsageEventType::Completed,
"req-submitted-terminal-after-seed",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
..UsageEventData::default()
},
),
)
.await;
assert_eq!(runtime.metrics_snapshot().terminal_submission_pending, 0);
assert!(runtime.metrics_snapshot().lifecycle_submission_pending >= 3);
let first_write_started = store.write_started.notified();
release_blocker.notify_one();
timeout(Duration::from_secs(1), first_write_started)
.await
.expect("terminal seed write should start");
timeout(Duration::from_secs(1), async {
while runtime.metrics_snapshot().terminal_submission_pending != 2 {
tokio::task::yield_now().await;
}
})
.await
.expect("submitted terminal should queue behind the terminal seed permit");
let second_write_started = store.write_started.notified();
store.release_writes.notify_one();
timeout(Duration::from_secs(1), second_write_started)
.await
.expect("submitted terminal write should start after the seed completes");
store.release_writes.notify_one();
timeout(Duration::from_secs(2), async {
while store.records.lock().expect("records lock").len() != 2
|| runtime.metrics_snapshot().lifecycle_submission_pending != 0
|| store.writes_completed.load(Ordering::Acquire) != 2
{
tokio::task::yield_now().await;
}
})
.await
.expect("queued seed and submitted terminal should both persist");
let records = store.records.lock().expect("records lock");
assert!(records
.iter()
.any(|record| record.request_id == "req-terminal-seed-before-submitted"));
assert!(records
.iter()
.any(|record| record.request_id == "req-submitted-terminal-after-seed"));
assert_eq!(
runtime.metrics_snapshot().terminal_submission_max_pending,
2
);
}
#[tokio::test]
async fn terminal_seed_waits_for_its_ordered_turn_before_admission() {
let config = UsageRuntimeConfig {
enabled: true,
queue_terminal_events: true,
worker_record_concurrency_limit: Some(1),
terminal_submission_max_in_flight: 1,
stream_key: "usage:events:test:terminal-seed-turn".to_string(),
consumer_group: "usage_consumers_test_terminal_seed_turn".to_string(),
..UsageRuntimeConfig::default()
};
let inner_queue: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let tracked_queue = Arc::new(FlakyAppendQueueStore::new(inner_queue, 0));
let queue: Arc<dyn RuntimeQueueStore> = tracked_queue.clone();
let store = BlockingPolicyQueueConfiguredUsageStore::new(queue);
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let request_id = "req-terminal-seed-waits-for-turn";
let plan = terminal_test_plan(request_id);
let policy_started = store.policy_started.notified();
runtime.record_pending(&store, build_lifecycle_usage_seed(&plan, None));
timeout(Duration::from_secs(1), policy_started)
.await
.expect("pending phase should block in body policy");
let (context_seed, payload_seed) = sync_terminal_test_seeds(request_id);
runtime
.record_sync_terminal(&store, context_seed, payload_seed)
.await;
let blocked_snapshot = runtime.metrics_snapshot();
assert_eq!(blocked_snapshot.terminal_submission_pending, 0);
assert_eq!(blocked_snapshot.terminal_submission_max_pending, 0);
assert_eq!(blocked_snapshot.terminal_submission_in_flight, 0);
assert!(blocked_snapshot.lifecycle_submission_pending >= 2);
store.release_blocked_policy();
timeout(Duration::from_secs(2), async {
loop {
store.release_blocked_policy();
let snapshot = runtime.metrics_snapshot();
if tracked_queue.successful_appends.load(Ordering::Acquire) == 1
&& snapshot.lifecycle_submission_pending == 0
&& snapshot.terminal_submission_pending == 0
{
break;
}
sleep(Duration::from_millis(1)).await;
}
})
.await
.expect("terminal seed should persist after its ordered turn is released");
let recovered_snapshot = runtime.metrics_snapshot();
assert_eq!(recovered_snapshot.terminal_submission_max_pending, 1);
assert_eq!(recovered_snapshot.terminal_submission_max_in_flight, 1);
assert_eq!(recovered_snapshot.terminal_submission_rejected_total, 0);
assert_eq!(
recovered_snapshot.terminal_enqueue_deferred_dropped_total,
0
);
}
#[tokio::test]
async fn terminal_seed_backlog_does_not_create_per_request_admission_waiters() {
const BACKLOG: usize = 16;
let config = UsageRuntimeConfig {
enabled: true,
queue_terminal_events: true,
worker_record_concurrency_limit: Some(1),
terminal_submission_max_in_flight: 1,
stream_key: "usage:events:test:terminal-seed-backlog".to_string(),
consumer_group: "usage_consumers_test_terminal_seed_backlog".to_string(),
..UsageRuntimeConfig::default()
};
let inner_queue: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let tracked_queue = Arc::new(FlakyAppendQueueStore::new(inner_queue, 0));
let queue: Arc<dyn RuntimeQueueStore> = tracked_queue.clone();
let store = BlockingPolicyQueueConfiguredUsageStore::new(queue);
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let policy_started = store.policy_started.notified();
runtime
.submit_terminal_event(
&store,
UsageEvent::new(
UsageEventType::Completed,
"req-terminal-seed-backlog-blocker",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
..UsageEventData::default()
},
),
)
.await;
timeout(Duration::from_secs(1), policy_started)
.await
.expect("blocking terminal should hold the only admission permit");
for index in 0..BACKLOG {
let request_id = format!("req-terminal-seed-backlog-{index}");
let (context_seed, payload_seed) = sync_terminal_test_seeds(&request_id);
runtime
.record_sync_terminal(&store, context_seed, payload_seed)
.await;
}
timeout(Duration::from_secs(1), async {
while runtime.metrics_snapshot().terminal_submission_pending < BACKLOG + 1 {
tokio::task::yield_now().await;
}
})
.await
.expect("terminal execution queue should receive the full backlog");
let blocked_snapshot = runtime.metrics_snapshot();
assert_eq!(blocked_snapshot.terminal_submission_pending, BACKLOG + 1);
assert_eq!(
blocked_snapshot.terminal_submission_max_pending,
BACKLOG + 1
);
assert_eq!(blocked_snapshot.terminal_submission_in_flight, 1);
assert!(blocked_snapshot.lifecycle_submission_pending <= BACKLOG + 1);
store.release_blocked_policy();
timeout(Duration::from_secs(5), async {
loop {
store.release_blocked_policy();
let snapshot = runtime.metrics_snapshot();
if tracked_queue.successful_appends.load(Ordering::Acquire) == BACKLOG + 1
&& snapshot.lifecycle_submission_pending == 0
&& snapshot.terminal_submission_pending == 0
{
break;
}
sleep(Duration::from_millis(1)).await;
}
})
.await
.expect("all terminal seeds should recover from admission backpressure");
let recovered_snapshot = runtime.metrics_snapshot();
assert_eq!(
recovered_snapshot.terminal_submission_max_pending,
BACKLOG + 1
);
assert_eq!(recovered_snapshot.terminal_submission_max_in_flight, 1);
assert_eq!(recovered_snapshot.terminal_submission_rejected_total, 0);
assert_eq!(recovered_snapshot.terminal_enqueue_failed_total, 0);
assert_eq!(
recovered_snapshot.terminal_enqueue_deferred_dropped_total,
0
);
}
#[tokio::test]
async fn terminal_permit_wait_does_not_block_unrelated_lifecycle_on_same_shard() {
let config = UsageRuntimeConfig {
enabled: true,
worker_record_concurrency_limit: Some(1),
terminal_submission_max_in_flight: 1,
..UsageRuntimeConfig::default()
};
let store = CloneQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default())),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let held_permit = runtime
.terminal_submission_state
.acquire()
.await
.expect("test should hold the only terminal permit");
let blocked_request_id = "req-terminal-permit-hol-blocked";
let (context_seed, payload_seed) = sync_terminal_test_seeds(blocked_request_id);
runtime
.record_sync_terminal(&store, context_seed, payload_seed)
.await;
timeout(Duration::from_secs(1), async {
while runtime.metrics_snapshot().terminal_submission_pending < 2 {
tokio::task::yield_now().await;
}
})
.await
.expect("terminal work should reach the dedicated execution queue");
let unrelated_request_id = "req-terminal-permit-hol-unrelated";
runtime.record_pending(
&store,
build_lifecycle_usage_seed(&terminal_test_plan(unrelated_request_id), None),
);
timeout(Duration::from_secs(1), async {
loop {
let unrelated_written = {
let records = store.records.lock().expect("records lock");
records.iter().any(|record| {
record.request_id == unrelated_request_id && record.status == "pending"
})
};
if unrelated_written {
break;
}
tokio::task::yield_now().await;
}
})
.await
.expect("unrelated lifecycle work must bypass terminal permit wait");
assert!(!store
.records
.lock()
.expect("records lock")
.iter()
.any(|record| record.request_id == blocked_request_id));
drop(held_permit);
timeout(Duration::from_secs(2), async {
loop {
let snapshot = runtime.metrics_snapshot();
let terminal_written = store
.records
.lock()
.expect("records lock")
.iter()
.any(|record| record.request_id == blocked_request_id);
if terminal_written
&& snapshot.terminal_submission_pending == 0
&& snapshot.lifecycle_submission_pending == 0
{
break;
}
tokio::task::yield_now().await;
}
})
.await
.expect("terminal work should drain after the permit is released");
}
#[tokio::test]
async fn terminal_build_does_not_block_unrelated_lifecycle_on_same_shard() {
let config = UsageRuntimeConfig {
enabled: true,
worker_record_concurrency_limit: Some(1),
terminal_submission_max_in_flight: 1,
..UsageRuntimeConfig::default()
};
let store = CloneQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default())),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let blocked_request_id = "req-terminal-build-hol-blocked";
let build_started = Arc::new(tokio::sync::Notify::new());
let release_build = Arc::new(tokio::sync::Notify::new());
runtime
.dispatch_terminal_seed(
&store,
blocked_request_id.to_string(),
LifecycleTerminalUsageSeed::BlockedBuild {
event: UsageEvent::new(
UsageEventType::Completed,
blocked_request_id,
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
..UsageEventData::default()
},
),
started: Arc::clone(&build_started),
release: Arc::clone(&release_build),
},
)
.await;
timeout(Duration::from_secs(1), build_started.notified())
.await
.expect("terminal build should start on the dedicated worker");
let unrelated_request_id = "req-terminal-build-hol-unrelated";
runtime.record_pending(
&store,
build_lifecycle_usage_seed(&terminal_test_plan(unrelated_request_id), None),
);
timeout(Duration::from_secs(1), async {
loop {
let unrelated_written = {
let records = store.records.lock().expect("records lock");
records.iter().any(|record| {
record.request_id == unrelated_request_id && record.status == "pending"
})
};
if unrelated_written {
break;
}
tokio::task::yield_now().await;
}
})
.await
.expect("unrelated lifecycle work must bypass terminal build");
assert!(!store
.records
.lock()
.expect("records lock")
.iter()
.any(|record| record.request_id == blocked_request_id));
release_build.notify_one();
timeout(Duration::from_secs(2), async {
loop {
let snapshot = runtime.metrics_snapshot();
let terminal_written = store
.records
.lock()
.expect("records lock")
.iter()
.any(|record| record.request_id == blocked_request_id);
if terminal_written
&& snapshot.terminal_submission_pending == 0
&& snapshot.lifecycle_submission_pending == 0
{
break;
}
tokio::task::yield_now().await;
}
})
.await
.expect("terminal build should drain after release");
}
#[tokio::test]
async fn terminal_submission_pending_decrements_when_waiter_is_cancelled() {
let state = Arc::new(super::TerminalSubmissionState::new(1));
let first = state
.acquire()
.await
.expect("first terminal submission should acquire");
let waiter_state = Arc::clone(&state);
let waiter = tokio::spawn(async move { waiter_state.acquire().await });
timeout(Duration::from_secs(1), async {
while state.pending() != 2 {
tokio::task::yield_now().await;
}
})
.await
.expect("cancelled waiter should be counted while it is queued");
waiter.abort();
assert!(
matches!(waiter.await, Err(err) if err.is_cancelled()),
"admission waiter task should report cancellation"
);
assert_eq!(state.pending(), 1);
drop(first);
assert_eq!(state.pending(), 0);
assert_eq!(state.max_pending(), 2);
}
#[tokio::test]
async fn direct_terminal_write_is_visible_to_submission_metrics_until_completion() {
let config = UsageRuntimeConfig {
enabled: true,
worker_record_concurrency_limit: Some(1),
terminal_submission_max_in_flight: 1,
..UsageRuntimeConfig::default()
};
let store = BlockingWriteQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue: Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default())),
write_started: Arc::new(tokio::sync::Notify::new()),
release_writes: Arc::new(tokio::sync::Notify::new()),
writes_completed: Arc::new(AtomicUsize::new(0)),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let write_started = store.write_started.notified();
let write_runtime = runtime.clone();
let write_store = store.clone();
let write = tokio::spawn(async move {
write_runtime
.record_terminal_event_direct(
&write_store,
UsageEvent::new(
UsageEventType::Completed,
"req-direct-terminal-submission-metrics",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
status_code: Some(200),
..UsageEventData::default()
},
),
)
.await;
});
timeout(Duration::from_secs(1), write_started)
.await
.expect("the direct terminal write should start");
let in_flight = runtime.metrics_snapshot();
assert_eq!(in_flight.terminal_submission_pending, 1);
assert_eq!(in_flight.terminal_submission_in_flight, 1);
store.release_writes.notify_one();
timeout(Duration::from_secs(1), write)
.await
.expect("the direct terminal write should finish")
.expect("the direct terminal write task should not panic");
let completed = runtime.metrics_snapshot();
assert_eq!(completed.terminal_submission_pending, 0);
assert_eq!(completed.terminal_submission_in_flight, 0);
assert_eq!(store.writes_completed.load(Ordering::Acquire), 1);
}
#[tokio::test]
async fn terminal_submission_admission_backpressures_and_recovers_all_events() {
const EXCESS_SUBMISSIONS: usize = 16;
let config = UsageRuntimeConfig {
enabled: true,
queue_terminal_events: true,
worker_record_concurrency_limit: Some(1),
terminal_submission_max_in_flight: 1,
terminal_enqueue_max_in_flight: 1,
stream_key: "usage:events:test:terminal-submission-admission".to_string(),
consumer_group: "usage_consumers_test_terminal_submission_admission".to_string(),
consumer_block_ms: 1,
..UsageRuntimeConfig::default()
};
let inner_queue: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let tracked_queue = Arc::new(FlakyAppendQueueStore::new(inner_queue, 0));
let queue: Arc<dyn RuntimeQueueStore> = tracked_queue.clone();
let store = BlockingPolicyQueueConfiguredUsageStore::new(queue);
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let policy_started = store.policy_started.notified();
runtime
.submit_terminal_event(
&store,
UsageEvent::new(
UsageEventType::Completed,
"req-terminal-submission-admission-first",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
total_tokens: Some(12),
status_code: Some(200),
..UsageEventData::default()
},
),
)
.await;
let first_policy_started = timeout(Duration::from_secs(1), policy_started).await;
let mut pending_submissions = Vec::with_capacity(EXCESS_SUBMISSIONS);
for index in 0..EXCESS_SUBMISSIONS {
let runtime = runtime.clone();
let store = store.clone();
pending_submissions.push(tokio::spawn(async move {
runtime
.submit_terminal_event(
&store,
UsageEvent::new(
UsageEventType::Completed,
format!("req-terminal-submission-admission-excess-{index}"),
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
total_tokens: Some(12),
status_code: Some(200),
..UsageEventData::default()
},
),
)
.await;
}));
}
for submission in pending_submissions {
timeout(Duration::from_secs(1), submission)
.await
.expect("terminal ownership handoff should not wait for admission")
.expect("terminal submission handoff task should complete");
}
timeout(Duration::from_secs(1), async {
while runtime.metrics_snapshot().terminal_submission_pending < EXCESS_SUBMISSIONS + 1 {
tokio::task::yield_now().await;
}
})
.await
.expect("terminal submissions should reach the execution backlog");
let saturated_snapshot = runtime.metrics_snapshot();
store.release_blocked_policy();
let all_completed = timeout(Duration::from_secs(2), async {
loop {
store.release_blocked_policy();
if tracked_queue.successful_appends.load(Ordering::Acquire)
== EXCESS_SUBMISSIONS + 1
&& runtime.metrics_snapshot().terminal_submission_in_flight == 0
&& runtime.metrics_snapshot().lifecycle_submission_pending == 0
{
break;
}
sleep(Duration::from_millis(1)).await;
}
})
.await;
let recovered_snapshot = runtime.metrics_snapshot();
assert!(
first_policy_started.is_ok(),
"first terminal submission should reach policy I/O"
);
assert!(all_completed.is_ok(), "all submissions should complete");
assert_eq!(
tracked_queue.successful_appends.load(Ordering::Acquire),
EXCESS_SUBMISSIONS + 1,
"every admitted terminal event should reach the queue"
);
assert_eq!(saturated_snapshot.terminal_submission_limit, 1);
assert_eq!(
saturated_snapshot.terminal_submission_pending,
EXCESS_SUBMISSIONS + 1
);
assert_eq!(
saturated_snapshot.terminal_submission_max_pending,
EXCESS_SUBMISSIONS + 1
);
assert_eq!(saturated_snapshot.terminal_submission_in_flight, 1);
assert_eq!(saturated_snapshot.terminal_submission_max_in_flight, 1);
assert_eq!(saturated_snapshot.terminal_submission_rejected_total, 0);
assert_eq!(store.policy_reads.load(Ordering::Acquire), 1);
assert_eq!(recovered_snapshot.terminal_submission_in_flight, 0);
}
#[tokio::test]
async fn terminal_enqueue_limit_bounds_primary_burst_and_recovers_all_events() {
const WORKERS: usize = 4;
const PRIMARY_LIMIT: usize = 4;
const BURST: usize = 64;
const FORCED_FAILURES: usize = 8;
let config = UsageRuntimeConfig {
enabled: true,
queue_terminal_events: true,
stream_key: "usage:events:test:terminal-dispatch-burst".to_string(),
consumer_group: "usage_consumers_test_terminal_dispatch_burst".to_string(),
consumer_block_ms: 1,
terminal_enqueue_max_in_flight: PRIMARY_LIMIT as u64,
enqueue_retry_initial_backoff_ms: 1,
enqueue_retry_max_backoff_ms: 5,
enqueue_retry_buffer_capacity: BURST,
enqueue_retry_workers: WORKERS,
..UsageRuntimeConfig::default()
};
let inner_queue: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let tracked_queue = Arc::new(
FlakyAppendQueueStore::new(Arc::clone(&inner_queue), FORCED_FAILURES)
.with_append_delay_ms(2),
);
let queue: Arc<dyn RuntimeQueueStore> = tracked_queue.clone();
let store = CloneQueueConfiguredUsageStore {
records: Arc::new(Mutex::new(Vec::new())),
queue,
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let mut submissions = Vec::with_capacity(BURST);
for index in 0..BURST {
let runtime = runtime.clone();
let store = store.clone();
submissions.push(tokio::spawn(async move {
runtime
.record_terminal_event(
&store,
UsageEvent::new(
UsageEventType::Completed,
format!("req-terminal-dispatch-burst-{index}"),
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
total_tokens: Some(12),
status_code: Some(200),
..UsageEventData::default()
},
),
)
.await;
}));
}
for submission in submissions {
submission
.await
.expect("terminal submission should not panic");
}
let scheduled = runtime.metrics_snapshot().enqueue_retry_scheduled_total;
wait_for_enqueue_dispatcher_to_drain(&runtime, scheduled).await;
let snapshot = runtime.metrics_snapshot();
assert_eq!(snapshot.enqueue_retry_scheduled_total, scheduled);
assert_eq!(snapshot.enqueue_retry_recovered_total, scheduled);
assert_eq!(snapshot.enqueue_retry_pending, 0);
assert_eq!(snapshot.enqueue_retry_closed_or_unavailable_total, 0);
assert_eq!(
tracked_queue.successful_appends.load(Ordering::Acquire)
+ store.records.lock().expect("records lock").len(),
BURST,
"every terminal event should reach Redis or bounded direct fallback"
);
assert_eq!(
tracked_queue.append_attempts.load(Ordering::Acquire),
tracked_queue.successful_appends.load(Ordering::Acquire)
+ snapshot.terminal_enqueue_failed_total as usize
+ snapshot.enqueue_retry_failed_total as usize
);
assert!(
tracked_queue.max_active_appends.load(Ordering::Acquire) <= PRIMARY_LIMIT + WORKERS,
"Redis queue enqueue concurrency must be bounded by primary and retry limits"
);
assert_eq!(snapshot.terminal_enqueue_in_flight, 0);
assert!(
snapshot.terminal_enqueue_deferred_direct_write_total
+ snapshot.terminal_enqueue_deferred_retry_total
> 0,
"burst submissions above the primary limit should use a bounded fallback"
);
assert_eq!(snapshot.terminal_enqueue_deferred_dropped_total, 0);
assert!(snapshot.terminal_direct_fallback_max_in_flight <= 32);
}
#[tokio::test]
async fn queue_append_failure_prefers_bounded_direct_write_before_retry() {
let config = UsageRuntimeConfig {
enabled: true,
queue_terminal_events: true,
stream_key: "usage:events:test:retry".to_string(),
consumer_group: "usage_consumers_test_retry".to_string(),
consumer_block_ms: 1,
enqueue_retry_initial_backoff_ms: 1,
enqueue_retry_max_backoff_ms: 5,
enqueue_retry_buffer_capacity: 16,
enqueue_retry_workers: 1,
..UsageRuntimeConfig::default()
};
let inner_queue: Arc<dyn RuntimeQueueStore> =
Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
let flaky_queue: Arc<dyn RuntimeQueueStore> =
Arc::new(FlakyAppendQueueStore::new(Arc::clone(&inner_queue), 1));
let queue = UsageQueue::new(Arc::clone(&inner_queue), config.clone())
.expect("usage queue should build");
queue
.ensure_consumer_group()
.await
.expect("consumer group should initialize");
let store = EnrichmentCountingQueueStore {
records: Mutex::new(Vec::new()),
queue: flaky_queue,
enrich_calls: AtomicUsize::new(0),
};
let runtime = UsageRuntime::new(config).expect("usage runtime should build");
let event = UsageEvent::new(
UsageEventType::Completed,
"req-terminal-retry-1",
UsageEventData {
user_id: Some("user-terminal-retry-1".to_string()),
provider_name: "openai".to_string(),
model: "gpt-5".to_string(),
total_tokens: Some(12),
status_code: Some(200),
..UsageEventData::default()
},
);
runtime.record_terminal_event(&store, event).await;
assert_eq!(store.enrich_calls.load(Ordering::Acquire), 1);
assert_eq!(store.records.lock().expect("records lock").len(), 1);
assert!(
queue
.read_group("usage-test-retry-consumer")
.await
.expect("queue read should succeed")
.is_empty(),
"successful direct fallback should not also schedule Redis retry"
);
let snapshot = runtime.metrics_snapshot();
assert_eq!(snapshot.terminal_direct_fallback_succeeded_total, 1);
assert_eq!(snapshot.enqueue_retry_scheduled_total, 0);
}
#[tokio::test]
async fn body_capture_policy_read_failure_is_short_cached_as_default() {
let runtime = UsageRuntime::new(UsageRuntimeConfig {
enabled: true,
..UsageRuntimeConfig::default()
})
.expect("usage runtime should build");
let store = FailingPolicyUsageStore::default();
let first = runtime.body_capture_policy_for(&store).await;
assert!(first.is_err(), "first failed read should be surfaced");
let second = runtime
.body_capture_policy_for(&store)
.await
.expect("short cached fallback should be returned");
let third = runtime
.body_capture_policy_for(&store)
.await
.expect("short cached fallback should be reused");
assert_eq!(second, UsageBodyCapturePolicy::default());
assert_eq!(third, UsageBodyCapturePolicy::default());
assert_eq!(
store.policy_reads.load(Ordering::Acquire),
1,
"policy read failures should be short cached to avoid concurrent DB storms"
);
}
#[test]
fn preserve_request_facts_ignores_post_capture_truncation_placeholders() {
let truncated = json!({
"truncated": true,
"reason": "body_capture_limit_exceeded"
});
let mut event = UsageEvent::new(
UsageEventType::Completed,
"req-truncated-preserve",
UsageEventData {
provider_name: "OpenAI".to_string(),
model: "gpt-5".to_string(),
request_body: Some(truncated.clone()),
request_body_state: Some(UsageBodyCaptureState::Truncated),
provider_request_body: Some(truncated),
provider_request_body_state: Some(UsageBodyCaptureState::Truncated),
request_metadata: Some(json!({
"requested_reasoning_effort": "xhigh",
"provider_reasoning_effort": "max",
"provider_service_tier": "priority"
})),
..UsageEventData::default()
},
);
preserve_request_facts(&mut event);
let metadata = event
.data
.request_metadata
.as_ref()
.expect("metadata remains");
assert_eq!(metadata["requested_reasoning_effort"], "xhigh");
assert_eq!(metadata["provider_reasoning_effort"], "max");
assert_eq!(metadata["provider_service_tier"], "priority");
}
#[test]
fn preserve_request_facts_clears_stale_facts_when_final_bodies_are_missing() {
let mut event = UsageEvent::new(
UsageEventType::Completed,
"req-missing-final-body",
UsageEventData {
provider_name: "OpenAI".to_string(),
model: "gpt-5".to_string(),
request_body_state: Some(UsageBodyCaptureState::None),
provider_request_body_state: Some(UsageBodyCaptureState::None),
request_metadata: Some(json!({
"requested_reasoning_effort": "xhigh",
"provider_reasoning_effort": "max",
"provider_service_tier": "priority",
"provider_actual_service_tier": "priority"
})),
..UsageEventData::default()
},
);
preserve_request_facts(&mut event);
let metadata = event
.data
.request_metadata
.as_ref()
.expect("response audit fact remains");
assert!(metadata.get("requested_reasoning_effort").is_none());
assert!(metadata.get("provider_reasoning_effort").is_none());
assert!(metadata.get("provider_service_tier").is_none());
assert_eq!(metadata["provider_actual_service_tier"], "priority");
}
#[test]
fn event_capture_budget_full_policy_preserves_facts_before_zero_budget_and_database_mapping() {
let mut event = UsageEvent::new(
UsageEventType::Completed,
"event-capture-budget-facts",
UsageEventData {
provider_name: "openai".to_string(),
model: "gpt-5.6-sol".to_string(),
endpoint_api_format: Some("openai:responses".to_string()),
input_tokens: Some(100),
output_tokens: Some(500),
total_tokens: Some(600),
cache_creation_input_tokens: Some(25),
cache_read_input_tokens: Some(0),
request_body: Some(json!({"reasoning": {"effort": "high"}})),
provider_request_body: Some(json!({
"model": "gpt-5.6-sol", "reasoning": {"effort": "medium"},
"service_tier": "priority"
})),
response_body: Some(json!({"service_tier": "Default"})),
request_metadata: Some(json!({
"plan_usage_reservation_token": "550e8400-e29b-41d4-a716-446655440000"
})),
..UsageEventData::default()
},
);
preserve_request_facts(&mut event);
preserve_provider_response_facts(&mut event);
apply_usage_body_capture_policy_to_event(
UsageBodyCapturePolicy {
record_level: UsageRequestRecordLevel::Full,
},
&mut event,
);
let budget = Arc::new(crate::event_capture_budget::EventCaptureMemoryBudget::new(
0,
));
event.data.apply_capture_memory_budget(Arc::clone(&budget));
let record = crate::build_upsert_usage_record_from_event(&event).expect("database mapping");
assert_eq!(record.input_tokens, Some(100));
assert_eq!(record.output_tokens, Some(500));
assert_eq!(record.cache_creation_input_tokens, Some(25));
assert_eq!(record.cache_read_input_tokens, Some(0));
assert!(record.request_body.is_none());
assert!(record.provider_request_body.is_none());
assert!(record.response_body.is_none());
assert_eq!(
record.provider_request_body_state,
Some(UsageBodyCaptureState::Truncated)
);
let metadata = record.request_metadata.expect("preserved billing facts");
assert_eq!(metadata["requested_reasoning_effort"], "high");
assert_eq!(metadata["provider_reasoning_effort"], "medium");
assert_eq!(metadata["provider_service_tier"], "priority");
assert_eq!(metadata["provider_actual_service_tier"], "default");
assert_eq!(metadata["provider_cache_ttl_minutes"], 30);
assert_eq!(
metadata["plan_usage_reservation_token"],
"550e8400-e29b-41d4-a716-446655440000"
);
assert_eq!(budget.retained_bytes(), 0);
}
#[test]
fn basic_request_record_level_strips_body_capture_but_preserves_derived_fields() {
let mut event = UsageEvent::new(
UsageEventType::Failed,
"req-basic-1",
UsageEventData {
provider_name: "OpenAI".to_string(),
model: "gpt-5".to_string(),
total_tokens: Some(42),
error_message: Some("upstream failed".to_string()),
request_body: Some(json!({
"messages":[{"role":"user","content":"hello"}],
"reasoning": {"effort": "xhigh"}
})),
request_body_ref: Some("usage://request/req-basic-1/request_body".to_string()),
provider_request_body: Some(json!({
"model":"gpt-5",
"reasoning": {"effort": "max"},
"service_tier": "priority"
})),
provider_request_body_ref: Some(
"usage://request/req-basic-1/provider_request_body".to_string(),
),
response_body: Some(json!({
"error":{"message":"bad gateway"},
"service_tier": "Default"
})),
response_body_ref: Some("usage://request/req-basic-1/response_body".to_string()),
client_response_body: Some(json!({"detail":"bad gateway"})),
client_response_body_ref: Some(
"usage://request/req-basic-1/client_response_body".to_string(),
),
request_metadata: Some(json!({
"requested_reasoning_effort": "low",
"provider_reasoning_effort": "medium",
"provider_service_tier": "standard"
})),
..UsageEventData::default()
},
);
preserve_request_facts(&mut event);
preserve_provider_response_facts(&mut event);
apply_usage_body_capture_policy_to_event(
UsageBodyCapturePolicy {
record_level: UsageRequestRecordLevel::Basic,
},
&mut event,
);
assert_eq!(event.data.total_tokens, Some(42));
assert_eq!(event.data.error_message.as_deref(), Some("upstream failed"));
assert!(event.data.request_body.is_none());
assert!(event.data.request_body_ref.is_none());
assert!(event.data.provider_request_body.is_none());
assert!(event.data.provider_request_body_ref.is_none());
assert!(event.data.response_body.is_none());
assert!(event.data.response_body_ref.is_none());
assert!(event.data.client_response_body.is_none());
assert!(event.data.client_response_body_ref.is_none());
assert_eq!(
event
.data
.request_metadata
.as_ref()
.and_then(|metadata| metadata.get("requested_reasoning_effort"))
.and_then(serde_json::Value::as_str),
Some("xhigh")
);
assert_eq!(
event
.data
.request_metadata
.as_ref()
.and_then(|metadata| metadata.get("provider_reasoning_effort"))
.and_then(serde_json::Value::as_str),
Some("max")
);
assert_eq!(
event
.data
.request_metadata
.as_ref()
.and_then(|metadata| metadata.get("provider_service_tier"))
.and_then(serde_json::Value::as_str),
Some("priority")
);
assert_eq!(
event
.data
.request_metadata
.as_ref()
.and_then(|metadata| metadata.get("provider_actual_service_tier"))
.and_then(serde_json::Value::as_str),
Some("default")
);
}
}