Repository navigation
Speed up marking device list changes as converted - #20098
Conversation
Schema DiffPlease check that this looks as expected! common (sqlite)Unchanged common (postgres)Unchanged main (sqlite)--- /tmp/schema_diff_az2vpfi0/before/main/full_schemas/9999/full.sql.sqlite 2001-05-25 12:42:42.000000000 +0000
+++ /tmp/schema_diff_az2vpfi0/after/main/full_schemas/9999/full.sql.sqlite 2001-05-25 12:42:42.000000000 +0000
@@ -1227,20 +1227,21 @@
CREATE INDEX local_media_repository_sha256 ON local_media_repository (sha256) WHERE sha256 IS NOT NULL;
CREATE INDEX remote_media_cache_sha256 ON remote_media_cache (sha256) WHERE sha256 IS NOT NULL;
CREATE INDEX sliding_sync_membership_snapshots_membership_event_id_idx ON sliding_sync_membership_snapshots (membership_event_id) ;
CREATE INDEX sliding_sync_membership_snapshots_user_id_stream_ordering ON sliding_sync_membership_snapshots (user_id, event_stream_ordering) ;
CREATE INDEX device_federation_inbox_received_ts_index ON device_federation_inbox (received_ts) ;
CREATE UNIQUE INDEX event_txn_id_device_id_txn_id2 ON event_txn_id_device_id (user_id, device_id, room_id, txn_id) ;
CREATE UNIQUE INDEX delayed_events_idx ON delayed_events (delay_id) ;
CREATE INDEX device_lists_changes_in_room_inserted_ts_idx ON device_lists_changes_in_room (inserted_ts) WHERE inserted_ts IS NOT NULL;
CREATE INDEX sliding_sync_connections_last_used_ts_idx ON sliding_sync_connections (last_used_ts) WHERE last_used_ts IS NOT NULL;
CREATE INDEX sliding_sync_connection_lazy_members_conn_pos_idx ON sliding_sync_connection_lazy_members (connection_position) WHERE connection_position IS NOT NULL;
+CREATE INDEX device_lists_changes_in_room_unconverted_idx ON device_lists_changes_in_room (user_id, device_id, room_id, stream_id) WHERE NOT converted_to_destinations;
/* WARNING: Script requires that SQLITE_DBCONFIG_DEFENSIVE be disabled */
INSERT INTO appservice_stream_position VALUES('X',0);
INSERT INTO federation_stream_position VALUES('federation',-1,'master');
INSERT INTO federation_stream_position VALUES('events',-1,'master');
INSERT INTO event_push_summary_stream_ordering VALUES('X',0);
INSERT INTO user_directory_stream_pos VALUES('X',-1);
INSERT INTO stats_incremental_position VALUES('X',1);
INSERT INTO event_push_summary_last_receipt_stream_id VALUES('X',0);
INSERT INTO device_lists_changes_converted_stream_position VALUES('X',1,'',NULL);
INSERT INTO room_forgetter_stream_pos VALUES('X',0);
main (postgres)--- /tmp/schema_diff_az2vpfi0/before/main/full_schemas/9999/full.sql.postgres 2001-05-25 12:42:42.000000000 +0000
+++ /tmp/schema_diff_az2vpfi0/after/main/full_schemas/9999/full.sql.postgres 2001-05-25 12:42:42.000000000 +0000
@@ -1654,20 +1654,21 @@
CREATE INDEX device_auth_providers_devices ON device_auth_providers USING btree (user_id, device_id);
CREATE INDEX device_auth_providers_sessions ON device_auth_providers USING btree (auth_provider_id, auth_provider_session_id);
CREATE INDEX device_federation_inbox_received_ts_index ON device_federation_inbox USING btree (received_ts);
CREATE INDEX device_federation_inbox_sender_id ON device_federation_inbox USING btree (origin, message_id);
CREATE INDEX device_federation_outbox_destination_id ON device_federation_outbox USING btree (destination, stream_id);
CREATE INDEX device_federation_outbox_id ON device_federation_outbox USING btree (stream_id);
CREATE INDEX device_inbox_stream_id_user_id ON device_inbox USING btree (stream_id, user_id);
CREATE INDEX device_inbox_user_stream_id ON device_inbox USING btree (user_id, device_id, stream_id);
CREATE INDEX device_lists_changes_in_room_by_room_idx ON device_lists_changes_in_room USING btree (room_id, stream_id);
CREATE INDEX device_lists_changes_in_room_inserted_ts_idx ON device_lists_changes_in_room USING btree (inserted_ts) WHERE (inserted_ts IS NOT NULL);
+CREATE INDEX device_lists_changes_in_room_unconverted_idx ON device_lists_changes_in_room USING btree (user_id, device_id, room_id, stream_id) WHERE (NOT converted_to_destinations);
CREATE UNIQUE INDEX device_lists_changes_in_stream_id ON device_lists_changes_in_room USING btree (stream_id, room_id);
CREATE INDEX device_lists_changes_in_stream_id_unconverted ON device_lists_changes_in_room USING btree (stream_id) WHERE (NOT converted_to_destinations);
CREATE UNIQUE INDEX device_lists_outbound_last_success_unique_idx ON device_lists_outbound_last_success USING btree (destination, user_id);
CREATE INDEX device_lists_outbound_pokes_id ON device_lists_outbound_pokes USING btree (destination, stream_id);
CREATE INDEX device_lists_outbound_pokes_stream ON device_lists_outbound_pokes USING btree (stream_id);
CREATE INDEX device_lists_outbound_pokes_user ON device_lists_outbound_pokes USING btree (destination, user_id);
CREATE UNIQUE INDEX device_lists_remote_cache_unique_id ON device_lists_remote_cache USING btree (user_id, device_id);
CREATE UNIQUE INDEX device_lists_remote_extremeties_unique_idx ON device_lists_remote_extremeties USING btree (user_id);
CREATE UNIQUE INDEX device_lists_remote_pending_user_device_id ON device_lists_remote_pending USING btree (user_id, device_id);
CREATE UNIQUE INDEX device_lists_remote_resync_idx ON device_lists_remote_resync USING btree (user_id);
state (sqlite)Unchanged state (postgres)Unchanged |
3c92d01 to
6ac75e2
Compare
The conversion of `device_lists_changes_in_room` rows into `device_lists_outbound_pokes` is DB-bound on `mark_redundant_device_lists_pokes`: its UPDATE has no usable index, so each call scans the unconverted backlog, getting slower the further behind the conversion is. Add a partial index matching the query via a background update, and skip the (safe-to-skip) UPDATE until the index has been built. Also add a metric reporting how far behind the conversion is, using the existing `inserted_ts` column.
6ac75e2 to
a029580
Compare
| # Add an index to speed up `mark_redundant_device_lists_pokes`. | ||
| self.db_pool.updates.register_background_index_update( | ||
| BG_UPDATE_ADD_UNCONVERTED_IDX, | ||
| index_name="device_lists_changes_in_room_unconverted_idx", | ||
| table="device_lists_changes_in_room", | ||
| columns=["user_id", "device_id", "room_id", "stream_id"], | ||
| where_clause="NOT converted_to_destinations", | ||
| ) |
There was a problem hiding this comment.
Perhaps we should just drop mark_redundant_device_lists_pokes as an optimization?
You mentioned that you "don't think it catches many redundant pokes anyway" (internal room)
There was a problem hiding this comment.
I think it is useful if you have the same device coming and going, but it's not necessary.
We could probably delete it entirely but I think a) we'd want to investigate it, and b) I think we still would want an index like the above anyway.
Reports the difference between the current device lists stream position and `device_lists_changes_converted_stream_position`, alongside the existing time-based lag metric. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Thn52seV72QdxwDx5etBPM
Read the converted stream position and the oldest unconverted row in one transaction, rather than one (or two) each. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Thn52seV72QdxwDx5etBPM
| "synapse_device_lists_changes_conversion_stream_lag", | ||
| "Number of stream IDs between the current device lists stream position and the position converted to outbound federation pokes", | ||
| labelnames=[SERVER_NAME_LABEL], | ||
| ) |
There was a problem hiding this comment.
@MadLittleMods I've also added this so you can see how many stream IDs we're behind on.
This helps because if there is a large batch of device updates in a short period of time the lag time might increase even though it's catching up.
|
|
||
| return not update_exists | ||
|
|
||
| async def have_completed_background_updates( |
There was a problem hiding this comment.
We can do the same lookup and update of _completed_background_updates here as well
There was a problem hiding this comment.
That's already handled via self._all_done?
There was a problem hiding this comment.
This is have_completed_background_updates(update_names) not has_completed_background_updates()
Updating our cache in have_completed_background_updates(update_names) makes just as much sense as has_completed_background_update(update_name) like we are doing
Co-authored-by: Eric Eastwood <erice@element.io>
413d3b2 to
edaddcc
Compare
|
Thanks @MadLittleMods ! |
…database (#20120) ### Background On `matrix.org`, since 2026-08-15, we are seeing the database CPU being saturated more than usual ([grafana](https://grafana.matrix.org/d/rYdddlPWk/node-exporter?orgId=1&from=2026-08-10T23:23:14.237Z&to=2026-08-17T23:23:14.237Z&timezone=browser&var-DS_PROMETHEUS=default&var-job=machine&var-node=matrix-db-01.matrix.org:9100&var-diskdevices=%5Ba-z%5D%2B%7Cnvme%5B0-9%5D%2Bn%5B0-9%5D%2B&refresh=1m&viewPanel=panel-77)) <img width="919" height="265" alt="CPU of database server" src="https://github.com/user-attachments/assets/9a43cf3c-bf9a-4ade-b70e-0bf0fb53f0ef" /> @reivilibre [found](https://matrix.to/#/!yHWhpxlXVaLcsgDUKb:matrix.org/$fUMf60IbFocpEuJNbmbEzFiCyKCFnAl4I4XpB27DUQs?via=banzan.uk&via=element.io&via=matrix.org) `mark_as_sent_devices_by_remote` spiking in the `DB transactions by total txn time` graph ([grafana](https://grafana.matrix.org/d/000000012/synapse?var-bucket_size=$__auto&orgId=1&from=2026-08-10T22:26:07.336Z&to=2026-08-17T22:26:07.336Z&timezone=browser&var-datasource=default&var-instance=matrix.org&var-job=synapse_federation_sender&var-index=$__all&showCategory=Thresholds&viewPanel=panel-11)) <img width="919" height="269" alt="'mark_as_sent_devices_by_remote' spiking in the 'DB transactions by total txn time' graph" src="https://github.com/user-attachments/assets/9336ce29-c045-4940-9c46-9e4906300f2d" /> And we indeed see a bunch of time being spent on `mark_as_sent_devices_by_remote` by looking at `pg_stat_statements`. The top two queries we're spending CPU on are the `SELECT` and `DELETE` statements in [`mark_as_sent_devices_by_remote`](https://github.com/element-hq/synapse/blob/94a5f2afb36a4afdc36813fd1c24b9a1c4aec252/synapse/storage/databases/main/devices.py#L922-L950). We also did some related work in this area recently with #20098 although it was tackling a different bottle-neck. ### This PR This PRs combines the two separate `SELECT` and `DELETE` queries which touch the same data into one `DELETE ... RETURNING ...` query. this means we get to save the cost of one of those statements (less CPU on the database) and fewer statements per transaction (less round-trips) means connections turn over faster, and can move on to process the next thing. This is a micro-optimization I spotted while reading [`_mark_as_sent_devices_by_remote_txn`](https://github.com/element-hq/synapse/blob/develop/synapse/storage/databases/main/devices.py#L922-L950) rather than a structural fix. I'm sure there are even better things to do to where we could even avoid this kind of work altogether but this seemed like a quick win especially given how much this particular transaction is saturating the database. Doing things faster doesn't necessarily mean we solve the saturated/starved CPU problem but it does mean the same task costs less CPU. From the `pg_stat_statements` samples above, we can expect to be up to ~27% more efficient with database CPU on this code path (derived from the `cpus_busy` numbers above `22.932 / (22.932 + 61.956)`). We're removing the `SELECT` (`61.956` `cpus_busy`) but I still expect the `DELETE` (`22.932` `cpus_busy`) to take on a similar cost as I'm sure it's a warm cache situation between them.
The conversion of
device_lists_changes_in_roomrows intodevice_lists_outbound_pokesis DB-bound onmark_redundant_device_lists_pokes. TheUPDATEuses the(room_id, stream_id)index, so each call scans the unconverted backlog, getting slower the further behind the conversion is.Add a partial index matching the query via a background update, and skip the (safe-to-skip)
UPDATEuntil the index has been built. Also add a metric reporting how far behind the conversion is, using the existinginserted_tscolumn.Fixes https://github.com/element-hq/backend-internal/issues/286