Backgrounds membership updates when changing the avatar or the display name#19311
Conversation
d475f86 to
29032a7
Compare
06a82f8 to
580db36
Compare
580db36 to
368cb19
Compare
anoadragon453
left a comment
There was a problem hiding this comment.
Thanks for doing this! Have you tried it out and seen what the user experience is like?
A few notes below, but nothing major.
synapse/handlers/profile.py
Outdated
| if last_room_id: | ||
| unhandled_room_ids = [] | ||
| for room_id in room_ids: | ||
| if room_id > last_room_id: | ||
| unhandled_room_ids.append(room_id) | ||
| room_ids = unhandled_room_ids |
There was a problem hiding this comment.
Since room_ids is already sorted, we can break after we find a room_id is greater than last_room_id.
There was a problem hiding this comment.
I don't think so, the unhandled rooms being at the end of the list and not the beginning.
I can however reversed the list and break in an else, on it.
There was a problem hiding this comment.
Ah! Or alternatively, take the remainder of the list once you reach the first unhandled room.
But both work.
There was a problem hiding this comment.
Looking at this again, it's starting to be pretty difficult to read. We could simplify this greatly using the built-in bisect module's bisect_right method to find the index of where to insert an item in a sorted list:
from bisect import bisect_right
# ...
if last_room_id:
room_ids = room_ids[bisect_right(room_ids, last_room_id):]There was a problem hiding this comment.
this is dangerous I think : we may have left the room in the meantime :)
I am going to try your alternate proposal and see if it looks clearer.
There was a problem hiding this comment.
Well copilot is right so let's go for the alternate.
I tested it, it is instant as expected. |
There was a problem hiding this comment.
Pull request overview
This PR aims to prevent long-running profile-driven membership updates (display name / avatar changes) from causing client timeouts by moving the work into a resumable background task.
Changes:
- Add profile-handler logic to schedule/cancel a per-user background task to update join state events after profile changes.
- Extend task scheduler execution wrapper to handle task cancellation explicitly.
- Add tests covering background membership update behavior and restart resumption, plus a changelog entry.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
synapse/handlers/profile.py |
Schedules a resumable update_join_states task instead of performing membership updates inline. |
synapse/util/task_scheduler.py |
Adjusts scheduled task wrapper behavior around cancellation/error handling. |
tests/handlers/test_profile.py |
Adds tests for membership propagation, background behavior, and “resume after restart”. |
changelog.d/19311.misc |
Documents the behavior change. |
Comments suppressed due to low confidence (1)
synapse/util/task_scheduler.py:497
- Catching
defer.CancelledErrorhere and then unconditionally calling_store.update_scheduled_task(... result=None ...)will overwrite any previously persistedtask.result(breaking the existing cancellation semantics/tests that expect progress to be preserved). It can also raiseKeyErroratself._running_tasks.pop(task.id)becauseon_cancel_taskalready removes the entry. Consider either lettingCancelledErrorpropagate (relying onon_cancel_taskto update status), or explicitly preserve the stored result on cancellation and make the_running_tasksremoval idempotent.
result = None
error = None
try:
(status, result, error) = await function(task)
except defer.CancelledError:
status = TaskStatus.CANCELLED
except Exception:
f = Failure()
logger.error(
"scheduled task %s failed",
task.id,
exc_info=(f.type, f.value, f.getTracebackObject()),
)
status = TaskStatus.FAILED
error = f.getErrorMessage()
await self._store.update_scheduled_task(
task.id,
self._clock.time_msec(),
status=status,
result=result,
error=error,
)
self._running_tasks.pop(task.id)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
anoadragon453
left a comment
There was a problem hiding this comment.
A few more things, but we're getting there!
synapse/handlers/profile.py
Outdated
| if last_room_id: | ||
| unhandled_room_ids = [] | ||
| for room_id in room_ids: | ||
| if room_id > last_room_id: | ||
| unhandled_room_ids.append(room_id) | ||
| room_ids = unhandled_room_ids |
There was a problem hiding this comment.
Looking at this again, it's starting to be pretty difficult to read. We could simplify this greatly using the built-in bisect module's bisect_right method to find the index of where to insert an item in a sorted list:
from bisect import bisect_right
# ...
if last_room_id:
room_ids = room_ids[bisect_right(room_ids, last_room_id):]There was a problem hiding this comment.
I think this is pretty much ready to go, thanks for your patience @MatMaul!
Just a couple small things before merging.
synapse/handlers/profile.py
Outdated
| # Filter out room IDs that have already been handled | ||
| # by finding the first room ID greater than the last handled room ID | ||
| # and slicing the list from that point onwards. | ||
| first_unhandled_room_idx = 0 | ||
| for idx, room_id in enumerate(room_ids): | ||
| if room_id > last_room_id: | ||
| first_unhandled_room_idx = idx | ||
| break | ||
| room_ids = room_ids[first_unhandled_room_idx:] |
There was a problem hiding this comment.
@MatMaul What did you think about this (potentially even simpler) implementation? #19311 (comment)
There was a problem hiding this comment.
I think you missed my comment here 🙂 #19311 (comment)
There was a problem hiding this comment.
Actually it seems like bisect doc is saying that it is using comparison and not equal so we should be fine.
Can you double check what I interpreted from the doc ?
Thanks !
There was a problem hiding this comment.
I've pushed an update since pretty sure of it from the doc, thanks for the trick!
There was a problem hiding this comment.
Oops, yes I did miss your comment! Apologies.
But yes, even if we leave a room, we're computing a new index based on the room ID, which disappearing values shouldn't impact.
Co-authored-by: Andrew Morgan <[email protected]>
anoadragon453
left a comment
There was a problem hiding this comment.
This now LGTM. Exciting quality-of-life feature, thanks @MatMaul!
This PR contains the following updates: | Package | Update | Change | |---|---|---| | [element-hq/synapse](https://github.com/element-hq/synapse) | minor | `v1.149.1` → `v1.150.0` | --- ### Release Notes <details> <summary>element-hq/synapse (element-hq/synapse)</summary> ### [`v1.150.0`](https://github.com/element-hq/synapse/releases/tag/v1.150.0) [Compare Source](element-hq/synapse@v1.149.1...v1.150.0) ### Synapse 1.150.0 (2026-03-24) No significant changes since 1.150.0rc1. ### Synapse 1.150.0rc1 (2026-03-17) #### Features - Add experimental support for the [MSC4370](matrix-org/matrix-spec-proposals#4370) Federation API `GET /extremities` endpoint. ([#​19314](element-hq/synapse#19314)) - [MSC4140: Cancellable delayed events](matrix-org/matrix-spec-proposals#4140): When persisting a delayed event to the timeline, include its `delay_id` in the event's `unsigned` section in `/sync` responses to the event sender. ([#​19479](element-hq/synapse#19479)) - Expose [MSC4354 Sticky Events](matrix-org/matrix-spec-proposals#4354) over the legacy (v3) /sync API. ([#​19487](element-hq/synapse#19487)) - When Matrix Authentication Service (MAS) integration is enabled, allow MAS to set the user locked status in Synapse. ([#​19554](element-hq/synapse#19554)) #### Bugfixes - Fix `Build and push complement image` CI job pointing to non-existent image. ([#​19523](element-hq/synapse#19523)) - Fix a bug introduced in v1.26.0 that caused deactivated, erased users to not be removed from the user directory. ([#​19542](element-hq/synapse#19542)) #### Improved Documentation - In the Admin API documentation, always express path parameters as `/<param>` instead of as `/$param`. ([#​19307](element-hq/synapse#19307)) - Update docs to clarify `outbound_federation_restricted_to` can also be used with the [Secure Border Gateway (SBG)](https://element.io/en/server-suite/secure-border-gateways). ([#​19517](element-hq/synapse#19517)) - Unify Complement developer docs. ([#​19518](element-hq/synapse#19518)) #### Internal Changes - Put membership updates in a background resumable task when changing the avatar or the display name. ([#​19311](element-hq/synapse#19311)) - Add in-repo Complement test to sanity check Synapse version matches git checkout (testing what we think we are). ([#​19476](element-hq/synapse#19476)) - Migrate `dev` dependencies to [PEP 735](https://peps.python.org/pep-0735/) dependency groups. ([#​19490](element-hq/synapse#19490)) - Remove the optional `systemd-python` dependency and the `systemd` extra on the `synapse` package. ([#​19491](element-hq/synapse#19491)) - Avoid re-computing the event ID when cloning events. ([#​19527](element-hq/synapse#19527)) - Allow caching of the `/versions` and `/auth_metadata` public endpoints. ([#​19530](element-hq/synapse#19530)) - Add a few labels to the number groupings in the `Processed request` logs. ([#​19548](element-hq/synapse#19548)) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My44NC4yIiwidXBkYXRlZEluVmVyIjoiNDMuODQuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW1hZ2UiXX0=--> Reviewed-on: https://gitea.alexlebens.dev/alexlebens/infrastructure/pulls/5040 Co-authored-by: Renovate Bot <[email protected]> Co-committed-by: Renovate Bot <[email protected]>
Membership updates can be quite long since it can trigger state resolution.
In the meantime the user will get a timeout error, and may try again, putting even more load on the server.
Pull Request Checklist