From 98b8e799cfc036c9c8e745c90a9b7ac9ddc8d2b6 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 28 Sep 2026 13:51:14 +1000 Subject: [PATCH] Correct the cursor reset docs The reset guard's docs overstated the bug and understated what a reset re-fetches: - An undone reset delayed history rather than losing it: cursors are per snode, so the history arrived through another snode, and was lost only in a one-snode swarm. - A clear by namespace counts as a reset of every swarm, so a swarm it didn't clear re-fetches from its previous cursor, not the beginning. - Only regular messages are deduplicated. Config messages rely on merging being repeatable, and a kick seen again is stopped only by the key generation check. - Resets and guarded writes must not be made from inside a DB transaction, since the guard's lock is held across the SQL. No behaviour change. --- .../libsignal/database/LastMessageHashResets.kt | 4 ++++ .../libsignal/database/LokiAPIDatabaseProtocol.kt | 15 +++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/session/libsignal/database/LastMessageHashResets.kt b/app/src/main/java/org/session/libsignal/database/LastMessageHashResets.kt index 2a8753caf1..d68febfb27 100644 --- a/app/src/main/java/org/session/libsignal/database/LastMessageHashResets.kt +++ b/app/src/main/java/org/session/libsignal/database/LastMessageHashResets.kt @@ -15,6 +15,10 @@ value class LastMessageHashEpoch internal constructor(internal val resets: Long) * * Every reset and every guarded write runs under this object's lock, together with its storage operation, so * a reset cannot land between a write's check and the write itself. + * + * So the lock is held across the SQL, and neither a reset nor a write may be made from inside a database + * transaction. A caller holding the database while it waits for this lock, against a write holding this lock + * while it waits for the database, would deadlock. */ class LastMessageHashResets { private var resets = 0L diff --git a/app/src/main/java/org/session/libsignal/database/LokiAPIDatabaseProtocol.kt b/app/src/main/java/org/session/libsignal/database/LokiAPIDatabaseProtocol.kt index 63318f1e8e..3995cf3151 100644 --- a/app/src/main/java/org/session/libsignal/database/LokiAPIDatabaseProtocol.kt +++ b/app/src/main/java/org/session/libsignal/database/LokiAPIDatabaseProtocol.kt @@ -15,10 +15,17 @@ interface LokiAPIDatabaseProtocol { /** * Writes [newValue] as the cursor, unless [publicKey]'s cursors have been reset since [since]. * - * A reset asks for the swarm's history to be fetched again. A poll that was in flight when it happened - * would otherwise finish afterwards and write its position back, undoing the reset, and the history - * would never be fetched. Its messages are still handled; only the cursor write is dropped, so the next - * poll starts from the beginning and dedupe absorbs what it fetches twice. + * A reset asks for the swarm's history to be fetched again. Cursors are kept per snode, so a poll that + * was in flight when it happened would otherwise finish afterwards and write its position back for the + * snode it polled, undoing the reset there. That snode's history would then arrive only through another + * snode, and in a one-snode swarm not at all. The poll's messages are still handled; only the cursor write + * is dropped. + * + * What the next poll fetches then depends on the reset. Where the cursors were cleared, it starts from + * the beginning. A clear by namespace counts as a reset of every swarm, so a swarm whose cursor it did not + * clear starts again from its previous cursor and fetches the in-flight poll's messages a second time. + * Regular messages are deduplicated, config messages rely on merging being repeatable, and a kick message + * seen again is ignored only by the key generation check in its handler. * * @return whether the cursor was written. */