Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
0d194d5
fix(relay): make readiness local and stop dropping sockets on DB errors
TheSentinel454 Sep 4, 2026
b794583
test(relay): cover Redis bootstrap timeout
TheSentinel454 Sep 4, 2026
e354cea
Fix relay permissions in PostgreSQL CI
TheSentinel454 Sep 5, 2026
4882aaf
fix(relay): sample readiness once per probe
Sep 16, 2026
3dd90fd
fix(relay): fail closed when the community lifecycle lookup errors
Sep 16, 2026
04bb1f1
refactor(relay): sample dependencies on a bounded per-pod loop
Sep 16, 2026
fe0ef07
refactor(relay): drop the dependency sample-age gauge
Sep 16, 2026
9bb5ad1
test(relay): bind the dependency sampler to real startup
Sep 16, 2026
1695099
feat(relay): publish the dependency sample completion timestamp
Sep 16, 2026
a40e431
docs(relay): scope the sample-timestamp gauge to the server contract
Sep 16, 2026
52c3a48
fix(relay): retain readiness completion timestamp metric across idle …
TheSentinel454 Sep 24, 2026
ac4ae53
test(ci): include relay readiness and router unit selectors
TheSentinel454 Sep 24, 2026
c34df32
relay readiness: keep completion epoch as gauge with republisher
TheSentinel454 Sep 24, 2026
7c201b7
fix(relay): stop a racing republish from regressing the completion epoch
TheSentinel454 Sep 25, 2026
133862e
fix(relay): unify readiness sampler+publisher startup seam
TheSentinel454 Sep 25, 2026
a221a06
fix(relay): reuse readiness runtime runners
TheSentinel454 Sep 25, 2026
91e5f96
test(relay): bound readiness sampler startup wait
TheSentinel454 Sep 25, 2026
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
2 changes: 2 additions & 0 deletions .github/workflows/_ci-relay.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ jobs:
with:
name: desktop-e2e-relay
path: target/ci
- name: Restore relay executable permission
run: chmod +x ./target/ci/buzz-relay
- name: PostgreSQL-backed tests
env:
BUZZ_POSTGRES_ADMIN_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/postgres
Expand Down
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,7 @@ pub enum AuthState { Pending { challenge: String }, Authenticated(AuthContext),
| GET | `/.well-known/nostr.json` | NIP-05 identity |
| GET | `/health` | Health check |
| GET | `/_liveness` | Liveness probe |
| GET | `/_readiness` | Readiness probe |
| GET | `/_readiness` | Readiness probe — local process lifecycle only |
| POST | `/events` | Submit a signed Nostr event over HTTP (same ingest path as WebSocket `EVENT`) |
| POST | `/query` | Query Nostr events over HTTP with NIP-01 filters |
| POST | `/count` | Count Nostr events over HTTP with NIP-45 filters |
Expand Down
2 changes: 1 addition & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ test-unit:
# the ~30s sqlx acquire timeout, so they do not belong in the infra-free
# unit job either.
cargo nextest run -p buzz-relay --lib \
-E 'test(/^api::admin::/) + test(/^handlers::channel_authz::/) + test(/^handlers::moderation_authz::/) + test(/^handlers::side_effects::tests::/) + test(/^storage_sweep::tests::/)'
-E 'test(/^api::admin::/) + test(/^handlers::channel_authz::/) + test(/^handlers::moderation_authz::/) + test(/^handlers::side_effects::tests::/) + test(/^storage_sweep::tests::/) + test(/^readiness::tests::/) + test(/^router::tests::/) + test(=state::tests::neither_a_confirmed_inactive_community_nor_a_failed_lookup_admits_the_socket)'
# ACP author-gate and queue tests protect the trust boundary between
# relay events and agent prompts. They are infra-free; ignored lifecycle
# tests remain excluded and run in their dedicated integration lanes.
Expand Down
3 changes: 2 additions & 1 deletion crates/buzz-relay/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ pub mod nip11;
pub mod protocol;
/// Durable NIP-PL matcher and delivery worker.
pub mod push_runtime;
mod readiness;
/// Readiness-probe telemetry and the per-pod dependency sampler behind `/_status`.
pub mod readiness;
/// Axum router construction.
pub mod router;
/// Shared application state.
Expand Down
25 changes: 24 additions & 1 deletion crates/buzz-relay/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ async fn run_relay_main(boot: BootTracker) -> anyhow::Result<()> {

let usage_interval_secs = usage_metrics_interval_secs();
let usage_idle_timeout_secs = usage_metrics_idle_timeout_secs(usage_interval_secs);
let dependency_sample_completion_republish_interval =
buzz_relay::readiness::dependency_sample_completion_republish_interval(
usage_idle_timeout_secs,
);
let (boot, ()) = boot.run_required(
StartupPhase::MetricsBind,
|| relay_metrics::try_install(config.metrics_port, usage_idle_timeout_secs),
Expand All @@ -244,6 +248,7 @@ async fn run_relay_main(boot: BootTracker) -> anyhow::Result<()> {
info!(
port = config.metrics_port,
idle_timeout_secs = usage_idle_timeout_secs,
completion_republish_secs = dependency_sample_completion_republish_interval.as_secs(),
"Prometheus metrics exporter started"
);

Expand Down Expand Up @@ -455,7 +460,13 @@ async fn run_relay_main(boot: BootTracker) -> anyhow::Result<()> {
cfg.create_pool(Some(deadpool_redis::Runtime::Tokio1))
.map_err(|e| anyhow::anyhow!("Redis pool creation failed: {e}"))?
};
let redis_health_pool = redis_pool.clone(); // cheap Arc clone — shared with readiness handler
let redis_health_pool = redis_pool.clone(); // cheap Arc clone — shared with AppState
// One-time bootstrap gate, deliberately before AppState and therefore before
// the health listener binds. Post-start Redis failures are dependency
// failures and must never move readiness; never having connected at all is
// a broken deployment, not a blip.
buzz_relay::state::verify_redis_command_path(&redis_health_pool).await?;
info!("Redis command path connected");
let pubsub = Arc::new(
PubSubManager::new(&config.redis_url, redis_pool)
.await
Expand Down Expand Up @@ -1043,6 +1054,16 @@ async fn run_relay_main(boot: BootTracker) -> anyhow::Result<()> {
));
}

// Per-pod dependency diagnostics runtime: one seam starts the dependency
// sampler and its independent completion-epoch republisher together.
{
let diagnostics_state = Arc::clone(&state);
buzz_relay::readiness::start_dependency_sampler_and_completion_publisher(
diagnostics_state,
dependency_sample_completion_republish_interval,
);
}

// Cross-pod connection-control consumer: receive disconnect commands from
// Redis pub/sub (published by the pod that recorded a ban) and close any
// matching local sockets. A member's live connections may land on any pod,
Expand Down Expand Up @@ -1216,6 +1237,8 @@ async fn run_relay_main(boot: BootTracker) -> anyhow::Result<()> {

serve(router, health_router, Arc::clone(&state)).await?;
state.community_revalidator_cancel.cancel();
state.dependency_sampler_cancel.cancel();
state.dependency_completion_publisher_cancel.cancel();

// Signal the audit worker to stop accepting, flush buffered entries, and
// exit. Uses a CancellationToken so it works regardless of how many
Expand Down
41 changes: 35 additions & 6 deletions crates/buzz-relay/src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ pub fn try_install(port: u16, gauge_idle_timeout_secs: u64) -> Result<(), Metric
metrics::set_global_recorder(recorder)
.map_err(|_error| MetricsInstallError::RecorderConflict)?;
describe_readiness_metrics();
describe_community_admission_metrics();
describe_db_pool_metrics();
describe_auth_metrics();
initialize_auth_metric_series();
Expand All @@ -306,24 +307,42 @@ pub fn install(port: u16, gauge_idle_timeout_secs: u64) {
.unwrap_or_else(|error| panic!("metrics exporter must install exactly once: {error}"));
}

/// Register the frozen readiness metric descriptions with the active recorder.
/// Register the frozen readiness and dependency-diagnostic metric descriptions.
///
/// The two `buzz_readiness_*` probe families describe local process lifecycle.
/// The dependency families keep their names for dashboard continuity but are
/// published by the per-pod dependency sampler, not by the Kubernetes probe or
/// by an `/_status` request — a shared-dependency failure no longer deroutes
/// the pod, and nobody has to read the endpoint for the metrics to move.
pub(crate) fn describe_readiness_metrics() {
metrics::describe_counter!(
"buzz_readiness_checks_total",
"Kubernetes health-listener readiness probes by terminal bounded reason"
"Kubernetes health-listener readiness probes by lifecycle reason (ready, shutting_down)"
);
metrics::describe_counter!(
"buzz_readiness_dependency_checks_total",
"Completed readiness dependency attempts by dependency and bounded outcome"
"Completed dependency-sampler attempts by dependency and bounded outcome"
);
metrics::describe_histogram!(
"buzz_readiness_check_duration_seconds",
metrics::Unit::Seconds,
"Completed readiness check duration without outcome label multiplication"
"Completed dependency-sampler check duration without outcome label multiplication"
);
metrics::describe_gauge!(
"buzz_readiness_state",
"Latest publishable readiness state by check, where 1 is ready and 0 is not ready"
"Latest private readiness-probe observation, where 1 is ready and 0 is shutting down"
);
metrics::describe_gauge!(
"buzz_readiness_dependency_sample_completed_timestamp_seconds",
"Unix time the cached /_status dependency report completed, absent until the first sample completes; sampler completion advances it and the publisher re-emits it"
);
}

/// Register the bounded community-admission contract.
pub(crate) fn describe_community_admission_metrics() {
metrics::describe_counter!(
"buzz_community_admission_checks_total",
"Durable community-active checks at socket admission by bounded outcome"
);
}

Expand Down Expand Up @@ -541,7 +560,17 @@ pub(crate) fn readiness_test_recorder() -> (
metrics_exporter_prometheus::PrometheusRecorder,
metrics_exporter_prometheus::PrometheusHandle,
) {
let recorder = configured_prometheus_builder(300).build_recorder();
readiness_test_recorder_with_idle_timeout(300)
}

#[cfg(test)]
pub(crate) fn readiness_test_recorder_with_idle_timeout(
gauge_idle_timeout_secs: u64,
) -> (
metrics_exporter_prometheus::PrometheusRecorder,
metrics_exporter_prometheus::PrometheusHandle,
) {
let recorder = configured_prometheus_builder(gauge_idle_timeout_secs).build_recorder();
let handle = recorder.handle();
(recorder, handle)
}
Expand Down
Loading
Loading