test: gate the child-SELECT (post-BEGIN) so the race proves an open transaction, not just a held lock
transaction() submits and awaits BEGIN before the DAO's first fetch runs; gating that fetch proves BEGIN has executed - SQLite is actually inside the open transaction - when task B is scheduled and queues.
This commit is contained in:
@@ -172,15 +172,24 @@ def test_upsert_concurrent_same_child_serializes(tmp_path) -> None:
|
|||||||
# Everything before arming (open, migrations, child create)
|
# Everything before arming (open, migrations, child create)
|
||||||
# and everything after (B's jobs, A's remaining jobs, reads)
|
# and everything after (B's jobs, A's remaining jobs, reads)
|
||||||
# pass straight through.
|
# pass straight through.
|
||||||
armed = {"active": False, "gated_once": False}
|
armed = {"active": False, "gated": False}
|
||||||
gate_open = asyncio.Event()
|
gate_open = asyncio.Event()
|
||||||
a_started = asyncio.Event()
|
a_started = asyncio.Event()
|
||||||
|
|
||||||
async def _gated_executor(fn, *args):
|
async def _gated_executor(fn, *args):
|
||||||
if armed["active"] and not armed["gated_once"]:
|
# Gate the SECOND gated-window job: the first is the
|
||||||
armed["gated_once"] = True
|
# transaction's BEGIN (submitted and awaited by
|
||||||
a_started.set()
|
# transaction() before upsert_by_child's child-SELECT
|
||||||
await gate_open.wait()
|
# runs). Pausing the child-existence SELECT proves the
|
||||||
|
# BEGIN has EXECUTED — SQLite is actually inside the open
|
||||||
|
# transaction, with A holding the connection lock — when
|
||||||
|
# task B gets scheduled and must queue.
|
||||||
|
if armed["active"]:
|
||||||
|
name = getattr(fn, "__name__", "")
|
||||||
|
if name == "_fetch_one" and not armed["gated"]:
|
||||||
|
armed["gated"] = True
|
||||||
|
a_started.set()
|
||||||
|
await gate_open.wait()
|
||||||
return fn(*args)
|
return fn(*args)
|
||||||
|
|
||||||
hass.async_add_executor_job = _gated_executor
|
hass.async_add_executor_job = _gated_executor
|
||||||
@@ -198,9 +207,10 @@ def test_upsert_concurrent_same_child_serializes(tmp_path) -> None:
|
|||||||
child.id, 2, "2026-09-07", "0,2|1,3"
|
child.id, 2, "2026-09-07", "0,2|1,3"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# Wait until task A's first transactional job is paused
|
# Wait until task A's child-existence SELECT (inside its
|
||||||
# (A holds the connection lock, transaction open), then
|
# open transaction, BEGIN already executed) is paused, then
|
||||||
# schedule task B so it must queue behind A.
|
# schedule task B so it must queue behind the open
|
||||||
|
# transaction.
|
||||||
await a_started.wait()
|
await a_started.wait()
|
||||||
armed["active"] = False
|
armed["active"] = False
|
||||||
task_b = asyncio.ensure_future(
|
task_b = asyncio.ensure_future(
|
||||||
|
|||||||
Reference in New Issue
Block a user