-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Remove whole table locks on push rule add/delete #16051
Changes from all commits
32e0bd3
0938542
ab88d3f
d90cef1
4dbee68
2ec17da
3282065
7d10544
576605e
376313e
7937ac0
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 @@ | ||
Remove whole table locks on push rule modifications. Contributed by Nick @ Beeper (@fizzadar). |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -449,26 +449,28 @@ def _add_push_rule_relative_txn( | |
before: str, | ||
after: str, | ||
) -> None: | ||
# Lock the table since otherwise we'll have annoying races between the | ||
# SELECT here and the UPSERT below. | ||
self.database_engine.lock_table(txn, "push_rules") | ||
|
||
relative_to_rule = before or after | ||
|
||
res = self.db_pool.simple_select_one_txn( | ||
txn, | ||
table="push_rules", | ||
keyvalues={"user_name": user_id, "rule_id": relative_to_rule}, | ||
retcols=["priority_class", "priority"], | ||
allow_none=True, | ||
) | ||
sql = """ | ||
SELECT priority, priority_class FROM push_rules | ||
WHERE user_name = ? AND rule_id = ? | ||
""" | ||
|
||
if isinstance(self.database_engine, PostgresEngine): | ||
sql += " FOR UPDATE" | ||
else: | ||
# Annoyingly SQLite doesn't support row level locking, so lock the whole table | ||
self.database_engine.lock_table(txn, "push_rules") | ||
|
||
txn.execute(sql, (user_id, relative_to_rule)) | ||
row = txn.fetchone() | ||
|
||
if not res: | ||
if row is None: | ||
raise RuleNotFoundException( | ||
"before/after rule not found: %s" % (relative_to_rule,) | ||
) | ||
|
||
base_priority_class, base_rule_priority = res | ||
base_rule_priority, base_priority_class = row | ||
|
||
if base_priority_class != priority_class: | ||
raise InconsistentRuleException( | ||
|
@@ -516,9 +518,18 @@ def _add_push_rule_highest_priority_txn( | |
conditions_json: str, | ||
actions_json: str, | ||
) -> None: | ||
# Lock the table since otherwise we'll have annoying races between the | ||
# SELECT here and the UPSERT below. | ||
self.database_engine.lock_table(txn, "push_rules") | ||
if isinstance(self.database_engine, PostgresEngine): | ||
# Postgres doesn't do FOR UPDATE on aggregate functions, so select the rows first | ||
# then re-select the count/max below. | ||
sql = """ | ||
SELECT * FROM push_rules | ||
WHERE user_name = ? and priority_class = ? | ||
FOR UPDATE | ||
""" | ||
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. Might be worth doing a 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. But also, you probably want to do this as part of the 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. Yep good spot - 2ec17da 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. Still missing a 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. |
||
txn.execute(sql, (user_id, priority_class)) | ||
else: | ||
# Annoyingly SQLite doesn't support row level locking, so lock the whole table | ||
self.database_engine.lock_table(txn, "push_rules") | ||
|
||
# find the highest priority rule in that class | ||
sql = ( | ||
|
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.
I wanted to describe the race to ensure we have a shared understanding. Given two rules
rule_A
andrule_B
with priorities 0 and 1, respectively. If you make two requests:rule_X
) afterrule_A
, this should makerule_B
a priority of 2;rule_X
priority 1; and leaverule_A
at 0.rule_Y
) afterrule_A
, this should makerule_B
a priority of 3;rule_X
priority 2;rule_Y
priority 1; and leaverule_A
at 0.This is in a transaction (I assume running at
READ COMMITTED
), so what happens if these race?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.
I believe the winner will be applied and the second transaction will be replayed using the new updated data, per (added some breaks to make it easier to read, my brain hurts!):
If my understanding is correct
READ COMMITTED
will effectively correct the issue by the replay.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.
That said! Synapse's default is actually one better,
REPEATABLE READ
, in which case things are much simpler:Which synapse automatically retries, which would replay the transaction as expected.
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.
Final note - there is an issue somewhere about switching to
READ COMMITTED
as the default, but it seems that would also suffice here in terms of the potential race conditions.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.
Thanks! I believe that you're correct (but my brain also hurts)!
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.
I'm not sure I agree here. As things stand I don't see why we'd necessarily replay the transactions, as we may not have updated/deleted/locked any of the rows we
SELECT
against (and inserting a new row that would have been picked up by aSELECT
isn't picked up by postgres except inSERIALIZABLE
isolation AIUI).I think what you want here is to run the selects with a
FOR SHARE
so that they do conflict with each other?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.
I guess most of the time the
UPDATE
will conflict, but if we have two requests to add a rule to the top of the push rules those transactions should conflict but won't?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.
Yeah that makes sense, will add
FOR SHARE
in 👍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.
Wait, we probably need a
FOR UPDATE
instead as we need theSELECT
statements to conflict with each other andFOR SHARE
won't do that https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-ROWS