Prune old rows in device_lists_changes_in_room table. - #19473
Conversation
We called `txn.rowcount` *after* we used the txn for something else, so we were no longer counting rows deleted by the prune but rows inserted into the cache stream. This caused a tight loop.
| return num_deleted | ||
|
|
||
| num_rows_deleted = 0 | ||
| while True: |
There was a problem hiding this comment.
Are we happy that there isn't much in the way of rate control on these deletions?
Co-authored-by: Olivier 'reivilibre' <oliverw@element.io>
Co-authored-by: Olivier 'reivilibre' <olivier@librepush.net>
reivilibre
left a comment
There was a problem hiding this comment.
Seems like it's basically there, save for a bit of potential raciness and maybe a test?
| if changes is not None: | ||
| local_changes = {(u, d) for u, d in changes if self.hs.is_mine_id(u)} | ||
| else: | ||
| # The `device_lists_stream_id` is too old, so we need to fall back |
There was a problem hiding this comment.
how much of a pain would it be to stand up a test against this case? I worry this is probably untested code. Being an edge case, it will therefore probably go unnoticed if it breaks until a very awkward moment. Would be nice to have something
Co-authored-by: Olivier 'reivilibre' <oliverw@element.io>
Co-authored-by: Olivier 'reivilibre' <oliverw@element.io>
Instead of checking it in a separate transaction, check in the transaction we're reading the table from.
Rather than trying to infer it from the minimum ID in the table. We were seeing issues in CI due to not all device list updates having entries in the `device_lists_changes_in_rooms` table due to the user not being in any rooms. This meant that the returned minimum ID was larger than expected, causing failures when calling `get_all_device_list_changes(..)`. By explicitly tracking the max pruned ID, we don't have to worry about problems with trying to infer the actual max pruned ID.
5e5c3f2 to
fee4c3f
Compare
|
@reivilibre sorry, the worker integration tests raised an issue around the fact that taking the minimum stream ID of |
reivilibre
left a comment
There was a problem hiding this comment.
Probably sane but would be good to have a really good idea of what the new table is for; it does feel like the kind of thing someone will dig up years down the line and have to scratch their head over it.
It doesn't sound insane so probably just a matter of getting the illustration right, e.g. with a step by step sequence to show what goes wrong with the 'simple' MIN(stream_id)
| -- the table cannot provide a complete answer. | ||
| -- | ||
| -- This replaces the previous approach of using MIN(stream_id) on the | ||
| -- device_lists_changes_in_room table, which incorrectly returned 0 when |
There was a problem hiding this comment.
Ironically this new table is more confusing to me, as in I don't see why this is preventing anything.
By my reading, COALESCE(MIN(stream_id), 0) is the reason the previous approach returned 0 when empty. (So I think the comment here is maybe a bit misleading, or doesn't quite reveal the reason we need this?)
Trying to understand the difference, here is a (step by step) comparison table as I see it:
Old _get_min_device_lists_changes_in_room |
New _get_max_pruned_device_lists_changes_in_room_txn |
|
|---|---|---|
| Seed (empty table) | returns 0 | returns 0 |
| New rows added with stream_ids 2..=15 (empty table) | returns 2 | still returns 0 |
| Prune with prune_before_stream_id 10 | now returns MIN(stream_id) = 10 | now returns 10 (explicitly stored) |
Old _get_min_device_lists_changes_in_room |
New _get_max_pruned_device_lists_changes_in_room_txn |
|
|---|---|---|
| Seed (populated table with stream_ids 2..=9) | returns MIN(stream_id) = 2 | returns MIN(stream_id) - 1 = 1 |
| New rows added with stream_ids 10..=15 (populated table) | still returns 2 | still returns 1 |
| Prune with prune_before_stream_id 10 | now returns MIN(stream_id) = 10 | now returns 10 (explicitly stored) |
Apart from the initial state before the first prune, it looks like both approaches behave the same between prunes?
What am I missing? Maybe the device_lists_changes_in_room rows get deleted as rooms get purged?
There was a problem hiding this comment.
By my reading, COALESCE(MIN(stream_id), 0) is the reason the previous approach returned 0 when empty. (So I think the comment here is maybe a bit misleading, or doesn't quite reveal the reason we need this?)
Argh, sorry that comment should have been deleted. I was messing around with testing getting LLM to generate the patch and it misunderstood the rationale (but got the change correct). I changed it locally but it got swallowed.
The actual problem (as per commit comment) comes from when we generate stream positions that don't have associated data in the device_lists_changes_in_rooms table, e.g. because the user isn't in any rooms. In that case if we insert a row into device_lists_stream table but not in device_lists_changes_in_rooms, and then later one that inserts rows into both, then MIN(stream_id) will return a stream ID greater than the first row even though its data hasn't been pruned (there was just no associated data to fetch).
reivilibre
left a comment
There was a problem hiding this comment.
LGTM otherwise, thanks!
| -- it's safe to read from that table for a given stream_id — if the | ||
| -- requested stream_id is < the value here, the data has been pruned and | ||
| -- the table cannot provide a complete answer. | ||
| CREATE TABLE IF NOT EXISTS device_lists_changes_in_room_max_pruned_stream_id ( |
There was a problem hiding this comment.
would be good to still have a little distilled comment about why we need this table, which is that device_lists_stream somehow ties into this (I don't have a great picture of what this is otherwise I'd suggest some wording)
Follows on from #19473. We should be recording where we have deleted up to in the same transaction as we perform the delete, rather than at the end. Also let's log more regularly, as the initial set of deletions will likely take a long time
Follows on from #19473. We should be recording where we have deleted up to in the same transaction as we perform the delete, rather than at the end. Also let's log more regularly, as the initial set of deletions will likely take a long time
Follows on from #19473. We should be recording where we have deleted up to in the same transaction as we perform the delete, rather than at the end. Also let's log more regularly, as the initial set of deletions will likely take a long time
Follows on from #19473. We should be recording where we have deleted up to in the same transaction as we perform the delete, rather than at the end. Also let's log more regularly, as the initial set of deletions will likely take a long time
Follows on from #19473. We should be recording where we have deleted up to in the same transaction as we perform the delete, rather than at the end. Also let's log more regularly, as the initial set of deletions will likely take a long time
Follows on from #19473. We should be recording where we have deleted up to in the same transaction as we perform the delete, rather than at the end. This code only starts deleting rows after a month (and the original PR isn't in a release yet), so no server should have run into this problem yet. Also let's log more regularly, as the initial set of deletions will likely take a long time.
…9473) Fixes element-hq#13043 The usages of the table mostly already correctly handled if we don't have old entries, as that was needed when we first added the table. I arbitrarily set the prune time to 30 days. The only use for old entries is for sync streams that haven't synced since then, and we should very rarely see sync streams that haven't been used in 30 days. Reviewable commit-by-commit. --------- Co-authored-by: Olivier 'reivilibre' <oliverw@element.io> Co-authored-by: Olivier 'reivilibre' <olivier@librepush.net>
Follows on from element-hq#19473. We should be recording where we have deleted up to in the same transaction as we perform the delete, rather than at the end. This code only starts deleting rows after a month (and the original PR isn't in a release yet), so no server should have run into this problem yet. Also let's log more regularly, as the initial set of deletions will likely take a long time.
Taken and adapted from #19473 Didn't add the "safety" table due to afaik that has more relevance with the device updates, and not so much here for the profile updates.
Tested on NetBSD 10 amd64 with 2026Q2 environment. # Synapse 1.156.0 (2026-07-07) ## Features - Expose [MSC4354 Sticky Events](matrix-org/matrix-spec-proposals#4354) over [MSC4186 (Simplified) Sliding Sync](matrix-org/matrix-spec-proposals#4186). ([\#19591](element-hq/synapse#19591)) - Stabilize support for sending ephemeral events to application services, as per [MSC2409](matrix-org/matrix-spec-proposals#2409). Contributed by @jason-famedly @ Famedly. ([\#19758](element-hq/synapse#19758)) - Include `allowed_room_ids` in the `/summary` client-server API response for rooms with restricted join rules, as required by Matrix 1.15. Contributed by @FrenchGithubUser @famedly. ([\#19762](element-hq/synapse#19762)) - [MSC4140: Cancellable delayed events](matrix-org/matrix-spec-proposals#4140): Allow authentication on delayed event management endpoints (such as `/restart`) to bypass ratelimits for unauthenticated requests based on the client IP address. ([\#19794](element-hq/synapse#19794)) - Add new metric `synapse_non_deactivated_user_count` which tracks the number of non-deactivated users in the database, split by `app_service`. ([\#19848](element-hq/synapse#19848)) - The `GET /_matrix/client/unstable/org.matrix.msc1763/retention/configuration` endpoint is now provided when retention is enabled and `experimental_features.msc1763_enabled` is enabled, based on [MSC1763](matrix-org/matrix-spec-proposals#1763). ([\#19853](element-hq/synapse#19853)) - Add experimental support for [MSC4491: Invite reasons in room creation](matrix-org/matrix-spec-proposals#4491). ([\#19874](element-hq/synapse#19874)) # Synapse 1.155.0 (2026-06-16) # Synapse 1.154.0 (2026-06-04) ## Features - Add support for [MSC4452: Preview URL capabilities API](matrix-org/matrix-spec-proposals#4452) which exposes a `io.element.msc4452.preview_url` capability. If `experimental_features.msc4452_enabled` is `true`, the `/_matrix/(client/v1/media|media/v3)/preview_url` endpoint now responds with a 403 status code when the capability is disabled. ([\#19715](element-hq/synapse#19715)) # Synapse 1.153.0 (2026-05-19) ## Features - Make ACLs apply to EDUs per [MSC4163](matrix-org/matrix-spec-proposals#4163). ([\#18475](element-hq/synapse#18475)) - Stabilize [MSC3266: Room summary API](matrix-org/matrix-spec-proposals#3266), removing the experimental config flag `msc3266_enabled`. Contributed by @dasha-uwu. ([\#19720](element-hq/synapse#19720)) - Partial [MSC4311](matrix-org/matrix-spec-proposals#4311) implementation: `m.room.create` is now a required part of stripped `invite_state`/`knock_state` . Contributed by @FrenchGithubUser @famedly. ([\#19722](element-hq/synapse#19722)) - Expose `tombstoned` and `replacement_room` in room details on admin API endpoint `GET /_synapse/admin/v1/rooms/<room_id>`. Contributed by Noah Markert. ([\#19737](element-hq/synapse#19737)) # Synapse 1.152.1 (2026-05-07) # Synapse 1.152.0 (2026-04-28) ## Features - Add a ["Listing quarantined media changes" Admin API](https://element-hq.github.io/synapse/latest/admin_api/media_admin_api.html#listing-quarantined-media-changes) for retrieving a paginated record of when media became (un)quarantined. ([\#19558](element-hq/synapse#19558), [\#19677](element-hq/synapse#19677), [\#19694](element-hq/synapse#19694)) - Advertise [MSC4445](matrix-org/matrix-spec-proposals#4445) sync timeline order in `unstable_features`. ([\#19642](element-hq/synapse#19642)) - Report the Rust compiler version used in the Prometheus metrics. Contributed by Noah Markert. ([\#19643](element-hq/synapse#19643)) - Passthrough 'article' and 'profile' OpenGraph metadata on URL preview requests. ([\#19659](element-hq/synapse#19659)) - Add a way to re-sign local events with a new signing key. ([\#19668](element-hq/synapse#19668)) - Support [MSC4450: Identity Provider selection for User-Interactive Authentication with Legacy Single Sign-On](matrix-org/matrix-spec-proposals#4450). ([\#19693](element-hq/synapse#19693)) - Add experimental support for [MSC4242](matrix-org/matrix-spec-proposals#4242): State DAGs. Excludes federation support. ([\#19424](element-hq/synapse#19424)) - Adds [Admin API](https://element-hq.github.io/synapse/latest/usage/administration/admin_api/index.html) endpoints to list, fetch and delete user reports. ([\#19657](element-hq/synapse#19657)) - Reduce database disk space usage by pruning old rows from `device_lists_changes_in_room`. ([\#19473](element-hq/synapse#19473), [\#19709](element-hq/synapse#19709)) # Synapse 1.151.0 (2026-04-07) ## Features - Add stable support for [MSC4284](matrix-org/matrix-spec-proposals#4284) Policy Servers. ([\#19503](element-hq/synapse#19503)) - Update and stabilize support for [MSC2666](matrix-org/matrix-spec-proposals#2666): Get rooms in common with another user. Contributed by @tulir @ Beeper. ([\#19511](element-hq/synapse#19511)) - Updated experimental support for [MSC4388: Secure out-of-band channel for sign in with QR](matrix-org/matrix-spec-proposals#4388). ([\#19573](element-hq/synapse#19573)) - Stabilize `room_version` and `encryption` fields in the space/room `/hierarchy` API (part of [MSC3266](matrix-org/matrix-spec-proposals#3266)). ([\#19576](element-hq/synapse#19576)) - Introduce a [configuration option](https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html#matrix_authentication_service) to allow using HTTP/2 over plaintext when Synapse connects to Matrix Authentication Service. ([\#19586](element-hq/synapse#19586)) ## Deprecations and Removals - Remove support for [MSC3852: Expose user agent information on Device](matrix-org/matrix-spec-proposals#3852) as the MSC was closed. ([\#19430](element-hq/synapse#19430)) # Synapse 1.150.0 (2026-03-24) ## Features - Add experimental support for the [MSC4370](matrix-org/matrix-spec-proposals#4370) Federation API `GET /extremities` endpoint. ([\#19314](element-hq/synapse#19314)) - [MSC4140: Cancellable delayed events](matrix-org/matrix-spec-proposals#4140): When persisting a delayed event to the timeline, include its `delay_id` in the event's `unsigned` section in `/sync` responses to the event sender. ([\#19479](element-hq/synapse#19479)) - Expose [MSC4354 Sticky Events](matrix-org/matrix-spec-proposals#4354) over the legacy (v3) /sync API. ([\#19487](element-hq/synapse#19487)) - When Matrix Authentication Service (MAS) integration is enabled, allow MAS to set the user locked status in Synapse. ([\#19554](element-hq/synapse#19554))
…saction gets retried. (#19947) Introduced in: #19473 Noticed in: #19556 (comment) I have not experienced the bug in the real world, it's just something I noticed by reading. -- Fix bug in `_prune_device_lists_changes_in_room` when transaction is retried The `nonlocal` variable is a footgun as it increments the counter even though the transaction did not commit yet and may still be retried. --------- Signed-off-by: Olivier 'reivilibre <oliverw@matrix.org>
Fixes #13043
The usages of the table mostly already correctly handled if we don't have old entries, as that was needed when we first added the table.
I arbitrarily set the prune time to 30 days. The only use for old entries is for sync streams that haven't synced since then, and we should very rarely see sync streams that haven't been used in 30 days.
Reviewable commit-by-commit.