fix(robustness): surface swallowed persistence-write failures + federation tombstone durability

§C of the 1.8.0 hardening plan: persistence writes whose Results were
silently dropped now log a warn/error with context (mesh contact
blocklist, scheduler state, content catalog, container registry,
update state, bitcoin relay, package install markers, server shutdown
state). §I: federation tombstones are now flushed durably in
storage/sync so cleared peers can't resurrect after a crash.

Tracker updated with shas in docs/1.8.0-RELEASE-HARDENING-PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-07-02 21:02:54 -04:00
co-authored by Claude Fable 5
parent 206d5fe8cf
commit 01cbec27ed
12 changed files with 102 additions and 40 deletions
@@ -862,7 +862,9 @@ async fn hydrate_tor_endpoint(data_dir: &Path, state: &mut BitcoinRelayState) {
let onion = onion.trim().trim_end_matches('/').to_string();
if !onion.is_empty() {
state.settings.tor_endpoint = Some(format!("http://{onion}/"));
let _ = save_relay_state(data_dir, state).await;
if let Err(e) = save_relay_state(data_dir, state).await {
tracing::warn!("Failed to persist relay tor endpoint: {e:#}");
}
}
}
}
+30 -12
View File
@@ -267,13 +267,16 @@ impl RpcHandler {
.context("Failed to connect to peer")?;
// Record which transport actually reached the peer (B14) so the UI
// reflects FIPS vs Tor truthfully instead of always showing Tor/none.
let _ = crate::federation::record_peer_transport(
if let Err(e) = crate::federation::record_peer_transport(
&self.config.data_dir,
None,
Some(onion),
&transport.to_string(),
)
.await;
.await
{
tracing::warn!("Failed to persist peer transport badge: {e:#}");
}
if response.status() == reqwest::StatusCode::PAYMENT_REQUIRED {
let body: serde_json::Value = response.json().await.unwrap_or_default();
@@ -348,13 +351,16 @@ impl RpcHandler {
.await
.context("Failed to connect to peer")?;
// Record which transport actually reached the peer (B14).
let _ = crate::federation::record_peer_transport(
if let Err(e) = crate::federation::record_peer_transport(
&self.config.data_dir,
None,
Some(onion),
&transport.to_string(),
)
.await;
.await
{
tracing::warn!("Failed to persist peer transport badge: {e:#}");
}
if !response.status().is_success() {
return Err(anyhow::anyhow!(
@@ -497,13 +503,16 @@ impl RpcHandler {
}
};
// Record which transport actually reached the peer (B14).
let _ = crate::federation::record_peer_transport(
if let Err(e) = crate::federation::record_peer_transport(
&self.config.data_dir,
None,
Some(onion),
&transport.to_string(),
)
.await;
.await
{
tracing::warn!("Failed to persist peer transport badge: {e:#}");
}
if response.status() == reqwest::StatusCode::PAYMENT_REQUIRED {
// Payment was rejected by the seller. Surface the most likely cause
@@ -763,13 +772,16 @@ impl RpcHandler {
}));
}
};
let _ = crate::federation::record_peer_transport(
if let Err(e) = crate::federation::record_peer_transport(
&self.config.data_dir,
None,
Some(onion),
&transport.to_string(),
)
.await;
.await
{
tracing::warn!("Failed to persist peer transport badge: {e:#}");
}
if response.status() == reqwest::StatusCode::PAYMENT_REQUIRED {
return Ok(serde_json::json!({
@@ -951,13 +963,16 @@ impl RpcHandler {
}));
}
};
let _ = crate::federation::record_peer_transport(
if let Err(e) = crate::federation::record_peer_transport(
&self.config.data_dir,
None,
Some(onion),
&transport.to_string(),
)
.await;
.await
{
tracing::warn!("Failed to persist peer transport badge: {e:#}");
}
if response.status() == reqwest::StatusCode::PAYMENT_REQUIRED {
return Ok(serde_json::json!({
@@ -1019,13 +1034,16 @@ impl RpcHandler {
.await
.context("Failed to connect to peer for preview")?;
// Record which transport actually reached the peer (B14).
let _ = crate::federation::record_peer_transport(
if let Err(e) = crate::federation::record_peer_transport(
&self.config.data_dir,
None,
Some(onion),
&transport.to_string(),
)
.await;
.await
{
tracing::warn!("Failed to persist peer transport badge: {e:#}");
}
if !response.status().is_success() {
return Err(anyhow::anyhow!(
+4 -1
View File
@@ -1,6 +1,7 @@
use super::super::RpcHandler;
use crate::mesh;
use anyhow::Result;
use tracing::warn;
impl RpcHandler {
/// mesh.status — Get mesh radio status, device info, and peer count.
@@ -283,7 +284,9 @@ impl RpcHandler {
let mut set = state.radio_contact_blocklist.write().await;
set.clear();
}
let _ = crate::mesh::save_ignored_radio_contacts(&data_dir, &[]).await;
if let Err(e) = crate::mesh::save_ignored_radio_contacts(&data_dir, &[]).await {
warn!("Failed to persist cleared radio-contact blocklist: {e:#}");
}
// Actually DELETE each radio contact from the firmware table (via
// CMD_REMOVE_CONTACT) so wiped peers don't just reappear on the next
@@ -32,6 +32,8 @@ pub(in crate::api::rpc) async fn install_log(msg: &str) {
.open(INSTALL_LOG)
.await
{
// Fire-and-forget by design: install-log lines are advisory, and a
// per-line warn would spam the very log stream that's failing.
let _ = f.write_all(line.as_bytes()).await;
}
}