commit a7bf20f220393b2905e4cd4fbb71564de71d1c62
parent d4a5a46bd0209d04787f83447f094a2c42c13dd2
Author: triesap <tyson@radroots.org>
Date: Sun, 14 Jun 2026 08:38:02 -0700
ops: gate readiness on server bind
Diffstat:
6 files changed, 50 insertions(+), 4 deletions(-)
diff --git a/crates/tangle/tests/version.rs b/crates/tangle/tests/version.rs
@@ -187,6 +187,7 @@ fn tangle_run_starts_server_and_stays_alive_until_shutdown() {
assert_eq!(health_value["status"], "ok");
assert_eq!(ready_value["status"], "ready");
+ assert_eq!(ready_value["checks"]["server_bind"], "ready");
assert_eq!(ready_value["checks"]["pocket_storage"], "ready");
assert_eq!(nip11_value["name"], "tangle");
assert_eq!(nip11_value["limitation"]["max_message_length"], 1_048_576);
diff --git a/crates/tangle_runtime/src/ops.rs b/crates/tangle_runtime/src/ops.rs
@@ -27,6 +27,7 @@ impl BaseRelayReadinessCheckStatus {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BaseRelayReadinessState {
config: BaseRelayReadinessCheckStatus,
+ server_bind: BaseRelayReadinessCheckStatus,
relay_identity: BaseRelayReadinessCheckStatus,
pocket_storage: BaseRelayReadinessCheckStatus,
group_projection: BaseRelayReadinessCheckStatus,
@@ -36,6 +37,7 @@ pub struct BaseRelayReadinessState {
impl BaseRelayReadinessState {
pub fn new(
config: BaseRelayReadinessCheckStatus,
+ server_bind: BaseRelayReadinessCheckStatus,
relay_identity: BaseRelayReadinessCheckStatus,
pocket_storage: BaseRelayReadinessCheckStatus,
group_projection: BaseRelayReadinessCheckStatus,
@@ -43,6 +45,7 @@ impl BaseRelayReadinessState {
) -> Self {
Self {
config,
+ server_bind,
relay_identity,
pocket_storage,
group_projection,
@@ -57,12 +60,30 @@ impl BaseRelayReadinessState {
BaseRelayReadinessCheckStatus::Ready,
BaseRelayReadinessCheckStatus::Ready,
BaseRelayReadinessCheckStatus::Ready,
+ BaseRelayReadinessCheckStatus::Ready,
+ )
+ }
+
+ pub fn runtime_ready_before_bind() -> Self {
+ Self::new(
+ BaseRelayReadinessCheckStatus::Ready,
+ BaseRelayReadinessCheckStatus::NotReady,
+ BaseRelayReadinessCheckStatus::Ready,
+ BaseRelayReadinessCheckStatus::Ready,
+ BaseRelayReadinessCheckStatus::Ready,
+ BaseRelayReadinessCheckStatus::Ready,
)
}
+ pub fn with_server_bind(mut self, server_bind: BaseRelayReadinessCheckStatus) -> Self {
+ self.server_bind = server_bind;
+ self
+ }
+
pub fn is_ready(&self) -> bool {
[
self.config,
+ self.server_bind,
self.relay_identity,
self.pocket_storage,
self.group_projection,
@@ -81,6 +102,7 @@ impl BaseRelayReadinessState {
},
checks: BaseRelayReadinessChecksDocument {
config: self.config.as_str().to_owned(),
+ server_bind: self.server_bind.as_str().to_owned(),
relay_identity: self.relay_identity.as_str().to_owned(),
pocket_storage: self.pocket_storage.as_str().to_owned(),
group_projection: self.group_projection.as_str().to_owned(),
@@ -104,6 +126,7 @@ pub struct BaseRelayReadinessDocument {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BaseRelayReadinessChecksDocument {
pub config: String,
+ pub server_bind: String,
pub relay_identity: String,
pub pocket_storage: String,
pub group_projection: String,
@@ -195,6 +218,7 @@ mod tests {
let ready_body = to_bytes(ready.into_body(), usize::MAX).await.expect("body");
let ready_value = serde_json::from_slice::<serde_json::Value>(&ready_body).expect("json");
assert_eq!(ready_value["status"], "ready");
+ assert_eq!(ready_value["checks"]["server_bind"], "ready");
assert_eq!(ready_value["checks"]["group_outbox_replay"], "ready");
let metrics_response = base_relay_ops_router(BaseRelayReadinessState::ready(), metrics)
.oneshot(
@@ -222,6 +246,7 @@ mod tests {
BaseRelayReadinessCheckStatus::Ready,
BaseRelayReadinessCheckStatus::Ready,
BaseRelayReadinessCheckStatus::Ready,
+ BaseRelayReadinessCheckStatus::Ready,
BaseRelayReadinessCheckStatus::NotReady,
BaseRelayReadinessCheckStatus::Ready,
);
@@ -242,6 +267,7 @@ mod tests {
let rejected_value =
serde_json::from_slice::<serde_json::Value>(&rejected_body).expect("json");
assert_eq!(rejected_value["status"], "not_ready");
+ assert_eq!(rejected_value["checks"]["server_bind"], "ready");
assert_eq!(rejected_value["checks"]["group_projection"], "not_ready");
}
}
diff --git a/crates/tangle_runtime/src/relay/core.rs b/crates/tangle_runtime/src/relay/core.rs
@@ -361,7 +361,7 @@ impl BaseRelay {
let groups = GroupService::from_config(&store, groups)?;
let subscriptions =
LiveSubscriptionSet::new(limits.max_pending_events(), limits.max_subscriptions())?;
- let readiness = BaseRelayReadinessState::ready();
+ let readiness = BaseRelayReadinessState::runtime_ready_before_bind();
Ok(Self {
store,
subscriptions,
diff --git a/crates/tangle_runtime/src/runtime.rs b/crates/tangle_runtime/src/runtime.rs
@@ -1095,7 +1095,19 @@ mod tests {
assert_eq!(runtime.metrics().active_sessions(), 0);
assert_eq!(runtime.metrics().stored_event_offsets(), 0);
assert!(runtime.relay().groups_enabled());
- assert!(runtime.readiness_state().is_ready());
+ assert!(!runtime.readiness_state().is_ready());
+ assert_eq!(
+ runtime.readiness_state().response().checks.server_bind,
+ "not_ready"
+ );
+ assert_eq!(
+ runtime
+ .readiness_state()
+ .response()
+ .checks
+ .group_outbox_replay,
+ "ready"
+ );
assert!(!*shutdown.borrow());
assert_eq!(runtime.event_bus().publish(StoreOffset::new(42)), 1);
diff --git a/crates/tangle_runtime/src/server.rs b/crates/tangle_runtime/src/server.rs
@@ -4,7 +4,7 @@ use crate::{
errors::BaseRelayError,
logging,
nip11::{BaseRelayInfoConfig, BaseRelayInfoDocument, base_relay_info_response},
- ops::{BaseRelayReadinessState, base_relay_ops_router},
+ ops::{BaseRelayReadinessCheckStatus, BaseRelayReadinessState, base_relay_ops_router},
runtime::{
TangleRuntime, TangleRuntimeHandle, TangleRuntimeLimits, TangleRuntimeMetrics,
TangleShutdownSignal,
@@ -65,7 +65,10 @@ pub async fn serve_listener_until_shutdown(
.map_err(|error| BaseRelayError::error(error.to_string()))?;
let relay_url = runtime.config().relay_url().to_owned();
let info = BaseRelayInfoConfig::new("tangle", runtime.config())?.build_document()?;
- let readiness = runtime.readiness_state().clone();
+ let readiness = runtime
+ .readiness_state()
+ .clone()
+ .with_server_bind(BaseRelayReadinessCheckStatus::Ready);
let limits = runtime.limits();
let metrics = runtime.metrics().clone();
let shutdown_signal = runtime.shutdown_signal().clone();
@@ -487,6 +490,9 @@ mod tests {
assert_eq!(root_without_accept.status(), http::StatusCode::NOT_FOUND);
assert_eq!(health.status(), http::StatusCode::OK);
assert_eq!(ready.status(), http::StatusCode::OK);
+ let ready_body = to_bytes(ready.into_body(), usize::MAX).await.expect("body");
+ let ready_value = serde_json::from_slice::<serde_json::Value>(&ready_body).expect("json");
+ assert_eq!(ready_value["checks"]["server_bind"], "ready");
assert_eq!(metrics.status(), http::StatusCode::OK);
let metrics_body = to_bytes(metrics.into_body(), usize::MAX)
.await
diff --git a/crates/tangle_runtime/tests/phase2_acceptance_targets.rs b/crates/tangle_runtime/tests/phase2_acceptance_targets.rs
@@ -52,6 +52,7 @@ async fn tangle_run_serves_until_shutdown() {
assert!(health.contains(r#""status":"ok""#));
assert!(ready.contains(r#""status":"ready""#));
+ assert!(ready.contains(r#""server_bind":"ready""#));
assert!(metrics.contains(r#""active_sessions":0"#));
assert!(metrics.contains(r#""stored_event_offsets":0"#));
assert!(nip11.contains(r#""name":"tangle""#));