//! Bounded, asynchronous persistence for data-plane invocation telemetry. //! //! Invocation responses must not wait for libSQL, so producers use //! [`tokio::sync::mpsc::Sender::try_send`] and one task writes ordered batches. //! When the queue is full or closed, the update is deliberately dropped and a //! warning is emitted. These counters and events are operational telemetry; //! they are not suitable as a billing ledger or audit log. use std::sync::Arc; use tokio::sync::{Mutex, mpsc, oneshot}; use crate::persistence::{InvocationUpdate, Persistence}; const QUEUE_CAPACITY: usize = 1024; const MAX_BATCH_SIZE: usize = 64; /// Non-blocking producer used by Wasmtime invocation threads. /// /// All producers share one FIFO queue. Queue order is preserved for accepted /// records, but a process crash can lose records that have not yet reached /// libSQL. pub(crate) struct InvocationPersistence { sender: mpsc::Sender, } enum Command { Record(InvocationUpdate), Flush(oneshot::Sender<()>), } impl InvocationPersistence { /// Starts the single ordered writer on the current Tokio runtime. /// /// `sync` is shared with full Console snapshot persistence. Taking it /// around each batch prevents a snapshot transaction from racing a metric /// delta transaction. pub(crate) fn start(persistence: Persistence, sync: Arc>) -> Self { let (sender, mut receiver) = mpsc::channel(QUEUE_CAPACITY); tokio::spawn(async move { while let Some(first) = receiver.recv().await { let mut batch = Vec::with_capacity(MAX_BATCH_SIZE); let mut flushes = Vec::new(); match first { Command::Record(update) => batch.push(update), Command::Flush(flush) => flushes.push(flush), } while batch.len() < MAX_BATCH_SIZE { match receiver.try_recv() { Ok(Command::Record(update)) => batch.push(update), Ok(Command::Flush(flush)) => flushes.push(flush), Err(_) => break, } } if !batch.is_empty() { let _sync = sync.lock().await; if let Err(error) = persistence.record_invocations(batch).await { tracing::error!(%error, "failed to persist invocation telemetry"); } } // Flush is an ordering barrier, not a durability receipt. A // database error above is logged rather than returned, and a // flush may wait for records dequeued after the marker when // those records fit in the same batch. for flush in flushes { let _ = flush.send(()); } } }); Self { sender } } /// Enqueues telemetry without extending API response latency. /// /// Saturation drops this update instead of applying backpressure to the /// Component invocation path. pub(crate) fn record(&self, update: InvocationUpdate) { if let Err(error) = self.sender.try_send(Command::Record(update)) { tracing::warn!(%error, "invocation telemetry queue is unavailable; update dropped"); } } /// Waits until all telemetry queued before this call has been attempted. /// /// This method is used before a control-plane snapshot so the snapshot /// reads the newest in-memory counters after older delta writes. It does /// not report persistence failures; those are visible through tracing. pub(crate) async fn flush(&self) { let (sender, receiver) = oneshot::channel(); if self.sender.send(Command::Flush(sender)).await.is_ok() { let _ = receiver.await; } } }