Rust SDK · BufferedRecorder

The default recorder buffers events in memory and flushes in batches on a background tokio task. record(...).await only enqueues, so it never blocks on the network:

use everscribe::recorder::RecorderOptions;
use std::time::Duration;

let rec = es.new_recorder(RecorderOptions {
    buffer_size: 2000,
    flush_interval: Duration::from_secs(2),
    ..Default::default()
});

Options

Configured with RecorderOptions { .., ..Default::default() }. Durations are std::time::Duration.

Field Default Purpose
buffer_size 1000 Capacity of the in-memory event buffer.
flush_size 100 Event count that triggers an immediate flush.
flush_interval 5s Max time between flushes when the size threshold isn't reached.
flush_timeout 30s Per-flush timeout for the batch HTTP call.
overflow DropNewest Behavior when the buffer is full.
drain_timeout 30s Max time close() waits for in-flight events.
request_timeout 10s Per-request HTTP timeout.
base_url production Override the ingestion endpoint (tests, staging).
auto_idempotency_key false Copy id into idempotency_key when empty.

Overflow policies

OverflowPolicy::DropNewest  // drop the incoming event, log a warning (default)
OverflowPolicy::Block       // await until space frees (the only awaiting case)
OverflowPolicy::Error       // return RecordError::BufferFull

Close & flush

  • close().await flushes pending events and waits up to drain_timeout, returning Err(DrainTimeout) if it can't finish. Call it exactly once, on the path that actually runs at shutdown.
  • flush().await drains everything buffered at call time. Useful in tests and for graceful-shutdown sync points.
  • stats() returns dropped / flushed / flush_errs / pending / buffer_size for observability.

Where close goes

axum::serve(...).await runs until the process is killed, so a close().await written after it is only reached if the server is shut down deliberately. Give serve a graceful-shutdown future and close the recorder once it returns.

First, keep a handle. The adapters take the recorder by value (EverscribeLayer::new(recorder, resolve) wraps it in an Arc internally and hands back no accessor), so put it behind your own Arc and give the layer a thin wrapper:

use std::sync::Arc;

use everscribe::event::Event;
use everscribe::recorder::{BufferedRecorder, RecordError, Recorder};

#[derive(Clone)]
struct SharedRecorder(Arc<BufferedRecorder>);

impl Recorder for SharedRecorder {
    async fn record(&self, e: Event) -> Result<(), RecordError> {
        self.0.record(e).await
    }
}

Then mount, serve, and drain:

let rec = Arc::new(es.new_recorder(RecorderOptions::default()));

let app = Router::new()
    .route("/login", post(login))
    .layer(EverscribeLayer::new(SharedRecorder(rec.clone()), resolve_actor));

let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
axum::serve(listener, app)
    .with_graceful_shutdown(shutdown_signal())
    .await?;

// Reached after the signal fired and in-flight requests finished, so the
// last request's events are in the buffer waiting to be drained.
if let Err(e) = rec.close().await {
    eprintln!("everscribe: drain incomplete: {e}");
}

The shutdown future is what makes the close reachable. SIGTERM is what a container runtime sends, so covering only ctrl_c leaves the deploy case dropping its buffer. Both need tokio's signal feature:

async fn shutdown_signal() {
    let ctrl_c = async {
        tokio::signal::ctrl_c().await.expect("ctrl-c handler");
    };
    let sigterm = async {
        tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
            .expect("SIGTERM handler")
            .recv()
            .await;
    };
    tokio::select! {
        _ = ctrl_c => {},
        _ = sigterm => {},
    }
}