Empirical test of PR #13 (commit fe6a8c9) against baseline 96300ee (master pre-fix).
With +TOPICLOCK +KEEPTOPIC, after a netsplit heals, ChanServ used to emit up to two back-to-back TOPIC commands per channel:
restore_topic()fromchan_handle_SJOINat channel re-creation.- TOPICLOCK override in
chan_handle_TOPICserver-branch when the same burst propagated the remote topic.
Both paths were gated only on synched == TRUE, not on per-server burst state. Fix introduces SERVER_FLAG_BURSTING check on both paths and defers reconciliation to a single synch_topics() call at m_gnotice "has synched to network data." EOB.
docker compose (/tmp/infra-pr22/compose.yaml): hub.azzurra.chat, leaf4.azzurra.chat, leaf6.azzurra.chat, services.azzurra.chat. Services binary swapped between runs via /tmp/topic-test/compose.override.yaml bind-mount. Oper-initiated SQUIT/CONNECT simulates splits without restarting containers — leaf4's local channel/user/topic state is preserved and bursts to hub on heal, which is the exact path the bug lives on.
Pcap sniffer: tcpdump -U -i eth0 -A -s 0 tcp port 7000 run via a netshoot sidecar sharing services' netns (--net container:azzurra-services). This captures the hub↔services S2S link.
Clients:
Cfounderon hub (port 6667). Opered, registers NickServ, creates+registers#test, sets+TOPICLOCK +KEEPTOPIC, sets initial topic T1.Cleafon leaf4 (port 6668). Opered. Joins#testFIRST so the SJOIN TS is leaf4-owned and propagates to hub before Cfounder joins — both sides share the same TS, preserved across the split.
SQUIT → wait → CONNECT. No topic change. Expected: 0 ChanServ TOPIC frames post-heal.
| Binary | ChanServ TOPIC frames | Total TOPIC frames |
|---|---|---|
| baseline | 0 | 0 |
| fix | 0 | 0 |
Fix does not introduce spurious TOPIC traffic when there is nothing to reconcile.
SQUIT → Cleaf sets T2-changed-during-split locally on leaf4 → CONNECT (leaf4 bursts T2 to hub, hub forwards to services).
| Binary | ChanServ TOPIC frames | Total TOPIC frames | Final topic on channel |
|---|---|---|---|
| baseline | 0 | 1 (T2 from leaf4) | T2 (wrong — KEEPTOPIC+TOPICLOCK silently violated) |
| fix | 1 (restores T1) | 2 (T2 burst + ChanServ T1 restore) | T1 (correct) |
Non-founder op attempts TOPIC change on a TOPICLOCKed channel during normal operation. Sniffer on svc-net did not capture frames — Cleaf's local TOPIC was rejected at leaf4 before propagation (ChanServ auto-deops non-founder post-REGISTER; +t blocks non-ops). Regression verified instead by code inspection: the PR only adds an src_bursting gate on the server-source branch; non-burst paths are byte-for-byte identical to baseline. No functional change outside the burst window.
09:16:28.925137 GNOTICE :Local CONNECT leaf4.azzurra.chat 7000 from Cfounder[...]
09:16:28.925492 :hub.azzurra.chat SERVER leaf4.azzurra.chat 2 :Azzurra testnet leaf v4
NICK Cleaf 2 1776762969 +oix ...
:leaf4.azzurra.chat SJOIN 1776762969 #test + :@Cleaf
09:16:28.925604 :ChanServ NOTICE Cleaf :.#test. is a registered channel.
09:16:28.925638 :ChanServ MODE #test -o Cleaf <-- TOPICLOCK enforcement (deop)
09:16:28.925655 :ChanServ MODE #test +r
09:16:28.925732 :leaf4.azzurra.chat TOPIC #test Cleaf 1776762976 :T2-changed-during-split <-- burst T2 (deferred, not overridden inline)
09:16:28.966228 GNOTICE :leaf4.azzurra.chat has synched to network data. <-- EOB, BURSTING cleared
09:16:28.966332 :ChanServ TOPIC #test Cfounder 1776762973 :T1-initial-from-Cfounder <-- synch_topics() restore
One ChanServ TOPIC, emitted after EOB by synch_topics(). ci->last_topic (T1) was preserved through the burst because record_topic() was gated by src_bursting.
09:17:51.219821 GNOTICE :Local CONNECT leaf4.azzurra.chat 7000 from Cfounder[...]
09:17:51.220122 :hub.azzurra.chat SERVER leaf4.azzurra.chat 2 :Azzurra testnet leaf v4
NICK Cleaf 2 1776763052 +oix ...
:leaf4.azzurra.chat SJOIN 1776763052 #test +r :@Cleaf
09:17:51.220386 :ChanServ NOTICE Cleaf :.#test. is a registered channel.
09:17:51.220391 :leaf4.azzurra.chat TOPIC #test Cleaf 1776763061 :T2-changed-during-split
09:17:51.220411 :ChanServ MODE #test -o Cleaf
09:17:51.220747 GNOTICE :leaf4.azzurra.chat has synched to network data.
<-- NO ChanServ TOPIC restore
Zero ChanServ TOPIC restore. The baseline path called record_topic() on the remote TOPIC during burst, overwriting ci->last_topic = T2. By the time EOB fires, there's no mismatch to reconcile — T2 wins silently. TOPICLOCK + KEEPTOPIC were silently bypassed.
(Historical issue #1 described this as "TOPIC set twice" — i.e. the other failure mode where restore_topic() fires at SJOIN and the TOPICLOCK override fires at TOPIC — in this run only one of the two paths visibly broke, but both are gated by the same fix.)
09:16:08 Cfounder >>> OPER testoper testoperpass
09:16:08 Cfounder >>> PRIVMSG NickServ :REGISTER secretpw x@x.com
09:16:09 Cleaf >>> OPER testoper testoperpass
09:16:10 Cleaf >>> JOIN #test # chan created on leaf4 side; SJOIN → hub with leaf4 TS
09:16:11 Cleaf >>> SAMODE #test +o Cleaf
09:16:11 Cfounder >>> JOIN #test # hub joins existing chan, same TS
09:16:11 Cfounder >>> SAMODE #test +o Cfounder
09:16:12 Cfounder >>> PRIVMSG ChanServ :REGISTER #test chanpw Test channel
09:16:13 Cfounder >>> PRIVMSG ChanServ :SET #test TOPICLOCK ON
09:16:13 Cfounder >>> PRIVMSG ChanServ :SET #test KEEPTOPIC ON
09:16:14 Cfounder >>> TOPIC #test :T1-initial-from-Cfounder
09:16:16 Cfounder >>> PRIVMSG #test :MARKER_SCENARIO_START_2
09:16:17 Cfounder >>> SQUIT leaf4.azzurra.chat :test-split
09:16:20 Cleaf >>> TOPIC #test :T2-changed-during-split # during split, leaf4 local state only
09:16:22 Cleaf >>> TOPIC #test # verify leaf4 sees T2
09:16:28 Cfounder >>> CONNECT leaf4.azzurra.chat 7000 hub.azzurra.chat
09:16:43 Cfounder >>> PRIVMSG #test :MARKER_SCENARIO_END_2
09:16:46 Cfounder >>> TOPIC #test # query final state
Scenarios 1 and 3 use the same setup minus the post-START behavior; full traces in cmds-{tag}-s{1,2,3}.txt.
| Scenario | Baseline | Fix | Verdict |
|---|---|---|---|
| 1 MATCH | 0 CS TOPIC (correct) | 0 CS TOPIC (correct) | fix: no regression |
| 2 MISMATCH | 0 CS TOPIC, T2 wins (bug) | 1 CS TOPIC, T1 restored at EOB | bug fixed |
| 3 REGRESSION | n/a — code-audit only: non-burst path unchanged | n/a | fix: no regression |
PR #13 can merge.