Skip to content

[DV-04] [P1] Make Python adapter cancellation and deadlines terminal before returning a result #564

Closed

Description

@wsdt

Priority: P1
Area: Python adapter SDK
Repository / target branch: wavect/semaprax / wavect/v090
Reviewed commit: 5428b0805fa3cf35859c843a53e30c3252ef378c
Review date: 6 October 2026

Defect

serve_cancellable has two writers for the same slot["env"]: the handler worker and its supervisor. The invocation loop starts the worker even when an earlier harness/cancel already set the slot's cancellation flag. The supervisor waits for done before checking cancellation or expiry. Once it chooses cancellation or timeout, it removes the invocation from live and sends the current shared slot["env"] without waiting for the handler to stop.

These choices produce three demonstrated failures:

  1. An invocation cancelled before its handler starts still executes the handler and can return complete.
  2. A worker finishing after the supervisor selected failed / SPX-HPK011 can overwrite that terminal failure with complete before serialization.
  3. A cancellation reply refused / SPX-HPK013 can be emitted while the handler is still running. A following invocation therefore overlaps work whose terminal reply has already released the caller's concurrency slot.

This helper ships in the adapter bundle and is used by Mini Jev and the decision-adapter starter. Its own docstring promises cancellation/deadline enforcement by the loop. The host's cooperative cancellation path accepts a received result and leaves the process alive; for safe classes the bridge maps a cancelled Completed outcome to confirmed-terminated. A supervisor-generated refusal is therefore stronger than the operation settlement it has actually established. The Python execution evidence below proves the SDK behavior; this review traces the host/bridge implication in source rather than claiming a live remote cancellation test.

Executed evidence

Both reproductions import the unmodified checkout's Python SDK.

  • repro_sdk_terminal.py sends an early cancellation before the invoke. The handler call count is 1, the flag is already true at entry, and the reply status is complete.
  • The same script uses threading.settrace solely to freeze a scheduling point between timeout selection and reply. It observes failed / SPX-HPK011 already selected, releases the late worker, then receives complete and that worker's payload. No implementation code or reply is replaced by the test.
  • repro_sdk_cancel.py controls handler cleanup with Events. Each of two sequential calls receives refused / SPX-HPK013 before its handler finishes. Peak running handlers is 2, even though the test waits for each terminal response before sending the next call. The fixture releases and finishes all workers afterward.

Smallest useful change

Give each invocation one synchronized terminal decision. A worker should return data to that owner, rather than overwriting the same mutable envelope used for timeout/cancellation. Check an already-set cancellation flag before scheduling or entering the handler; if it is already cancelled, dispatch zero handler work. Use one deadline captured at admission and recheck it before accepting successful completion.

Separate a cancellation request from proof that the operation ended. Keep ownership of the live worker through cleanup. Only return a cancellation completion that the host can treat as settled once the handler has actually stopped, or allow the existing process supervisor to enforce termination when cooperative cleanup fails to complete within its allowance. Do not keep sending a successful cancellation receipt while abandoning a still-running thread. Do not attempt to forcibly kill arbitrary Python threads.

Keep retained worker/supervisor records bounded and release completed records during the session rather than retaining every Thread in pending until shutdown. Preserve secret scrubbing, result size validation, capability-version checks, per-request correlation and non-replayability of possibly billable work.

Acceptance criteria

  • An early cancellation produces no handler dispatch and an honest cancellation result.
  • Deadline and cancellation decisions cannot be replaced by a late success under barrier-controlled interleavings.
  • Exactly one reply is emitted for each request, including cancellation/completion races.
  • A handler with deliberately delayed cleanup cannot be counted as physically finished or free its concurrency ownership while it remains live.
  • A non-cooperating handler reaches the existing process-level enforcement path; no unbounded detached handler accumulation occurs.
  • Sequential completed/cancelled calls leave bounded live and retained worker state.
  • A positive control still permits a normal successful call; actual cooperative cleanup can finish without an unnecessary duplicate invocation.
  • The existing adapter conformance harness checks terminal ownership and dispatch count, not only the presence of a cancellation diagnostic.

Existing issue boundary

#432 introduced the SDK/conformance surface and #454 introduced real bridge cancellation. #475 guards the Rust host's pre-dispatch path. This finding is the Python helper's independently executed early-cancel and dual-writer terminal race plus premature operation settlement. No existing retrieved issue names serve_cancellable or this race. Keep the existing Rust host checks; they do not make an adapter-generated terminal receipt truthful.

Verification handoff

Read AGENTS.md, docs/HARNESS-HOST-V1.md, docs/HARNESS-BRIDGE-V1.md and the SDK contract before editing. Put regressions in the existing Python SDK/conformance suite and the owning host/bridge harness where end-to-end settlement is asserted. Record the fixing commit, exact commands and nonzero executed counts. The supplied scripts assert the observed defects; convert them to desired-behavior assertions with the fix.

Pinned source references

Activity

  1. wsdt commented on Oct 6, 2026

    @wsdt
    MemberAuthor

    Fixed in b5e3f37 (pushed to main): serve_cancellable now has one lock-protected terminal decision per invocation; an early cancel dispatches no handler; deadlines are captured at admission; a late success cannot replace a cancel/deadline decision; terminal replies wait until the handler thread stops; a handler outliving cancel_grace gets no receipt and the adapter exits for host enforcement. Evidence: 7 new tests in test_serve_terminal.py (incl. 40 cancel/complete races with exactly one reply, bounded threads after 60 calls), SDK 11 OK, minijev-local 59 OK. Remaining before close: extend the embedded adapter conformance suite (sdk/python/decision_conformance.py, pinned via assets_list.rs) to check terminal ownership and dispatch count.

  2. wsdt commented on Oct 6, 2026

    @wsdt
    MemberAuthor

    Remaining criterion done in c278051 (pushed to main): the embedded decision.evaluate conformance suite (sdk/python/decision_conformance.py) now checks early-cancel zero dispatch, exactly one terminal reply, no reply while the handler runs, and late success never replacing SPX-HPK013/HPK011; fake_terminal_adapter.py negative controls (dispatch_after_cancel, double_reply, reply_while_running, late_success) each fail the suite while the real SDK loop passes. Evidence: SDK 16 tests OK; minijev-local 62, clef-local 41, systemone 174, starter 71 all OK under the extended suite; cargo test -p semaprax-harness 581 passed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions