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().awaitflushes pending events and waits up todrain_timeout, returningErr(DrainTimeout)if it can't finish. Call it exactly once, on the path that actually runs at shutdown.flush().awaitdrains everything buffered at call time. Useful in tests and for graceful-shutdown sync points.stats()returnsdropped/flushed/flush_errs/pending/buffer_sizefor 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 => {},
}
}