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:
@@ -38,6 +38,30 @@ def _modules():
|
||||
|
||||
FUNCTIONS = (ast.FunctionDef, ast.AsyncFunctionDef)
|
||||
|
||||
# The calls a mutation may answer with in place of an acknowledgement: each describes the resource
|
||||
# it wrote (or is the logout's cookie-clearing response). A domain function's REPORT dict is not one
|
||||
# of them — `{"synced": n}` is an acknowledgement, and gets spread into `{"ok": True, **report}`.
|
||||
SERIALIZERS = {
|
||||
"JSONResponse",
|
||||
"_collection_card",
|
||||
"_playlist_card",
|
||||
"_serialize",
|
||||
"_serialize_demo",
|
||||
"_serialize_job",
|
||||
"_serialize_msg",
|
||||
"_serialize_profile",
|
||||
"_serialize_user",
|
||||
"_summary",
|
||||
"admin_get_quota",
|
||||
"describe",
|
||||
"owner_view",
|
||||
}
|
||||
|
||||
|
||||
def _called(node: ast.Call) -> str:
|
||||
f = node.func
|
||||
return f.id if isinstance(f, ast.Name) else getattr(f, "attr", "")
|
||||
|
||||
|
||||
def _is_route(fn: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
||||
return any(
|
||||
@@ -55,16 +79,19 @@ def _is_mutation_route(fn: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
||||
|
||||
def test_every_mutation_answers_ok():
|
||||
"""`{"ok": true, …}` (or `{"deleted": id}`) for every mutation, sync or async — one field a
|
||||
client can read the same way everywhere. A serializer's return (a call) is exempt: it describes
|
||||
a resource, not an acknowledgement. A dict built elsewhere and returned by name is not: it hid
|
||||
two acknowledgements without `ok` from the literal-only scan, so it is spread into a literal."""
|
||||
client can read the same way everywhere. A serializer's return (SERIALIZERS) is exempt: it
|
||||
describes a resource, not an acknowledgement. A dict built elsewhere — returned by name, or
|
||||
straight from a domain call — is not: both hid acknowledgements without `ok` from a
|
||||
literal-only scan, so such a dict is spread into a literal."""
|
||||
missing = []
|
||||
for name, tree in _modules():
|
||||
for fn in ast.walk(tree):
|
||||
if not (isinstance(fn, FUNCTIONS) and _is_mutation_route(fn)):
|
||||
continue
|
||||
for node in ast.walk(fn):
|
||||
if not isinstance(node, ast.Return) or isinstance(node.value, ast.Call):
|
||||
if not isinstance(node, ast.Return):
|
||||
continue
|
||||
if isinstance(node.value, ast.Call) and _called(node.value) in SERIALIZERS:
|
||||
continue
|
||||
keys = (
|
||||
{k.value for k in node.value.keys if isinstance(k, ast.Constant)}
|
||||
|
||||
Reference in New Issue
Block a user