-
Notifications
You must be signed in to change notification settings - Fork 564
Update WorkerLock tests to better stress the WORKER_LOCK_MAX_RETRY_INTERVAL
#19772
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f24eafb
e90a919
f3be850
dcd3591
1b57897
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Update `WorkerLock` tests to better stress the `WORKER_LOCK_MAX_RETRY_INTERVAL`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -53,17 +53,17 @@ | |
| _RENEWAL_INTERVAL = Duration(seconds=30) | ||
|
|
||
| # How long before an acquired lock times out. | ||
| _LOCK_TIMEOUT_MS = 2 * 60 * 1000 | ||
| _LOCK_TIMEOUT = Duration(minutes=2) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just a refactor to use |
||
|
|
||
| _LOCK_REAP_INTERVAL = Duration(milliseconds=_LOCK_TIMEOUT_MS / 10.0) | ||
| _LOCK_REAP_INTERVAL = Duration(milliseconds=_LOCK_TIMEOUT.as_millis() / 10.0) | ||
|
|
||
|
|
||
| class LockStore(SQLBaseStore): | ||
| """Provides a best effort distributed lock between worker instances. | ||
|
|
||
| Locks are identified by a name and key. A lock is acquired by inserting into | ||
| the `worker_locks` table if a) there is no existing row for the name/key or | ||
| b) the existing row has a `last_renewed_ts` older than `_LOCK_TIMEOUT_MS`. | ||
| b) the existing row has a `last_renewed_ts` older than `_LOCK_TIMEOUT`. | ||
|
|
||
| When a lock is taken out the instance inserts a random `token`, the instance | ||
| that holds that token holds the lock until it drops (or times out). | ||
|
|
@@ -182,7 +182,7 @@ def _try_acquire_lock_txn(txn: LoggingTransaction) -> bool: | |
| self._instance_name, | ||
| token, | ||
| now, | ||
| now - _LOCK_TIMEOUT_MS, | ||
| now - _LOCK_TIMEOUT.as_millis(), | ||
| ), | ||
| ) | ||
|
|
||
|
|
@@ -340,7 +340,9 @@ async def _reap_stale_read_write_locks(self) -> None: | |
| """ | ||
|
|
||
| def reap_stale_read_write_locks_txn(txn: LoggingTransaction) -> None: | ||
| txn.execute(delete_sql, (self.clock.time_msec() - _LOCK_TIMEOUT_MS,)) | ||
| txn.execute( | ||
| delete_sql, (self.clock.time_msec() - _LOCK_TIMEOUT.as_millis(),) | ||
| ) | ||
| if txn.rowcount: | ||
| logger.info("Reaped %d stale locks", txn.rowcount) | ||
|
|
||
|
|
@@ -489,7 +491,7 @@ async def is_still_valid(self) -> bool: | |
| ) | ||
| return ( | ||
| last_renewed_ts is not None | ||
| and self._clock.time_msec() - _LOCK_TIMEOUT_MS < last_renewed_ts | ||
| and self._clock.time_msec() - _LOCK_TIMEOUT.as_millis() < last_renewed_ts | ||
| ) | ||
|
|
||
| async def __aenter__(self) -> None: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,8 +25,13 @@ | |
| from twisted.internet import defer | ||
| from twisted.internet.testing import MemoryReactor | ||
|
|
||
| from synapse.handlers.worker_lock import WORKER_LOCK_MAX_RETRY_INTERVAL | ||
| from synapse.server import HomeServer | ||
| from synapse.storage.databases.main.lock import _RENEWAL_INTERVAL | ||
| from synapse.storage.databases.main.lock import ( | ||
| _LOCK_REAP_INTERVAL, | ||
| _LOCK_TIMEOUT, | ||
| _RENEWAL_INTERVAL, | ||
| ) | ||
| from synapse.util.clock import Clock | ||
| from synapse.util.duration import Duration | ||
|
|
||
|
|
@@ -83,17 +88,42 @@ def test_timeouts_for_lock_locally(self) -> None: | |
| # Note: We use `_pump_by` instead of `pump`/`advance` as the `Lock` has an | ||
| # internal background looping call that runs every 30 seconds | ||
| # (`_RENEWAL_INTERVAL`) to renew the `Lock` and push it's "drop timeout" value | ||
| # further out by 2 minutes (`_LOCK_TIMEOUT_MS`). The `Lock` will prematurely | ||
| # further out by 2 minutes (`_LOCK_TIMEOUT`). The `Lock` will prematurely | ||
| # drop if this renewal is not allowed to run, which sours the test. | ||
| # self.pump(amount=Duration(hours=1)) | ||
| self._pump_by(amount=Duration(hours=1), by=_RENEWAL_INTERVAL) | ||
|
|
||
| # Make sure we haven't acquired the `lock2` yet (`lock1` still holds it) | ||
| self.assertNoResult(d2) | ||
|
|
||
| # Release the first lock (`lock1`). The second lock(`lock2`) should be | ||
| # automatically acquired by the `pump()` inside `get_success()` | ||
| self.get_success(lock1.__aexit__(None, None, None)) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Basically, the behavior described by this comment circumvents the retry timeout interval logic we're trying to stress. And the previous tests actually pass without any of the fixes from #19394 because of this happy-path flow. To explain further: When a lock is released, we immediately try to re-acquire the lock again
Instead, we want to avoid the lock released notification stuff and stress the retry interval which helps in situations where the lock holder goes stale, is reaped, and the other locks want to try to acquire the lock. I've tested to make sure these new tests fail with a version of Synapse before #19394
|
||
| # Drop the lock without releasing it. If we just normally released the lock | ||
| # (`self.get_success(lock1.__aexit__(None, None, None))`), the | ||
| # `add_lock_released_callback`/`notify_lock_released` cycle would signal that we | ||
| # should re-aquire the lock right away (on the next reactor tick). And we want | ||
| # to avoid that as the point of this test is to stress the retry timeout | ||
| # interval and `WORKER_LOCK_MAX_RETRY_INTERVAL`. | ||
| del lock1 | ||
|
|
||
| # Wait for `lock1` to go stale (it won't be renewed anymore because we deleted | ||
| # it just above) | ||
| self._pump_by( | ||
| amount=_LOCK_TIMEOUT, | ||
| by=_RENEWAL_INTERVAL, | ||
| ) | ||
|
|
||
| # Wait just enough time so `lock1` is reaped (found stale and forcefully drops | ||
| # the lock its holding) | ||
| self._pump_by( | ||
| amount=_LOCK_REAP_INTERVAL, | ||
| by=_RENEWAL_INTERVAL, | ||
| ) | ||
|
|
||
| # Wait just enough time so `lock2` tries re-acquiring the lock. Should be no | ||
| # longer than our `WORKER_LOCK_MAX_RETRY_INTERVAL`. | ||
| self._pump_by( | ||
| amount=WORKER_LOCK_MAX_RETRY_INTERVAL, | ||
| by=_RENEWAL_INTERVAL, | ||
| ) | ||
|
|
||
| # We should now have the lock | ||
| self.successResultOf(d2) | ||
|
|
@@ -219,17 +249,42 @@ def test_timeouts_for_lock_worker(self) -> None: | |
| # Note: We use `_pump_by` instead of `pump`/`advance` as the `Lock` has an | ||
| # internal background looping call that runs every 30 seconds | ||
| # (`_RENEWAL_INTERVAL`) to renew the `Lock` and push it's "drop timeout" value | ||
| # further out by 2 minutes (`_LOCK_TIMEOUT_MS`). The `Lock` will prematurely | ||
| # further out by 2 minutes (`_LOCK_TIMEOUT`). The `Lock` will prematurely | ||
| # drop if this renewal is not allowed to run, which sours the test. | ||
| # self.pump(amount=Duration(hours=1)) | ||
| self._pump_by(amount=Duration(hours=1), by=_RENEWAL_INTERVAL) | ||
|
|
||
| # Make sure we haven't acquired the `lock2` yet (`lock1` still holds it) | ||
| self.assertNoResult(d2) | ||
|
|
||
| # Release the first lock (`lock1`). The second lock(`lock2`) should be | ||
| # automatically acquired by the `pump()` inside `get_success()` | ||
| self.get_success(lock1.__aexit__(None, None, None)) | ||
| # Drop the lock without releasing it. If we just normally released the lock | ||
| # (`self.get_success(lock1.__aexit__(None, None, None))`), the | ||
| # `add_lock_released_callback`/`notify_lock_released` cycle would signal that we | ||
| # should re-aquire the lock right away (on the next reactor tick). And we want | ||
| # to avoid that as the point of this test is to stress the retry timeout | ||
| # interval and `WORKER_LOCK_MAX_RETRY_INTERVAL`. | ||
| del lock1 | ||
|
|
||
| # Wait for `lock1` to go stale (it won't be renewed anymore because we deleted | ||
| # it just above) | ||
| self._pump_by( | ||
| amount=_LOCK_TIMEOUT, | ||
| by=_RENEWAL_INTERVAL, | ||
| ) | ||
|
|
||
| # Wait just enough time so `lock1` is reaped (found stale and forcefully drops | ||
| # the lock its holding) | ||
| self._pump_by( | ||
| amount=_LOCK_REAP_INTERVAL, | ||
| by=_RENEWAL_INTERVAL, | ||
| ) | ||
|
|
||
| # Wait just enough time so `lock2` tries re-acquiring the lock. Should be no | ||
| # longer than our `WORKER_LOCK_MAX_RETRY_INTERVAL`. | ||
| self._pump_by( | ||
| amount=WORKER_LOCK_MAX_RETRY_INTERVAL, | ||
| by=_RENEWAL_INTERVAL, | ||
| ) | ||
|
|
||
| # We should now have the lock | ||
| self.successResultOf(d2) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The original reasoning here came from #19755
It was based on my flawed understanding on how the lock release notifications worked. It turns out we also
notify_lock_released(...)over replication when other workers tell us about it.