Skip to content

Expose MSC4354 Sticky Events over MSC4186 (Simplified) Sliding Sync.#19591

Open
reivilibre wants to merge 14 commits intodevelopfrom
rei/sticky_events_sliding_sync
Open

Expose MSC4354 Sticky Events over MSC4186 (Simplified) Sliding Sync.#19591
reivilibre wants to merge 14 commits intodevelopfrom
rei/sticky_events_sliding_sync

Conversation

@reivilibre
Copy link
Copy Markdown
Contributor

@reivilibre reivilibre commented Mar 20, 2026

Follows: #19487
Part of: MSC4354 whose experimental feature tracking issue is #19409

This PR implements the Sliding Sync (MSC4186) extension described in MSC4354, allowing sliding sync clients
to receive sticky events in a reliable way.

The logic is much the same as for oldschool sync (implementation in #19487),
although in the sliding sync extension, the client can choose their own limit
and must control their own pagination through an extra token in the extension request/response bodies.

EDIT: Note this PR does not yet send down existing sticky events in the room when the room has been newly-joined.
This newly-discovered gap is tracked at #19662 and will be addressed for both current sync and MSC4186 SSS soon.


This pull request is commit-by-commit review friendly.

  1. Add gather_optional_coroutines/7 overload

  2. Add fields for sticky events sliding sync extension

  3. Add explicit Absent utility type

  4. Implement sliding sync extension for sticky events

  5. Add sliding sync extension test

  6. drive-by docstring tweak on ordering

@reivilibre reivilibre changed the title Expose [MSC4354 Sticky Events](https://github.com/matrix-org/matrix-spec-proposals/pull/4354) over [MSC4186 (Simplified) Sliding Sync](https://github.com/matrix-org/matrix-spec-proposals/pull/4186). Expose MSC4354 Sticky Events over MSC4186 (Simplified) Sliding Sync. Mar 20, 2026
Comment thread synapse/util/sentinel.py
Comment on lines +26 to +27
If you want a Pydantic-compatible Sentinel that is suitable for expressing
'absent from some parsed JSON payload' or equivalent, see `Absent`.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Being honest, I discovered Sentinel.UNSET_SENTINEL after having written Absent, so this is post-hoc justification, however I also think it's valuable to have a clean domain-specific 'absent' marker that is not also repurposed for other uses throughout the code. The name Absent also seems clearer to me than Sentinel if I was reading a Pydantic schema, but it's also fair to say that a quick go-to-definition would clear that up.


Open to opinions though; it could be the case that it makes sense to unify them after all.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Probably makes sense to unify all of them as all of the use cases are absent use cases.

I'm not really understanding the distinction between "non-API-facing" either.

Comment thread synapse/handlers/sliding_sync/extensions.py Outdated
@reivilibre reivilibre force-pushed the rei/sticky_events_sliding_sync branch 4 times, most recently from 360c60b to adb3191 Compare March 20, 2026 16:20
Comment thread pyproject.toml Outdated
@reivilibre reivilibre force-pushed the rei/sticky_events_sliding_sync branch from 97efd43 to 6883dcd Compare March 20, 2026 17:24
@reivilibre reivilibre marked this pull request as ready for review March 20, 2026 17:25
@reivilibre reivilibre requested a review from a team as a code owner March 20, 2026 17:25
Comment thread synapse/handlers/sliding_sync/extensions.py Outdated
Signed-off-by: Olivier 'reivilibre <oliverw@matrix.org>
Fixes
builtins.NotImplementedError: Cannot check isinstance when validating
from json, use a JsonOrPython validator instead.
This reverts commit 6883dcd.

EPEL 10 only had 2.9.2 so try to work around issue without updating
builtins.NotImplementedError: Cannot check isinstance when validating
from json, use a JsonOrPython validator instead.
@reivilibre reivilibre force-pushed the rei/sticky_events_sliding_sync branch from 6883dcd to c5c1b01 Compare April 7, 2026 16:14
@reivilibre reivilibre requested review from a team and erikjohnston and removed request for erikjohnston April 7, 2026 17:44
Comment thread synapse/types/__init__.py
Comment on lines +123 to +124
For a Sentinel for internal (non-API-facing) use, instead consider
`Sentinel.UNSET_SENTINEL`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Wording seems off

Suggested change
For a Sentinel for internal (non-API-facing) use, instead consider
`Sentinel.UNSET_SENTINEL`.
For internal (non-API-facing) use, consider `Sentinel.UNSET_SENTINEL`.

(also applies to the Absent docstring below)

Comment thread synapse/types/__init__.py
Comment on lines +129 to +131
# Making this an Enum member makes this compatible with type narrowing,
# meaning `x is not Absent` will narrow `x: int | AbsentType` to `x: int` etc.
_Absent = object()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should align the comments between this and Sentinel.UNSET_SENTINEL

Comment thread synapse/types/__init__.py
Comment on lines +133 to +136
@classmethod
def __get_pydantic_core_schema__(
cls, source_type: object, handler: GetCoreSchemaHandler
) -> CoreSchema:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should comment why we define this

Comment thread synapse/types/__init__.py
NonNegativeStrictInt = Annotated[StrictInt, annotated_types.Ge(0)]
"""A strict integer that must be greater than or equal to zero.

Should be preferred in place of Pydantic's own (lax) NonNegativeInt.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why? (comment)

Comment thread synapse/util/sentinel.py
Comment on lines +26 to +27
If you want a Pydantic-compatible Sentinel that is suitable for expressing
'absent from some parsed JSON payload' or equivalent, see `Absent`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Probably makes sense to unify all of them as all of the use cases are absent use cases.

I'm not really understanding the distinction between "non-API-facing" either.

Comment on lines +1008 to +1020
since_token = sticky_events_request.since or SlidingSyncStickyEventsToken(
sticky_events_stream_id=0
)
(
sticky_events_to_id,
room_to_event_ids,
) = await self.store.get_sticky_events_in_rooms(
all_interested_room_ids,
from_id=since_token.sticky_events_stream_id,
to_id=to_token.sticky_events_key,
now=now,
limit=min(sticky_events_request.limit, StickyEvent.MAX_EVENTS_IN_SYNC),
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The Sticky Events extension having it's own pagination inside Sliding Sync seems like a smell to me.

Related discussion: matrix-org/matrix-spec-proposals#4354 (comment)

PATTERN = re.compile(r"^sticky_([0-9]+)$")

def __init__(self, *, sticky_events_stream_id: int) -> None:
self.sticky_events_stream_id = sticky_events_stream_id
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re: #19365 (comment)

Seems like we should be using MultiWriterStreamToken or we should at-least FIXME with our desire

Comment on lines +133 to +136
@classmethod
def __get_pydantic_core_schema__(
cls, source_type: object, handler: GetCoreSchemaHandler
) -> CoreSchema:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Comment to explain why we have these methods

response_body, _ = self.do_sync(sync_body, tok=user1_tok)
self._assert_sticky_events_response(response_body, None)

def test_wait_for_new_data(self) -> None:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would expect to also see test_wait_for_new_data_timeout (like other extensions)

"""


class SlidingSyncStickyEventsExtensionTestCase(SlidingSyncBase):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I didn't analyze the tests that closely yet. Skimming-wise the coverage seemed pretty good

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants