Skip to content

Conversation

@reivilibre
Copy link
Contributor

@reivilibre reivilibre commented Jan 9, 2026

Part of: MSC4354

Follows: #19340 (a necessary bugfix for /event/ to set this metadata)

Partially supersedes: #18968

This PR implements the first batch of work to support MSC4354 Sticky Events.

Sticky events are events that have been configured with a finite 'stickiness' duration,
capped to 1 hour per current MSC draft.

Whilst an event is sticky, we provide stronger delivery guarantees for the event, both to
our clients and to remote homeservers, essentially making it reliable delivery as long as we
have a functional connection to the client/server and until the stickiness expires.

This PR merely supports creating sticky events and receiving the sticky TTL metadata in clients.
It is not suitable for trialling sticky events since none of the other semantics are implemented.

The current plan is to follow this PR up with more PRs, roughly parcelled up as follows:

  • Implement the sliding sync extension specified in MSC4354, to proactively notify
  • Implement the oldschool sync support, specified in MSC4354, to do the same but for clients still using oldschool sync.
  • Notice new servers joining rooms and proactively tell them about sticky events.
  • Add special federation catch-up support for sticky events, so that (as long as we have a connection) they don't get 'dropped' in the gaps between federation /send requests.
  • Re-evaluate soft-failed sticky events when we think that might be possible
  1. Add MSC4354 experimental feature flag

  2. Expose MSC4354 enablement on /versions

  3. Add constants for sticky events

  4. Add sticky_events table

  5. Add sticky events store and stream

  6. EventBase: add the concept of sticky_duration

  7. EventBuilder: allow building events with sticky event fields

  8. store method: insert_sticky_events_txn

  9. When persisting currently-sticky events, add to sticky event stream

  10. Allow clients to send sticky events
    Including delayed events

  11. Add test helper for sending sticky events

  12. Expose the sticky event TTL to clients

  13. Add a test for sticky TTL calculation and exposure to clients

@reivilibre reivilibre requested a review from a team as a code owner January 9, 2026 16:36
@reivilibre reivilibre changed the title Support sending and receiving [MSC4354 Sticky Event](https://github.com/matrix-org/matrix-spec-proposals/pull/4354) metadata. Support sending and receiving MSC4354 Sticky Event metadata. Jan 9, 2026
@reivilibre reivilibre force-pushed the rei/sticky_events1 branch 2 times, most recently from e49d4bd to 78ee6e8 Compare January 16, 2026 09:00

class StickyEvent:
QUERY_PARAM_NAME: Final = "org.matrix.msc4354.sticky_duration_ms"
FIELD_NAME: Final = "msc4354_sticky"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
FIELD_NAME: Final = "msc4354_sticky"
EVENT_FIELD_NAME: Final = "msc4354_sticky"

Maximum stickiness duration as specified in MSC4354.
Ensures that data in the /sync response can go down and not grow unbounded.
"""
MAX_EVENTS_IN_SYNC: Final = 100
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 100? (we should explain in the attribute docstring)

(also not used yet)



class StickyEvent:
QUERY_PARAM_NAME: Final = "org.matrix.msc4354.sticky_duration_ms"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should explain what endpoint this applies to


class StickyEvent:
QUERY_PARAM_NAME: Final = "org.matrix.msc4354.sticky_duration_ms"
FIELD_NAME: Final = "msc4354_sticky"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The separation between StickyEventField and StickyEvent.FIELD_NAME is slightly mind bending.

Comment on lines +2655 to +2658
if self._msc4354_enabled and event.sticky_duration():
# The de-outliered event is sticky. Update the sticky events table to ensure
# we deliver this down /sync.
self.store.insert_sticky_events_txn(txn, [event])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we going to give up on events that were valid sticky events sent within the hour but before msc4354_enabled?

Seems like a fine trade-off but we should at-least comment about it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Didn't consider that, not too worried about it, but on the other hand we could track sticky events even if we don't deliver them, whilst the feature is turned off.

Thoughts overall?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No specific thoughts. Both have tradeoffs. With the current setup, you can completely turn the feature off and I think it's pretty acceptable to drop them on the floor since this an hour window max. But it could also just make more sense for things to behave the same.

For things like the Sliding Sync feature, we had to think about this a lot harder as there was no time limit.

Comment on lines +578 to +580
event_dict[StickyEvent.FIELD_NAME] = {
"duration_ms": event.sticky_duration_ms,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like we should re-use StickyEventField here

(applies to other places)

f"{EventUnsignedContentFields.STICKY_TTL} field unexpectedly found in {event_id}: {event}",
)

def test_sticky_event_via_event_endpoint(self) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also have a test for when a sticky event is sent but msc4354_enabled: False

Comment on lines 49 to 63
@attr.s(slots=True, frozen=True, auto_attribs=True)
class EventDetails:
room_id: RoomID
type: EventType
state_key: StateKey | None
origin_server_ts: Timestamp | None
content: JsonDict
device_id: DeviceID | None
sticky_duration_ms: int | None


@attr.s(slots=True, frozen=True, auto_attribs=True)
class DelayedEventDetails(EventDetails):
delay_id: DelayID
user_localpart: UserLocalpart
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked into the details but should we have a similarly named StickyEventDetails that includes sticky_duration_ms instead of in EventDetails

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is purely tracking the sticky duration so that when we send the delayed event, we can mark it as sticky at that time.

Interestingly DelayedEventDetails inherits from EventDetails but there are no standalone usage of EventDetails.

I guess the idea was just to compartmentalise 'this is information for sending the event' and 'this is the information for scheduling it', then again I don't know why the user localpart ( = sender ?) is stored on the delayed event side then.

Anyway, to me it seems reasonable to have it be part of the event details themselves. If we had StickyEventDetails in addition, DelayedEventDetails would have to inherit from that and I don't think the extra layer would help us with anything. (maybe I'm missing something from this design though)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants