fix(api): a waiting reorder deals from the fresh order; the commit check sees staged writes
Round 2 of the deep review, on round 1's own fixes:
- `ORDER BY position … FOR UPDATE` sorts before it locks, so the reorder that waited got the
fresh rows in its STALE order and dealt the pins it left out (a tab with Plex off) back to their
old slots. It locks in id order — one global order, so two reorders cannot deadlock — and sorts
the fresh rows itself. The race test now sends that partial list and fails unless tab B really
blocked.
- The request check saw only writes the handler flushed: a `db.delete` left staged, or a flush
followed by a released savepoint, passed. Staged changes count at the end of the request, and
only the ROOT transaction's end clears it (a savepoint test drives the dependency directly).
- The `ok` test exempted every call as a serializer, hiding four domain REPORTS (YouTube sync,
revert, push; Plex sync) without `ok`. They are spread into `{"ok": True, …}`; the exemption is a
named list of real serializers.
This commit is contained in:
@@ -108,8 +108,9 @@ def _request_dep(db, unfinished: list[str]):
|
||||
session after the request, which rolls back anything flushed but not committed — yet here every
|
||||
request shares the test's session, so a follow-up GET or an assertion reads the flushed write
|
||||
and the suite stays green over a handler that forgot to commit (deep review, debt burn). So a
|
||||
request that ends normally with a flushed, uncommitted write is recorded, and the test fails on
|
||||
it at teardown. The test's own staged rows are flushed before the request starts — they are the
|
||||
request that ends normally with a write it did not commit — flushed, or still only staged — is
|
||||
recorded, and the test fails on it at teardown. Only the ROOT transaction's end clears it: a
|
||||
savepoint released inside the handler commits nothing. The test's own staged rows are flushed before the request starts — they are the
|
||||
test's business, not the handler's. A request that raises is exempt: its writes are meant to
|
||||
roll back."""
|
||||
|
||||
@@ -120,15 +121,17 @@ def _request_dep(db, unfinished: list[str]):
|
||||
def flushed(session, ctx):
|
||||
state["dirty"] = True
|
||||
|
||||
def ended(session):
|
||||
state["dirty"] = False
|
||||
def ended(session, transaction):
|
||||
if transaction.parent is None:
|
||||
state["dirty"] = False
|
||||
|
||||
hooks = (("after_flush", flushed), ("after_commit", ended), ("after_rollback", ended))
|
||||
hooks = (("after_flush", flushed), ("after_transaction_end", ended))
|
||||
for name, fn in hooks:
|
||||
event.listen(db, name, fn)
|
||||
try:
|
||||
yield db
|
||||
if state["dirty"]:
|
||||
staged = db.new or db.deleted or any(db.is_modified(o) for o in db.dirty)
|
||||
if state["dirty"] or staged:
|
||||
unfinished.append(f"{conn.scope.get('method', 'WS')} {conn.url.path}")
|
||||
finally:
|
||||
for name, fn in hooks:
|
||||
@@ -146,12 +149,13 @@ def db(_schema):
|
||||
session = SessionLocal()
|
||||
unfinished: list[str] = []
|
||||
session.info["request"] = _request_dep(session, unfinished)
|
||||
session.info["unfinished"] = unfinished # the requests recorded so far (the check's own test)
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
session.rollback()
|
||||
session.close()
|
||||
assert unfinished == [], "a handler flushed a write and never committed it"
|
||||
assert unfinished == [], "a handler left a write uncommitted"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
Reference in New Issue
Block a user