Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ PGUSER=buzz
PGPASSWORD=buzz_dev
PGDATABASE=buzz

# Fleet usage telemetry is replica-only and independent of serving-read
# routing. This freshness budget defaults to 30000ms; 0 disables the database
# telemetry families. When the reader is absent, stale, or unavailable, the
# relay skips the collection and reports its availability as 0 instead of
# falling back to the writer.
# BUZZ_USAGE_METRICS_REPLICA_MAX_AGE_MS=30000
# Per-community metric series are disabled by default to bound relay memory and
# monitoring cardinality. `all` is the temporary rollback mode.
# BUZZ_USAGE_METRICS_PER_COMMUNITY=off

# -----------------------------------------------------------------------------
# Redis 7
# -----------------------------------------------------------------------------
Expand Down
44 changes: 44 additions & 0 deletions crates/buzz-db/src/runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ pub struct Db {
/// bounded-stale read semantics are a product decision, not an
/// invariant, so the gate ships off.
pub(crate) replica_read_max_age: Option<Duration>,
/// Replica freshness budget used only by the leader's fleet telemetry
/// queries. This is independent of serving-read rollout policy: telemetry
/// may be stale or skipped, but it must never fall back to the writer.
pub(crate) usage_metrics_replica_max_age: Option<Duration>,
/// Whether the reader endpoint supports the Aurora PostgreSQL identity
/// function ([`replica_fence::AURORA_IDENTITY_FN`]) — probed
/// once per process on the first routed read (on a plain autocommit
Expand Down Expand Up @@ -454,6 +458,10 @@ pub struct DbConfig {
/// than the staleness gate never routes anyway, so a larger budget
/// would only misrepresent the config.
pub replica_read_max_age_ms: u64,
/// Replica freshness budget for usage telemetry, in milliseconds
/// (`BUZZ_USAGE_METRICS_REPLICA_MAX_AGE_MS`). `0` disables fleet database
/// telemetry. This does not enable replica routing for serving reads.
pub usage_metrics_replica_max_age_ms: u64,
/// Session `lock_timeout` in milliseconds for writer connections (env
/// `BUZZ_DB_LOCK_TIMEOUT_MS`). `0` disables the timeout.
pub lock_timeout_ms: u64,
Expand Down Expand Up @@ -481,6 +489,7 @@ impl Default for DbConfig {
max_lifetime_secs: 1800,
idle_timeout_secs: 600,
replica_read_max_age_ms: 0,
usage_metrics_replica_max_age_ms: 30_000,
lock_timeout_ms: DEFAULT_LOCK_TIMEOUT_MS,
idle_txn_timeout_ms: DEFAULT_IDLE_TXN_TIMEOUT_MS,
statement_timeout_ms: 0,
Expand Down Expand Up @@ -542,13 +551,16 @@ impl Db {
None => None,
};
let replica_read_max_age = read_budget_from_ms(config.replica_read_max_age_ms);
let usage_metrics_replica_max_age =
read_budget_from_ms(config.usage_metrics_replica_max_age_ms);
Ok(Self {
pool,
max_connections: config.max_connections,
read_pool,
read_max_connections,
fence: std::sync::Arc::new(replica_fence::ReplicaFence::new()),
replica_read_max_age,
usage_metrics_replica_max_age,
reader_aurora_identity: std::sync::Arc::new(std::sync::OnceLock::new()),
})
}
Expand Down Expand Up @@ -697,6 +709,7 @@ impl Db {
read_pool: None,
fence: std::sync::Arc::new(replica_fence::ReplicaFence::new()),
replica_read_max_age: None,
usage_metrics_replica_max_age: None,
reader_aurora_identity: std::sync::Arc::new(std::sync::OnceLock::new()),
}
}
Expand All @@ -716,6 +729,7 @@ impl Db {
read_pool: Some(read_pool),
fence: std::sync::Arc::new(replica_fence::ReplicaFence::new()),
replica_read_max_age: None,
usage_metrics_replica_max_age: Some(replica_fence::FENCE_STALENESS),
reader_aurora_identity: std::sync::Arc::new(std::sync::OnceLock::new()),
}
}
Expand Down Expand Up @@ -1020,6 +1034,36 @@ impl Db {
Ok(result)
}

/// Route a usage-telemetry query only to a proved, sufficiently fresh
/// reader. Unlike serving reads, an unavailable reader is recorded as
/// skipped and never represented as or executed on the writer.
pub(crate) async fn route_usage_read(
&self,
path: &'static str,
) -> Option<(sqlx::Transaction<'static, sqlx::Postgres>, &'static str)> {
let skip = |reason| {
Self::record_route(path, "skipped", reason);
None
};
let Some(read_pool) = &self.read_pool else {
return skip("disabled");
};
let Some(budget) = self.usage_metrics_replica_max_age else {
return skip("disabled");
};
let Some(newest) = self.fence.newest() else {
return skip("uninitialized");
};
if newest.committed_at.elapsed() > budget {
return skip("stale");
}
match self.proved_reader(read_pool).await {
Ok((tx, entry)) if entry.committed_at.elapsed() <= budget => Some((tx, "fresh")),
Ok((_tx, _entry)) => skip("stale"),
Err(reason) => skip(reason),
}
}

/// Shared route decision for one read: evaluate the predicate against a
/// proved reader session and record the decision. Fail closed to the
/// writer everywhere.
Expand Down
Loading