Skip to content

Introduce support for MSC4429: Profile Updates for Legacy Sync#19556

Open
anoadragon453 wants to merge 115 commits into
developfrom
anoa/msc4429
Open

Introduce support for MSC4429: Profile Updates for Legacy Sync#19556
anoadragon453 wants to merge 115 commits into
developfrom
anoa/msc4429

Conversation

@anoadragon453

@anoadragon453 anoadragon453 commented Mar 13, 2026

Copy link
Copy Markdown
Member

This PR implements support for MSC4429: Profile Updates for Legacy Sync.

Recommended to review commit-by-commit. Commit history has become messy, suggest reviewing as a single item.

Paired with matrix-org/complement#849 and branch https://github.com/matrix-org/sytest/tree/anoa/msc4429

Tracking issue for removing unstable identifiers in Synapse: #19891

Pull Request Checklist

  • Pull request is based on the develop branch
  • Pull request includes a changelog file. The entry should:
    • Be a short description of your change which makes sense to users. "Fixed a bug that prevented receiving messages from other servers." instead of "Moved X method from EventStore to EventWorkerStore.".
    • Use markdown where necessary, mostly for code blocks.
    • End with either a period (.) or an exclamation mark (!).
    • Start with a capital letter.
    • Feel free to credit yourself, by adding a sentence "Contributed by @github_username." or "Contributed by [Your Name]." to the end of the entry.
  • Code style is correct (run the linters)

Copilot AI left a comment

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.

Pull request overview

Implements experimental MSC4429 support by introducing a new profile_updates stream (persisted + replicated) and surfacing profile field changes in legacy /sync when enabled.

Changes:

  • Add profile_updates stream tracking (DB schema, stream token field, notifier + replication stream plumbing).
  • Emit MSC4429 profile updates in /sync responses and add filter support for selecting profile fields.
  • Record profile field updates on profile mutations (set/unset displayname/avatar/custom fields, deactivation cleanup).

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/rest/client/test_rooms.py Updates hardcoded stream tokens to match the new StreamToken shape.
tests/rest/admin/test_room.py Updates hardcoded stream tokens to match the new StreamToken shape.
synapse/types/init.py Adds PROFILE_UPDATES stream key and extends StreamToken serialization/parsing.
synapse/streams/events.py Includes profile_updates_key in current/bounded stream tokens.
synapse/storage/schema/main/delta/94/01_profile_updates_seq.sql.postgres Adds Postgres sequence for the new stream.
synapse/storage/schema/main/delta/94/01_profile_updates.sql Adds profile_updates table + indexes.
synapse/storage/schema/init.py Bumps schema version to 94 and documents the change.
synapse/storage/databases/main/profile.py Adds profile update persistence/query APIs and stream ID generator wiring.
synapse/rest/client/versions.py Advertises org.matrix.msc4429 in unstable_features.
synapse/rest/client/sync.py Encodes MSC4429 profile update payload into /sync response.
synapse/replication/tcp/streams/_base.py Adds ProfileUpdatesStream replication stream.
synapse/replication/tcp/streams/init.py Registers ProfileUpdatesStream for replication.
synapse/replication/tcp/handler.py Replicates ProfileUpdatesStream from configured writers.
synapse/replication/tcp/client.py Handles incoming profile update replication and notifies listeners.
synapse/notifier.py Allows StreamKeyType.PROFILE_UPDATES to wake /sync waiters.
synapse/handlers/sync.py Generates initial + incremental MSC4429 profile update sections in /sync.
synapse/handlers/profile.py Records profile update markers on profile mutations and notifies.
synapse/config/experimental.py Adds msc4429_enabled config flag.
synapse/api/filtering.py Adds /sync filter support for selecting profile fields (MSC4429-gated).
docker/complement/conf/workers-shared-extra.yaml.j2 Enables MSC4429 in complement worker test config.
changelog.d/19556.feature Adds changelog entry for MSC4429 experimental support.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread synapse/replication/tcp/client.py Outdated
Comment thread synapse/storage/schema/main/delta/94/01_profile_updates.sql Outdated
Comment thread synapse/storage/databases/main/profile.py
Comment thread synapse/handlers/sync.py Outdated
@anoadragon453 anoadragon453 force-pushed the anoa/msc4429 branch 2 times, most recently from 83e699f to a846945 Compare March 17, 2026 16:21
anoadragon453 added a commit to matrix-org/sytest that referenced this pull request Mar 17, 2026
anoadragon453 added a commit to matrix-org/sytest that referenced this pull request Mar 17, 2026
@anoadragon453 anoadragon453 marked this pull request as ready for review March 18, 2026 15:58
@anoadragon453 anoadragon453 requested a review from a team as a code owner March 18, 2026 15:58
We create a stream table dedicated to tracking profile updates. The sync
machinary can this stream to understand whether a profile update
needs to be included in a client's sync response.
Add a new stream type for profile updates. This allows sync processes to
determine which profile updates a given user has or hasn't seen yet.
Replicate changes to the profile updates stream so that sync workers
know there's a new profile update, and wake up sync waiters accordingly.

Updates are keyed by `room_id` - sync waiters should only wake up if
a user they share a room with updates their profile.
Based on the provided since token and filter.
And update relevant documentation/integration test config.
Prevent the `ProfileRestServlet` from handling requests intended for the
`ProfileFieldRestServlet`.

If a requester tried to call PUT `.../profile/@user:domain/(key_id?)` a
worker that did not mount `ProfileFieldRestServlet`, they would then
receive a `405 Method Not Allowed` instead of the expected `404 Not
Found`.
Comment thread docs/upgrade.md Outdated
Comment thread synapse/handlers/sync.py Outdated
Comment thread synapse/handlers/sync.py Outdated
@jaywink

jaywink commented Jun 29, 2026

Copy link
Copy Markdown
Member

I believe this is ready for a new look. Thanks for all the reviews so far! 🙏

@jaywink jaywink changed the title Experimental support for MSC4429: Profile Updates for Legacy Sync Introduce support for MSC4429: Profile Updates for Legacy Sync Jun 30, 2026
jaywink added 2 commits July 1, 2026 11:22
… updates

Otherwise if the user has multiple clients doing incremental sync, they wont get their own profile updates to their other clients. For initial because it should probably match the incremental behaviour + it avoids clients needing to look up the profile separately potentially.
Comment thread docker/complement/conf/workers-shared-extra.yaml.j2 Outdated
Co-authored-by: Olivier 'reivilibre' <oliverw@element.io>

@reivilibre reivilibre left a comment

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.

This broadly seems on the right track, but I'm left wondering if there isn't some light strengthening we can make to the data model and the worker architecture (if you will) to make some of the more painful corners go away.

The sync logic is also maybe a little gnarly (to me), but that could be par for the course with sync.

I think if I was doing this again, the sync portion would probably be better as its own PR separate from the introduction of a new stream (as tricky as that could be) as there is a LOT to digest in this one PR.

Comment thread synapse/api/filtering.py Outdated
Comment thread synapse/handlers/profile.py Outdated
Comment thread synapse/api/constants.py Outdated
Comment on lines +145 to +148
await self.store.track_profile_updates_per_user(
stream_id=stream_id,
user_ids=users_who_share_rooms,
)

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.

it feels like this should be done in the same transaction as adding the profile update, as it again feels like the update can be lost half-way?
(Maybe I'm missing something as to why it doesn't matter)

Comment thread synapse/api/constants.py Outdated
membership=Membership.LEAVE,
)

elif self._msc4429_enabled and event.membership == Membership.JOIN:

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 think this might trigger for name changes. I haven't looked carefully if it's safe to do so, but I feel like this probably needs to consider the previous membership.

Comment thread synapse/handlers/sync.py Outdated

def get_lazy_loaded_profile_fields_cache(
self, cache_key: tuple[str, str | None, str, str]
) -> LruCache[str, str]:

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.

needs docstring, especially with a difficult signature like this

Comment thread synapse/handlers/sync.py Outdated
user_id: {"profile_updates": updates}
for user_id, updates in sync_result.profile_updates.items()
}

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.

(no good place to anchor this comment)

I think we are missing some tests for the actual wiring of this logic into sync itself.

In general I think we would benefit from a few API-level ('rest') sync tests against the profiles functionality.

Cases that seem relatively untested atm:

  • filters (that they actually exclude data)
  • (I guess, based on the review) returning falsy-valued data — though maybe that starts getting painful

And in general, non-sync cases that I think would benefit from coverage; I guess this is more profile-update related, but doing it at the 'rest' level might make sense:

  • exercising join → leave → re-join → leave to try and rule out bugs in that aspect
  • (from the review?) test that a displayname-only change doesn't write JOINED entries to all rooms

Comment thread synapse/handlers/sync.py Outdated
)
cache = self.get_lazy_loaded_profile_fields_cache(cache_key)
# Only send the field if we haven't recently sent it
if cache.get(other_user_id) is None:

@reivilibre reivilibre Jul 1, 2026

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'm struggling to wrap my head around it as the interactions are quite complex, but doesn't this mean 'did we send this field (for any value) recently as a lazy-loaded user that sent a message in the timeline'?
If so, what happens if you update your field; does this (wrongly?) quash the update?

Maybe this points to a missing test along the lines of:

  1. syncing user syncs (initial sync, lazy loading)
  2. user sets profile field, sends event in timeline
  3. syncing user syncs (lazy loading), gets the profile value, this adds the user to their lazy-loading 'profile has been sent' cache
  4. user updates profile field, sends another event in timeline (?)
  5. syncing user syncs (lazy loading), check they get the NEW profile value [I think currently they won't]

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants