Skip to content

Backgrounds membership updates when changing the avatar or the display name#19311

Merged
anoadragon453 merged 14 commits intoelement-hq:developfrom
tchapgouv:update-join-states-task
Mar 5, 2026
Merged

Backgrounds membership updates when changing the avatar or the display name#19311
anoadragon453 merged 14 commits intoelement-hq:developfrom
tchapgouv:update-join-states-task

Conversation

@MatMaul
Copy link
Copy Markdown
Contributor

@MatMaul MatMaul commented Dec 16, 2025

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

@MatMaul MatMaul force-pushed the update-join-states-task branch 3 times, most recently from d475f86 to 29032a7 Compare February 6, 2026 16:22
@MatMaul MatMaul marked this pull request as ready for review February 6, 2026 16:25
@MatMaul MatMaul requested a review from a team as a code owner February 6, 2026 16:25
@MatMaul MatMaul force-pushed the update-join-states-task branch 2 times, most recently from 06a82f8 to 580db36 Compare February 17, 2026 19:52
@MatMaul MatMaul force-pushed the update-join-states-task branch from 580db36 to 368cb19 Compare February 17, 2026 21:26
Copy link
Copy Markdown
Member

@anoadragon453 anoadragon453 left a comment

Choose a reason for hiding this comment

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

Thanks for doing this! Have you tried it out and seen what the user experience is like?

A few notes below, but nothing major.

Comment on lines +645 to +650
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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since room_ids is already sorted, we can break after we find a room_id is greater than last_room_id.

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.

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.

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.

Done!

Copy link
Copy Markdown
Member

@anoadragon453 anoadragon453 Mar 3, 2026

Choose a reason for hiding this comment

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

Ah! Or alternatively, take the remainder of the list once you reach the first unhandled room.

But both work.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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):]

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.

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.

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.

Well copilot is right so let's go for the alternate.

@MatMaul
Copy link
Copy Markdown
Contributor Author

MatMaul commented Mar 3, 2026

Thanks for doing this! Have you tried it out and seen what the user experience is like?

I tested it, it is instant as expected.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.CancelledError here and then unconditionally calling _store.update_scheduled_task(... result=None ...) will overwrite any previously persisted task.result (breaking the existing cancellation semantics/tests that expect progress to be preserved). It can also raise KeyError at self._running_tasks.pop(task.id) because on_cancel_task already removes the entry. Consider either letting CancelledError propagate (relying on on_cancel_task to update status), or explicitly preserve the stored result on cancellation and make the _running_tasks removal 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.

Copy link
Copy Markdown
Member

@anoadragon453 anoadragon453 left a comment

Choose a reason for hiding this comment

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

A few more things, but we're getting there!

Comment on lines +645 to +650
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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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):]

@MatMaul MatMaul requested a review from anoadragon453 March 4, 2026 08:35
Copy link
Copy Markdown
Member

@anoadragon453 anoadragon453 left a comment

Choose a reason for hiding this comment

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

I think this is pretty much ready to go, thanks for your patience @MatMaul!

Just a couple small things before merging.

Comment on lines +647 to +655
# 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:]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@MatMaul What did you think about this (potentially even simpler) implementation? #19311 (comment)

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.

I think you missed my comment here 🙂 #19311 (comment)

Copy link
Copy Markdown
Contributor Author

@MatMaul MatMaul Mar 5, 2026

Choose a reason for hiding this comment

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

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 !

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.

I've pushed an update since pretty sure of it from the doc, thanks for the trick!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

@MatMaul MatMaul requested a review from anoadragon453 March 5, 2026 09:58
Copy link
Copy Markdown
Member

@anoadragon453 anoadragon453 left a comment

Choose a reason for hiding this comment

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

This now LGTM. Exciting quality-of-life feature, thanks @MatMaul!

@anoadragon453 anoadragon453 merged commit 699a898 into element-hq:develop Mar 5, 2026
41 checks passed
alexlebens pushed a commit to alexlebens/infrastructure that referenced this pull request Mar 24, 2026
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. ([#&#8203;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. ([#&#8203;19479](element-hq/synapse#19479))
- Expose [MSC4354 Sticky Events](matrix-org/matrix-spec-proposals#4354) over the legacy (v3) /sync API. ([#&#8203;19487](element-hq/synapse#19487))
- When Matrix Authentication Service (MAS) integration is enabled, allow MAS to set the user locked status in Synapse. ([#&#8203;19554](element-hq/synapse#19554))

#### Bugfixes

- Fix `Build and push complement image` CI job pointing to non-existent image. ([#&#8203;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. ([#&#8203;19542](element-hq/synapse#19542))

#### Improved Documentation

- In the Admin API documentation, always express path parameters as `/<param>` instead of as `/$param`. ([#&#8203;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). ([#&#8203;19517](element-hq/synapse#19517))
- Unify Complement developer docs. ([#&#8203;19518](element-hq/synapse#19518))

#### Internal Changes

- Put membership updates in a background resumable task when changing the avatar or the display name. ([#&#8203;19311](element-hq/synapse#19311))
- Add in-repo Complement test to sanity check Synapse version matches git checkout (testing what we think we are). ([#&#8203;19476](element-hq/synapse#19476))
- Migrate `dev` dependencies to [PEP 735](https://peps.python.org/pep-0735/) dependency groups. ([#&#8203;19490](element-hq/synapse#19490))
- Remove the optional `systemd-python` dependency and the `systemd` extra on the `synapse` package. ([#&#8203;19491](element-hq/synapse#19491))
- Avoid re-computing the event ID when cloning events. ([#&#8203;19527](element-hq/synapse#19527))
- Allow caching of the `/versions` and `/auth_metadata` public endpoints. ([#&#8203;19530](element-hq/synapse#19530))
- Add a few labels to the number groupings in the `Processed request` logs. ([#&#8203;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]>
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.

4 participants