From 7eac9c44b6efaed885bf8a5087af896601addc87 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Sun, 18 Oct 2026 21:32:58 +0000 Subject: [PATCH] Decide a state group's children first when marking unreferenced ones The background update that marks unreferenced state groups for deletion reads a batch of groups joined to their next groups, then decides each row in turn: a group is a candidate if every next group is. That needs a group's next groups decided before it, but the query had no outer ORDER BY, so the order was whatever the join produced. When a group came first, its next group looked undecided, the group was kept, and once the next group was deleted it was left behind for good. `test_clear_unreferenced_state_groups` caught this about one run in ten on Postgres, and more often as other changes moved the timing. A next group is always created after its prev group, so has a higher ID: ordering by ID descending decides it first. The test now passes every time. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PVLsNh6akt4ifEizY4D1ta --- changelog.d/unreferenced-state-groups-order.bugfix | 1 + synapse/storage/controllers/purge_events.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog.d/unreferenced-state-groups-order.bugfix diff --git a/changelog.d/unreferenced-state-groups-order.bugfix b/changelog.d/unreferenced-state-groups-order.bugfix new file mode 100644 index 0000000000..6051027072 --- /dev/null +++ b/changelog.d/unreferenced-state-groups-order.bugfix @@ -0,0 +1 @@ +Fix unreferenced state groups sometimes not being marked for deletion, depending on the order the database returned rows in. diff --git a/synapse/storage/controllers/purge_events.py b/synapse/storage/controllers/purge_events.py index 8a2053d25a..e461114fcd 100644 --- a/synapse/storage/controllers/purge_events.py +++ b/synapse/storage/controllers/purge_events.py @@ -374,6 +374,7 @@ class PurgeEventsStorageController: ) as s LEFT JOIN state_group_edges AS e ON (s.id = e.prev_state_group) LEFT JOIN state_groups_pending_deletion AS d ON (e.state_group = d.state_group) + ORDER BY s.id DESC """ txn.execute(state_group_sql, (last_checked_state_group, batch_size))