From 1cbe01ffdd0765c1a4969834f58c3c087697c804 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Fri, 23 Oct 2026 22:56:07 +0000 Subject: [PATCH 1/2] Wait for the query acknowledgement before stopping Python and Rust clients worker.php already waits, inside the original 30 s budget, for the Worker's acknowledgement of its query result before it stops: the independent caller can see the committed result first. The Python and Rust clients stopped immediately, so a fast caller could end the Worker before its acknowledgement arrived (#708 attempt 1, MySQL 8.0, PHP target, timely). Both now wait the same bounded way. The submission and acknowledgement assertions are unchanged. --- .../server-parity/query-authority/python.py | 11 +++++++++++ .../server-parity/query-authority/rust/src/main.rs | 7 +++++++ 2 files changed, 18 insertions(+) diff --git a/scripts/conformance/server-parity/query-authority/python.py b/scripts/conformance/server-parity/query-authority/python.py index 023aa84cb..52e60c9fa 100644 --- a/scripts/conformance/server-parity/query-authority/python.py +++ b/scripts/conformance/server-parity/query-authority/python.py @@ -117,6 +117,17 @@ async def main(): if MODE == 'timely': result = await client.query_workflow(namespace, 'value', args=[]) assert result['result'] == EXPECTED and type(result['result']['long']) is int + # The independent caller can observe the commit before the + # Worker receives its acknowledgement. Wait inside the + # original budget before stopping it, as worker.php does. + PHASE = 'acknowledgement' + while not any(x['event'] == 'acknowledgement' for x in events()): + if runner.done(): + await runner + raise RuntimeError('Worker stopped before its acknowledgement') + if time.monotonic() - started >= 30: + raise AssertionError('No acknowledgement inside the original budget') + await asyncio.sleep(.02) else: try: await client.query_workflow(namespace, 'value', args=[]) diff --git a/scripts/conformance/server-parity/query-authority/rust/src/main.rs b/scripts/conformance/server-parity/query-authority/rust/src/main.rs index 45fae759d..d413a6dbc 100644 --- a/scripts/conformance/server-parity/query-authority/rust/src/main.rs +++ b/scripts/conformance/server-parity/query-authority/rust/src/main.rs @@ -169,6 +169,13 @@ async fn main() -> Result<()> { let query = client.query_workflow(&namespace, "value", json!([])).await; if mode == "timely" { assert_eq!(query?, expected()); + // The independent caller can observe the commit before the Worker + // receives its acknowledgement. Wait inside the original budget + // before stopping it, as worker.php does. + wait(until, || { + !named(&events(Path::new(journal)), "acknowledgement").is_empty() + }) + .await?; } else { match query { Err(Error::QueryFailed(failure)) => { From 911b89a67da880c974da0ed20ae54396f89502f7 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Sat, 24 Oct 2026 00:23:44 +0000 Subject: [PATCH 2/2] Wait for the expiry evidence before stopping the Python query client In late-handler-sync mode the client stopped as soon as the handler returned, but the installed SDK logs the lost authority after that, so the expired-authority assertion could see none (#713 attempt 2, PHP target). worker.php waits for its Worker's refusal; every non-timely Python mode now also waits, inside the 30 s original budget, for the expired-authority evidence before stopping. Assertions are unchanged. --- .../server-parity/query-authority/python.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/scripts/conformance/server-parity/query-authority/python.py b/scripts/conformance/server-parity/query-authority/python.py index 52e60c9fa..e3f0d8b1c 100644 --- a/scripts/conformance/server-parity/query-authority/python.py +++ b/scripts/conformance/server-parity/query-authority/python.py @@ -136,12 +136,19 @@ async def main(): assert error.status == 504 and error.reason == 'query_worker_execution_timeout' # Delivery is recorded before the SDK receives its response. # Wait for its actual refusal before stopping the worker. + # A late handler's callback counter moves before the SDK + # logs the lost authority, so every mode also waits for + # that evidence, as worker.php waits for its refusal. PHASE = 'expiry-observation' - while (MODE == 'late-poll' and not any( - expired_losses(event['claim']['query_task_id']) - for event in events() if event['event'] == 'claim') + while (not any(expired_losses(event['claim']['query_task_id']) + for event in events() if event['event'] == 'claim') or MODE == 'late-handler-async' and COUNTERS['cancelled'] != 1 or MODE == 'late-handler-sync' and COUNTERS['returned'] != 1): + if runner.done(): + await runner + raise RuntimeError('Worker stopped before its expiry evidence') + if time.monotonic() - started >= 30: + raise AssertionError('No expiry evidence inside the original budget') await asyncio.sleep(.02) finally: PHASE = 'worker-stop'