Repository navigation
Catch up MSC4354 Sticky Events to remote homeservers that have missed them. - #20165
Merged
Merged
Conversation
reivilibre
force-pushed
the
rei/sticky_events_fed_backlog
branch
2 times, most recently
from
August 28, 2026 17:38
19f9df7 to
ff65c5e
Compare
Contributor
Schema DiffPlease check that this looks as expected! common (sqlite)Unchanged common (postgres)Unchanged main (sqlite)--- /tmp/schema_diff_vc7yy24k/before/main/full_schemas/9999/full.sql.sqlite 2026-10-02 10:39:20.322790882 +0000
+++ /tmp/schema_diff_vc7yy24k/after/main/full_schemas/9999/full.sql.sqlite 2026-10-02 10:39:09.954713358 +0000
@@ -1194,20 +1194,45 @@
-- The full user ID of the local user that should receive the profile update.
user_id TEXT NOT NULL,
-- Unix timestamp (milliseconds). Used to determine when to prune rows (to prevent the table
-- from growing indefinitely).
inserted_ts BIGINT NOT NULL,
PRIMARY KEY (user_id, stream_id)
);
+CREATE TABLE destination_rooms_sticky_events_backlog (
+ -- Server name of the remote homeserver.
+ destination TEXT NOT NULL,
+
+ -- Room ID in which sticky events have been missed.
+ room_id TEXT NOT NULL
+ -- Only track this information for rooms we know about;
+ -- if we delete a room locally then also delete the tracking info.
+ REFERENCES rooms(room_id) ON DELETE CASCADE,
+
+ -- Position in the sticky events stream, corresponding to the
+ -- `sticky_events.stream_id` of the first sticky event that
+ -- has yet to be sent.
+ --
+ -- Because sticky events must be sent in order (as per MSC4354),
+ -- all subsequent sticky events for the same room with higher
+ -- `stream_id`s are also unsent.
+ --
+ -- Not a foreign key because we must still support expiration of sticky events.
+ sticky_events_stream_position INTEGER NOT NULL,
+
+ -- It's enough to track one position per (destination, room_id) pair.
+ PRIMARY KEY (destination, room_id)
+);
+CREATE INDEX destination_rooms_sticky_events_backlog_room_id ON destination_rooms_sticky_events_backlog (room_id);
CREATE UNIQUE INDEX receipts_graph_unique_index ON receipts_graph (room_id, receipt_type, user_id) WHERE thread_id IS NULL;
CREATE UNIQUE INDEX receipts_linearized_unique_index ON receipts_linearized (room_id, receipt_type, user_id) WHERE thread_id IS NULL;
CREATE INDEX device_lists_changes_in_room_by_room_idx ON device_lists_changes_in_room (room_id, stream_id) ;
CREATE INDEX events_jump_to_date_idx ON events (room_id, origin_server_ts) WHERE NOT outlier;
CREATE INDEX room_membership_user_room_idx ON room_memberships (user_id, room_id) ;
CREATE UNIQUE INDEX profiles_full_user_id_key ON profiles (full_user_id) ;
CREATE UNIQUE INDEX full_users_unique_idx ON user_filters (full_user_id, filter_id) ;
CREATE INDEX event_push_actions_stream_highlight_index ON event_push_actions (highlight, stream_ordering) WHERE highlight=0;
CREATE INDEX current_state_events_stream_ordering_idx ON current_state_events (event_stream_ordering) ;
CREATE INDEX local_current_membership_stream_ordering_idx ON local_current_membership (event_stream_ordering) ;
main (postgres)--- /tmp/schema_diff_vc7yy24k/before/main/full_schemas/9999/full.sql.postgres 2026-10-02 10:39:20.791793642 +0000
+++ /tmp/schema_diff_vc7yy24k/after/main/full_schemas/9999/full.sql.postgres 2026-10-02 10:39:10.462717334 +0000
@@ -203,20 +203,25 @@
CREATE TABLE destination_rooms (
destination text NOT NULL,
room_id text NOT NULL,
stream_ordering bigint NOT NULL
);
COMMENT ON TABLE destination_rooms IS 'Information about transmission of PDUs in a given room to a given remote homeserver.';
COMMENT ON COLUMN destination_rooms.destination IS 'server name of remote homeserver in question';
COMMENT ON COLUMN destination_rooms.room_id IS 'room ID in question';
COMMENT ON COLUMN destination_rooms.stream_ordering IS '`stream_ordering` of the most recent PDU in this room that needs to be sent (by us) to this homeserver.
This can only be pointing to our own PDU because we are only responsible for sending our own PDUs.';
+CREATE TABLE destination_rooms_sticky_events_backlog (
+ destination text NOT NULL,
+ room_id text NOT NULL,
+ sticky_events_stream_position integer NOT NULL
+);
CREATE TABLE destinations (
destination text NOT NULL,
retry_last_ts bigint,
retry_interval bigint,
failure_ts bigint,
last_successful_stream_ordering bigint
);
COMMENT ON TABLE destinations IS 'Information about remote homeservers and the health of our connection to them.';
COMMENT ON COLUMN destinations.destination IS 'server name of remote homeserver in question';
COMMENT ON COLUMN destinations.retry_last_ts IS 'The last time we tried and failed to reach the remote server, in ms.
@@ -1433,20 +1438,22 @@
ALTER TABLE ONLY current_state_events
ADD CONSTRAINT current_state_events_room_id_type_state_key_key UNIQUE (room_id, type, state_key);
ALTER TABLE ONLY dehydrated_devices
ADD CONSTRAINT dehydrated_devices_pkey PRIMARY KEY (user_id);
ALTER TABLE ONLY delayed_events
ADD CONSTRAINT delayed_events_pkey PRIMARY KEY (user_localpart, delay_id);
ALTER TABLE ONLY delayed_events_stream_pos
ADD CONSTRAINT delayed_events_stream_pos_lock_key UNIQUE (lock);
ALTER TABLE ONLY destination_rooms
ADD CONSTRAINT destination_rooms_pkey PRIMARY KEY (destination, room_id);
+ALTER TABLE ONLY destination_rooms_sticky_events_backlog
+ ADD CONSTRAINT destination_rooms_sticky_events_backlog_pkey PRIMARY KEY (destination, room_id);
ALTER TABLE ONLY destinations
ADD CONSTRAINT destinations_pkey PRIMARY KEY (destination);
ALTER TABLE ONLY device_lists_changes_converted_stream_position
ADD CONSTRAINT device_lists_changes_converted_stream_position_lock_key UNIQUE (lock);
ALTER TABLE ONLY device_lists_changes_in_room_max_pruned_stream_id
ADD CONSTRAINT device_lists_changes_in_room_max_pruned_stream_id_lock_key UNIQUE (lock);
ALTER TABLE ONLY device_lists_remote_pending
ADD CONSTRAINT device_lists_remote_pending_pkey PRIMARY KEY (stream_id);
ALTER TABLE ONLY devices
ADD CONSTRAINT device_uniqueness UNIQUE (user_id, device_id);
@@ -1644,20 +1651,21 @@
CREATE INDEX current_state_delta_stream_room_idx ON current_state_delta_stream USING btree (room_id, stream_id);
CREATE INDEX current_state_events_member_index ON current_state_events USING btree (state_key) WHERE (type = 'm.room.member'::text);
CREATE INDEX current_state_events_members_room_index ON current_state_events USING btree (room_id, membership) WHERE (type = 'm.room.member'::text);
CREATE INDEX current_state_events_stream_ordering_idx ON current_state_events USING btree (event_stream_ordering);
CREATE UNIQUE INDEX delayed_events_idx ON delayed_events USING btree (delay_id);
CREATE INDEX delayed_events_is_processed ON delayed_events USING btree (is_processed);
CREATE INDEX delayed_events_room_state_event_idx ON delayed_events USING btree (room_id, event_type, state_key) WHERE (state_key IS NOT NULL);
CREATE INDEX delayed_events_send_ts ON delayed_events USING btree (send_ts);
CREATE INDEX deleted_pushers_stream_id ON deleted_pushers USING btree (stream_id);
CREATE INDEX destination_rooms_room_id ON destination_rooms USING btree (room_id);
+CREATE INDEX destination_rooms_sticky_events_backlog_room_id ON destination_rooms_sticky_events_backlog USING btree (room_id);
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);
@@ -1850,20 +1858,22 @@
CREATE TRIGGER check_event_stream_ordering BEFORE INSERT OR UPDATE ON room_memberships FOR EACH ROW EXECUTE FUNCTION check_event_stream_ordering();
CREATE TRIGGER check_partial_state_events BEFORE INSERT OR UPDATE ON partial_state_events FOR EACH ROW EXECUTE FUNCTION check_partial_state_events();
CREATE TRIGGER delete_read_write_lock_parent_trigger AFTER DELETE ON worker_read_write_locks FOR EACH ROW EXECUTE FUNCTION delete_read_write_lock_parent();
CREATE TRIGGER upsert_read_write_lock_parent_trigger BEFORE INSERT ON worker_read_write_locks FOR EACH ROW EXECUTE FUNCTION upsert_read_write_lock_parent();
ALTER TABLE ONLY access_tokens
ADD CONSTRAINT access_tokens_refresh_token_id_fkey FOREIGN KEY (refresh_token_id) REFERENCES refresh_tokens(id) ON DELETE CASCADE;
ALTER TABLE ONLY destination_rooms
ADD CONSTRAINT destination_rooms_destination_fkey FOREIGN KEY (destination) REFERENCES destinations(destination);
ALTER TABLE ONLY destination_rooms
ADD CONSTRAINT destination_rooms_room_id_fkey FOREIGN KEY (room_id) REFERENCES rooms(room_id);
+ALTER TABLE ONLY destination_rooms_sticky_events_backlog
+ ADD CONSTRAINT destination_rooms_sticky_events_backlog_room_id_fkey FOREIGN KEY (room_id) REFERENCES rooms(room_id) ON DELETE CASCADE;
ALTER TABLE ONLY event_edges
ADD CONSTRAINT event_edges_event_id_fkey FOREIGN KEY (event_id) REFERENCES events(event_id);
ALTER TABLE ONLY event_failed_pull_attempts
ADD CONSTRAINT event_failed_pull_attempts_room_id_fkey FOREIGN KEY (room_id) REFERENCES rooms(room_id);
ALTER TABLE ONLY event_forward_extremities
ADD CONSTRAINT event_forward_extremities_event_id FOREIGN KEY (event_id) REFERENCES events(event_id) DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE ONLY current_state_events
ADD CONSTRAINT event_stream_ordering_fkey FOREIGN KEY (event_stream_ordering) REFERENCES events(stream_ordering) NOT VALID;
ALTER TABLE ONLY local_current_membership
ADD CONSTRAINT event_stream_ordering_fkey FOREIGN KEY (event_stream_ordering) REFERENCES events(stream_ordering) NOT VALID;
state (sqlite)Unchanged state (postgres)Unchanged |
reivilibre
force-pushed
the
rei/sticky_events_fed_backlog
branch
from
September 1, 2026 10:40
ff65c5e to
79d0686
Compare
4 tasks done
reivilibre
added a commit
that referenced
this pull request
Sep 15, 2026
…eparation and completion. (#20166) A key refactoring for, and split out of, #20165 Would be easier to land first to isolate the diff. Should be a standalone change with no behavioural change. Motivation is that #20165 will round-robin between 'main queue' transactions and 'sticky event' transactions. To keep the data flow clear, I wanted to insert a typed struct (well, `attrs` dataclass) as an interface between the 'preparation' of a transaction and its 'completion'. Doing this whilst keeping the asynchronous context manager style did not lead to a readable result in my opinion. (I would also say the async context manager is a touch 'magic' / obscures control flow, but I suspect this is largely down to opinion.) Replace _TransactionQueueManager with prepare/complete transaction methods --------- Signed-off-by: Olivier 'reivilibre <oliverw@matrix.org>
reivilibre
force-pushed
the
rei/sticky_events_fed_backlog
branch
3 times, most recently
from
September 15, 2026 16:53
978f82b to
a6b625f
Compare
reivilibre
force-pushed
the
rei/sticky_events_fed_backlog
branch
from
September 15, 2026 16:55
a6b625f to
31e987f
Compare
reivilibre
force-pushed
the
rei/sticky_events_fed_backlog
branch
from
September 15, 2026 17:18
31e987f to
8d3fa18
Compare
reivilibre
marked this pull request as ready for review
September 16, 2026 09:24
reivilibre
requested review from
MadLittleMods
and removed request for
a team
September 16, 2026 09:24
HarisDotParis
pushed a commit
to HarisDotParis/synapse
that referenced
this pull request
Sep 16, 2026
…eparation and completion. (element-hq#20166) A key refactoring for, and split out of, element-hq#20165 Would be easier to land first to isolate the diff. Should be a standalone change with no behavioural change. Motivation is that element-hq#20165 will round-robin between 'main queue' transactions and 'sticky event' transactions. To keep the data flow clear, I wanted to insert a typed struct (well, `attrs` dataclass) as an interface between the 'preparation' of a transaction and its 'completion'. Doing this whilst keeping the asynchronous context manager style did not lead to a readable result in my opinion. (I would also say the async context manager is a touch 'magic' / obscures control flow, but I suspect this is largely down to opinion.) Replace _TransactionQueueManager with prepare/complete transaction methods --------- Signed-off-by: Olivier 'reivilibre <oliverw@matrix.org>
Co-authored-by: Eric Eastwood <erice@element.io>
Co-authored-by: Eric Eastwood <erice@element.io>
MadLittleMods
approved these changes
Oct 2, 2026
reivilibre
added a commit
that referenced
this pull request
Oct 15, 2026
…are newly joining the room. (#20337) Part of: MSC4354 (Experimental Feature tracking issue #19409) Follows: #20165 (Sticky Event federation backlog + catch-up transactions) The MSC says that we need to tell a newly-joining server about all of our sticky events (yes, even the ones before the server joined the room). This PR introduces the mechanism, building on the 'Sticky Event backlog' mechanism introduced in #20165, to make that happen. Most of the logic is taken from the original Sticky Events PR #18968, but the Sticky Event backlog system did not exist back then. (The benefit of having the persistent Sticky Event backlog is that we don't need to rely on the federation senders not crashing to make sure we get around to sending the Sticky Events out.) - Add NEW_SERVER_JOINED replication command. (This is a cross-worker 'wake-up call' to check for a sticky event backlog) We send it when persisting a state change that causes a new server to be in the room. We handle it by waking up federation senders for the destination, including checking the sticky event backlog tables. - When a server newly joins a room, we insert a sticky event backlog row for that server, if there are any Sticky Events needing to be sent. --------- Signed-off-by: Olivier 'reivilibre <oliverw@matrix.org>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of: MSC4354 (Sticky Events)
Our locally-sent Sticky Events need to be reliably (within bounds of possibility) sent to remote homeservers.
This PR adds the mechanism to make this happen.
Notable omissions:
Review Guidance
The following commit is refactoring and hopefully stands alone, with no behavioural changes. I suggest considering it on its own.
user_is_local_like_patternAfter that, I recommend looking at the database schema (delta introducing a table) to understand the data model
for tracking backlogged sticky events. That's more or less the central part of this PR.
It may be helpful to remember that Sticky Events are events that are temporarily 'promoted' such that we want to ensure that all users and all homeservers receive them,
without letting them fall down 'gaps' of unretrieved history. Essentially, it's a temporary delivery reliability increase.
There are then roughly three logical pieces to review:
we have succeeded in sending them
is for sticky events to not have any special processing)
The docstring on
mark_backlogged_sticky_events_after_catchup_transactioncontains a diagrammatical explanation of thismechanism and how it works.
real-time PDUs and EDUs
Or if you prefer, it may be easiest to just start looking at the storage changes
to get an idea of the storage contract exposed by the database layer, since
the federation transmission logic itself is (I think) fairly understandable,
as long as you trust that contract.
Happy to pair, explain, add comments, anything; just let me know!
Replace _TransactionQueueManager with prepare/complete transaction methodsSplit out at Refactor the federation transmission code to delineate transaction preparation and completion. #20166
Pull out
user_is_local_like_patternunwraputilityAdd
destination_room_sticky_events_backlogtableIntroduce a StickyEventStreamPosition NewType
Add store methods
Wire sticky event backlog transactions into the per-destination queue
Add mark of backlog after catch-up transaction
Add tests