Skip to content

Commit 0de9440

Browse files
alex-poorclaude
andcommitted
Give the desktop peer the two things the phones already had
A peer left running longer than five minutes caught up once on connect and then received nothing, for ever, while reporting healthy totals. Two causes, both of them things Ayni has and the peer did not. THE ADDRESS BOOK WAS NEVER PERSISTED. `AddressBook::builder()` was called without `.store()`, so p2panda kept it in memory: `node_infos_v1` and `topics2node_infos_v1` held ZERO rows while the peer reported `pool 4`. Its own documentation describes that book as how it identifies network partitions "which can be automatically healed" — a peer syncing with one node and never another is that sentence. Now persisted to its own SQLite file, separate from the keys store because p2panda-store uses max_connections(1) and two pools on one file is a writer-contention bug. 0 -> 4 nodes, 0 -> 12 topic-interest rows. AND IT NEVER RE-SUBSCRIBED ON A STALL. `Replicator::restream` exists and Ayni has called it since the phones hit this exact symptom: catch-up delivers on connect, then `LiveModeStarted` keeps firing while `received_live_operations` stays 0. The peer now does the same after three stalled passes — three, not one, because a quiet minute is ordinary when the publisher has nothing to say. Measured: `holding` flat at 1759 for three passes, `re-subscribed 8 topic(s)`, then 1762 on the next pass. Also adds the diagnostics that made this findable: `holding` from the store rather than `received()` (which counts store events and read 3,400 against 1,729 actually held), the sync event log, and a stall detector. ⚠️ THIS IS A WORKAROUND, THE SAME ONE THE PHONES RUN. It recovers from live mode going silent; it does not explain why live mode goes silent. Data arrives in bursts every few minutes rather than continuously. The underlying question is upstream-shaped and still open. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent cd0e6ee commit 0de9440

2 files changed

Lines changed: 59 additions & 2 deletions

File tree

‎crates/diaswarm-net/src/swarm.rs‎

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,41 @@ impl Swarm {
172172
let store = store.into();
173173
std::fs::create_dir_all(&store)?;
174174

175-
let book = AddressBook::builder().spawn().await.context("address book")?;
175+
// **GIVE THE ADDRESS BOOK A STORE, WHICH IS WHAT IT IS FOR.**
176+
// `AddressBook::builder()` with no store keeps everything in memory, so
177+
// `node_infos_v1` and `topics2node_infos_v1` stay empty for ever and a
178+
// peer restarts knowing nobody. Measured on a laptop: `pool 4` reported
179+
// while the persisted node table held **zero** rows.
180+
//
181+
// p2panda's own description of what is being thrown away: the address
182+
// book "is an important tool to watch for transport information
183+
// changes, keep track of stale nodes and identify network partitions
184+
// which can be automatically healed". A peer that syncs with one node
185+
// and never another is that last sentence exactly.
186+
//
187+
// **ITS OWN FILE, NOT THE KEYS STORE.** `p2panda-store` builds its pool
188+
// with `max_connections(1)` and no busy timeout, so two pools on one
189+
// SQLite file is a writer-contention bug rather than an untidiness —
190+
// the note on `Pooled` in `diaswarm-android` says so at length. A
191+
// separate file costs nothing and shares nothing.
192+
//
193+
// Done here rather than as a parameter because `Swarm` already owns a
194+
// directory and there are 47 call sites that should not have to care.
195+
let book_url = format!("sqlite://{}", store.join("addressbook.sqlite").display());
196+
let book = match p2panda_store::SqliteStoreBuilder::new()
197+
.database_url(&book_url)
198+
.create_database(true)
199+
.build()
200+
.await
201+
{
202+
Ok(db) => AddressBook::builder().store(db).spawn().await.context("address book")?,
203+
Err(e) => {
204+
// A peer with an unpersisted address book still works; it just
205+
// forgets everyone on restart. Worth saying, not worth refusing.
206+
eprintln!("diaswarm: address book not persisted ({e}) — peers are forgotten on restart");
207+
AddressBook::builder().spawn().await.context("address book")?
208+
}
209+
};
176210

177211
// SEED WHOEVER WE ALREADY KNOW, BEFORE THE ENDPOINT STARTS. A follower
178212
// scanned exactly one thing: the subject's node id. That is enough to

‎crates/diaswarm-peer/src/main.rs‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,11 @@ async fn main() -> Result<()> {
234234
diaswarm_net::share::carry_share(&swarm, &replicator, &dir, &own, args.adopt).await;
235235
let bytes = disk_bytes(&dir);
236236
let stored = held(&store, &replicator.carried()).await;
237+
// **HOW MANY SUBJECTS IT HAS HEARD OF BUT IS NOT YET CARRYING.**
238+
// Without this, "carrying 1" is ambiguous between "nobody has told me
239+
// about anybody" and "I was told and did not act", which are different
240+
// bugs in different files. Costs one in-memory read.
241+
let heard_waiting = swarm.keys_wanted().await.map(|w| w.len()).unwrap_or(0);
237242
if stored > was_held {
238243
stalled_for = 0;
239244
} else if stored > 0 {
@@ -251,8 +256,9 @@ async fn main() -> Result<()> {
251256
);
252257
} else {
253258
println!(
254-
"pool {} · carrying {} (+{}) · holding {} · pushes {} · {}{}",
259+
"pool {} · heard {} · carrying {} (+{}) · holding {} · pushes {} · {}{}",
255260
t.pool,
261+
heard_waiting,
256262
s.carrying,
257263
s.adopted,
258264
stored,
@@ -265,6 +271,23 @@ async fn main() -> Result<()> {
265271
},
266272
);
267273
}
274+
// **RE-SUBSCRIBE WHEN IT HAS CLEARLY STOPPED**, which is the
275+
// remedy Ayni has had since the phones hit this exact symptom:
276+
// catch-up delivers once on connect and live mode then produces
277+
// nothing for ever, `received_live_operations: 0` on every
278+
// session while `LiveModeStarted` keeps firing. Dropping the
279+
// handles and streaming the topics again is what brings it
280+
// back there, and the peer simply never had it.
281+
//
282+
// Three passes, not one: a pass with nothing new is ordinary
283+
// when the publisher has nothing to say, and re-subscribing on
284+
// every quiet minute would be its own kind of broken.
285+
if stalled_for >= 3 && stalled_for % 3 == 0 {
286+
match replicator.restream().await {
287+
Ok(n) => println!(" stalled — re-subscribed {n} topic(s)"),
288+
Err(e) => println!(" stalled — re-subscribe failed: {e:#}"),
289+
}
290+
}
268291
// The events, when asked for, or unprompted once a stall is
269292
// undeniable — because that is the moment somebody needs them.
270293
if args.events > 0 || stalled_for == 3 {

0 commit comments

Comments
 (0)