Broadcast a session's QUIT once when its clients leave together (closes #154) #168

Merged
clawbot merged 1 commits from issue-154-quit-once into next 2026-10-21 01:48:27 +02:00
Collaborator

Fixes #154, including the JOIN race added in #154 (comment).

db.QuitSession ends a session in one transaction: it reads the sessions sharing a channel with it, parts it from each channel, deleting any left empty, with the statements of PartChannelAndDeleteIfEmpty, deletes the session and, only when that delete removed the row, stores the QUIT and queues it to those sessions. db.LogoutClient deletes the client and counts the remaining ones in the same transaction; HandleLogout reaches it through the new Service.Logout. BroadcastQuit and Logout wake those sessions' long polls after the commit. InsertMessage and EnqueueToSession now wrap helpers the teardown also calls inside its transaction. DeleteClient, ClientCountForSession, DeleteSession, PartChannel and DeleteChannelIfEmpty lost their last callers and are removed.

Not visible in the diff:

  • The QUIT is queued before the nick is free, so nothing a new holder of the nick sends reaches a peer ahead of it.
  • A logout whose transaction fails now changes nothing and answers 500; before, a failed count was logged and the session torn down anyway.
  • The long-poll tests send the QUIT only once the poll waits on the broker, because of #157.

Disclosures:

  • Judgement call: the HTTP race tests start a server per trial, because each trial logs in and logins are rate limited.
  • Judgement call: the check that the session delete removed a row stays, as the issue asks, though no test can fail without it: a deleted session has no channel peers left.

Model: opus-5-5

Fixes https://git.eeqj.de/sneak/neoirc/issues/154, including the JOIN race added in https://git.eeqj.de/sneak/neoirc/issues/154#issuecomment-152075. `db.QuitSession` ends a session in one transaction: it reads the sessions sharing a channel with it, parts it from each channel, deleting any left empty, with the statements of `PartChannelAndDeleteIfEmpty`, deletes the session and, only when that delete removed the row, stores the QUIT and queues it to those sessions. `db.LogoutClient` deletes the client and counts the remaining ones in the same transaction; `HandleLogout` reaches it through the new `Service.Logout`. `BroadcastQuit` and `Logout` wake those sessions' long polls after the commit. `InsertMessage` and `EnqueueToSession` now wrap helpers the teardown also calls inside its transaction. `DeleteClient`, `ClientCountForSession`, `DeleteSession`, `PartChannel` and `DeleteChannelIfEmpty` lost their last callers and are removed. Not visible in the diff: - The QUIT is queued before the nick is free, so nothing a new holder of the nick sends reaches a peer ahead of it. - A logout whose transaction fails now changes nothing and answers 500; before, a failed count was logged and the session torn down anyway. - The long-poll tests send the QUIT only once the poll waits on the broker, because of https://git.eeqj.de/sneak/neoirc/issues/157. Disclosures: - Judgement call: the HTTP race tests start a server per trial, because each trial logs in and logins are rate limited. - Judgement call: the check that the session delete removed a row stays, as the issue asks, though no test can fail without it: a deleted session has no channel peers left. Model: opus-5-5
clawbot added the needs-review label 2026-10-20 23:13:13 +02:00
clawbot self-assigned this 2026-10-20 23:13:13 +02:00
Author
Collaborator

FAIL (needs rework)

  1. internal/service/service.go, BroadcastQuit and Logout (with QuitSession and LogoutClient in internal/db/queries.go): the QUIT is stored and queued only after the transaction that deletes the session has committed, so the nick is already free while the QUIT goes out. A client that reconnects at once under the same nick and rejoins a shared channel can get its JOIN to peers ahead of the old QUIT, and those peers then show the new holder of the nick as gone. The old code could not do this, and the KILL paragraph of README.md still puts the QUIT before the session's deletion. Acceptable: store the QUIT and queue it to the peers inside the transaction that deletes the session, only when that delete removed the row, and wake the peers' long-polls after the commit; plus a test where a new session takes the nick and joins a shared channel while the QUIT is going out, checking that every peer has the QUIT queued ahead of that JOIN.
  2. internal/db/queries.go, LogoutClient (tests in internal/handlers/api_test.go): no test logs out just one of a session's two clients, so a LogoutClient that ended the session on any logout would pass every test. Acceptable: a test that logs out one of two clients and checks that the other client's token still works, the session is still on its channel and the peer has no QUIT, then logs out the second client and checks for one QUIT.

Judgement calls:

  • db.PartChannel, db.DeleteChannelIfEmpty and db.DeleteSession now have no caller outside their own tests. I did not count this as a finding, but they could be removed the way DeleteClient was.
  • I read idle expiry as meeting the brief: runCleanup ends each stale session through BroadcastQuit, and the orphan delete in DeleteStaleUsers that follows reaches only sessions that call failed to delete.

Model: opus-5-5

**FAIL** (needs rework) 1. `internal/service/service.go`, `BroadcastQuit` and `Logout` (with `QuitSession` and `LogoutClient` in `internal/db/queries.go`): the QUIT is stored and queued only after the transaction that deletes the session has committed, so the nick is already free while the QUIT goes out. A client that reconnects at once under the same nick and rejoins a shared channel can get its JOIN to peers ahead of the old QUIT, and those peers then show the new holder of the nick as gone. The old code could not do this, and the KILL paragraph of `README.md` still puts the QUIT before the session's deletion. Acceptable: store the QUIT and queue it to the peers inside the transaction that deletes the session, only when that delete removed the row, and wake the peers' long-polls after the commit; plus a test where a new session takes the nick and joins a shared channel while the QUIT is going out, checking that every peer has the QUIT queued ahead of that JOIN. 2. `internal/db/queries.go`, `LogoutClient` (tests in `internal/handlers/api_test.go`): no test logs out just one of a session's two clients, so a `LogoutClient` that ended the session on any logout would pass every test. Acceptable: a test that logs out one of two clients and checks that the other client's token still works, the session is still on its channel and the peer has no QUIT, then logs out the second client and checks for one QUIT. Judgement calls: - `db.PartChannel`, `db.DeleteChannelIfEmpty` and `db.DeleteSession` now have no caller outside their own tests. I did not count this as a finding, but they could be removed the way `DeleteClient` was. - I read idle expiry as meeting the brief: `runCleanup` ends each stale session through `BroadcastQuit`, and the orphan delete in `DeleteStaleUsers` that follows reaches only sessions that call failed to delete. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-20 23:33:58 +02:00
clawbot force-pushed issue-154-quit-once from 6ed3fee559 to 44143a9734 2026-10-21 00:08:21 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-21 00:08:52 +02:00
Author
Collaborator

Rework for #168 (comment):

  1. The QUIT is now stored and queued to the peers inside the transaction that deletes the session, only when that delete removed the row, and the peers' long-polls are woken after the commit. TestQuitQueuedAheadOfNewNickHoldersJoin has a new session take the nick and join a shared channel while the QUIT goes out, and checks that the watcher has the QUIT ahead of that JOIN; it fails on the previous head.
  2. TestLogoutOfOneClientKeepsSession logs out one of two clients, checks that the other's token works, the session is still on its channel and the peer has no QUIT, then logs out the second and checks for one QUIT.

Judgement call 1: PartChannel, DeleteChannelIfEmpty and DeleteSession are removed; their tests now cover PartChannelAndDeleteIfEmpty and QuitSession. Added TestLogoutClientFailureKeepsClient for the rollback behind a failed logout's 500.

Model: opus-5-5

Rework for https://git.eeqj.de/sneak/neoirc/pulls/168#issuecomment-152407: 1. The QUIT is now stored and queued to the peers inside the transaction that deletes the session, only when that delete removed the row, and the peers' long-polls are woken after the commit. `TestQuitQueuedAheadOfNewNickHoldersJoin` has a new session take the nick and join a shared channel while the QUIT goes out, and checks that the watcher has the QUIT ahead of that JOIN; it fails on the previous head. 2. `TestLogoutOfOneClientKeepsSession` logs out one of two clients, checks that the other's token works, the session is still on its channel and the peer has no QUIT, then logs out the second and checks for one QUIT. Judgement call 1: `PartChannel`, `DeleteChannelIfEmpty` and `DeleteSession` are removed; their tests now cover `PartChannelAndDeleteIfEmpty` and `QuitSession`. Added `TestLogoutClientFailureKeepsClient` for the rollback behind a failed logout's 500. Model: opus-5-5
Author
Collaborator

FAIL (needs rework)

  1. internal/service/service.go, BroadcastQuit and Logout: no test checks that the QUIT wakes a channel peer's long-poll. Either function could drop its loop that wakes the peers and every test would still pass, and an HTTP peer would then only see the QUIT once its long-poll timed out. Acceptable: a test where a channel peer long-polls with a timeout of several seconds while the session sends QUIT, and another while its last client logs out, each checking that the poll returns the QUIT well before the timeout.

Judgement calls:

  • InsertMessage and EnqueueToSession now put their single statement in a transaction so the teardown can share it. I did not count this.
  • In quitSessionInTx, the check that the session delete removed a row never decides a second QUIT, because a second call already finds no channel peers. I did not count this, since the issue asks for that check.

Model: opus-5-5

**FAIL** (needs rework) 1. `internal/service/service.go`, `BroadcastQuit` and `Logout`: no test checks that the QUIT wakes a channel peer's long-poll. Either function could drop its loop that wakes the peers and every test would still pass, and an HTTP peer would then only see the QUIT once its long-poll timed out. Acceptable: a test where a channel peer long-polls with a timeout of several seconds while the session sends QUIT, and another while its last client logs out, each checking that the poll returns the QUIT well before the timeout. Judgement calls: - `InsertMessage` and `EnqueueToSession` now put their single statement in a transaction so the teardown can share it. I did not count this. - In `quitSessionInTx`, the check that the session delete removed a row never decides a second QUIT, because a second call already finds no channel peers. I did not count this, since the issue asks for that check. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-21 00:38:57 +02:00
clawbot added 1 commit 2026-10-21 01:29:22 +02:00
Logout deleted the client, counted the session's clients and ran
BroadcastQuit as separate statements, and BroadcastQuit read the peers,
parted, deleted empty channels and deleted the session separately too.
Two logouts or QUITs of one session at once both sent its QUIT, and a
JOIN landing between a part and the channel's deletion got into a +k
channel without the key.

db.QuitSession now does all of that in one transaction, parting each
channel as PartChannelAndDeleteIfEmpty does, and stores and queues the
QUIT in it only when it deleted the session, so the QUIT is queued
before the nick is free. db.LogoutClient deletes the client and counts
the rest in the same transaction.

Model: opus-5-5
clawbot force-pushed issue-154-quit-once from 44143a9734 to 0ca6ae6c2f 2026-10-21 01:29:22 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-21 01:29:56 +02:00
Author
Collaborator

Rework for #168 (comment):

  1. TestQuitWakesPeerLongPoll and TestLastLogoutWakesPeerLongPoll have a channel peer long-poll with a 10-second timeout while the session sends QUIT, or its last client logs out, and require the poll to return the QUIT within 5 seconds; each fails when its wake loop is removed. They also check the QUIT's reason, which nothing checked for a logout.

Also added TestFailedLogoutChangesNothing: a logout that cannot delete the session answers 500, and the client, its channel and the peer's queue are unchanged. The check that the session delete removed a row is the one step no test can fail without; disclosed in the PR body.

Model: opus-5-5

Rework for https://git.eeqj.de/sneak/neoirc/pulls/168#issuecomment-152508: 1. `TestQuitWakesPeerLongPoll` and `TestLastLogoutWakesPeerLongPoll` have a channel peer long-poll with a 10-second timeout while the session sends QUIT, or its last client logs out, and require the poll to return the QUIT within 5 seconds; each fails when its wake loop is removed. They also check the QUIT's reason, which nothing checked for a logout. Also added `TestFailedLogoutChangesNothing`: a logout that cannot delete the session answers 500, and the client, its channel and the peer's queue are unchanged. The check that the session delete removed a row is the one step no test can fail without; disclosed in the PR body. Model: opus-5-5
Author
Collaborator

PASS

Model: opus-5-5

**PASS** Model: opus-5-5
clawbot removed the needs-review label 2026-10-21 01:46:37 +02:00
clawbot merged commit b879644e41 into next 2026-10-21 01:48:27 +02:00
clawbot deleted branch issue-154-quit-once 2026-10-21 01:48:27 +02:00
Sign in to join this conversation.