From 8381a456ebcf8fef33e230f09560848e1c4febdc Mon Sep 17 00:00:00 2001 From: jmaeagle99 <44687433+jmaeagle99@users.noreply.github.com> Date: Fri, 2 Oct 2026 16:13:26 -0700 Subject: [PATCH 01/14] feat: external storage foundation types --- temporalio/lib/temporalio/converters.rb | 9 + .../temporalio/converters/external_storage.rb | 74 +++++++ .../temporalio/converters/storage_driver.rb | 56 +++++ .../storage_driver_activity_info.rb | 37 ++++ .../converters/storage_driver_claim.rb | 36 ++++ .../storage_driver_retrieve_context.rb | 28 +++ .../storage_driver_select_context.rb | 31 +++ .../storage_driver_store_context.rb | 31 +++ .../storage_driver_workflow_info.rb | 39 ++++ temporalio/lib/temporalio/error.rb | 17 ++ .../internal/external_storage_references.rb | 58 ++++++ .../converters/external_storage.rbi | 40 ++++ .../temporalio/converters/storage_driver.rbi | 27 +++ .../storage_driver_activity_info.rbi | 27 +++ .../converters/storage_driver_claim.rbi | 20 ++ .../storage_driver_retrieve_context.rbi | 11 + .../storage_driver_select_context.rbi | 33 +++ .../storage_driver_store_context.rbi | 33 +++ .../storage_driver_workflow_info.rbi | 27 +++ temporalio/rbi/temporalio/error.rbi | 7 + .../internal/external_storage_references.rbi | 26 +++ .../converters/external_storage.rbs | 23 +++ .../temporalio/converters/storage_driver.rbs | 18 ++ .../storage_driver_activity_info.rbs | 17 ++ .../converters/storage_driver_claim.rbs | 11 + .../storage_driver_retrieve_context.rbs | 8 + .../storage_driver_select_context.rbs | 13 ++ .../storage_driver_store_context.rbs | 13 ++ .../storage_driver_workflow_info.rbs | 17 ++ temporalio/sig/temporalio/error.rbs | 4 + .../internal/external_storage_references.rbs | 18 ++ temporalio/test/external_storage_test.rb | 194 ++++++++++++++++++ temporalio/test/in_memory_storage_driver.rb | 37 ++++ temporalio/test/sig/external_storage_test.rbs | 6 + 34 files changed, 1046 insertions(+) create mode 100644 temporalio/lib/temporalio/converters/external_storage.rb create mode 100644 temporalio/lib/temporalio/converters/storage_driver.rb create mode 100644 temporalio/lib/temporalio/converters/storage_driver_activity_info.rb create mode 100644 temporalio/lib/temporalio/converters/storage_driver_claim.rb create mode 100644 temporalio/lib/temporalio/converters/storage_driver_retrieve_context.rb create mode 100644 temporalio/lib/temporalio/converters/storage_driver_select_context.rb create mode 100644 temporalio/lib/temporalio/converters/storage_driver_store_context.rb create mode 100644 temporalio/lib/temporalio/converters/storage_driver_workflow_info.rb create mode 100644 temporalio/lib/temporalio/internal/external_storage_references.rb create mode 100644 temporalio/rbi/temporalio/converters/external_storage.rbi create mode 100644 temporalio/rbi/temporalio/converters/storage_driver.rbi create mode 100644 temporalio/rbi/temporalio/converters/storage_driver_activity_info.rbi create mode 100644 temporalio/rbi/temporalio/converters/storage_driver_claim.rbi create mode 100644 temporalio/rbi/temporalio/converters/storage_driver_retrieve_context.rbi create mode 100644 temporalio/rbi/temporalio/converters/storage_driver_select_context.rbi create mode 100644 temporalio/rbi/temporalio/converters/storage_driver_store_context.rbi create mode 100644 temporalio/rbi/temporalio/converters/storage_driver_workflow_info.rbi create mode 100644 temporalio/rbi/temporalio/internal/external_storage_references.rbi create mode 100644 temporalio/sig/temporalio/converters/external_storage.rbs create mode 100644 temporalio/sig/temporalio/converters/storage_driver.rbs create mode 100644 temporalio/sig/temporalio/converters/storage_driver_activity_info.rbs create mode 100644 temporalio/sig/temporalio/converters/storage_driver_claim.rbs create mode 100644 temporalio/sig/temporalio/converters/storage_driver_retrieve_context.rbs create mode 100644 temporalio/sig/temporalio/converters/storage_driver_select_context.rbs create mode 100644 temporalio/sig/temporalio/converters/storage_driver_store_context.rbs create mode 100644 temporalio/sig/temporalio/converters/storage_driver_workflow_info.rbs create mode 100644 temporalio/sig/temporalio/internal/external_storage_references.rbs create mode 100644 temporalio/test/external_storage_test.rb create mode 100644 temporalio/test/in_memory_storage_driver.rb create mode 100644 temporalio/test/sig/external_storage_test.rbs diff --git a/temporalio/lib/temporalio/converters.rb b/temporalio/lib/temporalio/converters.rb index ba21be0b..42c5aa34 100644 --- a/temporalio/lib/temporalio/converters.rb +++ b/temporalio/lib/temporalio/converters.rb @@ -1,6 +1,15 @@ # frozen_string_literal: true require 'temporalio/converters/data_converter' +require 'temporalio/converters/external_storage' +require 'temporalio/converters/storage_driver' +require 'temporalio/converters/storage_driver_activity_info' +require 'temporalio/converters/storage_driver_claim' +require 'temporalio/converters/storage_driver_retrieve_context' +require 'temporalio/converters/storage_driver_select_context' +require 'temporalio/converters/storage_driver_store_context' +require 'temporalio/converters/storage_driver_workflow_info' +require 'temporalio/internal/external_storage_references' module Temporalio # Module for data conversion. diff --git a/temporalio/lib/temporalio/converters/external_storage.rb b/temporalio/lib/temporalio/converters/external_storage.rb new file mode 100644 index 00000000..c73389e7 --- /dev/null +++ b/temporalio/lib/temporalio/converters/external_storage.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'temporalio/converters/storage_driver' + +module Temporalio + module Converters + # Configuration for offloading large payloads to an external storage system. + # + # This is validated when constructed rather than when used, so a misconfiguration surfaces at startup instead of on + # the first payload large enough to offload. + # + # @note WARNING: This API is experimental and may change in the future. + # + # @!visibility private + class ExternalStorage + # Shared with every other SDK, so the same payload offloads regardless of which one wrote it. + DEFAULT_PAYLOAD_SIZE_THRESHOLD = 256 * 1024 + + # @return [Array] Drivers available for storing and retrieving payloads. + attr_reader :drivers + + # @return [Proc] Selector choosing which driver stores each payload. Called with a + # {StorageDriverSelectContext} and a payload, returning a {StorageDriver} or nil to pass the payload through. + # Only called for payloads that meet {#payload_size_threshold}, and must return one of {#drivers} or nil. + attr_reader :driver_selector + + # @return [Integer] Minimum encoded payload size, in bytes, that is offloaded. Payloads at or above this size are + # offloaded; smaller ones are left inline. Zero offloads every payload. + attr_reader :payload_size_threshold + + # Create external storage configuration. + # + # @param drivers [Array] Drivers available for storing and retrieving payloads. At least one is + # required, and every driver must have a unique, non-empty name. Retrieval routes by the name recorded in + # history, so a driver must remain configured for as long as any payload it stored can still be read. + # @param driver_selector [Proc, nil] Selector choosing which driver stores each payload. Required when more than + # one driver is given; with a single driver it may be nil, in which case that driver stores every eligible + # payload. + # @param payload_size_threshold [Integer] Minimum encoded payload size, in bytes, to offload. + def initialize(drivers:, driver_selector: nil, payload_size_threshold: DEFAULT_PAYLOAD_SIZE_THRESHOLD) + raise ArgumentError, 'At least one driver is required' if drivers.empty? + + raise ArgumentError, 'payload_size_threshold cannot be negative' if payload_size_threshold.negative? + + if drivers.size > 1 && driver_selector.nil? + raise ArgumentError, 'driver_selector is required when more than one driver is given' + end + + @drivers_by_name = {} + drivers.each do |driver| + # The name is the routing key written into history, so a driver without one can never be resolved on read. + raise ArgumentError, 'Driver name cannot be empty' if driver.name.nil? || driver.name.empty? + raise ArgumentError, "Multiple drivers given with name '#{driver.name}'" if @drivers_by_name.key?(driver.name) + + @drivers_by_name[driver.name] = driver + end + @drivers_by_name.freeze + + @drivers = drivers.dup.freeze + # Normalized so callers never have to distinguish the single-driver case. + @driver_selector = driver_selector || ->(_context, _payload) { drivers.first } + @payload_size_threshold = payload_size_threshold + end + + # Get the driver registered under the given name. + # + # @param name [String] Driver name recorded in the payload reference. + # @return [StorageDriver, nil] The driver, or nil if none is registered under that name. + def driver(name) + @drivers_by_name[name] + end + end + end +end diff --git a/temporalio/lib/temporalio/converters/storage_driver.rb b/temporalio/lib/temporalio/converters/storage_driver.rb new file mode 100644 index 00000000..922c5cc1 --- /dev/null +++ b/temporalio/lib/temporalio/converters/storage_driver.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Temporalio + module Converters + # Base class for storing and retrieving payloads in an external storage system. + # + # Payloads at or over the configured size threshold are handed to a driver and replaced on the wire by a small + # reference, so the payload data itself never reaches the Temporal server. + # + # Implementations are called concurrently and must be thread safe. Methods block, and may perform I/O directly. + # To work a batch concurrently, fan out with threads: Ruby releases its global lock during blocking I/O, so threads + # give real parallelism, and they work whether or not the worker is running under a fiber scheduler. + # {::Fiber.schedule} must not be used, since it raises when the worker is configured with a payload codec thread + # pool instead of a fiber scheduler. {Temporalio::Worker::ThreadPool} can be used to bound that fan out. + # + # @note WARNING: This API is experimental and may change in the future. + # + # @!visibility private + class StorageDriver + # Name of this driver instance, unique among the drivers registered on one {ExternalStorage}. This is written + # into history alongside every payload the driver stores and is used to route retrieval back to the same driver. + # Renaming a deployed driver makes every payload stored under the old name unretrievable. + # + # @return [String] Driver instance name. + def name + raise NotImplementedError + end + + # Identifier for this driver implementation, for example +aws.s3driver+. Unlike {name}, this is identical across + # every instance of the same implementation and across SDK languages. + # + # @return [String] Driver implementation identifier. + def type + raise NotImplementedError + end + + # Store the given payloads. + # + # @param context [StorageDriverStoreContext] Context for this store operation. + # @param payloads [Enumerable] Payloads to store. This value should not be mutated. + # @return [Array] One claim per payload, in the same order. + def store(context, payloads) + raise NotImplementedError + end + + # Retrieve the payloads for the given claims. + # + # @param context [StorageDriverRetrieveContext] Context for this retrieve operation. + # @param claims [Enumerable] Claims to retrieve. This value should not be mutated. + # @return [Array] One payload per claim, in the same order. + def retrieve(context, claims) + raise NotImplementedError + end + end + end +end diff --git a/temporalio/lib/temporalio/converters/storage_driver_activity_info.rb b/temporalio/lib/temporalio/converters/storage_driver_activity_info.rb new file mode 100644 index 00000000..79292567 --- /dev/null +++ b/temporalio/lib/temporalio/converters/storage_driver_activity_info.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Temporalio + module Converters + # Identity of a standalone activity a payload is being stored on behalf of. + # + # @note WARNING: This API is experimental and may change in the future. + # + # @!visibility private + class StorageDriverActivityInfo + # @return [String] Activity namespace. + attr_reader :namespace + + # @return [String, nil] Activity ID, if known. + attr_reader :id + + # @return [String, nil] Run ID, if known. + attr_reader :run_id + + # @return [String, nil] Activity type name, if known. + attr_reader :type + + # Create activity target information. + # + # @param namespace [String] Activity namespace. + # @param id [String, nil] Activity ID. + # @param run_id [String, nil] Run ID. + # @param type [String, nil] Activity type name. + def initialize(namespace:, id: nil, run_id: nil, type: nil) + @namespace = namespace + @id = id + @run_id = run_id + @type = type + end + end + end +end diff --git a/temporalio/lib/temporalio/converters/storage_driver_claim.rb b/temporalio/lib/temporalio/converters/storage_driver_claim.rb new file mode 100644 index 00000000..7abd2fed --- /dev/null +++ b/temporalio/lib/temporalio/converters/storage_driver_claim.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Temporalio + module Converters + # Driver-defined reference to an externally stored payload, used to retrieve it later. + # + # @note WARNING: This API is experimental and may change in the future. + # + # @!visibility private + class StorageDriverClaim + # @return [Hash] Data the driver needs to retrieve the payload. This is written into history, so + # it must contain everything required for retrieval and must not contain secrets. + attr_reader :claim_data + + # Create a claim. + # + # @param claim_data [Hash] Data identifying the stored payload. + def initialize(claim_data) + @claim_data = claim_data + end + + # @param other [Object] Value to compare with. + # @return [Boolean] Whether the other value is a claim identifying the same stored payload. + def ==(other) + other.is_a?(StorageDriverClaim) && claim_data == other.claim_data + end + + alias eql? == + + # @return [Integer] Hash derived from the claim data. + def hash + claim_data.hash + end + end + end +end diff --git a/temporalio/lib/temporalio/converters/storage_driver_retrieve_context.rb b/temporalio/lib/temporalio/converters/storage_driver_retrieve_context.rb new file mode 100644 index 00000000..858888f7 --- /dev/null +++ b/temporalio/lib/temporalio/converters/storage_driver_retrieve_context.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'temporalio/cancellation' + +module Temporalio + module Converters + # Context given to {StorageDriver#retrieve}. + # + # This deliberately carries no target. A payload must be retrievable from its {StorageDriverClaim} alone, since the + # same payload can be read from a different execution than the one that stored it. + # + # @note WARNING: This API is experimental and may change in the future. Members may be added, so accept keyword + # arguments defensively if constructing this yourself. + # + # @!visibility private + class StorageDriverRetrieveContext + # @return [Cancellation] Cancelled when the SDK abandons this retrieve operation. + attr_reader :cancellation + + # Create a retrieve context. + # + # @param cancellation [Cancellation] Cancellation for the operation. + def initialize(cancellation:) + @cancellation = cancellation + end + end + end +end diff --git a/temporalio/lib/temporalio/converters/storage_driver_select_context.rb b/temporalio/lib/temporalio/converters/storage_driver_select_context.rb new file mode 100644 index 00000000..50626059 --- /dev/null +++ b/temporalio/lib/temporalio/converters/storage_driver_select_context.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'temporalio/cancellation' + +module Temporalio + module Converters + # Context given to the driver selector. + # + # @note WARNING: This API is experimental and may change in the future. Members may be added, so accept keyword + # arguments defensively if constructing this yourself. + # + # @!visibility private + class StorageDriverSelectContext + # @return [StorageDriverWorkflowInfo, StorageDriverActivityInfo, nil] The execution the payload is being stored + # on behalf of, or nil when there is no associated execution. + attr_reader :target + + # @return [Cancellation] Cancelled when the SDK abandons the operation this selection is part of. + attr_reader :cancellation + + # Create a select context. + # + # @param target [StorageDriverWorkflowInfo, StorageDriverActivityInfo, nil] Execution being stored for. + # @param cancellation [Cancellation] Cancellation for the operation. + def initialize(target:, cancellation:) + @target = target + @cancellation = cancellation + end + end + end +end diff --git a/temporalio/lib/temporalio/converters/storage_driver_store_context.rb b/temporalio/lib/temporalio/converters/storage_driver_store_context.rb new file mode 100644 index 00000000..013646af --- /dev/null +++ b/temporalio/lib/temporalio/converters/storage_driver_store_context.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'temporalio/cancellation' + +module Temporalio + module Converters + # Context given to {StorageDriver#store}. + # + # @note WARNING: This API is experimental and may change in the future. Members may be added, so accept keyword + # arguments defensively if constructing this yourself. + # + # @!visibility private + class StorageDriverStoreContext + # @return [StorageDriverWorkflowInfo, StorageDriverActivityInfo, nil] The execution the payloads are being stored + # on behalf of, or nil when there is no associated execution. + attr_reader :target + + # @return [Cancellation] Cancelled when the SDK abandons this store operation. + attr_reader :cancellation + + # Create a store context. + # + # @param target [StorageDriverWorkflowInfo, StorageDriverActivityInfo, nil] Execution being stored for. + # @param cancellation [Cancellation] Cancellation for the operation. + def initialize(target:, cancellation:) + @target = target + @cancellation = cancellation + end + end + end +end diff --git a/temporalio/lib/temporalio/converters/storage_driver_workflow_info.rb b/temporalio/lib/temporalio/converters/storage_driver_workflow_info.rb new file mode 100644 index 00000000..1d63bbe3 --- /dev/null +++ b/temporalio/lib/temporalio/converters/storage_driver_workflow_info.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Temporalio + module Converters + # Identity of the workflow a payload is being stored on behalf of. Also used for workflow activities, which store + # against their owning workflow rather than themselves. + # + # @note WARNING: This API is experimental and may change in the future. + # + # @!visibility private + class StorageDriverWorkflowInfo + # @return [String] Workflow namespace. + attr_reader :namespace + + # @return [String, nil] Workflow ID, if known. + attr_reader :id + + # @return [String, nil] Run ID, if known. Unset when the run is not yet determined, such as when starting a child + # workflow or continuing as new. + attr_reader :run_id + + # @return [String, nil] Workflow type name, if known. + attr_reader :type + + # Create workflow target information. + # + # @param namespace [String] Workflow namespace. + # @param id [String, nil] Workflow ID. + # @param run_id [String, nil] Run ID. + # @param type [String, nil] Workflow type name. + def initialize(namespace:, id: nil, run_id: nil, type: nil) + @namespace = namespace + @id = id + @run_id = run_id + @type = type + end + end + end +end diff --git a/temporalio/lib/temporalio/error.rb b/temporalio/lib/temporalio/error.rb index 8d967864..e1d4c171 100644 --- a/temporalio/lib/temporalio/error.rb +++ b/temporalio/lib/temporalio/error.rb @@ -116,6 +116,23 @@ def initialize(details) end end + # Error that occurs when a payload stored in external storage is encountered but no external storage is + # configured to retrieve it. This usually means the client or worker reading the payload is configured + # differently from the one that wrote it. + # + # @note WARNING: This API is experimental and may change in the future. + # + # @!visibility private + class ExternalStorageNotConfiguredError < Error + # @!visibility private + def initialize( + message = '[TMPRL1105] Encountered a reference to a payload in external storage, but no external storage ' \ + 'is configured to retrieve it. Configure external storage with a driver able to retrieve it.' + ) + super + end + end + # Error raised by a client for a general RPC failure. class RPCError < Error # @return [Code] Status code for the error. diff --git a/temporalio/lib/temporalio/internal/external_storage_references.rb b/temporalio/lib/temporalio/internal/external_storage_references.rb new file mode 100644 index 00000000..13528a4b --- /dev/null +++ b/temporalio/lib/temporalio/internal/external_storage_references.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'temporalio/api' +require 'temporalio/api/sdk/v1/external_storage' + +module Temporalio + module Internal + # The on-the-wire form of a payload that has been offloaded to external storage. + # + # An offloaded payload is replaced by one whose data is the proto-JSON form of an + # +ExternalStorageReference+. This shape is part of the wire contract and must not be changed unilaterally. + # + # @!visibility private + module ExternalStorageReferences + ENCODING = 'json/protobuf' + MESSAGE_TYPE = 'temporal.api.sdk.v1.ExternalStorageReference' + + class << self + # @param payload [Api::Common::V1::Payload] Payload to check. + # @return [Boolean] Whether the payload is a reference to externally stored data. + def reference?(payload) + payload.metadata['encoding'] == ENCODING && payload.metadata['messageType'] == MESSAGE_TYPE + end + + # @param payload [Api::Common::V1::Payload] Payload to parse. + # @return [Api::Sdk::V1::ExternalStorageReference, nil] Parsed reference, or nil if not a reference. + def parse_reference(payload) + return nil unless reference?(payload) + + # Another SDK may add fields to the reference before this one knows about them, and an unknown field must + # not make an otherwise-valid payload unreadable. + Api::Sdk::V1::ExternalStorageReference.decode_json( + payload.data, ignore_unknown_fields: true + ) + end + + # @param driver_name [String] Name of the driver that stored the payload. + # @param claim_data [Hash] Driver data identifying the stored payload. + # @param original_size_bytes [Integer] Encoded size of the payload that was offloaded. + # @return [Api::Common::V1::Payload] Reference payload that replaces the offloaded payload on the wire. + def create_reference_payload(driver_name:, claim_data:, original_size_bytes:) + reference = Api::Sdk::V1::ExternalStorageReference.new( + driver_name:, claim_data: claim_data.to_h + ) + Api::Common::V1::Payload.new( + metadata: { 'encoding' => ENCODING.b, 'messageType' => MESSAGE_TYPE.b }, + data: reference.to_json.b, + # The original size is kept so the server and UI can report what the payload would have been without + # having to fetch it from storage. + external_payloads: [ + Api::Common::V1::Payload::ExternalPayloadDetails.new(size_bytes: original_size_bytes) + ] + ) + end + end + end + end +end diff --git a/temporalio/rbi/temporalio/converters/external_storage.rbi b/temporalio/rbi/temporalio/converters/external_storage.rbi new file mode 100644 index 00000000..818b314e --- /dev/null +++ b/temporalio/rbi/temporalio/converters/external_storage.rbi @@ -0,0 +1,40 @@ +# typed: true + +class Temporalio::Converters::ExternalStorage + extend T::Sig + + DEFAULT_PAYLOAD_SIZE_THRESHOLD = T.let(T.unsafe(nil), Integer) + + sig { returns(T::Array[Temporalio::Converters::StorageDriver]) } + attr_reader :drivers + + sig do + returns( + T.proc.params( + context: Temporalio::Converters::StorageDriverSelectContext, + payload: Temporalio::Api::Common::V1::Payload + ).returns(T.nilable(Temporalio::Converters::StorageDriver)) + ) + end + attr_reader :driver_selector + + sig { returns(Integer) } + attr_reader :payload_size_threshold + + sig do + params( + drivers: T::Array[Temporalio::Converters::StorageDriver], + driver_selector: T.nilable( + T.proc.params( + context: Temporalio::Converters::StorageDriverSelectContext, + payload: Temporalio::Api::Common::V1::Payload + ).returns(T.nilable(Temporalio::Converters::StorageDriver)) + ), + payload_size_threshold: Integer + ).void + end + def initialize(drivers:, driver_selector: T.unsafe(nil), payload_size_threshold: T.unsafe(nil)); end + + sig { params(name: String).returns(T.nilable(Temporalio::Converters::StorageDriver)) } + def driver(name); end +end diff --git a/temporalio/rbi/temporalio/converters/storage_driver.rbi b/temporalio/rbi/temporalio/converters/storage_driver.rbi new file mode 100644 index 00000000..62f645ee --- /dev/null +++ b/temporalio/rbi/temporalio/converters/storage_driver.rbi @@ -0,0 +1,27 @@ +# typed: true + +class Temporalio::Converters::StorageDriver + extend T::Sig + + sig { returns(String) } + def name; end + + sig { returns(String) } + def type; end + + sig do + params( + context: Temporalio::Converters::StorageDriverStoreContext, + payloads: T::Enumerable[Temporalio::Api::Common::V1::Payload] + ).returns(T::Array[Temporalio::Converters::StorageDriverClaim]) + end + def store(context, payloads); end + + sig do + params( + context: Temporalio::Converters::StorageDriverRetrieveContext, + claims: T::Enumerable[Temporalio::Converters::StorageDriverClaim] + ).returns(T::Array[Temporalio::Api::Common::V1::Payload]) + end + def retrieve(context, claims); end +end diff --git a/temporalio/rbi/temporalio/converters/storage_driver_activity_info.rbi b/temporalio/rbi/temporalio/converters/storage_driver_activity_info.rbi new file mode 100644 index 00000000..9e954ec4 --- /dev/null +++ b/temporalio/rbi/temporalio/converters/storage_driver_activity_info.rbi @@ -0,0 +1,27 @@ +# typed: true + +class Temporalio::Converters::StorageDriverActivityInfo + extend T::Sig + + sig { returns(String) } + attr_reader :namespace + + sig { returns(T.nilable(String)) } + attr_reader :id + + sig { returns(T.nilable(String)) } + attr_reader :run_id + + sig { returns(T.nilable(String)) } + attr_reader :type + + sig do + params( + namespace: String, + id: T.nilable(String), + run_id: T.nilable(String), + type: T.nilable(String) + ).void + end + def initialize(namespace:, id: T.unsafe(nil), run_id: T.unsafe(nil), type: T.unsafe(nil)); end +end diff --git a/temporalio/rbi/temporalio/converters/storage_driver_claim.rbi b/temporalio/rbi/temporalio/converters/storage_driver_claim.rbi new file mode 100644 index 00000000..d5542228 --- /dev/null +++ b/temporalio/rbi/temporalio/converters/storage_driver_claim.rbi @@ -0,0 +1,20 @@ +# typed: true + +class Temporalio::Converters::StorageDriverClaim + extend T::Sig + + sig { returns(T::Hash[String, String]) } + attr_reader :claim_data + + sig { params(claim_data: T::Hash[String, String]).void } + def initialize(claim_data); end + + sig { params(other: Object).returns(T::Boolean) } + def ==(other); end + + sig { params(other: Object).returns(T::Boolean) } + def eql?(other); end + + sig { returns(Integer) } + def hash; end +end diff --git a/temporalio/rbi/temporalio/converters/storage_driver_retrieve_context.rbi b/temporalio/rbi/temporalio/converters/storage_driver_retrieve_context.rbi new file mode 100644 index 00000000..8041c94f --- /dev/null +++ b/temporalio/rbi/temporalio/converters/storage_driver_retrieve_context.rbi @@ -0,0 +1,11 @@ +# typed: true + +class Temporalio::Converters::StorageDriverRetrieveContext + extend T::Sig + + sig { returns(Temporalio::Cancellation) } + attr_reader :cancellation + + sig { params(cancellation: Temporalio::Cancellation).void } + def initialize(cancellation:); end +end diff --git a/temporalio/rbi/temporalio/converters/storage_driver_select_context.rbi b/temporalio/rbi/temporalio/converters/storage_driver_select_context.rbi new file mode 100644 index 00000000..4cfc755b --- /dev/null +++ b/temporalio/rbi/temporalio/converters/storage_driver_select_context.rbi @@ -0,0 +1,33 @@ +# typed: true + +class Temporalio::Converters::StorageDriverSelectContext + extend T::Sig + + sig do + returns( + T.nilable( + T.any( + Temporalio::Converters::StorageDriverWorkflowInfo, + Temporalio::Converters::StorageDriverActivityInfo + ) + ) + ) + end + attr_reader :target + + sig { returns(Temporalio::Cancellation) } + attr_reader :cancellation + + sig do + params( + target: T.nilable( + T.any( + Temporalio::Converters::StorageDriverWorkflowInfo, + Temporalio::Converters::StorageDriverActivityInfo + ) + ), + cancellation: Temporalio::Cancellation + ).void + end + def initialize(target:, cancellation:); end +end diff --git a/temporalio/rbi/temporalio/converters/storage_driver_store_context.rbi b/temporalio/rbi/temporalio/converters/storage_driver_store_context.rbi new file mode 100644 index 00000000..22085a9f --- /dev/null +++ b/temporalio/rbi/temporalio/converters/storage_driver_store_context.rbi @@ -0,0 +1,33 @@ +# typed: true + +class Temporalio::Converters::StorageDriverStoreContext + extend T::Sig + + sig do + returns( + T.nilable( + T.any( + Temporalio::Converters::StorageDriverWorkflowInfo, + Temporalio::Converters::StorageDriverActivityInfo + ) + ) + ) + end + attr_reader :target + + sig { returns(Temporalio::Cancellation) } + attr_reader :cancellation + + sig do + params( + target: T.nilable( + T.any( + Temporalio::Converters::StorageDriverWorkflowInfo, + Temporalio::Converters::StorageDriverActivityInfo + ) + ), + cancellation: Temporalio::Cancellation + ).void + end + def initialize(target:, cancellation:); end +end diff --git a/temporalio/rbi/temporalio/converters/storage_driver_workflow_info.rbi b/temporalio/rbi/temporalio/converters/storage_driver_workflow_info.rbi new file mode 100644 index 00000000..226a942b --- /dev/null +++ b/temporalio/rbi/temporalio/converters/storage_driver_workflow_info.rbi @@ -0,0 +1,27 @@ +# typed: true + +class Temporalio::Converters::StorageDriverWorkflowInfo + extend T::Sig + + sig { returns(String) } + attr_reader :namespace + + sig { returns(T.nilable(String)) } + attr_reader :id + + sig { returns(T.nilable(String)) } + attr_reader :run_id + + sig { returns(T.nilable(String)) } + attr_reader :type + + sig do + params( + namespace: String, + id: T.nilable(String), + run_id: T.nilable(String), + type: T.nilable(String) + ).void + end + def initialize(namespace:, id: T.unsafe(nil), run_id: T.unsafe(nil), type: T.unsafe(nil)); end +end diff --git a/temporalio/rbi/temporalio/error.rbi b/temporalio/rbi/temporalio/error.rbi index ad2b41ad..cec26a2b 100644 --- a/temporalio/rbi/temporalio/error.rbi +++ b/temporalio/rbi/temporalio/error.rbi @@ -281,6 +281,13 @@ class Temporalio::Error::ScheduleAlreadyRunningError < ::Temporalio::Error def initialize; end end +class Temporalio::Error::ExternalStorageNotConfiguredError < ::Temporalio::Error + extend T::Sig + + sig { params(message: String).void } + def initialize(message = T.unsafe(nil)); end +end + class Temporalio::Error::RPCError < ::Temporalio::Error sig { params(message: String, code: Integer, raw_grpc_status: T.nilable(Object)).void } def initialize(message, code:, raw_grpc_status:); end diff --git a/temporalio/rbi/temporalio/internal/external_storage_references.rbi b/temporalio/rbi/temporalio/internal/external_storage_references.rbi new file mode 100644 index 00000000..dc9905dc --- /dev/null +++ b/temporalio/rbi/temporalio/internal/external_storage_references.rbi @@ -0,0 +1,26 @@ +# typed: true + +module Temporalio::Internal::ExternalStorageReferences + extend T::Sig + + ENCODING = T.let(T.unsafe(nil), String) + MESSAGE_TYPE = T.let(T.unsafe(nil), String) + + sig { params(payload: Temporalio::Api::Common::V1::Payload).returns(T::Boolean) } + def self.reference?(payload); end + + sig do + params(payload: Temporalio::Api::Common::V1::Payload) + .returns(T.nilable(Temporalio::Api::Sdk::V1::ExternalStorageReference)) + end + def self.parse_reference(payload); end + + sig do + params( + driver_name: String, + claim_data: T::Hash[String, String], + original_size_bytes: Integer + ).returns(Temporalio::Api::Common::V1::Payload) + end + def self.create_reference_payload(driver_name:, claim_data:, original_size_bytes:); end +end diff --git a/temporalio/sig/temporalio/converters/external_storage.rbs b/temporalio/sig/temporalio/converters/external_storage.rbs new file mode 100644 index 00000000..c903d59a --- /dev/null +++ b/temporalio/sig/temporalio/converters/external_storage.rbs @@ -0,0 +1,23 @@ +module Temporalio + module Converters + class ExternalStorage + DEFAULT_PAYLOAD_SIZE_THRESHOLD: Integer + + type driverSelector = ^(StorageDriverSelectContext, Api::Common::V1::Payload) -> StorageDriver? + + attr_reader drivers: Array[StorageDriver] + attr_reader driver_selector: driverSelector + attr_reader payload_size_threshold: Integer + + @drivers_by_name: Hash[String, StorageDriver] + + def initialize: ( + drivers: Array[StorageDriver], + ?driver_selector: driverSelector?, + ?payload_size_threshold: Integer + ) -> void + + def driver: (String name) -> StorageDriver? + end + end +end diff --git a/temporalio/sig/temporalio/converters/storage_driver.rbs b/temporalio/sig/temporalio/converters/storage_driver.rbs new file mode 100644 index 00000000..c35a0b14 --- /dev/null +++ b/temporalio/sig/temporalio/converters/storage_driver.rbs @@ -0,0 +1,18 @@ +module Temporalio + module Converters + type storageDriverTargetInfo = StorageDriverWorkflowInfo | StorageDriverActivityInfo + + class StorageDriver + def name: -> String + def type: -> String + def store: ( + StorageDriverStoreContext context, + Enumerable[Api::Common::V1::Payload] payloads + ) -> Array[StorageDriverClaim] + def retrieve: ( + StorageDriverRetrieveContext context, + Enumerable[StorageDriverClaim] claims + ) -> Array[Api::Common::V1::Payload] + end + end +end diff --git a/temporalio/sig/temporalio/converters/storage_driver_activity_info.rbs b/temporalio/sig/temporalio/converters/storage_driver_activity_info.rbs new file mode 100644 index 00000000..4ddb563d --- /dev/null +++ b/temporalio/sig/temporalio/converters/storage_driver_activity_info.rbs @@ -0,0 +1,17 @@ +module Temporalio + module Converters + class StorageDriverActivityInfo + attr_reader namespace: String + attr_reader id: String? + attr_reader run_id: String? + attr_reader type: String? + + def initialize: ( + namespace: String, + ?id: String?, + ?run_id: String?, + ?type: String? + ) -> void + end + end +end diff --git a/temporalio/sig/temporalio/converters/storage_driver_claim.rbs b/temporalio/sig/temporalio/converters/storage_driver_claim.rbs new file mode 100644 index 00000000..60f57f85 --- /dev/null +++ b/temporalio/sig/temporalio/converters/storage_driver_claim.rbs @@ -0,0 +1,11 @@ +module Temporalio + module Converters + class StorageDriverClaim + attr_reader claim_data: Hash[String, String] + def initialize: (Hash[String, String] claim_data) -> void + def ==: (untyped other) -> bool + def eql?: (untyped other) -> bool + def hash: -> Integer + end + end +end diff --git a/temporalio/sig/temporalio/converters/storage_driver_retrieve_context.rbs b/temporalio/sig/temporalio/converters/storage_driver_retrieve_context.rbs new file mode 100644 index 00000000..4169dbb8 --- /dev/null +++ b/temporalio/sig/temporalio/converters/storage_driver_retrieve_context.rbs @@ -0,0 +1,8 @@ +module Temporalio + module Converters + class StorageDriverRetrieveContext + attr_reader cancellation: Cancellation + def initialize: (cancellation: Cancellation) -> void + end + end +end diff --git a/temporalio/sig/temporalio/converters/storage_driver_select_context.rbs b/temporalio/sig/temporalio/converters/storage_driver_select_context.rbs new file mode 100644 index 00000000..e4b8aa4c --- /dev/null +++ b/temporalio/sig/temporalio/converters/storage_driver_select_context.rbs @@ -0,0 +1,13 @@ +module Temporalio + module Converters + class StorageDriverSelectContext + attr_reader target: storageDriverTargetInfo? + attr_reader cancellation: Cancellation + + def initialize: ( + target: storageDriverTargetInfo?, + cancellation: Cancellation + ) -> void + end + end +end diff --git a/temporalio/sig/temporalio/converters/storage_driver_store_context.rbs b/temporalio/sig/temporalio/converters/storage_driver_store_context.rbs new file mode 100644 index 00000000..a36eceb1 --- /dev/null +++ b/temporalio/sig/temporalio/converters/storage_driver_store_context.rbs @@ -0,0 +1,13 @@ +module Temporalio + module Converters + class StorageDriverStoreContext + attr_reader target: storageDriverTargetInfo? + attr_reader cancellation: Cancellation + + def initialize: ( + target: storageDriverTargetInfo?, + cancellation: Cancellation + ) -> void + end + end +end diff --git a/temporalio/sig/temporalio/converters/storage_driver_workflow_info.rbs b/temporalio/sig/temporalio/converters/storage_driver_workflow_info.rbs new file mode 100644 index 00000000..b27139bc --- /dev/null +++ b/temporalio/sig/temporalio/converters/storage_driver_workflow_info.rbs @@ -0,0 +1,17 @@ +module Temporalio + module Converters + class StorageDriverWorkflowInfo + attr_reader namespace: String + attr_reader id: String? + attr_reader run_id: String? + attr_reader type: String? + + def initialize: ( + namespace: String, + ?id: String?, + ?run_id: String?, + ?type: String? + ) -> void + end + end +end diff --git a/temporalio/sig/temporalio/error.rbs b/temporalio/sig/temporalio/error.rbs index 72675e9b..1cd4771b 100644 --- a/temporalio/sig/temporalio/error.rbs +++ b/temporalio/sig/temporalio/error.rbs @@ -49,6 +49,10 @@ module Temporalio def initialize: (Activity::CancellationDetails details) -> void end + class ExternalStorageNotConfiguredError < Error + def initialize: (?String message) -> void + end + class RPCError < Error attr_reader code: Code::enum diff --git a/temporalio/sig/temporalio/internal/external_storage_references.rbs b/temporalio/sig/temporalio/internal/external_storage_references.rbs new file mode 100644 index 00000000..b4cf9312 --- /dev/null +++ b/temporalio/sig/temporalio/internal/external_storage_references.rbs @@ -0,0 +1,18 @@ +module Temporalio + module Internal + module ExternalStorageReferences + ENCODING: String + MESSAGE_TYPE: String + + def self.reference?: (Api::Common::V1::Payload payload) -> bool + def self.parse_reference: ( + Api::Common::V1::Payload payload + ) -> Api::Sdk::V1::ExternalStorageReference? + def self.create_reference_payload: ( + driver_name: String, + claim_data: Hash[String, String], + original_size_bytes: Integer + ) -> Api::Common::V1::Payload + end + end +end diff --git a/temporalio/test/external_storage_test.rb b/temporalio/test/external_storage_test.rb new file mode 100644 index 00000000..d93be5f4 --- /dev/null +++ b/temporalio/test/external_storage_test.rb @@ -0,0 +1,194 @@ +# frozen_string_literal: true + +require 'base64' +require 'in_memory_storage_driver' +require 'securerandom' +require 'temporalio/cancellation' +require 'temporalio/converters/external_storage' +require 'temporalio/converters/storage_driver_activity_info' +require 'temporalio/converters/storage_driver_claim' +require 'temporalio/converters/storage_driver_retrieve_context' +require 'temporalio/converters/storage_driver_select_context' +require 'temporalio/converters/storage_driver_store_context' +require 'temporalio/converters/storage_driver_workflow_info' +require 'temporalio/error' +require 'temporalio/internal/external_storage_references' +require 'test' + +class ExternalStorageTest < Test + References = Temporalio::Internal::ExternalStorageReferences + + def test_reference_payload_round_trips + payload = References.create_reference_payload( + driver_name: 'mem', claim_data: { 'key' => 'k' }, original_size_bytes: 385 + ) + + assert References.reference?(payload) + assert_equal 'json/protobuf', payload.metadata['encoding'] + assert_equal 'temporal.api.sdk.v1.ExternalStorageReference', payload.metadata['messageType'] + assert_equal 385, payload.external_payloads.first.size_bytes + + reference = References.parse_reference(payload) + assert_equal 'mem', reference.driver_name + assert_equal({ 'key' => 'k' }, reference.claim_data.to_h) + end + + def test_ordinary_payload_is_not_a_reference + payload = Temporalio::Api::Common::V1::Payload.new(metadata: { 'encoding' => 'json/plain' }, data: '{}') + + refute References.reference?(payload) + assert_nil References.parse_reference(payload) + end + + def test_reference_from_another_sdk_parses + # Verbatim from the Go SDK's TestClaimDeserialization_OtherSdk_ProtoJSON fixture: compact and differently ordered + # JSON, which is what another SDK actually puts on the wire. + data = Base64.decode64( + 'eyJjbGFpbURhdGEiOnsiYnVja2V0IjoidGVzdC1idWNrZXQiLCJoYXNoX2FsZ29yaXRobSI6InNoYTI1NiIsImhhc2hfdmFsdWUi' \ + 'OiI2Y2EyMmMzNDU2MGNmMzVhYzI0NDI3ZGM3NjE5YzlhYjQ3MmE4MmNmMThmMjg2ZjI3ODcxNjQ5YTJiNTYwOGM4Iiwia2V5Ijoi' \ + 'djAvbnMvZGVmYXVsdC93dC9MYXJnZUlPV29ya2Zsb3cvd2kvZjFkMmE0YWMtZjhjYi00NWQzLTkwOGMtOTNhMGYzM2FiMjQ1L3Jp' \ + 'L251bGwvZC9zaGEyNTYvNmNhMjJjMzQ1NjBjZjM1YWMyNDQyN2RjNzYxOWM5YWI0NzJhODJjZjE4ZjI4NmYyNzg3MTY0OWEyYjU2' \ + 'MDhjOCJ9LCJkcml2ZXJOYW1lIjoiYXdzLnMzZHJpdmVyIn0=' + ) + payload = Temporalio::Api::Common::V1::Payload.new( + metadata: { 'encoding' => 'json/protobuf', 'messageType' => References::MESSAGE_TYPE }, + data: + ) + + reference = References.parse_reference(payload) + + assert_equal 'aws.s3driver', reference.driver_name + assert_equal 'test-bucket', reference.claim_data['bucket'] + assert_equal 4, reference.claim_data.size + end + + def test_single_driver_synthesizes_selector + driver = InMemoryStorageDriver.new + storage = Temporalio::Converters::ExternalStorage.new(drivers: [driver]) + + assert_equal 256 * 1024, storage.payload_size_threshold + assert_same driver, storage.driver_selector.call(select_context, payload('x')) + assert_same driver, storage.driver('mem') + assert_nil storage.driver('missing') + end + + def test_no_drivers_rejected + err = assert_raises(ArgumentError) { Temporalio::Converters::ExternalStorage.new(drivers: []) } + assert_includes err.message, 'At least one driver' + end + + def test_empty_driver_name_rejected + err = assert_raises(ArgumentError) do + Temporalio::Converters::ExternalStorage.new(drivers: [InMemoryStorageDriver.new(name: '')]) + end + assert_includes err.message, 'name cannot be empty' + end + + def test_duplicate_driver_names_rejected + err = assert_raises(ArgumentError) do + Temporalio::Converters::ExternalStorage.new( + drivers: [InMemoryStorageDriver.new(name: 'dup'), InMemoryStorageDriver.new(name: 'dup')], + driver_selector: ->(_context, _payload) {} + ) + end + assert_includes err.message, "name 'dup'" + end + + def test_multiple_drivers_require_selector + err = assert_raises(ArgumentError) do + Temporalio::Converters::ExternalStorage.new( + drivers: [InMemoryStorageDriver.new(name: 'a'), InMemoryStorageDriver.new(name: 'b')] + ) + end + assert_includes err.message, 'driver_selector is required' + end + + def test_negative_threshold_rejected + err = assert_raises(ArgumentError) do + Temporalio::Converters::ExternalStorage.new(drivers: [InMemoryStorageDriver.new], payload_size_threshold: -1) + end + assert_includes err.message, 'cannot be negative' + end + + def test_zero_threshold_allowed + # Zero offloads every payload rather than disabling offload. + storage = Temporalio::Converters::ExternalStorage.new( + drivers: [InMemoryStorageDriver.new], payload_size_threshold: 0 + ) + + assert_equal 0, storage.payload_size_threshold + end + + def test_selector_may_decline + storage = Temporalio::Converters::ExternalStorage.new( + drivers: [InMemoryStorageDriver.new], driver_selector: ->(_context, _payload) {} + ) + + assert_nil storage.driver_selector.call(select_context, payload('x')) + end + + def test_driver_store_and_retrieve_round_trip + driver = InMemoryStorageDriver.new + payloads = [payload('one'), payload('two')] + + claims = driver.store(store_context, payloads) + restored = driver.retrieve(Temporalio::Converters::StorageDriverRetrieveContext.new(cancellation:), claims) + + assert_equal 2, claims.size + assert_equal payloads, restored + end + + def test_claims_with_equal_content_are_equal + first = Temporalio::Converters::StorageDriverClaim.new({ 'a' => '1', 'b' => '2' }) + second = Temporalio::Converters::StorageDriverClaim.new({ 'b' => '2', 'a' => '1' }) + + assert_equal first, second + assert_equal first.hash, second.hash + assert first.eql?(second) + end + + def test_claims_with_different_content_are_not_equal + claim = Temporalio::Converters::StorageDriverClaim.new({ 'key' => 'k' }) + + refute_equal claim, Temporalio::Converters::StorageDriverClaim.new({ 'key' => 'other' }) + refute_equal claim, Temporalio::Converters::StorageDriverClaim.new({ 'another' => 'k' }) + refute_equal claim, Temporalio::Converters::StorageDriverClaim.new({ 'key' => 'k', 'x' => 'y' }) + refute_equal claim, 'not a claim' + end + + def test_target_info_shapes + workflow = Temporalio::Converters::StorageDriverWorkflowInfo.new(namespace: 'ns', id: 'wf', run_id: 'r', + type: 'T') + activity = Temporalio::Converters::StorageDriverActivityInfo.new(namespace: 'ns') + + assert_equal %w[ns wf r T], [workflow.namespace, workflow.id, workflow.run_id, workflow.type] + assert_equal 'ns', activity.namespace + assert_nil activity.id + assert_nil activity.run_id + assert_nil activity.type + end + + def test_not_configured_error_names_its_code + err = Temporalio::Error::ExternalStorageNotConfiguredError.new + + assert_includes err.message, 'TMPRL1105' + end + + private + + def cancellation + Temporalio::Cancellation.new + end + + def select_context + Temporalio::Converters::StorageDriverSelectContext.new(target: nil, cancellation:) + end + + def store_context + Temporalio::Converters::StorageDriverStoreContext.new(target: nil, cancellation:) + end + + def payload(data) + Temporalio::Api::Common::V1::Payload.new(metadata: { 'encoding' => 'json/plain' }, data:) + end +end diff --git a/temporalio/test/in_memory_storage_driver.rb b/temporalio/test/in_memory_storage_driver.rb new file mode 100644 index 00000000..adad309f --- /dev/null +++ b/temporalio/test/in_memory_storage_driver.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'temporalio/api' +require 'temporalio/converters/storage_driver' +require 'temporalio/converters/storage_driver_claim' + +# Storage driver that keeps payloads in a hash, recording what it was asked to do so tests can assert on batching +# and targets. +class InMemoryStorageDriver < Temporalio::Converters::StorageDriver + attr_reader :stored, :store_calls, :retrieve_calls, :name, :type + + def initialize(name: 'mem', type: 'test.memdriver') + super() + @name = name + @type = type + @stored = {} + @store_calls = [] + @retrieve_calls = [] + end + + def store(context, payloads) + @store_calls << [context, payloads.size] + payloads.map do |payload| + key = SecureRandom.uuid + @stored[key] = payload.to_proto + Temporalio::Converters::StorageDriverClaim.new({ 'key' => key }) + end + end + + def retrieve(_context, claims) + @retrieve_calls << claims.size + claims.map do |claim| + bytes = @stored.fetch(claim.claim_data.fetch('key')) + Temporalio::Api::Common::V1::Payload.decode(bytes) + end + end +end diff --git a/temporalio/test/sig/external_storage_test.rbs b/temporalio/test/sig/external_storage_test.rbs new file mode 100644 index 00000000..c6dea28f --- /dev/null +++ b/temporalio/test/sig/external_storage_test.rbs @@ -0,0 +1,6 @@ +class ExternalStorageTest < Test + def cancellation: -> Temporalio::Cancellation + def select_context: -> Temporalio::Converters::StorageDriverSelectContext + def store_context: -> Temporalio::Converters::StorageDriverStoreContext + def payload: (String data) -> Temporalio::Api::Common::V1::Payload +end From 6906c57f5ffbd31710333b844fa1a294d0319338 Mon Sep 17 00:00:00 2001 From: jmaeagle99 <44687433+jmaeagle99@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:18:45 -0700 Subject: [PATCH 02/14] fix: isolate default driver selector from the caller's array --- .../lib/temporalio/converters/external_storage.rb | 2 +- temporalio/test/external_storage_test.rb | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/temporalio/lib/temporalio/converters/external_storage.rb b/temporalio/lib/temporalio/converters/external_storage.rb index c73389e7..0dd65588 100644 --- a/temporalio/lib/temporalio/converters/external_storage.rb +++ b/temporalio/lib/temporalio/converters/external_storage.rb @@ -58,7 +58,7 @@ def initialize(drivers:, driver_selector: nil, payload_size_threshold: DEFAULT_P @drivers = drivers.dup.freeze # Normalized so callers never have to distinguish the single-driver case. - @driver_selector = driver_selector || ->(_context, _payload) { drivers.first } + @driver_selector = driver_selector || ->(_context, _payload) { @drivers.first } @payload_size_threshold = payload_size_threshold end diff --git a/temporalio/test/external_storage_test.rb b/temporalio/test/external_storage_test.rb index d93be5f4..ade41c8a 100644 --- a/temporalio/test/external_storage_test.rb +++ b/temporalio/test/external_storage_test.rb @@ -72,6 +72,19 @@ def test_single_driver_synthesizes_selector assert_nil storage.driver('missing') end + def test_mutating_the_given_driver_array_does_not_affect_storage + driver = InMemoryStorageDriver.new + drivers = [driver] + storage = Temporalio::Converters::ExternalStorage.new(drivers:) + + drivers << InMemoryStorageDriver.new(name: 'added') + drivers.clear + + assert_same driver, storage.drivers.first + assert_same driver, storage.driver_selector.call(select_context, payload('x')) + assert_nil storage.driver('added') + end + def test_no_drivers_rejected err = assert_raises(ArgumentError) { Temporalio::Converters::ExternalStorage.new(drivers: []) } assert_includes err.message, 'At least one driver' From 3165b7bbaf4394f86e66257695dff91534c2d126 Mon Sep 17 00:00:00 2001 From: jmaeagle99 <44687433+jmaeagle99@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:31:17 -0700 Subject: [PATCH 03/14] refactor: define external storage value types with Data --- .../storage_driver_activity_info.rb | 30 ++++++-------- .../converters/storage_driver_claim.rb | 30 +++++--------- .../storage_driver_retrieve_context.rb | 14 ++----- .../storage_driver_select_context.rb | 23 +++-------- .../storage_driver_store_context.rb | 23 +++-------- .../storage_driver_workflow_info.rb | 36 +++++++---------- .../storage_driver_activity_info.rbi | 37 +++++++++++------- .../converters/storage_driver_claim.rbi | 24 ++++++------ .../storage_driver_retrieve_context.rbi | 19 ++++++--- .../storage_driver_select_context.rbi | 39 ++++++++++++------- .../storage_driver_store_context.rbi | 39 ++++++++++++------- .../storage_driver_workflow_info.rbi | 37 +++++++++++------- .../converters/storage_driver_claim.rbs | 6 +-- .../storage_driver_retrieve_context.rbs | 1 + temporalio/test/external_storage_test.rb | 12 +++--- temporalio/test/in_memory_storage_driver.rb | 2 +- 16 files changed, 180 insertions(+), 192 deletions(-) diff --git a/temporalio/lib/temporalio/converters/storage_driver_activity_info.rb b/temporalio/lib/temporalio/converters/storage_driver_activity_info.rb index 79292567..89e8f169 100644 --- a/temporalio/lib/temporalio/converters/storage_driver_activity_info.rb +++ b/temporalio/lib/temporalio/converters/storage_driver_activity_info.rb @@ -2,35 +2,27 @@ module Temporalio module Converters + StorageDriverActivityInfo = Data.define( + :namespace, + :id, + :run_id, + :type + ) + # Identity of a standalone activity a payload is being stored on behalf of. # # @note WARNING: This API is experimental and may change in the future. # # @!visibility private class StorageDriverActivityInfo - # @return [String] Activity namespace. - attr_reader :namespace - - # @return [String, nil] Activity ID, if known. - attr_reader :id - - # @return [String, nil] Run ID, if known. - attr_reader :run_id - - # @return [String, nil] Activity type name, if known. - attr_reader :type - # Create activity target information. # # @param namespace [String] Activity namespace. - # @param id [String, nil] Activity ID. - # @param run_id [String, nil] Run ID. - # @param type [String, nil] Activity type name. + # @param id [String, nil] Activity ID, if known. + # @param run_id [String, nil] Run ID, if known. + # @param type [String, nil] Activity type name, if known. def initialize(namespace:, id: nil, run_id: nil, type: nil) - @namespace = namespace - @id = id - @run_id = run_id - @type = type + super end end end diff --git a/temporalio/lib/temporalio/converters/storage_driver_claim.rb b/temporalio/lib/temporalio/converters/storage_driver_claim.rb index 7abd2fed..a3b5bc23 100644 --- a/temporalio/lib/temporalio/converters/storage_driver_claim.rb +++ b/temporalio/lib/temporalio/converters/storage_driver_claim.rb @@ -2,34 +2,24 @@ module Temporalio module Converters + StorageDriverClaim = Data.define(:claim_data) + # Driver-defined reference to an externally stored payload, used to retrieve it later. # # @note WARNING: This API is experimental and may change in the future. # # @!visibility private class StorageDriverClaim - # @return [Hash] Data the driver needs to retrieve the payload. This is written into history, so - # it must contain everything required for retrieval and must not contain secrets. - attr_reader :claim_data - # Create a claim. # - # @param claim_data [Hash] Data identifying the stored payload. - def initialize(claim_data) - @claim_data = claim_data - end - - # @param other [Object] Value to compare with. - # @return [Boolean] Whether the other value is a claim identifying the same stored payload. - def ==(other) - other.is_a?(StorageDriverClaim) && claim_data == other.claim_data - end - - alias eql? == - - # @return [Integer] Hash derived from the claim data. - def hash - claim_data.hash + # @param claim_data [Hash] Key/value pairs the driver needs to retrieve the payload later. This + # is written into history, so it must contain everything required for retrieval and must not contain secrets. + def initialize(claim_data:) + # Copied and frozen because a claim is compared and hashed by this data, so a later mutation would silently + # change its identity. + # steep:ignore:start + super(claim_data: claim_data.dup.freeze) + # steep:ignore:end end end end diff --git a/temporalio/lib/temporalio/converters/storage_driver_retrieve_context.rb b/temporalio/lib/temporalio/converters/storage_driver_retrieve_context.rb index 858888f7..a3be180a 100644 --- a/temporalio/lib/temporalio/converters/storage_driver_retrieve_context.rb +++ b/temporalio/lib/temporalio/converters/storage_driver_retrieve_context.rb @@ -4,6 +4,8 @@ module Temporalio module Converters + StorageDriverRetrieveContext = Data.define(:cancellation) + # Context given to {StorageDriver#retrieve}. # # This deliberately carries no target. A payload must be retrievable from its {StorageDriverClaim} alone, since the @@ -13,16 +15,6 @@ module Converters # arguments defensively if constructing this yourself. # # @!visibility private - class StorageDriverRetrieveContext - # @return [Cancellation] Cancelled when the SDK abandons this retrieve operation. - attr_reader :cancellation - - # Create a retrieve context. - # - # @param cancellation [Cancellation] Cancellation for the operation. - def initialize(cancellation:) - @cancellation = cancellation - end - end + class StorageDriverRetrieveContext; end # rubocop:disable Lint/EmptyClass end end diff --git a/temporalio/lib/temporalio/converters/storage_driver_select_context.rb b/temporalio/lib/temporalio/converters/storage_driver_select_context.rb index 50626059..afd860b0 100644 --- a/temporalio/lib/temporalio/converters/storage_driver_select_context.rb +++ b/temporalio/lib/temporalio/converters/storage_driver_select_context.rb @@ -4,28 +4,17 @@ module Temporalio module Converters + StorageDriverSelectContext = Data.define( + :target, + :cancellation + ) + # Context given to the driver selector. # # @note WARNING: This API is experimental and may change in the future. Members may be added, so accept keyword # arguments defensively if constructing this yourself. # # @!visibility private - class StorageDriverSelectContext - # @return [StorageDriverWorkflowInfo, StorageDriverActivityInfo, nil] The execution the payload is being stored - # on behalf of, or nil when there is no associated execution. - attr_reader :target - - # @return [Cancellation] Cancelled when the SDK abandons the operation this selection is part of. - attr_reader :cancellation - - # Create a select context. - # - # @param target [StorageDriverWorkflowInfo, StorageDriverActivityInfo, nil] Execution being stored for. - # @param cancellation [Cancellation] Cancellation for the operation. - def initialize(target:, cancellation:) - @target = target - @cancellation = cancellation - end - end + class StorageDriverSelectContext; end # rubocop:disable Lint/EmptyClass end end diff --git a/temporalio/lib/temporalio/converters/storage_driver_store_context.rb b/temporalio/lib/temporalio/converters/storage_driver_store_context.rb index 013646af..7aaa5ed5 100644 --- a/temporalio/lib/temporalio/converters/storage_driver_store_context.rb +++ b/temporalio/lib/temporalio/converters/storage_driver_store_context.rb @@ -4,28 +4,17 @@ module Temporalio module Converters + StorageDriverStoreContext = Data.define( + :target, + :cancellation + ) + # Context given to {StorageDriver#store}. # # @note WARNING: This API is experimental and may change in the future. Members may be added, so accept keyword # arguments defensively if constructing this yourself. # # @!visibility private - class StorageDriverStoreContext - # @return [StorageDriverWorkflowInfo, StorageDriverActivityInfo, nil] The execution the payloads are being stored - # on behalf of, or nil when there is no associated execution. - attr_reader :target - - # @return [Cancellation] Cancelled when the SDK abandons this store operation. - attr_reader :cancellation - - # Create a store context. - # - # @param target [StorageDriverWorkflowInfo, StorageDriverActivityInfo, nil] Execution being stored for. - # @param cancellation [Cancellation] Cancellation for the operation. - def initialize(target:, cancellation:) - @target = target - @cancellation = cancellation - end - end + class StorageDriverStoreContext; end # rubocop:disable Lint/EmptyClass end end diff --git a/temporalio/lib/temporalio/converters/storage_driver_workflow_info.rb b/temporalio/lib/temporalio/converters/storage_driver_workflow_info.rb index 1d63bbe3..ce81b3d6 100644 --- a/temporalio/lib/temporalio/converters/storage_driver_workflow_info.rb +++ b/temporalio/lib/temporalio/converters/storage_driver_workflow_info.rb @@ -2,37 +2,29 @@ module Temporalio module Converters - # Identity of the workflow a payload is being stored on behalf of. Also used for workflow activities, which store - # against their owning workflow rather than themselves. + StorageDriverWorkflowInfo = Data.define( + :namespace, + :id, + :run_id, + :type + ) + + # Identity of the workflow a payload is being stored on behalf of. Also used for workflow activities, which + # store against their owning workflow rather than themselves. # # @note WARNING: This API is experimental and may change in the future. # # @!visibility private class StorageDriverWorkflowInfo - # @return [String] Workflow namespace. - attr_reader :namespace - - # @return [String, nil] Workflow ID, if known. - attr_reader :id - - # @return [String, nil] Run ID, if known. Unset when the run is not yet determined, such as when starting a child - # workflow or continuing as new. - attr_reader :run_id - - # @return [String, nil] Workflow type name, if known. - attr_reader :type - # Create workflow target information. # # @param namespace [String] Workflow namespace. - # @param id [String, nil] Workflow ID. - # @param run_id [String, nil] Run ID. - # @param type [String, nil] Workflow type name. + # @param id [String, nil] Workflow ID, if known. + # @param run_id [String, nil] Run ID, if known. Unset when the run is not yet determined, such as + # when starting a child workflow or continuing as new. + # @param type [String, nil] Workflow type name, if known. def initialize(namespace:, id: nil, run_id: nil, type: nil) - @namespace = namespace - @id = id - @run_id = run_id - @type = type + super end end end diff --git a/temporalio/rbi/temporalio/converters/storage_driver_activity_info.rbi b/temporalio/rbi/temporalio/converters/storage_driver_activity_info.rbi index 9e954ec4..89b1ca48 100644 --- a/temporalio/rbi/temporalio/converters/storage_driver_activity_info.rbi +++ b/temporalio/rbi/temporalio/converters/storage_driver_activity_info.rbi @@ -1,27 +1,36 @@ # typed: true -class Temporalio::Converters::StorageDriverActivityInfo - extend T::Sig +class Temporalio::Converters::StorageDriverActivityInfo < ::Data + sig do + params( + namespace: String, + id: T.nilable(String), + run_id: T.nilable(String), + type: T.nilable(String) + ).void + end + def initialize(namespace:, id: T.unsafe(nil), run_id: T.unsafe(nil), type: T.unsafe(nil)); end sig { returns(String) } - attr_reader :namespace + def namespace; end sig { returns(T.nilable(String)) } - attr_reader :id + def id; end sig { returns(T.nilable(String)) } - attr_reader :run_id + def run_id; end sig { returns(T.nilable(String)) } - attr_reader :type + def type; end - sig do - params( - namespace: String, - id: T.nilable(String), - run_id: T.nilable(String), - type: T.nilable(String) - ).void + class << self + sig { params(args: T.untyped).returns(Temporalio::Converters::StorageDriverActivityInfo) } + def [](*args); end + + sig { returns(T::Array[Symbol]) } + def members; end + + sig { params(args: T.untyped).returns(Temporalio::Converters::StorageDriverActivityInfo) } + def new(*args); end end - def initialize(namespace:, id: T.unsafe(nil), run_id: T.unsafe(nil), type: T.unsafe(nil)); end end diff --git a/temporalio/rbi/temporalio/converters/storage_driver_claim.rbi b/temporalio/rbi/temporalio/converters/storage_driver_claim.rbi index d5542228..1aefbd97 100644 --- a/temporalio/rbi/temporalio/converters/storage_driver_claim.rbi +++ b/temporalio/rbi/temporalio/converters/storage_driver_claim.rbi @@ -1,20 +1,20 @@ # typed: true -class Temporalio::Converters::StorageDriverClaim - extend T::Sig +class Temporalio::Converters::StorageDriverClaim < ::Data + sig { params(claim_data: T::Hash[String, String]).void } + def initialize(claim_data:); end sig { returns(T::Hash[String, String]) } - attr_reader :claim_data - - sig { params(claim_data: T::Hash[String, String]).void } - def initialize(claim_data); end + def claim_data; end - sig { params(other: Object).returns(T::Boolean) } - def ==(other); end + class << self + sig { params(args: T.untyped).returns(Temporalio::Converters::StorageDriverClaim) } + def [](*args); end - sig { params(other: Object).returns(T::Boolean) } - def eql?(other); end + sig { returns(T::Array[Symbol]) } + def members; end - sig { returns(Integer) } - def hash; end + sig { params(args: T.untyped).returns(Temporalio::Converters::StorageDriverClaim) } + def new(*args); end + end end diff --git a/temporalio/rbi/temporalio/converters/storage_driver_retrieve_context.rbi b/temporalio/rbi/temporalio/converters/storage_driver_retrieve_context.rbi index 8041c94f..0dee3622 100644 --- a/temporalio/rbi/temporalio/converters/storage_driver_retrieve_context.rbi +++ b/temporalio/rbi/temporalio/converters/storage_driver_retrieve_context.rbi @@ -1,11 +1,20 @@ # typed: true -class Temporalio::Converters::StorageDriverRetrieveContext - extend T::Sig +class Temporalio::Converters::StorageDriverRetrieveContext < ::Data + sig { params(cancellation: Temporalio::Cancellation).void } + def initialize(cancellation:); end sig { returns(Temporalio::Cancellation) } - attr_reader :cancellation + def cancellation; end - sig { params(cancellation: Temporalio::Cancellation).void } - def initialize(cancellation:); end + class << self + sig { params(args: T.untyped).returns(Temporalio::Converters::StorageDriverRetrieveContext) } + def [](*args); end + + sig { returns(T::Array[Symbol]) } + def members; end + + sig { params(args: T.untyped).returns(Temporalio::Converters::StorageDriverRetrieveContext) } + def new(*args); end + end end diff --git a/temporalio/rbi/temporalio/converters/storage_driver_select_context.rbi b/temporalio/rbi/temporalio/converters/storage_driver_select_context.rbi index 4cfc755b..b6b9ef9f 100644 --- a/temporalio/rbi/temporalio/converters/storage_driver_select_context.rbi +++ b/temporalio/rbi/temporalio/converters/storage_driver_select_context.rbi @@ -1,7 +1,18 @@ # typed: true -class Temporalio::Converters::StorageDriverSelectContext - extend T::Sig +class Temporalio::Converters::StorageDriverSelectContext < ::Data + sig do + params( + target: T.nilable( + T.any( + Temporalio::Converters::StorageDriverWorkflowInfo, + Temporalio::Converters::StorageDriverActivityInfo + ) + ), + cancellation: Temporalio::Cancellation + ).void + end + def initialize(target:, cancellation:); end sig do returns( @@ -13,21 +24,19 @@ class Temporalio::Converters::StorageDriverSelectContext ) ) end - attr_reader :target + def target; end sig { returns(Temporalio::Cancellation) } - attr_reader :cancellation + def cancellation; end - sig do - params( - target: T.nilable( - T.any( - Temporalio::Converters::StorageDriverWorkflowInfo, - Temporalio::Converters::StorageDriverActivityInfo - ) - ), - cancellation: Temporalio::Cancellation - ).void + class << self + sig { params(args: T.untyped).returns(Temporalio::Converters::StorageDriverSelectContext) } + def [](*args); end + + sig { returns(T::Array[Symbol]) } + def members; end + + sig { params(args: T.untyped).returns(Temporalio::Converters::StorageDriverSelectContext) } + def new(*args); end end - def initialize(target:, cancellation:); end end diff --git a/temporalio/rbi/temporalio/converters/storage_driver_store_context.rbi b/temporalio/rbi/temporalio/converters/storage_driver_store_context.rbi index 22085a9f..b4da1088 100644 --- a/temporalio/rbi/temporalio/converters/storage_driver_store_context.rbi +++ b/temporalio/rbi/temporalio/converters/storage_driver_store_context.rbi @@ -1,7 +1,18 @@ # typed: true -class Temporalio::Converters::StorageDriverStoreContext - extend T::Sig +class Temporalio::Converters::StorageDriverStoreContext < ::Data + sig do + params( + target: T.nilable( + T.any( + Temporalio::Converters::StorageDriverWorkflowInfo, + Temporalio::Converters::StorageDriverActivityInfo + ) + ), + cancellation: Temporalio::Cancellation + ).void + end + def initialize(target:, cancellation:); end sig do returns( @@ -13,21 +24,19 @@ class Temporalio::Converters::StorageDriverStoreContext ) ) end - attr_reader :target + def target; end sig { returns(Temporalio::Cancellation) } - attr_reader :cancellation + def cancellation; end - sig do - params( - target: T.nilable( - T.any( - Temporalio::Converters::StorageDriverWorkflowInfo, - Temporalio::Converters::StorageDriverActivityInfo - ) - ), - cancellation: Temporalio::Cancellation - ).void + class << self + sig { params(args: T.untyped).returns(Temporalio::Converters::StorageDriverStoreContext) } + def [](*args); end + + sig { returns(T::Array[Symbol]) } + def members; end + + sig { params(args: T.untyped).returns(Temporalio::Converters::StorageDriverStoreContext) } + def new(*args); end end - def initialize(target:, cancellation:); end end diff --git a/temporalio/rbi/temporalio/converters/storage_driver_workflow_info.rbi b/temporalio/rbi/temporalio/converters/storage_driver_workflow_info.rbi index 226a942b..e8455172 100644 --- a/temporalio/rbi/temporalio/converters/storage_driver_workflow_info.rbi +++ b/temporalio/rbi/temporalio/converters/storage_driver_workflow_info.rbi @@ -1,27 +1,36 @@ # typed: true -class Temporalio::Converters::StorageDriverWorkflowInfo - extend T::Sig +class Temporalio::Converters::StorageDriverWorkflowInfo < ::Data + sig do + params( + namespace: String, + id: T.nilable(String), + run_id: T.nilable(String), + type: T.nilable(String) + ).void + end + def initialize(namespace:, id: T.unsafe(nil), run_id: T.unsafe(nil), type: T.unsafe(nil)); end sig { returns(String) } - attr_reader :namespace + def namespace; end sig { returns(T.nilable(String)) } - attr_reader :id + def id; end sig { returns(T.nilable(String)) } - attr_reader :run_id + def run_id; end sig { returns(T.nilable(String)) } - attr_reader :type + def type; end - sig do - params( - namespace: String, - id: T.nilable(String), - run_id: T.nilable(String), - type: T.nilable(String) - ).void + class << self + sig { params(args: T.untyped).returns(Temporalio::Converters::StorageDriverWorkflowInfo) } + def [](*args); end + + sig { returns(T::Array[Symbol]) } + def members; end + + sig { params(args: T.untyped).returns(Temporalio::Converters::StorageDriverWorkflowInfo) } + def new(*args); end end - def initialize(namespace:, id: T.unsafe(nil), run_id: T.unsafe(nil), type: T.unsafe(nil)); end end diff --git a/temporalio/sig/temporalio/converters/storage_driver_claim.rbs b/temporalio/sig/temporalio/converters/storage_driver_claim.rbs index 60f57f85..5b9deb70 100644 --- a/temporalio/sig/temporalio/converters/storage_driver_claim.rbs +++ b/temporalio/sig/temporalio/converters/storage_driver_claim.rbs @@ -2,10 +2,8 @@ module Temporalio module Converters class StorageDriverClaim attr_reader claim_data: Hash[String, String] - def initialize: (Hash[String, String] claim_data) -> void - def ==: (untyped other) -> bool - def eql?: (untyped other) -> bool - def hash: -> Integer + + def initialize: (claim_data: Hash[String, String]) -> void end end end diff --git a/temporalio/sig/temporalio/converters/storage_driver_retrieve_context.rbs b/temporalio/sig/temporalio/converters/storage_driver_retrieve_context.rbs index 4169dbb8..90b67f6c 100644 --- a/temporalio/sig/temporalio/converters/storage_driver_retrieve_context.rbs +++ b/temporalio/sig/temporalio/converters/storage_driver_retrieve_context.rbs @@ -2,6 +2,7 @@ module Temporalio module Converters class StorageDriverRetrieveContext attr_reader cancellation: Cancellation + def initialize: (cancellation: Cancellation) -> void end end diff --git a/temporalio/test/external_storage_test.rb b/temporalio/test/external_storage_test.rb index ade41c8a..f488afa0 100644 --- a/temporalio/test/external_storage_test.rb +++ b/temporalio/test/external_storage_test.rb @@ -152,8 +152,8 @@ def test_driver_store_and_retrieve_round_trip end def test_claims_with_equal_content_are_equal - first = Temporalio::Converters::StorageDriverClaim.new({ 'a' => '1', 'b' => '2' }) - second = Temporalio::Converters::StorageDriverClaim.new({ 'b' => '2', 'a' => '1' }) + first = Temporalio::Converters::StorageDriverClaim.new(claim_data: { 'a' => '1', 'b' => '2' }) + second = Temporalio::Converters::StorageDriverClaim.new(claim_data: { 'b' => '2', 'a' => '1' }) assert_equal first, second assert_equal first.hash, second.hash @@ -161,11 +161,11 @@ def test_claims_with_equal_content_are_equal end def test_claims_with_different_content_are_not_equal - claim = Temporalio::Converters::StorageDriverClaim.new({ 'key' => 'k' }) + claim = Temporalio::Converters::StorageDriverClaim.new(claim_data: { 'key' => 'k' }) - refute_equal claim, Temporalio::Converters::StorageDriverClaim.new({ 'key' => 'other' }) - refute_equal claim, Temporalio::Converters::StorageDriverClaim.new({ 'another' => 'k' }) - refute_equal claim, Temporalio::Converters::StorageDriverClaim.new({ 'key' => 'k', 'x' => 'y' }) + refute_equal claim, Temporalio::Converters::StorageDriverClaim.new(claim_data: { 'key' => 'other' }) + refute_equal claim, Temporalio::Converters::StorageDriverClaim.new(claim_data: { 'another' => 'k' }) + refute_equal claim, Temporalio::Converters::StorageDriverClaim.new(claim_data: { 'key' => 'k', 'x' => 'y' }) refute_equal claim, 'not a claim' end diff --git a/temporalio/test/in_memory_storage_driver.rb b/temporalio/test/in_memory_storage_driver.rb index adad309f..66ed2254 100644 --- a/temporalio/test/in_memory_storage_driver.rb +++ b/temporalio/test/in_memory_storage_driver.rb @@ -23,7 +23,7 @@ def store(context, payloads) payloads.map do |payload| key = SecureRandom.uuid @stored[key] = payload.to_proto - Temporalio::Converters::StorageDriverClaim.new({ 'key' => key }) + Temporalio::Converters::StorageDriverClaim.new(claim_data: { 'key' => key }) end end From 4aee16ccb7b4b69ba99b8af966c9303620580c82 Mon Sep 17 00:00:00 2001 From: jmaeagle99 <44687433+jmaeagle99@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:33:32 -0700 Subject: [PATCH 04/14] test: cover unknown fields in external storage references --- temporalio/test/external_storage_test.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/temporalio/test/external_storage_test.rb b/temporalio/test/external_storage_test.rb index f488afa0..cee5857e 100644 --- a/temporalio/test/external_storage_test.rb +++ b/temporalio/test/external_storage_test.rb @@ -40,6 +40,19 @@ def test_ordinary_payload_is_not_a_reference assert_nil References.parse_reference(payload) end + def test_reference_with_unknown_field_parses + # A newer SDK may add fields to the reference before this one knows about them. + payload = Temporalio::Api::Common::V1::Payload.new( + metadata: { 'encoding' => References::ENCODING, 'messageType' => References::MESSAGE_TYPE }, + data: '{"driverName":"mem","claimData":{"k":"v"},"fieldFromANewerSdk":42}' + ) + + reference = References.parse_reference(payload) + + assert_equal 'mem', reference.driver_name + assert_equal({ 'k' => 'v' }, reference.claim_data.to_h) + end + def test_reference_from_another_sdk_parses # Verbatim from the Go SDK's TestClaimDeserialization_OtherSdk_ProtoJSON fixture: compact and differently ordered # JSON, which is what another SDK actually puts on the wire. From 565f99567ab326ae6eed63bf653239d52105ed1b Mon Sep 17 00:00:00 2001 From: jmaeagle99 <44687433+jmaeagle99@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:37:08 -0700 Subject: [PATCH 05/14] chore: add todo for parse failures --- .../lib/temporalio/internal/external_storage_references.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/temporalio/lib/temporalio/internal/external_storage_references.rb b/temporalio/lib/temporalio/internal/external_storage_references.rb index 13528a4b..15502b22 100644 --- a/temporalio/lib/temporalio/internal/external_storage_references.rb +++ b/temporalio/lib/temporalio/internal/external_storage_references.rb @@ -24,6 +24,10 @@ def reference?(payload) # @param payload [Api::Common::V1::Payload] Payload to parse. # @return [Api::Sdk::V1::ExternalStorageReference, nil] Parsed reference, or nil if not a reference. + # + # TODO: Surface parse failures as a Temporal error instead of Google::Protobuf::ParseError. The runner is the + # right layer for it, since only the caller knows which payload failed; a malformed reference cannot name its + # own driver. def parse_reference(payload) return nil unless reference?(payload) From 24a6988feecef7dfe286679c92211d49cb1f1b79 Mon Sep 17 00:00:00 2001 From: jmaeagle99 <44687433+jmaeagle99@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:43:42 -0700 Subject: [PATCH 06/14] fix: require driver names to be non-empty Strings --- .../lib/temporalio/converters/external_storage.rb | 10 ++++++---- temporalio/test/external_storage_test.rb | 10 +++++++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/temporalio/lib/temporalio/converters/external_storage.rb b/temporalio/lib/temporalio/converters/external_storage.rb index 0dd65588..fa84b52a 100644 --- a/temporalio/lib/temporalio/converters/external_storage.rb +++ b/temporalio/lib/temporalio/converters/external_storage.rb @@ -48,11 +48,13 @@ def initialize(drivers:, driver_selector: nil, payload_size_threshold: DEFAULT_P @drivers_by_name = {} drivers.each do |driver| - # The name is the routing key written into history, so a driver without one can never be resolved on read. - raise ArgumentError, 'Driver name cannot be empty' if driver.name.nil? || driver.name.empty? - raise ArgumentError, "Multiple drivers given with name '#{driver.name}'" if @drivers_by_name.key?(driver.name) + name = driver.name + # The name is the routing key written into history and is written to a string proto field, so anything but + # a non-empty String cannot be resolved on read. A Symbol in particular passes a bare empty? check. + raise ArgumentError, 'Driver name must be a non-empty String' unless name.is_a?(String) && !name.empty? + raise ArgumentError, "Multiple drivers given with name '#{name}'" if @drivers_by_name.key?(name) - @drivers_by_name[driver.name] = driver + @drivers_by_name[name] = driver end @drivers_by_name.freeze diff --git a/temporalio/test/external_storage_test.rb b/temporalio/test/external_storage_test.rb index cee5857e..da7a6381 100644 --- a/temporalio/test/external_storage_test.rb +++ b/temporalio/test/external_storage_test.rb @@ -107,7 +107,15 @@ def test_empty_driver_name_rejected err = assert_raises(ArgumentError) do Temporalio::Converters::ExternalStorage.new(drivers: [InMemoryStorageDriver.new(name: '')]) end - assert_includes err.message, 'name cannot be empty' + assert_includes err.message, 'must be a non-empty String' + end + + def test_symbol_driver_name_rejected + # A Symbol passes a bare empty? check but cannot be looked up by String or written to the proto. + err = assert_raises(ArgumentError) do + Temporalio::Converters::ExternalStorage.new(drivers: [InMemoryStorageDriver.new(name: :mem)]) + end + assert_includes err.message, 'must be a non-empty String' end def test_duplicate_driver_names_rejected From e288697efb4495f23610545538b8479dcdd2fc36 Mon Sep 17 00:00:00 2001 From: jmaeagle99 <44687433+jmaeagle99@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:54:45 -0700 Subject: [PATCH 07/14] chore: misc fixes and doc updates --- .../temporalio/converters/external_storage.rb | 7 +++++-- .../temporalio/converters/storage_driver.rb | 6 +++--- .../temporalio/converters/storage_driver.rbs | 2 -- .../converters/storage_driver_target_info.rbs | 5 +++++ temporalio/test/external_storage_test.rb | 20 ++++++++++++++++++- 5 files changed, 32 insertions(+), 8 deletions(-) create mode 100644 temporalio/sig/temporalio/converters/storage_driver_target_info.rbs diff --git a/temporalio/lib/temporalio/converters/external_storage.rb b/temporalio/lib/temporalio/converters/external_storage.rb index fa84b52a..861b223e 100644 --- a/temporalio/lib/temporalio/converters/external_storage.rb +++ b/temporalio/lib/temporalio/converters/external_storage.rb @@ -36,11 +36,14 @@ class ExternalStorage # @param driver_selector [Proc, nil] Selector choosing which driver stores each payload. Required when more than # one driver is given; with a single driver it may be nil, in which case that driver stores every eligible # payload. - # @param payload_size_threshold [Integer] Minimum encoded payload size, in bytes, to offload. + # @param payload_size_threshold [Integer] Minimum encoded payload size, in bytes, to offload. Must be a + # non-negative Integer. def initialize(drivers:, driver_selector: nil, payload_size_threshold: DEFAULT_PAYLOAD_SIZE_THRESHOLD) raise ArgumentError, 'At least one driver is required' if drivers.empty? - raise ArgumentError, 'payload_size_threshold cannot be negative' if payload_size_threshold.negative? + unless payload_size_threshold.is_a?(Integer) && payload_size_threshold >= 0 + raise ArgumentError, 'payload_size_threshold must be a non-negative Integer' + end if drivers.size > 1 && driver_selector.nil? raise ArgumentError, 'driver_selector is required when more than one driver is given' diff --git a/temporalio/lib/temporalio/converters/storage_driver.rb b/temporalio/lib/temporalio/converters/storage_driver.rb index 922c5cc1..0193831f 100644 --- a/temporalio/lib/temporalio/converters/storage_driver.rb +++ b/temporalio/lib/temporalio/converters/storage_driver.rb @@ -8,8 +8,8 @@ module Converters # reference, so the payload data itself never reaches the Temporal server. # # Implementations are called concurrently and must be thread safe. Methods block, and may perform I/O directly. - # To work a batch concurrently, fan out with threads: Ruby releases its global lock during blocking I/O, so threads - # give real parallelism, and they work whether or not the worker is running under a fiber scheduler. + # To work a batch concurrently, fan out with threads: threads overlap blocking I/O, since Ruby releases its global + # lock around it, and they work whether or not the worker is running under a fiber scheduler. # {::Fiber.schedule} must not be used, since it raises when the worker is configured with a payload codec thread # pool instead of a fiber scheduler. {Temporalio::Worker::ThreadPool} can be used to bound that fan out. # @@ -26,7 +26,7 @@ def name raise NotImplementedError end - # Identifier for this driver implementation, for example +aws.s3driver+. Unlike {name}, this is identical across + # Identifier for this driver implementation, for example +aws.s3driver+. Unlike {#name}, this is identical across # every instance of the same implementation and across SDK languages. # # @return [String] Driver implementation identifier. diff --git a/temporalio/sig/temporalio/converters/storage_driver.rbs b/temporalio/sig/temporalio/converters/storage_driver.rbs index c35a0b14..79c5594e 100644 --- a/temporalio/sig/temporalio/converters/storage_driver.rbs +++ b/temporalio/sig/temporalio/converters/storage_driver.rbs @@ -1,7 +1,5 @@ module Temporalio module Converters - type storageDriverTargetInfo = StorageDriverWorkflowInfo | StorageDriverActivityInfo - class StorageDriver def name: -> String def type: -> String diff --git a/temporalio/sig/temporalio/converters/storage_driver_target_info.rbs b/temporalio/sig/temporalio/converters/storage_driver_target_info.rbs new file mode 100644 index 00000000..f0a08633 --- /dev/null +++ b/temporalio/sig/temporalio/converters/storage_driver_target_info.rbs @@ -0,0 +1,5 @@ +module Temporalio + module Converters + type storageDriverTargetInfo = StorageDriverWorkflowInfo | StorageDriverActivityInfo + end +end diff --git a/temporalio/test/external_storage_test.rb b/temporalio/test/external_storage_test.rb index da7a6381..0dc4280e 100644 --- a/temporalio/test/external_storage_test.rb +++ b/temporalio/test/external_storage_test.rb @@ -98,6 +98,13 @@ def test_mutating_the_given_driver_array_does_not_affect_storage assert_nil storage.driver('added') end + def test_drivers_is_frozen + storage = Temporalio::Converters::ExternalStorage.new(drivers: [InMemoryStorageDriver.new]) + + assert_predicate storage.drivers, :frozen? + assert_raises(FrozenError) { storage.drivers << InMemoryStorageDriver.new(name: 'added') } + end + def test_no_drivers_rejected err = assert_raises(ArgumentError) { Temporalio::Converters::ExternalStorage.new(drivers: []) } assert_includes err.message, 'At least one driver' @@ -141,7 +148,18 @@ def test_negative_threshold_rejected err = assert_raises(ArgumentError) do Temporalio::Converters::ExternalStorage.new(drivers: [InMemoryStorageDriver.new], payload_size_threshold: -1) end - assert_includes err.message, 'cannot be negative' + assert_includes err.message, 'non-negative Integer' + end + + def test_non_integer_threshold_rejected + [nil, '256', 256.0].each do |value| + err = assert_raises(ArgumentError) do + Temporalio::Converters::ExternalStorage.new( + drivers: [InMemoryStorageDriver.new], payload_size_threshold: value # steep:ignore + ) + end + assert_includes err.message, 'non-negative Integer' + end end def test_zero_threshold_allowed From e9972d0996c364b9e18e36d05e1db703c6171fe0 Mon Sep 17 00:00:00 2001 From: jmaeagle99 <44687433+jmaeagle99@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:55:15 -0700 Subject: [PATCH 08/14] fix: add mutex to test driver --- temporalio/test/in_memory_storage_driver.rb | 27 +++++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/temporalio/test/in_memory_storage_driver.rb b/temporalio/test/in_memory_storage_driver.rb index 66ed2254..1aacef35 100644 --- a/temporalio/test/in_memory_storage_driver.rb +++ b/temporalio/test/in_memory_storage_driver.rb @@ -1,36 +1,53 @@ # frozen_string_literal: true +require 'securerandom' require 'temporalio/api' require 'temporalio/converters/storage_driver' require 'temporalio/converters/storage_driver_claim' # Storage driver that keeps payloads in a hash, recording what it was asked to do so tests can assert on batching # and targets. +# +# One instance is shared across whatever concurrency a test drives it with, so all of its state is guarded and the +# readers hand back snapshots rather than the live collections. class InMemoryStorageDriver < Temporalio::Converters::StorageDriver - attr_reader :stored, :store_calls, :retrieve_calls, :name, :type + attr_reader :name, :type def initialize(name: 'mem', type: 'test.memdriver') super() @name = name @type = type + @mutex = Mutex.new @stored = {} @store_calls = [] @retrieve_calls = [] end + def stored + @mutex.synchronize { @stored.dup } + end + + def store_calls + @mutex.synchronize { @store_calls.dup } + end + + def retrieve_calls + @mutex.synchronize { @retrieve_calls.dup } + end + def store(context, payloads) - @store_calls << [context, payloads.size] + @mutex.synchronize { @store_calls << [context, payloads.size] } payloads.map do |payload| key = SecureRandom.uuid - @stored[key] = payload.to_proto + @mutex.synchronize { @stored[key] = payload.to_proto } Temporalio::Converters::StorageDriverClaim.new(claim_data: { 'key' => key }) end end def retrieve(_context, claims) - @retrieve_calls << claims.size + @mutex.synchronize { @retrieve_calls << claims.size } claims.map do |claim| - bytes = @stored.fetch(claim.claim_data.fetch('key')) + bytes = @mutex.synchronize { @stored.fetch(claim.claim_data.fetch('key')) } Temporalio::Api::Common::V1::Payload.decode(bytes) end end From f89909d3a4a2924e771c529b09f21d5a391fea6d Mon Sep 17 00:00:00 2001 From: jmaeagle99 <44687433+jmaeagle99@users.noreply.github.com> Date: Wed, 7 Oct 2026 11:14:52 -0700 Subject: [PATCH 09/14] fix: restore member docs lost in the Data conversion --- .../converters/storage_driver_activity_info.rb | 15 ++++++++++++--- .../converters/storage_driver_claim.rb | 7 +++++-- .../storage_driver_retrieve_context.rb | 3 +++ .../converters/storage_driver_select_context.rb | 6 ++++++ .../converters/storage_driver_store_context.rb | 6 ++++++ .../converters/storage_driver_workflow_info.rb | 17 +++++++++++++---- 6 files changed, 45 insertions(+), 9 deletions(-) diff --git a/temporalio/lib/temporalio/converters/storage_driver_activity_info.rb b/temporalio/lib/temporalio/converters/storage_driver_activity_info.rb index 89e8f169..b16456cf 100644 --- a/temporalio/lib/temporalio/converters/storage_driver_activity_info.rb +++ b/temporalio/lib/temporalio/converters/storage_driver_activity_info.rb @@ -13,14 +13,23 @@ module Converters # # @note WARNING: This API is experimental and may change in the future. # + # @!attribute [r] namespace + # @return [String] Activity namespace. + # @!attribute [r] id + # @return [String, nil] Activity ID, if known. + # @!attribute [r] run_id + # @return [String, nil] Run ID, if known. + # @!attribute [r] type + # @return [String, nil] Activity type name, if known. + # # @!visibility private class StorageDriverActivityInfo # Create activity target information. # # @param namespace [String] Activity namespace. - # @param id [String, nil] Activity ID, if known. - # @param run_id [String, nil] Run ID, if known. - # @param type [String, nil] Activity type name, if known. + # @param id [String, nil] Activity ID. + # @param run_id [String, nil] Run ID. + # @param type [String, nil] Activity type name. def initialize(namespace:, id: nil, run_id: nil, type: nil) super end diff --git a/temporalio/lib/temporalio/converters/storage_driver_claim.rb b/temporalio/lib/temporalio/converters/storage_driver_claim.rb index a3b5bc23..c5c1f005 100644 --- a/temporalio/lib/temporalio/converters/storage_driver_claim.rb +++ b/temporalio/lib/temporalio/converters/storage_driver_claim.rb @@ -8,12 +8,15 @@ module Converters # # @note WARNING: This API is experimental and may change in the future. # + # @!attribute [r] claim_data + # @return [Hash] Data the driver needs to retrieve the payload. This is written into history, so + # it must contain everything required for retrieval and must not contain secrets. + # # @!visibility private class StorageDriverClaim # Create a claim. # - # @param claim_data [Hash] Key/value pairs the driver needs to retrieve the payload later. This - # is written into history, so it must contain everything required for retrieval and must not contain secrets. + # @param claim_data [Hash] Data identifying the stored payload. def initialize(claim_data:) # Copied and frozen because a claim is compared and hashed by this data, so a later mutation would silently # change its identity. diff --git a/temporalio/lib/temporalio/converters/storage_driver_retrieve_context.rb b/temporalio/lib/temporalio/converters/storage_driver_retrieve_context.rb index a3be180a..85907382 100644 --- a/temporalio/lib/temporalio/converters/storage_driver_retrieve_context.rb +++ b/temporalio/lib/temporalio/converters/storage_driver_retrieve_context.rb @@ -14,6 +14,9 @@ module Converters # @note WARNING: This API is experimental and may change in the future. Members may be added, so accept keyword # arguments defensively if constructing this yourself. # + # @!attribute [r] cancellation + # @return [Cancellation] Cancelled when the SDK abandons this retrieve operation. + # # @!visibility private class StorageDriverRetrieveContext; end # rubocop:disable Lint/EmptyClass end diff --git a/temporalio/lib/temporalio/converters/storage_driver_select_context.rb b/temporalio/lib/temporalio/converters/storage_driver_select_context.rb index afd860b0..a5ef5be7 100644 --- a/temporalio/lib/temporalio/converters/storage_driver_select_context.rb +++ b/temporalio/lib/temporalio/converters/storage_driver_select_context.rb @@ -14,6 +14,12 @@ module Converters # @note WARNING: This API is experimental and may change in the future. Members may be added, so accept keyword # arguments defensively if constructing this yourself. # + # @!attribute [r] target + # @return [StorageDriverWorkflowInfo, StorageDriverActivityInfo, nil] The execution the payload is being stored + # on behalf of, or nil when there is no associated execution. + # @!attribute [r] cancellation + # @return [Cancellation] Cancelled when the SDK abandons the operation this selection is part of. + # # @!visibility private class StorageDriverSelectContext; end # rubocop:disable Lint/EmptyClass end diff --git a/temporalio/lib/temporalio/converters/storage_driver_store_context.rb b/temporalio/lib/temporalio/converters/storage_driver_store_context.rb index 7aaa5ed5..e50f3f76 100644 --- a/temporalio/lib/temporalio/converters/storage_driver_store_context.rb +++ b/temporalio/lib/temporalio/converters/storage_driver_store_context.rb @@ -14,6 +14,12 @@ module Converters # @note WARNING: This API is experimental and may change in the future. Members may be added, so accept keyword # arguments defensively if constructing this yourself. # + # @!attribute [r] target + # @return [StorageDriverWorkflowInfo, StorageDriverActivityInfo, nil] The execution the payloads are being stored + # on behalf of, or nil when there is no associated execution. + # @!attribute [r] cancellation + # @return [Cancellation] Cancelled when the SDK abandons this store operation. + # # @!visibility private class StorageDriverStoreContext; end # rubocop:disable Lint/EmptyClass end diff --git a/temporalio/lib/temporalio/converters/storage_driver_workflow_info.rb b/temporalio/lib/temporalio/converters/storage_driver_workflow_info.rb index ce81b3d6..6a8c8381 100644 --- a/temporalio/lib/temporalio/converters/storage_driver_workflow_info.rb +++ b/temporalio/lib/temporalio/converters/storage_driver_workflow_info.rb @@ -14,15 +14,24 @@ module Converters # # @note WARNING: This API is experimental and may change in the future. # + # @!attribute [r] namespace + # @return [String] Workflow namespace. + # @!attribute [r] id + # @return [String, nil] Workflow ID, if known. + # @!attribute [r] run_id + # @return [String, nil] Run ID, if known. Unset when the run is not yet determined, such as when starting a child + # workflow or continuing as new. + # @!attribute [r] type + # @return [String, nil] Workflow type name, if known. + # # @!visibility private class StorageDriverWorkflowInfo # Create workflow target information. # # @param namespace [String] Workflow namespace. - # @param id [String, nil] Workflow ID, if known. - # @param run_id [String, nil] Run ID, if known. Unset when the run is not yet determined, such as - # when starting a child workflow or continuing as new. - # @param type [String, nil] Workflow type name, if known. + # @param id [String, nil] Workflow ID. + # @param run_id [String, nil] Run ID. + # @param type [String, nil] Workflow type name. def initialize(namespace:, id: nil, run_id: nil, type: nil) super end From 21f40c402c927954c8105d530be05302f29ec938 Mon Sep 17 00:00:00 2001 From: jmaeagle99 <44687433+jmaeagle99@users.noreply.github.com> Date: Wed, 7 Oct 2026 11:17:10 -0700 Subject: [PATCH 10/14] fix: copy claim data values so the freeze is not shallow --- .../temporalio/converters/storage_driver_claim.rb | 5 +++-- temporalio/test/external_storage_test.rb | 13 +++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/temporalio/lib/temporalio/converters/storage_driver_claim.rb b/temporalio/lib/temporalio/converters/storage_driver_claim.rb index c5c1f005..a6eee0b3 100644 --- a/temporalio/lib/temporalio/converters/storage_driver_claim.rb +++ b/temporalio/lib/temporalio/converters/storage_driver_claim.rb @@ -19,9 +19,10 @@ class StorageDriverClaim # @param claim_data [Hash] Data identifying the stored payload. def initialize(claim_data:) # Copied and frozen because a claim is compared and hashed by this data, so a later mutation would silently - # change its identity. + # change its identity. The values are copied individually because freezing the Hash leaves them mutable in + # place and still aliased to the caller's objects. Ruby already copies and freezes String keys on insert. # steep:ignore:start - super(claim_data: claim_data.dup.freeze) + super(claim_data: claim_data.transform_values { |value| value.dup.freeze }.freeze) # steep:ignore:end end end diff --git a/temporalio/test/external_storage_test.rb b/temporalio/test/external_storage_test.rb index 0dc4280e..dca1f94c 100644 --- a/temporalio/test/external_storage_test.rb +++ b/temporalio/test/external_storage_test.rb @@ -199,6 +199,19 @@ def test_claims_with_equal_content_are_equal assert first.eql?(second) end + def test_mutating_the_given_claim_data_does_not_affect_the_claim + value = +'my-bucket' + claim = Temporalio::Converters::StorageDriverClaim.new(claim_data: { 'bucket' => value }) + hash_before = claim.hash + + value << '-tampered' + + assert_equal 'my-bucket', claim.claim_data.fetch('bucket') + assert_equal hash_before, claim.hash + assert_predicate claim.claim_data, :frozen? + assert_raises(FrozenError) { claim.claim_data.fetch('bucket') << 'x' } + end + def test_claims_with_different_content_are_not_equal claim = Temporalio::Converters::StorageDriverClaim.new(claim_data: { 'key' => 'k' }) From c82b64e7f8c26607a9f204ea2973be81c5c27994 Mon Sep 17 00:00:00 2001 From: jmaeagle99 <44687433+jmaeagle99@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:35:19 -0700 Subject: [PATCH 11/14] fix: suppress runtime sig errors in the wrong-type threshold test --- temporalio/test/external_storage_test.rb | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/temporalio/test/external_storage_test.rb b/temporalio/test/external_storage_test.rb index dca1f94c..19397a4b 100644 --- a/temporalio/test/external_storage_test.rb +++ b/temporalio/test/external_storage_test.rb @@ -152,13 +152,21 @@ def test_negative_threshold_rejected end def test_non_integer_threshold_rejected - [nil, '256', 256.0].each do |value| - err = assert_raises(ArgumentError) do - Temporalio::Converters::ExternalStorage.new( - drivers: [InMemoryStorageDriver.new], payload_size_threshold: value # steep:ignore - ) + block = proc do + [nil, '256', 256.0].each do |value| + err = assert_raises(ArgumentError) do + Temporalio::Converters::ExternalStorage.new( + drivers: [InMemoryStorageDriver.new], payload_size_threshold: value # steep:ignore + ) + end + assert_includes err.message, 'non-negative Integer' end - assert_includes err.message, 'non-negative Integer' + end + # Passing the wrong type is the point of this test, so the runtime signature check must not record it. + if defined?(SigApplicator) + SigApplicator.suppress_errors { block.call } + else + block.call end end From 640764ef62c0d39406dc01ee61039512e3f5af6c Mon Sep 17 00:00:00 2001 From: Justin Anderson <44687433+jmaeagle99@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:16:16 -0700 Subject: [PATCH 12/14] Update temporalio/lib/temporalio/converters/storage_driver_claim.rb Co-authored-by: Gregory Michael Travis --- temporalio/lib/temporalio/converters/storage_driver_claim.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/temporalio/lib/temporalio/converters/storage_driver_claim.rb b/temporalio/lib/temporalio/converters/storage_driver_claim.rb index a6eee0b3..bdc85bad 100644 --- a/temporalio/lib/temporalio/converters/storage_driver_claim.rb +++ b/temporalio/lib/temporalio/converters/storage_driver_claim.rb @@ -20,7 +20,7 @@ class StorageDriverClaim def initialize(claim_data:) # Copied and frozen because a claim is compared and hashed by this data, so a later mutation would silently # change its identity. The values are copied individually because freezing the Hash leaves them mutable in - # place and still aliased to the caller's objects. Ruby already copies and freezes String keys on insert. + # place and still aliased to the caller's objects. # steep:ignore:start super(claim_data: claim_data.transform_values { |value| value.dup.freeze }.freeze) # steep:ignore:end From d29cb9109a8835d7c9c3cae41436e842d0413308 Mon Sep 17 00:00:00 2001 From: Justin Anderson <44687433+jmaeagle99@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:16:26 -0700 Subject: [PATCH 13/14] Update temporalio/lib/temporalio/converters/storage_driver_retrieve_context.rb Co-authored-by: Gregory Michael Travis --- .../temporalio/converters/storage_driver_retrieve_context.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/temporalio/lib/temporalio/converters/storage_driver_retrieve_context.rb b/temporalio/lib/temporalio/converters/storage_driver_retrieve_context.rb index 85907382..b6bbbb0e 100644 --- a/temporalio/lib/temporalio/converters/storage_driver_retrieve_context.rb +++ b/temporalio/lib/temporalio/converters/storage_driver_retrieve_context.rb @@ -8,7 +8,7 @@ module Converters # Context given to {StorageDriver#retrieve}. # - # This deliberately carries no target. A payload must be retrievable from its {StorageDriverClaim} alone, since the + # This deliberately carries no execution target. A payload must be retrievable from its {StorageDriverClaim} alone, since the # same payload can be read from a different execution than the one that stored it. # # @note WARNING: This API is experimental and may change in the future. Members may be added, so accept keyword From f056bf1c97a444b525512185ac10c227173140dc Mon Sep 17 00:00:00 2001 From: jmaeagle99 <44687433+jmaeagle99@users.noreply.github.com> Date: Fri, 9 Oct 2026 12:02:45 -0700 Subject: [PATCH 14/14] fix: reformat comment --- .../temporalio/converters/storage_driver_retrieve_context.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/temporalio/lib/temporalio/converters/storage_driver_retrieve_context.rb b/temporalio/lib/temporalio/converters/storage_driver_retrieve_context.rb index b6bbbb0e..412384eb 100644 --- a/temporalio/lib/temporalio/converters/storage_driver_retrieve_context.rb +++ b/temporalio/lib/temporalio/converters/storage_driver_retrieve_context.rb @@ -8,8 +8,8 @@ module Converters # Context given to {StorageDriver#retrieve}. # - # This deliberately carries no execution target. A payload must be retrievable from its {StorageDriverClaim} alone, since the - # same payload can be read from a different execution than the one that stored it. + # This deliberately carries no execution target. A payload must be retrievable from its {StorageDriverClaim} + # alone, since the same payload can be read from a different execution than the one that stored it. # # @note WARNING: This API is experimental and may change in the future. Members may be added, so accept keyword # arguments defensively if constructing this yourself.