@@ -1437,9 +1437,16 @@ def test_T558b_a_hold_cannot_land_while_a_prune_holds_the_lock() -> None:
14371437 "from ctrlrun.postgres import PostgresStateStore\n "
14381438 "from ctrlrun.retention import Hold\n "
14391439 f"s = PostgresStateStore({ POSTGRES_URL !r} , schema={ schema !r} )\n "
1440+ # **The handshake, and why it is not optional.** Until this line the child is importing
1441+ # `ctrlrun` and opening a Postgres connection, and on a loaded runner that can outlast
1442+ # the parent's whole sleep. Without it the parent measures its own patience rather than
1443+ # the child's blocking: CI saw `waited 0.135s` on a run where the child reached the lock
1444+ # only after the prune had released it, and the test failed because it could not prove
1445+ # the property rather than because the property was false.
1446+ "print('ready', flush=True)\n "
14401447 "start = time.time()\n "
14411448 "s.put_hold(Hold('late', 1, 3, 'litigation', 'cli:bob', datetime.now(UTC)))\n "
1442- "print('%.3f' % (time.time() - start))\n "
1449+ "print('%.3f' % (time.time() - start), flush=True )\n "
14431450 "s.close()\n "
14441451 )
14451452 try :
@@ -1459,7 +1466,12 @@ def test_T558b_a_hold_cannot_land_while_a_prune_holds_the_lock() -> None:
14591466 stdout = subprocess .PIPE ,
14601467 stderr = subprocess .STDOUT ,
14611468 )
1462- time .sleep (1.5 )
1469+ # Read the child's `ready` before starting the clock: after this it is connected
1470+ # and its next statement is the one that must block.
1471+ handshake = child .stdout .readline ().strip () if child .stdout else ""
1472+ assert handshake == "ready" , f"the child never reached the lock: { handshake !r} "
1473+ held_for = 1.0
1474+ time .sleep (held_for )
14631475 still_waiting = child .poll () is None
14641476 output , _ = child .communicate (timeout = 60 )
14651477 parent .close ()
@@ -1468,12 +1480,12 @@ def test_T558b_a_hold_cannot_land_while_a_prune_holds_the_lock() -> None:
14681480 "a hold landed while a prune held the lock, so it can be placed over receipts the "
14691481 f"prune is about to delete: { output } "
14701482 )
1471- # The child's own measurement starts after its interpreter and imports, so it is
1472- # necessarily less than the parent's sleep. `still_waiting` above is the real proof;
1473- # this is the coarse floor that separates blocking from an unblocked call, which takes
1474- # single-digit milliseconds.
1483+ # With the handshake above, the child's clock starts at the lock rather than at its
1484+ # interpreter, so this is now a measurement of blocking and not of startup. The floor is
1485+ # half the hold, which leaves room for scheduling on a busy runner while staying far
1486+ # above the single-digit milliseconds an uncontended `put_hold` takes .
14751487 waited = float (output .strip ().splitlines ()[- 1 ])
1476- assert waited >= 0.25 , f"the child did not block on the lock: waited { waited } s"
1488+ assert waited >= held_for / 2 , f"the child did not block on the lock: waited { waited } s"
14771489 finally :
14781490 PostgresStateStore .drop_schema (POSTGRES_URL , schema )
14791491
0 commit comments