Skip to content

fix: Cap WorkerLock timeout intervals to 60 seconds#19394

Merged
reivilibre merged 29 commits into
element-hq:developfrom
famedly:jason/cap-worker-locks
May 5, 2026
Merged

fix: Cap WorkerLock timeout intervals to 60 seconds#19394
reivilibre merged 29 commits into
element-hq:developfrom
famedly:jason/cap-worker-locks

Conversation

@jason-famedly

@jason-famedly jason-famedly commented Jan 20, 2026

Copy link
Copy Markdown
Contributor

Fixes the symptoms of #19315 / #19588 but not the underlying reason causing the number to grow so large in the first place.

ValueError: Exceeds the limit (4300 digits) for integer string conversion; use sys.set_int_max_str_digits() to increase the limit

Copied from the original pull request on Famedly's Synapse repo (with some edits):

Basing the time interval around a 5 seconds leaves a big window of waiting especially as this window is doubled each retry, when another worker could be making progress but can not.

Right now, the retry interval in seconds looks like [0.2, 5, 10, 20, 40, 80, 160, 320, (continues to double)] after which logging should start about excessive times and (relatively quickly) end up with an extremely large retry interval with an unrealistic expectation past the heat death of the universe. 1 year in seconds = 31,536,000.

With this change, retry intervals in seconds should look more like:

[
0.2, 
0.4, 
0.8, 
1.6, 
3.2, 
6.4, 
12.8, 
25.6, 
51.2, 
60, < never goes higher than this
]

Logging about excessive wait times will start at 10 minutes.

Previous breakdown when we were using 15 minutes
[
0.2, 
0.4, 
0.8, 
1.6, 
3.2, 
6.4, 
12.8, 
25.6, 
51.2, 
102.4,  # 1.7 minutes
204.8,  # 3.41 minutes
409.6,  # 6.83 minutes
819.2,  # 13.65 minutes  < logging about excessive times will start here, 13th iteration
900,  # 15 minutes < never goes higher than this
]

Further suggested work in this area could be to define the cap, the retry interval starting point and the multiplier depending on how frequently this lock should be checked. See data below for reasons why. Increasing the jitter range may also be a good idea

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)

@jason-famedly jason-famedly requested a review from a team as a code owner January 20, 2026 12:42
@CLAassistant

CLAassistant commented Jan 20, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@MadLittleMods MadLittleMods added the A-Workers (includes replication) label Jan 20, 2026
Comment thread changelog.d/19394.bugfix Outdated
Comment thread changelog.d/19394.bugfix Outdated
Comment thread synapse/handlers/worker_lock.py Outdated
Comment thread changelog.d/19394.bugfix Outdated
Co-authored-by: Eric Eastwood <madlittlemods@gmail.com>
@denzs

denzs commented Jan 23, 2026

Copy link
Copy Markdown

After the issue occured again in our prod:

2026-01-22 13:36:53.725errorsynapse.handlers.worker_lock - 280 - WARNING - sentinel - Lock timeout is getting excessive: 5120s. There may be a deadlock.
2026-01-22 13:36:53.981errorsynapse.handlers.worker_lock - 280 - WARNING - sentinel - Lock timeout is getting excessive: 10240s. There may be a deadlock.
2026-01-22 13:36:54.560errorsynapse.handlers.worker_lock - 280 - WARNING - sentinel - Lock timeout is getting excessive: 20480s. There may be a deadlock.
2026-01-22 13:36:54.798errorsynapse.handlers.worker_lock - 280 - WARNING - sentinel - Lock timeout is getting excessive: 40960s. There may be a deadlock.
2026-01-22 13:36:56.342errorsynapse.handlers.worker_lock - 280 - WARNING - sentinel - Lock timeout is getting excessive: 81920s. There may be a deadlock.

My hypothesis would be: the issue is not primarily about the dimensions of the growing timeout but, about the timeout being ignored at all?

At least the logged timeout is not reflected in the timestamp deltas of the log lines?!

@jason-famedly

Copy link
Copy Markdown
Contributor Author

After the issue occured again in our prod:

2026-01-22 13:36:53.725errorsynapse.handlers.worker_lock - 280 - WARNING - sentinel - Lock timeout is getting excessive: 5120s. There may be a deadlock.
2026-01-22 13:36:53.981errorsynapse.handlers.worker_lock - 280 - WARNING - sentinel - Lock timeout is getting excessive: 10240s. There may be a deadlock.
2026-01-22 13:36:54.560errorsynapse.handlers.worker_lock - 280 - WARNING - sentinel - Lock timeout is getting excessive: 20480s. There may be a deadlock.
2026-01-22 13:36:54.798errorsynapse.handlers.worker_lock - 280 - WARNING - sentinel - Lock timeout is getting excessive: 40960s. There may be a deadlock.
2026-01-22 13:36:56.342errorsynapse.handlers.worker_lock - 280 - WARNING - sentinel - Lock timeout is getting excessive: 81920s. There may be a deadlock.

My hypothesis would be: the issue is not primarily about the dimensions of the growing timeout but, about the timeout being ignored at all?

At least the logged timeout is not reflected in the timestamp deltas of the log lines?!

Yes there is more than one thing going on here. This fix(switch max() to min() and adjust iteration assumptions) is only to fix obnoxiously long strings of numbers that are trying to reach infinity not being introduced in the first place. The underlying cause is something else: timeouts seem to not be honored as well what the request that triggers the situation at all is doing to cause the locks to pile up and not make progress

@jason-famedly

Copy link
Copy Markdown
Contributor Author

Added some additional work onto this:

  1. Adjusted for the fact that the retry interval was actually for time outs, by renaming it
  2. Adjusted that a timeout interval should only be increased if an actual timeout was reached
  3. Added a warning level log to the generic Exception to see if it is even being hit
  4. Added/adjusted tests to check for time outs being hit(there is a typo in one that I will fix after the first test series is run)

This allows that a normal notification of another lock being released should not increment a timeout, when a time out has not actually occurred. It should cut down on what may end up otherwise being excessive log spam about locks having a long timeout duration when such is not true. The jitter value is maintained for the timeout, to help avoid a "thundering herd" situation when all locks may time out at the same time.

@jason-famedly jason-famedly changed the title fix: Cap WorkerLock retry intervals to 15 minutes fix: Cap WorkerLock timeout intervals to 15 minutes Jan 26, 2026
Comment thread synapse/handlers/worker_lock.py Outdated
Comment thread changelog.d/19394.bugfix Outdated
Comment thread synapse/handlers/worker_lock.py Outdated
Comment thread synapse/handlers/worker_lock.py Outdated
Comment thread synapse/handlers/worker_lock.py Outdated
Comment thread synapse/handlers/worker_lock.py Outdated
Comment thread synapse/handlers/worker_lock.py Outdated
Comment thread tests/handlers/test_worker_lock.py Outdated
Comment thread synapse/handlers/worker_lock.py Outdated
Comment thread tests/handlers/test_worker_lock.py Outdated
Comment thread tests/handlers/test_worker_lock.py Outdated
Comment on lines +75 to +77
# Recall that pump() will advance time of the given amount 100 times, this
# amounts to about 10 seconds passing
self.pump(0.1)

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.

To be clear, nothing for you to do here yet. Just noting this discussion so we can think about what to do for these scenarios.

Since #19602, pump() is deprecated and we can be more precise here by using self.reactor.advance(...)

But actually trying this out here and there some internal details leaking through and we would end up with this which isn't great.

Suggested change
# Recall that pump() will advance time of the given amount 100 times, this
# amounts to about 10 seconds passing
self.pump(0.1)
# Wait for database queries to run so we get past the
# `try_acquire_lock(...)` in `WaitingLock` and the timeout actually gets
# scheduled
self.reactor.advance(0)
# Wait some time which should trigger waiting loop to timeout and retry
# acquiring the lock a few times
self.reactor.advance(10)

Related comments around how the database queries are ran (added in matrix-org/synapse#8497):

synapse/tests/server.py

Lines 660 to 669 in 76b4fdc

# check for more "callLater" callbacks added by the thread callback
# This isn't required in a regular reactor, but it ends up meaning that
# our database queries can complete in a single call to `advance` [1] which
# simplifies tests.
#
# [1]: we replace the threadpool backing the db connection pool with a
# mock ThreadPool which doesn't really use threads; but we still use
# reactor.callFromThread to feed results back from the db functions to the
# main thread.
super().advance(0)

cc @reivilibre (someone who also has been in the loop around pump() vs advance())

I can maybe see how pump(...) became a thing 🤔 but it's still imprecise.

Comment thread tests/handlers/test_worker_lock.py Outdated
Comment thread synapse/handlers/worker_lock.py Outdated
Comment thread tests/handlers/test_worker_lock.py Outdated
Comment thread synapse/handlers/worker_lock.py Outdated
@MadLittleMods MadLittleMods changed the title fix: Cap WorkerLock timeout intervals to 15 minutes fix: Cap WorkerLock timeout intervals to 60 seconds May 4, 2026
Comment on lines +119 to +120
while self.reactor.seconds() < end_time_s:
self.reactor.advance(by.as_secs())

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.

Just to note for people who might come back to look at this PR, the implementation I recommended here isn't quite precise as it may advance time by one by increment beyond the amount. But it's fine for our use case ⏩

Comment thread changelog.d/19394.bugfix Outdated
Comment thread tests/handlers/test_worker_lock.py Outdated
Comment thread synapse/handlers/worker_lock.py Outdated
Comment thread synapse/handlers/worker_lock.py
Comment thread synapse/handlers/worker_lock.py Outdated
jason-famedly and others added 2 commits May 5, 2026 05:31
Co-authored-by: Eric Eastwood <madlittlemods@gmail.com>

@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.

Generally fine other than a little footgun with our Duration class!

Comment thread synapse/handlers/worker_lock.py Outdated

@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.

Thank you!

@reivilibre reivilibre enabled auto-merge (squash) May 5, 2026 12:43
@reivilibre reivilibre disabled auto-merge May 5, 2026 13:40
@reivilibre reivilibre merged commit 3f58bc5 into element-hq:develop May 5, 2026
37 of 41 checks passed
start_ts_ms: int = 0
deferred: "defer.Deferred[None]" = attr.Factory(defer.Deferred)
_inner_lock: Lock | None = None
_retry_interval: float = 0.1

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.

Fixes the symptoms of #19315 / #19588 but not the underlying reason causing the number to grow so large in the first place.

I think this fixes the actual problems, not just the symptoms.

The root cause is that _retry_interval can grow past Duration(timedelta)'s max value and raise an OverflowError. When this occurs, there is a tight-loop where the OverflowError propagates up from Duration()clock.call_later()timeout_deferred()await timeout_deferred(...), where it is caught by the bare except Exception: pass at line 254-255. The loop immediately continues to the next iteration with no waiting at all.

Even the initially proposed change here fixed the problem of the _retry_interval/timeout growing too large. Additionally, this PR has since evolved to only increase the timeout when we actually encounter a defer.TimeoutError which is another fix that would solve this.

It's possible that this code could tight-loop again if we encounter some other exception over and over but I guess that's expected and our "critical" section is already as small as possible around timeout_deferred(...). We could have some tight-loop detection but feels overkill as at some point, we have to trust timeout_deferred(...) to do the right thing. Overall, the situation is better here as this PR adds some logging for the general exception case instead of swallowing the error and moving on so you can actually notice any tight-loop root cause now.

Random dev notes

Reproduce Duration(timedelta) raising OverflowError
$ poetry run python
>>> Duration(seconds=10 ** 14)
Traceback (most recent call last):
  File "<python-input-5>", line 1, in <module>
    Duration(seconds=10 ** 14)
    ~~~~~~~~^^^^^^^^^^^^^^^^^^
OverflowError: days=1157407407; must have magnitude <= 999999999

reivilibre pushed a commit that referenced this pull request May 7, 2026
Fixes the symptoms of #19315
/ #19588 but not the
underlying reason causing the number to grow so large in the first
place.

```
ValueError: Exceeds the limit (4300 digits) for integer string conversion; use sys.set_int_max_str_digits() to increase the limit
```

Copied from the original pull request on [Famedly's Synapse
repo](famedly/synapse#221) (with some edits):

Basing the time interval around a 5 seconds leaves a big window of
waiting especially as this window is doubled each retry, when another
worker could be making progress but can not.

Right now, the retry interval in seconds looks like `[0.2, 5, 10, 20,
40, 80, 160, 320, (continues to double)]` after which logging should
start about excessive times and (relatively quickly) end up with an
extremely large retry interval with an unrealistic expectation past the
heat death of the universe. 1 year in seconds = 31,536,000.

With this change, retry intervals in seconds should look more like:

```
[
0.2,
0.4,
0.8,
1.6,
3.2,
6.4,
12.8,
25.6,
51.2,
60, < never goes higher than this
]
```

Logging about excessive wait times will start at 10 minutes.

<details>
<summary>Previous breakdown when we were using 15 minutes</summary>

```
[
0.2,
0.4,
0.8,
1.6,
3.2,
6.4,
12.8,
25.6,
51.2,
102.4,  # 1.7 minutes
204.8,  # 3.41 minutes
409.6,  # 6.83 minutes
819.2,  # 13.65 minutes  < logging about excessive times will start here, 13th iteration
900,  # 15 minutes < never goes higher than this
]
```
</details>

Further suggested work in this area could be to define the cap, the
retry interval starting point and the multiplier depending on how
frequently this lock should be checked. See data below for reasons why.
Increasing the jitter range may also be a good idea

---------

Co-authored-by: Eric Eastwood <madlittlemods@gmail.com>
(cherry picked from commit 3f58bc5)
MadLittleMods added a commit that referenced this pull request May 7, 2026
Better to retry more quickly than have workers wait around. 5 seconds is
still a reasonable gap in time to not overwhelm anything.

This matters most in cross-worker scenarios. When locks are on the same
worker, when the lock holder releases, we signal to other locks (with
the same name/key) that they should try reacquiring the lock
immediately. But locks on other workers only re-check based on their
retry `_timeout_interval`.

Updating to 5 seconds to match the previous intentions based on the
[flawed
code](https://github.com/element-hq/synapse/blob/6100f6e4f7fb0c72f1ae2802683ebc811c0e3a77/synapse/handlers/worker_lock.py#L278).
We can assume they were trying to have 5 seconds as the max value to
retry.

Spawning from
#19394 (comment)
MadLittleMods added a commit that referenced this pull request May 7, 2026
So people have to specify which time unit they want to use.

Spawning from
#19394 (comment)
MadLittleMods added a commit that referenced this pull request May 12, 2026
…_INTERVAL` (#19772)

There is no behavioral change, only a change to the tests. See
#19772 (comment)
for an explanation of why the tests needed changing (and diff comments).

Follow-up to #19394. The test
discussion originally happened in
#19394 (comment)

This is spawning from thinking about the problem again.
FrenchGithubUser pushed a commit to famedly/synapse-upstreaming that referenced this pull request Jun 12, 2026
Fixes the symptoms of element-hq#19315
/ element-hq#19588 but not the
underlying reason causing the number to grow so large in the first
place.

```
ValueError: Exceeds the limit (4300 digits) for integer string conversion; use sys.set_int_max_str_digits() to increase the limit
```

Copied from the original pull request on [Famedly's Synapse
repo](famedly/synapse#221) (with some edits):

Basing the time interval around a 5 seconds leaves a big window of
waiting especially as this window is doubled each retry, when another
worker could be making progress but can not.

Right now, the retry interval in seconds looks like `[0.2, 5, 10, 20,
40, 80, 160, 320, (continues to double)]` after which logging should
start about excessive times and (relatively quickly) end up with an
extremely large retry interval with an unrealistic expectation past the
heat death of the universe. 1 year in seconds = 31,536,000.

With this change, retry intervals in seconds should look more like:

```
[
0.2, 
0.4, 
0.8, 
1.6, 
3.2, 
6.4, 
12.8, 
25.6, 
51.2, 
60, < never goes higher than this
]
```

Logging about excessive wait times will start at 10 minutes.

<details>
<summary>Previous breakdown when we were using 15 minutes</summary>

```
[
0.2, 
0.4, 
0.8, 
1.6, 
3.2, 
6.4, 
12.8, 
25.6, 
51.2, 
102.4,  # 1.7 minutes
204.8,  # 3.41 minutes
409.6,  # 6.83 minutes
819.2,  # 13.65 minutes  < logging about excessive times will start here, 13th iteration
900,  # 15 minutes < never goes higher than this
]
```
</details>

Further suggested work in this area could be to define the cap, the
retry interval starting point and the multiplier depending on how
frequently this lock should be checked. See data below for reasons why.
Increasing the jitter range may also be a good idea

---------

Co-authored-by: Eric Eastwood <madlittlemods@gmail.com>
FrenchGithubUser pushed a commit to famedly/synapse-upstreaming that referenced this pull request Jun 12, 2026
Better to retry more quickly than have workers wait around. 5 seconds is
still a reasonable gap in time to not overwhelm anything.

This matters most in cross-worker scenarios. When locks are on the same
worker, when the lock holder releases, we signal to other locks (with
the same name/key) that they should try reacquiring the lock
immediately. But locks on other workers only re-check based on their
retry `_timeout_interval`.

Updating to 5 seconds to match the previous intentions based on the
[flawed
code](https://github.com/element-hq/synapse/blob/6100f6e4f7fb0c72f1ae2802683ebc811c0e3a77/synapse/handlers/worker_lock.py#L278).
We can assume they were trying to have 5 seconds as the max value to
retry.

Spawning from
element-hq#19394 (comment)
FrenchGithubUser pushed a commit to famedly/synapse-upstreaming that referenced this pull request Jun 12, 2026
…19756)

So people have to specify which time unit they want to use.

Spawning from
element-hq#19394 (comment)
FrenchGithubUser pushed a commit to famedly/synapse-upstreaming that referenced this pull request Jun 12, 2026
…_INTERVAL` (element-hq#19772)

There is no behavioral change, only a change to the tests. See
element-hq#19772 (comment)
for an explanation of why the tests needed changing (and diff comments).

Follow-up to element-hq#19394. The test
discussion originally happened in
element-hq#19394 (comment)

This is spawning from thinking about the problem again.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Workers (includes replication) X-Release-Blocker

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants