Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
b009d3e
wip
GregoryTravis Jun 18, 2026
bcf1e9c
comments, assert_eventually_paused
GregoryTravis Jun 23, 2026
6eddd1f
wip
GregoryTravis Jun 23, 2026
ed16a37
Unit test for unobservable params. test_update_options_all_fields.
GregoryTravis Jun 24, 2026
ccee65c
Bump sdk-core to v0.4.0 (d936c6cc) for activity operator command RPCs.
GregoryTravis Jun 24, 2026
b3ad5f7
Redundant test
GregoryTravis Jun 25, 2026
5d46dea
Merge branch 'main' into gmt/operator-commands
GregoryTravis Jun 25, 2026
d816ea7
Task queue update fix
GregoryTravis Jun 26, 2026
abb6b3d
CHANGELOG
GregoryTravis Jun 26, 2026
f4bceb0
30s timeout
GregoryTravis Jul 30, 2026
d9185f6
WIP: pre-main-merge — accumulated tier2 code + optionC-attempt regen
Jul 30, 2026
5f205a3
Merge sdk-ruby main into gmt/operator-commands (bring in v0.5 sdk-cor…
Jul 30, 2026
19c5913
SAA-OC + Option C on sdk-core v0.5: submodule at sdk-rust-scratch (ma…
Jul 30, 2026
85f81aa
Fix heartbeat tests
GregoryTravis Jul 31, 2026
5ffa7f6
Confirm UpdateOptions surface handles start_delay
GregoryTravis Jul 31, 2026
fd7693d
upstream update
GregoryTravis Jul 31, 2026
44ed201
update submodule
GregoryTravis Aug 3, 2026
3a54e9d
upstream update
GregoryTravis Aug 11, 2026
81f7ced
fmt, submodule
GregoryTravis Aug 13, 2026
c1bf444
Merge branch 'main' into gmt/operator-commands
GregoryTravis Aug 13, 2026
7cff123
Merge branch 'main' into gmt/operator-commands
GregoryTravis Aug 13, 2026
949022b
cleanup
GregoryTravis Aug 13, 2026
4d1215f
test: update options requires at least one option, and enforce it as …
GregoryTravis Aug 14, 2026
71c5c0d
Add PAUSED, test_describe_paused_activity_reports_paused_status
GregoryTravis Aug 14, 2026
a2211da
test_update_options_on_paused_activity; don't compare with delta for …
GregoryTravis Aug 14, 2026
7a68ca0
Rename UpdatedOptions to ActivityExecutionOptions
GregoryTravis Aug 14, 2026
c32fca1
Round out implementation of four payload details fields, default false.
GregoryTravis Aug 17, 2026
c8412b1
get input/result/failure
GregoryTravis Aug 17, 2026
753af47
result_hint
GregoryTravis Aug 17, 2026
fa5b19e
Merge branch 'main' into gmt/operator-commands
GregoryTravis Aug 18, 2026
c548ff6
Merge branch 'main' into gmt/operator-commands
GregoryTravis Aug 19, 2026
52a272a
Revert .ruby_version and Cargo.lock changes used for upstream tracking.
GregoryTravis Aug 19, 2026
7b7d663
Move activity_options proto conversion into interceptor. Strip unrequ…
GregoryTravis Aug 20, 2026
0443862
Merge branch 'main' into gmt/operator-commands
GregoryTravis Aug 20, 2026
c63c477
move changelog and shorten
GregoryTravis Aug 20, 2026
1e2fba2
Updated types for previous change and eveyrthing else
GregoryTravis Aug 20, 2026
1fb81d3
cleanup
GregoryTravis Aug 20, 2026
4696a81
Removed an untyped
GregoryTravis Aug 20, 2026
3c3f58f
Remove reset/heartbeat tests
GregoryTravis Aug 25, 2026
ba5a1ca
Changelog
GregoryTravis Aug 26, 2026
c0fd410
Merge branch 'main' into gmt/operator-commands
GregoryTravis Aug 26, 2026
b697ab3
Add total_heartbeat_count and test
GregoryTravis Aug 26, 2026
0d366c8
Update server release version
GregoryTravis Aug 26, 2026
1f7fdaa
Merge branch 'main' into gmt/operator-commands
GregoryTravis Aug 26, 2026
93821fd
Fix clearing a duration option, and test it
GregoryTravis Aug 26, 2026
a9bf36d
Assert every reset flag reaches the request
GregoryTravis Aug 26, 2026
800a2ed
Test that describe input and result hints reach the converter
GregoryTravis Aug 26, 2026
3297336
Replace three describe tests with one test_describe_payloads
GregoryTravis Aug 26, 2026
44baebc
Narrow desc.failure before reading message, for steep
GregoryTravis Aug 26, 2026
b7b632c
Unit-test that the describe opt-ins reach the request
GregoryTravis Aug 26, 2026
ebe9f05
Unit-test that unrequested payloads are stripped client-side
GregoryTravis Aug 26, 2026
1708d80
Test that restore-original routes through the update interceptor
GregoryTravis Aug 26, 2026
6ae3869
remove non-pr files
GregoryTravis Aug 27, 2026
642eb66
remove describeOptInsReachTheRequest tests
GregoryTravis Aug 27, 2026
89df79d
cleanup
GregoryTravis Aug 27, 2026
c932756
cleanup
GregoryTravis Aug 27, 2026
22eda70
cleanup
GregoryTravis Aug 27, 2026
9ed46b1
remove test_restore_original_options_routes_through_update
GregoryTravis Aug 27, 2026
a183d80
Model update activity options on sdk precedent
GregoryTravis Aug 28, 2026
cb49c38
Remove mocked id checks
GregoryTravis Sep 1, 2026
20d7344
Remove long start delay wait
GregoryTravis Sep 1, 2026
f4dec60
start delayed in restore test
GregoryTravis Sep 1, 2026
3ceaf0f
missing files
GregoryTravis Sep 1, 2026
a80466d
cleanup
GregoryTravis Sep 1, 2026
8785611
Merge branch 'main' into gmt/operator-commands
GregoryTravis Sep 1, 2026
a3faec8
fix rbi for test
GregoryTravis Sep 2, 2026
0ff75ac
update changelog
GregoryTravis Sep 2, 2026
6dc02c4
Do not remove unrequested optional describe payloads
GregoryTravis Sep 2, 2026
014bbf5
Add test_interceptor_receives_command_arguments, include inputs in Re…
GregoryTravis Sep 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,23 @@ to docs, or any other relevant information.

### Added

### Changed
#### Standalone Activity operator commands

### Deprecated
- `Client::ActivityHandle` now supports operator commands for standalone activities: `#pause`,
`#unpause`, `#reset`, and `#update_options`.
- Added opt-in payload fields to `Client::ActivityHandle#describe`: `include_input:`,
`include_outcome:`, `include_heartbeat_details:` and `include_last_failure:`, all default
`false`.
- Add missing description fields: `execution_time`, `start_delay`, `total_heartbeat_count`.

### :boom: Breaking Changes

- `Description` payload fields are now opt-in: `input`, `outcome`, `heartbeat_details`, `last_failure`.

### Changed

### Deprecated

### Fixed
- Canceling a fiber-executor activity no longer wedges the worker if using a `Fiber#transfer` based scheduler
(such as `async`). The exception is now delivered through the scheduler's `fiber_interrupt` hook when it
Expand Down
83 changes: 82 additions & 1 deletion temporalio/lib/temporalio/client/activity_execution.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ def schedule_time
Internal::ProtoUtils.timestamp_to_time(@raw_info.schedule_time)
end

# @return [Time, nil] When the first activity task was made available for dispatch. Equals
# schedule_time + start_delay; equal to schedule_time when no start delay is set.
def execution_time
Internal::ProtoUtils.timestamp_to_time(@raw_info.execution_time)
end

# @return [Time, nil] When the activity reached a terminal state.
def close_time
Internal::ProtoUtils.timestamp_to_time(@raw_info.close_time)
Expand Down Expand Up @@ -112,7 +118,17 @@ def heartbeat_timeout
Internal::ProtoUtils.duration_to_seconds(@raw_info.heartbeat_timeout)
end

# @return [Boolean] Whether the activity has recorded any heartbeat details.
# @return [Float, nil] Delay in seconds before the first activity task is made available for
# dispatch. Not applied to retry attempts.
def start_delay
Internal::ProtoUtils.duration_to_seconds(@raw_info.start_delay)
end

# Whether heartbeat details are present on this description. False when the activity
# recorded none, and also when {ActivityHandle#describe} was called without
# `include_heartbeat_details:`.
#
# @return [Boolean] Whether heartbeat details are present.
def has_heartbeat_details? # rubocop:disable Naming/PredicatePrefix
!@raw_info.heartbeat_details&.payloads.nil? && !@raw_info.heartbeat_details.payloads.empty?
end
Expand All @@ -125,6 +141,57 @@ def heartbeat_details(hints: nil)
@data_converter.from_payloads(@raw_info.heartbeat_details, hints:)
end

# Whether the activity's input is present. False unless {ActivityHandle#describe} was
# called with `include_input:`.
#
# @return [Boolean] Whether input is present.
def has_input? # rubocop:disable Naming/PredicatePrefix
!@raw_description.input.nil?
end

# Deserialized activity input, one element per argument. Empty when no input is present.
#
# @param hints [Array<Object>, nil] Hints, if any, to assist conversion.
# @return [Array<Object>] Converted arguments.
def input(hints: nil)
@data_converter.from_payloads(@raw_description.input, hints:)
end

# Whether the activity closed with a successful result. False while the activity is still
# running, when it closed with a failure, and when {ActivityHandle#describe} was called
# without `include_outcome:`.
#
# @return [Boolean] Whether a result is present.
def has_result? # rubocop:disable Naming/PredicatePrefix
@raw_description.outcome&.value == :result
end

# Deserialized result the activity closed with. Nil when no result is present (still
# running, closed with a failure, or `include_outcome:` was not requested).
#
# @param result_hint [Object, nil] Hint, if any, to assist conversion.
# @return [Object, nil] Converted result.
def result(result_hint: nil)
return nil unless has_result?

@data_converter.from_payloads(
@raw_description.outcome.result, hints: Array(result_hint)
).first
end

# Failure the activity closed with. Nil when the activity did not close with a failure or
# when {ActivityHandle#describe} was called without `include_outcome:`.
#
# This is the terminal outcome; {#last_failure} is the failure of the most recent attempt,
# which may be set while the activity is still retrying.
#
# @return [Error::Failure, nil] Converted failure.
def failure
return nil unless @raw_description.outcome&.value == :failure

@data_converter.from_failure(@raw_description.outcome.failure)
end

# @return [RetryPolicy] Retry policy in effect for this activity.
def retry_policy
RetryPolicy._from_proto(@raw_info.retry_policy)
Expand All @@ -145,6 +212,20 @@ def attempt
@raw_info.attempt
end

# @return [Integer] Total number of heartbeats recorded across all attempts.
def total_heartbeat_count
@raw_info.total_heartbeat_count
end

# Whether a last failure is present on this description. False when the activity has no
# failed attempt, and also when {ActivityHandle#describe} was called without
# `include_last_failure:`.
#
# @return [Boolean] Whether a last failure is present.
def has_last_failure? # rubocop:disable Naming/PredicatePrefix
!@raw_info.last_failure.nil?
end

# @return [Error::Failure, nil] Failure of the last failed attempt if any.
def last_failure
return nil unless @raw_info.last_failure
Expand Down
39 changes: 39 additions & 0 deletions temporalio/lib/temporalio/client/activity_execution_options.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

require 'temporalio/internal/proto_utils'
require 'temporalio/priority'
require 'temporalio/retry_policy'

module Temporalio
class Client
# The resolved options of a standalone activity execution, as returned by
# {ActivityHandle#update_options}. Reflects the activity's options as the server resolved them
# after the update was applied.
#
# WARNING: Standalone Activities are experimental.
ActivityExecutionOptions = Data.define(
:task_queue,
:schedule_to_close_timeout,
:schedule_to_start_timeout,
:start_to_close_timeout,
:heartbeat_timeout,
:retry_policy,
:priority,
:start_delay
) do
# @!visibility private
def self._from_proto(options)
new(
task_queue: Internal::ProtoUtils.string_or(options.task_queue&.name, nil),
schedule_to_close_timeout: Internal::ProtoUtils.duration_to_seconds(options.schedule_to_close_timeout),
schedule_to_start_timeout: Internal::ProtoUtils.duration_to_seconds(options.schedule_to_start_timeout),
start_to_close_timeout: Internal::ProtoUtils.duration_to_seconds(options.start_to_close_timeout),
heartbeat_timeout: Internal::ProtoUtils.duration_to_seconds(options.heartbeat_timeout),
retry_policy: options.retry_policy ? RetryPolicy._from_proto(options.retry_policy) : nil,
priority: Priority._from_proto(options.priority),
start_delay: Internal::ProtoUtils.duration_to_seconds(options.start_delay)
)
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module ActivityExecutionStatus
CANCELED = Api::Enums::V1::ActivityExecutionStatus::ACTIVITY_EXECUTION_STATUS_CANCELED
TERMINATED = Api::Enums::V1::ActivityExecutionStatus::ACTIVITY_EXECUTION_STATUS_TERMINATED
TIMED_OUT = Api::Enums::V1::ActivityExecutionStatus::ACTIVITY_EXECUTION_STATUS_TIMED_OUT
PAUSED = Api::Enums::V1::ActivityExecutionStatus::ACTIVITY_EXECUTION_STATUS_PAUSED
end
end
end
152 changes: 151 additions & 1 deletion temporalio/lib/temporalio/client/activity_handle.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@

require 'temporalio/api'
require 'temporalio/client/activity_execution'
require 'temporalio/client/activity_execution_options'
require 'temporalio/client/activity_options'
require 'temporalio/client/interceptor'
require 'temporalio/error'
require 'temporalio/internal/proto_utils'
require 'temporalio/priority'
require 'temporalio/retry_policy'

module Temporalio
class Client
Expand Down Expand Up @@ -55,15 +60,35 @@ def result(result_hint: nil, rpc_options: nil)

# Describe the activity.
#
# The payload-bearing fields are opt-in because they can be arbitrarily large; request them
# only when needed. Each has a corresponding predicate on the returned description that
# reports whether the server supplied it.
#
# @param include_input [Boolean] If true and the activity received input, include the input.
# @param include_outcome [Boolean] If true and the activity is closed, include the outcome.
# @param include_heartbeat_details [Boolean] If true and the activity recorded heartbeat
# details, include them.
# @param include_last_failure [Boolean] If true and the activity has a failed attempt, include
# the last failure.
# @param rpc_options [RPCOptions, nil] Advanced RPC options.
#
# @return [ActivityExecution::Description] Activity description.
# @raise [Error::RPCError] RPC error from call.
def describe(rpc_options: nil)
def describe(
include_input: false,
include_outcome: false,
include_heartbeat_details: false,
include_last_failure: false,
rpc_options: nil
)
@client._impl.describe_activity(
Interceptor::DescribeActivityInput.new(
activity_id: id,
activity_run_id: run_id,
include_input:,
include_outcome:,
include_heartbeat_details:,
include_last_failure:,
rpc_options:
)
)
Expand Down Expand Up @@ -103,6 +128,131 @@ def terminate(reason = nil, rpc_options: nil)
nil
end

# Pause the activity. A paused activity is not scheduled or retried until it is unpaused via
# {#unpause}.
#
# WARNING: Standalone Activities are experimental.
#
# @param reason [String, nil] Optional reason recorded on the server.
# @param rpc_options [RPCOptions, nil] Advanced RPC options.
# @raise [Error::RPCError] RPC error from call.
def pause(reason = nil, rpc_options: nil)
@client._impl.pause_activity(
Interceptor::PauseActivityInput.new(
activity_id: id,
activity_run_id: run_id,
reason:,
rpc_options:
)
)
nil
end

# Unpause the activity, allowing it to be scheduled or retried again.
#
# WARNING: Standalone Activities are experimental.
#
# @param reason [String, nil] Optional reason recorded on the server.
# @param jitter [Float, nil] If set, the activity will start at a random time within this
# duration (in seconds).
# @param rpc_options [RPCOptions, nil] Advanced RPC options.
# @raise [Error::RPCError] RPC error from call.
def unpause(reason: nil, jitter: nil, rpc_options: nil)
@client._impl.unpause_activity(
Interceptor::UnpauseActivityInput.new(
activity_id: id,
activity_run_id: run_id,
reason:,
jitter:,
rpc_options:
)
)
nil
end

# Reset the activity. Resetting sets the attempt count back to the start, resets the activity's
# timeouts, and clears any recorded heartbeat details.
#
# WARNING: Standalone Activities are experimental.
#
# @param keep_paused [Boolean] If true and the activity is paused, it remains paused after reset.
# @param jitter [Float, nil] If set and the activity is in backoff, it will start at a random
# time within this duration (in seconds).
# @param restore_original_options [Boolean] If true, restore the activity options to the
# originals it was created with.
# @param reset_heartbeat [Boolean] If true, additionally discard any persisted heartbeat details.
# @param rpc_options [RPCOptions, nil] Advanced RPC options.
# @raise [Error::RPCError] RPC error from call.
def reset(keep_paused: false, jitter: nil, restore_original_options: false,
reset_heartbeat: false, rpc_options: nil)
@client._impl.reset_activity(
Interceptor::ResetActivityInput.new(
activity_id: id,
activity_run_id: run_id,
keep_paused:,
jitter:,
restore_original_options:,
reset_heartbeat:,
rpc_options:
)
)
nil
end

# Update the activity's options. Only the options named by `updates` are changed; anything
# not named is left as-is.
#
# Updates are created from the keys on {ActivityOptions}, via {ActivityOptions::Key#value_set}
# to set an option or {ActivityOptions::Key#value_unset} to clear it.
#
# WARNING: Standalone Activities are experimental.
#
# @param updates [Array<ActivityOptions::Update>] The option updates to apply. At least one is
# required unless `restore_original` is true.
# @param restore_original [Boolean] If true, restore the options to the originals the activity
# was created with. Mutually exclusive with any update.
# @param rpc_options [RPCOptions, nil] Advanced RPC options.
#
# @return [ActivityExecutionOptions] The activity options after the update.
#
# @raise [ArgumentError] If a non-update is given, if `restore_original` is combined with any
# update, or if no update is provided and `restore_original` is false.
# @raise [Error::RPCError] RPC error from call.
def update_options(*updates, restore_original: false, rpc_options: nil)
unless updates.all?(ActivityOptions::Update)
raise ArgumentError,
'Updates must be created via ActivityOptions::Key#value_set or #value_unset'
end

if restore_original && !updates.empty?
raise ArgumentError, 'restore_original cannot be combined with any option update'
elsif !restore_original && updates.empty?
raise ArgumentError,
'At least one option update must be given, or restore_original must be used'
end

# For repeated keys, later values override previous ones.
by_path = updates.to_h { |update| [update.key.name, update] }

proto = Api::Activity::V1::ActivityOptions.new
by_path.each_value do |update|
# An unset update names its path but leaves the field absent, which is how the server is
# told to clear the option rather than set it to a value.
update.key._apply(proto, update.value) unless update.value.nil?
end

@client._impl.update_activity_options(
Interceptor::UpdateActivityOptionsInput.new(
activity_id: id,
activity_run_id: run_id,
activity_options: proto,
update_mask: Google::Protobuf::FieldMask.new(paths: by_path.keys),
restore_original:,
rpc_options:
)
)
end

private

def _process_outcome(outcome, hint)
Expand Down
Loading
Loading