Skip to content

Standalone Activities operator commands and optional payloads - #1809

Draft
GregoryTravis wants to merge 54 commits into
mainfrom
gmt/operator-commands
Draft

Standalone Activities operator commands and optional payloads#1809
GregoryTravis wants to merge 54 commits into
mainfrom
gmt/operator-commands

Conversation

@GregoryTravis

Copy link
Copy Markdown
Contributor

Add operator commands for standalone activities

Adds pause, unpause, reset, and update-options to standalone activities, plus the
describe surface needed to observe their effects.

API

On ActivityHandle:

await handle.pause(reason="...")
await handle.unpause(reason="...", jitter=timedelta(seconds=5))
await handle.reset(
    keep_paused=False,
    jitter=None,
    restore_original_options=False,
    reset_heartbeat=False,
)
await handle.update_options(updates) -> ActivityExecutionOptions
await handle.restore_original_options() -> ActivityExecutionOptions

Describe payloads

ActivityHandle.describe gains four opt-in keyword arguments: include_input, include_outcome, include_heartbeat_details,
include_last_failure.

Description fields

Expose additional existing ActivityExecutionDescription fields: execution_time, start_delay, total_heartbeat_count, schedule_to_close_timeout, schedule_to_start_timeout,
start_to_close_timeout, heartbeat_timeout.

DescribeActivityExecutionRequest gates four payload-bearing fields behind
per-call flags (api#792): include_input, include_outcome,
include_heartbeat_details and include_last_failure.

This carries them through DescribeActivityInput and onto the request. The
public ActivityHandle.describe signature is unchanged and passes all-false, so
no caller-visible behavior changes yet; the next step exposes them and flips
heartbeat details and last failure to opt-in.
Adds include_input, include_outcome, include_heartbeat_details and
include_last_failure to ActivityHandle.describe, all defaulting to false to
match Rust's ActivityDescribeOptions.

This is a behavior change: heartbeat details and the last failure were
previously returned unconditionally and must now be requested. The proto's
rationale is that these fields carry arbitrarily large payloads and should not
be fetched unless needed.
The describe response carries input and outcome at the response level rather
than inside info, so _from_execution_info now takes both and the impl passes
them through.

Adds raw_input/input, raw_outcome/result/failure and the has_input/has_result
predicates. Decoding happens at construction, matching how last_failure and the
heartbeat details are already handled; the raw fields remain available for
callers that need type hints to convert correctly.

failure is the terminal outcome and is deliberately distinct from last_failure,
which is the most recent attempt's failure and may be set while the activity is
still retrying.
The server is expected to omit them, but an older or buggy server may return
them anyway, which would make the description's has_input/has_result and the
heartbeat/last-failure fields disagree with what the describe call asked for.
Pause holds a running activity; unpause resumes it, optionally starting at a
random point within a jitter window rather than immediately.

Both commands follow the existing cancel/terminate shape: an interceptor input,
an OutboundInterceptor method, the impl building the request, the handle method,
and the public exports. They are added together because they are symmetric and
share test scaffolding.
Reset rewinds the activity to its first attempt and resets its timeouts.
keep_paused leaves a paused activity paused, restore_original_options reverts
the options to the ones the activity was created with, and reset_heartbeat
additionally discards persisted heartbeat details.

reset_heartbeat is opt-in because the server no longer clears heartbeat details
by default (api#848 with server temporal#11417).
update_options takes a sequence of ActivityOptionsUpdate values built from
ActivityOptionsKeys. An option named by an update is changed, an option not
named is left untouched, and value_unset() clears one — the three states the
field mask needs. This follows SearchAttributeKey.value_set/value_unset in
temporalio.common, which is the SDK's existing precedent for the same problem.

restore_original is a separate method rather than a field because the proto
states the flag cannot be combined with any other option; a request that does
so is rejected by the server, so the options object should not be able to
express it.

Both return ActivityExecutionOptions, the server's resolved view after the call.
api#834 added ACTIVITY_EXECUTION_STATUS_PAUSED to the overall activity status.
A running activity that is paused reports PAUSE_REQUESTED as its run state and
only reaches PAUSED once the worker drops the attempt; an activity paused while
still scheduled reaches PAUSED directly.
The describe result carries schedule_to_close, schedule_to_start,
start_to_close and heartbeat timeouts, plus start_delay and execution_time, but
Python surfaced none of them. Ruby and Java both expose all six, and asserting
that an options update actually applied server-side needs them.

execution_time is scheduled_time plus start_delay, and equals scheduled_time
when no start delay is set (api#807).
Ports the Ruby/Java operator-command suite: pause, unpause, reset and
update_options, each asserting an observable server-side state change
rather than a successful RPC. Also covers heartbeat preservation per
command, describe reporting PAUSED, update-options on a paused activity,
both arms of the outcome oneof, and that the four describe payload
fields really are opt-in.
Documents the new ActivityHandle operator commands and the opt-in
describe payload fields under the unreleased Added heading.
Identity, the per-call request ID, the reason and the jitter cannot be
checked by observing activity state, so cover them with unit tests that
capture each outgoing request against a stubbed service. Also pins that
an omitted jitter is left off the wire rather than sent as an explicit
zero duration, that the reset flags default off, and that the update mask
names only the options that were changed.

Mirrors the Ruby suite's unobservable-request-fields test.
Asserts that each handle method routes through its own outbound
interceptor method carrying the caller's arguments, and that
restore_original_options shares the update interceptor, distinguished by
the restore_original flag and an empty set of updates.

Runs against a stubbed service rather than a live server: the routing
does not depend on server behavior, so this stays green in CI while the
functional suite waits on a CLI release.
Covers the two things a live server cannot demonstrate on its own: that
the flags the caller set reach the request, and that payloads a server
returns without being asked are dropped client-side so the has_*
accessors always agree with the request.

Matches the coverage the Go port already had.
An empty update would send an empty field mask and silently change
nothing. Raise ValueError before the round trip instead, pointing at
restore_original_options for reverting.

Matches Ruby, which raises ArgumentError, and Go, which returns an error.
The public handle methods never build that combination, but an
interceptor can, and the impl silently dropped the updates rather than
refusing them. The server rejects it, so raise before the round trip.

Matches the guard the Go port already had.
_from_execution_info took info plus five more arguments, four of which
the single caller read straight off the response it was already holding.
Take the response instead and derive them, leaving only namespace and the
data converter, which come from client state rather than the wire.

Renamed to _from_describe_response, since it no longer takes an
ActivityExecutionInfo. This also matches the Ruby, Java and Go ports,
which all hold the whole response rather than reaching into it.
Clearing is the third state of the partial update: the path is named in
the field mask so the server acts on it, while the proto field is left
unset so the value is removed rather than set. Nothing exercised it.

Brings Python level with Go, which already tested the equivalent
zero-valued change.
Ported from sdk-python#1782, written against our current API shape.

Replaces test_describe_payload_fields_are_opt_in,
test_describe_input_and_result_are_opt_in and test_describe_outcome_failure
with a single test covering all three, and covering more:

- one activity that heartbeats, fails once then succeeds, so a single
  describe carries input, result, heartbeat details and a last failure at
  the same time. The three tests it replaces each used a different
  purpose-built activity, so no describe ever held them together.
- pins last_failure present while failure (the terminal outcome) is None
  on a succeeded activity that failed once. That distinction between
  GetFailure and GetLastFailure was untested anywhere.
- asserts the decoded accessors are empty, not merely that has_* is false.
@GregoryTravis
GregoryTravis requested a review from a team as a code owner September 2, 2026 17:13
@GregoryTravis
GregoryTravis marked this pull request as draft September 2, 2026 17:13
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.

1 participant