Skip to content

Add Request/Response sample with update-based responses (fixes #6) - #166

Open
barmoshe wants to merge 7 commits into
temporalio:mainfrom
barmoshe:main
Open

Add Request/Response sample with update-based responses (fixes #6)#166
barmoshe wants to merge 7 commits into
temporalio:mainfrom
barmoshe:main

Conversation

@barmoshe

@barmoshe barmoshe commented Mar 11, 2025

Copy link
Copy Markdown

Reworked around @workflow.update, per @cretz's and @brianstrauch's feedback that #6 and the Go sample it was ported from both predate Workflow Update. reqrespactivity/ is replaced by reqrespupdate/, a port of samples-go/reqrespupdate.

What it shows

  • The update handler runs the uppercase activity and returns the result directly to the caller. No response task queue, no callback activity, no request IDs.
  • A request counter and continue-as-new to bound history.
  • An update validator that rejects requests while a run drains toward continue-as-new, so the requester backs off and retries against the fresh run. A validator rejection is not written to history, which is the point when the reason for continuing as new is history size.

Where it differs from the Go sample

  • workflow.all_handlers_finished() replaces Go's hand-rolled pendingUpdates counter, matching message_passing/safe_message_handlers. message_passing/waiting_for_handlers/README.md notes the continue-as-new drain case is not illustrated there, so this sample covers it.
  • The request threshold and the reject-on-drain flag are fields on a workflow input dataclass rather than a package constant, so tests can drive them.
  • The requester tells the backoff rejection apart from a real failure by error type, and retries only the former.

Placement

Top-level reqrespupdate/ for parity with samples-go. Happy to move it under message_passing/ instead if you would rather it live with the other message-passing samples.

Testing

  • tests/reqrespupdate/workflow_test.py: a basic request/response test, and one that drives the threshold low and asserts every request still succeeds across a continue-as-new.
  • uv run poe lint clean (ruff and mypy across the repo).
  • Backpressure verified directly: with the threshold at 2 and ten concurrent requests, exactly 2 succeeded and 8 were rejected with the backoff error, and the workflow continued as new.
  • Manual run against temporal server start-dev: responses stream once per second, and killing the worker mid-stream stalls requests until it restarts, after which they resume.

@CLAassistant

CLAassistant commented Mar 11, 2025

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@cretz

cretz commented Mar 11, 2025

Copy link
Copy Markdown
Contributor

#6 and the Go samples predate Workflow Update. We now recommend using workflow update for request/response instead of the "response activity" pattern.

@brianstrauch

brianstrauch commented Jul 30, 2026

Copy link
Copy Markdown
Member

@barmoshe — checking in on this one. Are you still interested in picking it up? If we don't hear back in the next couple of weeks we'll close this to keep the queue tidy.

@barmoshe

Copy link
Copy Markdown
Author

@brianstrauch thanks for the ping, and yes, still interested. Happy to rework it around @workflow.update.

Before I write it I'd rather use your offer to help scope, because the obvious minimal version overlaps hello/hello_update.py. What I had in mind:

  • Replace reqrespactivity/ with an update-based sample. The update handler runs the uppercase activity and returns the result straight to the caller, so the response task queue, the callback activity and the request IDs all go away.
  • Keep the parts of the Go reqrespupdate sample that are about durability rather than about the old pattern: a request counter, continue-as-new to bound history, and an update validator that rejects new requests while a run drains, with the requester retrying against the fresh run.
  • Use workflow.all_handlers_finished() for the drain rather than a hand-rolled pending counter, matching message_passing/safe_message_handlers. message_passing/waiting_for_handlers/README.md notes the continue-as-new drain case isn't illustrated in that sample, so this one would cover it.
  • Add tests under tests/, which the original PR didn't have.

Two questions:

  1. Is that the right amount? I could cut it down to just the update handler returning the response, or go further. My read is that the durability bits are where the teaching value is, since the bare handler is already covered by hello_update, but you know the sample set better than I do.
  2. Placement: top-level reqrespupdate/ for parity with samples-go, or under message_passing/?

I'll start on the version above so nothing is waiting on me, and adjust to whatever you say. I'll merge current main into the branch and push there, so #166 updates in place.

@barmoshe
barmoshe requested a review from a team as a code owner July 30, 2026 09:47
barmoshe and others added 3 commits July 30, 2026 12:47
…io#6)

Issue temporalio#6 and the Go sample it was ported from predate Workflow Update. Per
maintainer feedback, replace the response-activity port with an update-based
sample: the update handler runs the uppercase activity and returns the result
to the caller, removing the response task queue, the callback activity and the
request IDs.

Ports the durability parts of samples-go/reqrespupdate: a request counter,
continue-as-new to bound history, and an update validator that rejects requests
while a run drains so the requester backs off and retries against the fresh run.

Uses workflow.all_handlers_finished() for the drain rather than Go's hand-rolled
pending counter, matching message_passing/safe_message_handlers.
message_passing/waiting_for_handlers notes the continue-as-new drain case is not
illustrated there, so this sample covers it.

Adds tests, which the original sample did not have.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@barmoshe
barmoshe requested a review from a team as a code owner July 30, 2026 10:12
@barmoshe barmoshe changed the title Add Request/Response sample with activity-based responses (fixes #6) Add Request/Response sample with update-based responses (fixes #6) Jul 30, 2026
The tests were skipping when env.supports_time_skipping, on the assumption
that the Java test server does not support workflow update. It does: both
tests pass against it, repeatedly. CI runs the suite twice, once per
environment, so the guard was halving this sample's coverage for no reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@brianstrauch

Copy link
Copy Markdown
Member

(1) Looks good, (2) I'd opt for parity with samples-go

@barmoshe

Copy link
Copy Markdown
Author

Thanks, that settles both. The branch already matches: the rework is pushed as 4245b7c, with reqrespupdate/ at the top level for parity with samples-go, and the durability parts kept as described (request counter, continue-as-new, and a validator that rejects new requests while a run drains so the requester retries against the fresh run).

One thing I can't do from a fork: Continuous Integration and Opengrep SAST are both sitting at action_required, waiting on a maintainer to approve the workflow runs. Could you release them? Locally uv run poe lint is clean and tests/reqrespupdate/workflow_test.py passes against both the local and the time-skipping server.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Temporal Python sample demonstrating request/response using Workflow Update (instead of activity/callback or query approaches), including continue-as-new for bounded history and validator-based backpressure while draining toward continue-as-new.

Changes:

  • Introduces reqrespupdate/ sample: update handler executes an activity and returns the result directly to the caller, with bounded-history continue-as-new behavior.
  • Adds a requester/worker/starter runnable trio plus README documentation for running the sample locally.
  • Adds tests covering basic request/response and repeated continue-as-new behavior; wires the new package into the top-level README and pyproject.toml.

Reviewed changes

Copilot reviewed 9 out of 11 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/reqrespupdate/workflow_test.py New tests for update-based request/response and continue-as-new behavior
tests/reqrespupdate/init.py Declares test package/module
reqrespupdate/workflow.py Implements the update-based uppercase workflow with validator backpressure + continue-as-new
reqrespupdate/activities.py Adds uppercase activity used by the workflow
reqrespupdate/worker.py Adds runnable worker for the sample
reqrespupdate/starter.py Adds runnable workflow starter for the sample
reqrespupdate/requester.py Adds runnable requester client exercising updates and retry-on-backoff
reqrespupdate/README.md Documents the sample and the continue-as-new/backpressure rationale
reqrespupdate/init.py Declares package
README.md Adds sample to the repository’s sample index
pyproject.toml Adds reqrespupdate to packaged modules list
Comments suppressed due to low confidence (2)

tests/reqrespupdate/workflow_test.py:55

  • After changing request_uppercase to take (client, workflow_id, ...), the test should call it using the workflow ID (so it can follow the continue-as-new chain) and terminate via an unpinned handle for consistency.
        handle = await client.start_workflow(
            UppercaseWorkflow.run,
            UppercaseWorkflowInput(),
            id=f"reqrespupdate-{uuid.uuid4()}",
            task_queue=task_queue,
        )
        try:
            for text in ["foo", "bar", "baz"]:
                assert await request_uppercase(handle, text) == text.upper()
        finally:
            await handle.terminate()

tests/reqrespupdate/workflow_test.py:84

  • This test currently calls request_uppercase(handle, ...) using the handle returned from start_workflow, which is typically pinned to the original run. Once the workflow continues-as-new, retries should target the workflow ID (unpinned) to reach the fresh run; otherwise the retry loop may never succeed. Align the test with the sample requester by calling request_uppercase(client, handle.id, ...) and terminating via an unpinned handle.
        handle = await client.start_workflow(
            UppercaseWorkflow.run,
            # Low enough that the run continues as new several times over the
            # requests below.
            UppercaseWorkflowInput(requests_before_continue_as_new=2),
            id=f"reqrespupdate-{uuid.uuid4()}",
            task_queue=task_queue,
        )
        try:
            # Whether any individual request is rejected depends on how the
            # continue-as-new interleaves with it, so we assert the contract the
            # requester actually relies on: every request eventually succeeds,
            # and the workflow does continue as new underneath.
            for i in range(6):
                assert await request_uppercase(handle, f"foo{i}") == f"FOO{i}"

            description = await client.get_workflow_handle(handle.id).describe()
            assert description.run_id != handle.first_execution_run_id
        finally:
            await client.get_workflow_handle(handle.id).terminate()

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/reqrespupdate/workflow_test.py Outdated
Comment thread tests/reqrespupdate/workflow_test.py Outdated
Comment thread reqrespupdate/workflow.py
Comment thread tests/reqrespupdate/workflow_test.py Outdated
Comment thread tests/reqrespupdate/workflow_test.py
- Bound the test retry helper with a max attempt count and a short sleep, so
  a workflow that stops continuing as new fails the test instead of retrying
  forever and hanging CI.
- Skip both tests on the time-skipping server, matching the other update
  tests in the repo (temporalio/sdk-java#1903).
- Reject requests_before_continue_as_new < 1 in the workflow constructor.
  Zero made the run continue as new immediately and forever while rejecting
  every request.
- Move TASK_QUEUE and WORKFLOW_ID into __init__.py, as sleep_for_days and
  message_passing/waiting_for_handlers do, instead of the worker hardcoding
  the task queue name.
- Use the existing handle in the continue-as-new test rather than re-fetching
  it; start_workflow does not pin the handle to a run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@barmoshe

Copy link
Copy Markdown
Author

Pushed 6e93145 with a few fixes of my own: the test retry loop is bounded now so a stuck workflow fails instead of hanging, both tests skip on the time-skipping server like the other update tests, and requests_before_continue_as_new < 1 is rejected rather than spinning a continue-as-new chain. Also moved the task queue and workflow ID into __init__.py.

Kept reject_update_on_pending_continue_as_new since the Go workflow takes it as a parameter.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 1 comment.

Comment thread tests/reqrespupdate/workflow_test.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants