From 9d88b8758f7beb1565dfd0e993cd3acd35cd6527 Mon Sep 17 00:00:00 2001 From: DeRauk Gibble Date: Tue, 5 Oct 2021 12:34:00 -0400 Subject: [PATCH 001/125] Pass config to the error handler instead of using the global config --- lib/temporal/activity/poller.rb | 2 +- lib/temporal/activity/task_processor.rb | 6 +++--- lib/temporal/error_handler.rb | 4 ++-- lib/temporal/workflow.rb | 2 +- lib/temporal/workflow/poller.rb | 2 +- lib/temporal/workflow/task_processor.rb | 4 ++-- spec/unit/lib/temporal/activity/task_processor_spec.rb | 6 +++--- spec/unit/lib/temporal/workflow/task_processor_spec.rb | 4 ++-- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/temporal/activity/poller.rb b/lib/temporal/activity/poller.rb index c07eba7e..4157f0f3 100644 --- a/lib/temporal/activity/poller.rb +++ b/lib/temporal/activity/poller.rb @@ -78,7 +78,7 @@ def poll_for_task rescue StandardError => error Temporal.logger.error("Unable to poll activity task queue", { namespace: namespace, task_queue: task_queue, error: error.inspect }) - Temporal::ErrorHandler.handle(error) + Temporal::ErrorHandler.handle(error, config) nil end diff --git a/lib/temporal/activity/task_processor.rb b/lib/temporal/activity/task_processor.rb index 173a2bc9..f676cf21 100644 --- a/lib/temporal/activity/task_processor.rb +++ b/lib/temporal/activity/task_processor.rb @@ -41,7 +41,7 @@ def process # Do not complete asynchronous activities, these should be completed manually respond_completed(result) unless context.async? rescue StandardError, ScriptError => error - Temporal::ErrorHandler.handle(error, metadata: metadata) + Temporal::ErrorHandler.handle(error, config, metadata: metadata) respond_failed(error) ensure @@ -76,7 +76,7 @@ def respond_completed(result) rescue StandardError => error Temporal.logger.error("Unable to complete Activity", metadata.to_h.merge(error: error.inspect)) - Temporal::ErrorHandler.handle(error, metadata: metadata) + Temporal::ErrorHandler.handle(error, config, metadata: metadata) end def respond_failed(error) @@ -90,7 +90,7 @@ def respond_failed(error) rescue StandardError => error Temporal.logger.error("Unable to fail Activity task", metadata.to_h.merge(error: error.inspect)) - Temporal::ErrorHandler.handle(error, metadata: metadata) + Temporal::ErrorHandler.handle(error, config, metadata: metadata) end end end diff --git a/lib/temporal/error_handler.rb b/lib/temporal/error_handler.rb index 9702edcb..98fca99f 100644 --- a/lib/temporal/error_handler.rb +++ b/lib/temporal/error_handler.rb @@ -1,7 +1,7 @@ module Temporal module ErrorHandler - def self.handle(error, metadata: nil) - Temporal.configuration.error_handlers.each do |handler| + def self.handle(error, configuration, metadata: nil) + configuration.error_handlers.each do |handler| handler.call(error, metadata: metadata) rescue StandardError => e Temporal.logger.error("Error handler failed", { error: e.inspect }) diff --git a/lib/temporal/workflow.rb b/lib/temporal/workflow.rb index 06bf2b80..fcda49fd 100644 --- a/lib/temporal/workflow.rb +++ b/lib/temporal/workflow.rb @@ -20,7 +20,7 @@ def self.execute_in_context(context, input) Temporal.logger.error("Workflow execution failed", context.metadata.to_h.merge(error: error.inspect)) Temporal.logger.debug(error.backtrace.join("\n")) - Temporal::ErrorHandler.handle(error, metadata: context.metadata) + Temporal::ErrorHandler.handle(error, @context.config, metadata: context.metadata) context.fail(error) ensure diff --git a/lib/temporal/workflow/poller.rb b/lib/temporal/workflow/poller.rb index f312d0f9..c0e7b950 100644 --- a/lib/temporal/workflow/poller.rb +++ b/lib/temporal/workflow/poller.rb @@ -77,7 +77,7 @@ def poll_for_task connection.poll_workflow_task_queue(namespace: namespace, task_queue: task_queue) rescue StandardError => error Temporal.logger.error("Unable to poll Workflow task queue", { namespace: namespace, task_queue: task_queue, error: error.inspect }) - Temporal::ErrorHandler.handle(error) + Temporal::ErrorHandler.handle(error, config) nil end diff --git a/lib/temporal/workflow/task_processor.rb b/lib/temporal/workflow/task_processor.rb index c22e2c7d..f2d0a3c2 100644 --- a/lib/temporal/workflow/task_processor.rb +++ b/lib/temporal/workflow/task_processor.rb @@ -40,7 +40,7 @@ def process complete_task(commands) rescue StandardError => error - Temporal::ErrorHandler.handle(error, metadata: metadata) + Temporal::ErrorHandler.handle(error, config, metadata: metadata) fail_task(error) ensure @@ -110,7 +110,7 @@ def fail_task(error) rescue StandardError => error Temporal.logger.error("Unable to fail Workflow task", metadata.to_h.merge(error: error.inspect)) - Temporal::ErrorHandler.handle(error, metadata: metadata) + Temporal::ErrorHandler.handle(error, config, metadata: metadata) end end end diff --git a/spec/unit/lib/temporal/activity/task_processor_spec.rb b/spec/unit/lib/temporal/activity/task_processor_spec.rb index 91e1eccf..bb5159c7 100644 --- a/spec/unit/lib/temporal/activity/task_processor_spec.rb +++ b/spec/unit/lib/temporal/activity/task_processor_spec.rb @@ -11,7 +11,7 @@ Fabricate( :api_activity_task, activity_name: activity_name, - input: Temporal.configuration.converter.to_payloads(input) + input: config.converter.to_payloads(input) ) end let(:metadata) { Temporal::Metadata.generate(Temporal::Metadata::ACTIVITY_TYPE, task) } @@ -70,7 +70,7 @@ reported_error = nil reported_metadata = nil - Temporal.configuration.on_error do |error, metadata: nil| + config.on_error do |error, metadata: nil| reported_error = error reported_metadata = metadata.to_h end @@ -187,7 +187,7 @@ reported_error = nil reported_metadata = nil - Temporal.configuration.on_error do |error, metadata: nil| + config.on_error do |error, metadata: nil| reported_error = error reported_metadata = metadata end diff --git a/spec/unit/lib/temporal/workflow/task_processor_spec.rb b/spec/unit/lib/temporal/workflow/task_processor_spec.rb index d1537bcc..695b3da6 100644 --- a/spec/unit/lib/temporal/workflow/task_processor_spec.rb +++ b/spec/unit/lib/temporal/workflow/task_processor_spec.rb @@ -44,7 +44,7 @@ reported_error = nil reported_metadata = nil - Temporal.configuration.on_error do |error, metadata: nil| + config.on_error do |error, metadata: nil| reported_error = error reported_metadata = metadata end @@ -154,7 +154,7 @@ reported_error = nil reported_metadata = nil - Temporal.configuration.on_error do |error, metadata: nil| + config.on_error do |error, metadata: nil| reported_error = error reported_metadata = metadata end From f0fee2d6daaccad61bc04cbe6bb9b7f5a6017a5f Mon Sep 17 00:00:00 2001 From: DeRauk Gibble Date: Tue, 5 Oct 2021 15:10:36 -0400 Subject: [PATCH 002/125] Fix example tests --- lib/temporal/testing/local_workflow_context.rb | 4 ++-- lib/temporal/workflow.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/temporal/testing/local_workflow_context.rb b/lib/temporal/testing/local_workflow_context.rb index 3642a7d3..f9422af8 100644 --- a/lib/temporal/testing/local_workflow_context.rb +++ b/lib/temporal/testing/local_workflow_context.rb @@ -9,7 +9,7 @@ module Temporal module Testing class LocalWorkflowContext - attr_reader :metadata + attr_reader :metadata, :config def initialize(execution, workflow_id, run_id, disabled_releases, metadata, config = Temporal.configuration) @last_event_id = 0 @@ -188,7 +188,7 @@ def cancel(target, cancelation_id) private - attr_reader :execution, :run_id, :workflow_id, :disabled_releases, :config + attr_reader :execution, :run_id, :workflow_id, :disabled_releases def completed! @completed = true diff --git a/lib/temporal/workflow.rb b/lib/temporal/workflow.rb index fcda49fd..3b5dcfe6 100644 --- a/lib/temporal/workflow.rb +++ b/lib/temporal/workflow.rb @@ -20,7 +20,7 @@ def self.execute_in_context(context, input) Temporal.logger.error("Workflow execution failed", context.metadata.to_h.merge(error: error.inspect)) Temporal.logger.debug(error.backtrace.join("\n")) - Temporal::ErrorHandler.handle(error, @context.config, metadata: context.metadata) + Temporal::ErrorHandler.handle(error, context.config, metadata: context.metadata) context.fail(error) ensure From 9c58a49c299fd0cf6389a09aeffae8a9002dbf66 Mon Sep 17 00:00:00 2001 From: DeRauk Gibble Date: Wed, 20 Oct 2021 16:37:30 -0400 Subject: [PATCH 003/125] Make the config property in the workflow context publicly readable --- lib/temporal/workflow/context.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index d4541840..709211c8 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -15,7 +15,7 @@ module Temporal class Workflow class Context - attr_reader :metadata + attr_reader :metadata, :config def initialize(state_manager, dispatcher, workflow_class, metadata, config) @state_manager = state_manager @@ -258,7 +258,7 @@ def cancel(target, cancelation_id) private - attr_reader :state_manager, :dispatcher, :workflow_class, :config + attr_reader :state_manager, :dispatcher, :workflow_class def completed! @completed = true From fba30dfc060c201436ac99e22caa63b8e1bca948 Mon Sep 17 00:00:00 2001 From: Anthony Dmitriyev Date: Fri, 22 Oct 2021 18:19:41 +0100 Subject: [PATCH 004/125] [Fix] Retryer GRPC error lookup (#109) * Fix issue with GRPC error lookup in Retryer * Rename spec file for retryer to contain _spec --- lib/temporal/connection/retryer.rb | 18 ++++++++++-------- .../connection/{retryer.rb => retryer_spec.rb} | 0 2 files changed, 10 insertions(+), 8 deletions(-) rename spec/unit/lib/temporal/connection/{retryer.rb => retryer_spec.rb} (100%) diff --git a/lib/temporal/connection/retryer.rb b/lib/temporal/connection/retryer.rb index d70ba3b4..2948f05f 100644 --- a/lib/temporal/connection/retryer.rb +++ b/lib/temporal/connection/retryer.rb @@ -1,3 +1,5 @@ +require 'grpc/errors' + module Temporal module Connection module Retryer @@ -11,15 +13,15 @@ module Retryer # No amount of retrying will help in these cases. def self.do_not_retry_errors [ - GRPC::AlreadyExists, - GRPC::Cancelled, - GRPC::FailedPrecondition, - GRPC::InvalidArgument, + ::GRPC::AlreadyExists, + ::GRPC::Cancelled, + ::GRPC::FailedPrecondition, + ::GRPC::InvalidArgument, # If the activity has timed out, the server will return this and will never accept a retry - GRPC::NotFound, - GRPC::PermissionDenied, - GRPC::Unauthenticated, - GRPC::Unimplemented, + ::GRPC::NotFound, + ::GRPC::PermissionDenied, + ::GRPC::Unauthenticated, + ::GRPC::Unimplemented, ] end diff --git a/spec/unit/lib/temporal/connection/retryer.rb b/spec/unit/lib/temporal/connection/retryer_spec.rb similarity index 100% rename from spec/unit/lib/temporal/connection/retryer.rb rename to spec/unit/lib/temporal/connection/retryer_spec.rb From be33f00ff70e761d3d5caf03d483019b80a02542 Mon Sep 17 00:00:00 2001 From: DeRauk Gibble Date: Mon, 1 Nov 2021 07:14:57 -0400 Subject: [PATCH 005/125] [Feature] Add id and domain to workflow context's metadata (#110) * Start syncing id and domain on workflow context metadata * Fixed tests Co-authored-by: DeRauk Gibble --- lib/temporal/metadata.rb | 14 +--- lib/temporal/metadata/workflow.rb | 8 +- lib/temporal/testing/temporal_override.rb | 7 +- lib/temporal/testing/workflow_override.rb | 6 +- lib/temporal/workflow/executor.rb | 22 +++++- lib/temporal/workflow/state_manager.rb | 3 +- lib/temporal/workflow/task_processor.rb | 2 +- .../workflow_metadata_fabricator.rb | 2 + .../temporal/activity/task_processor_spec.rb | 2 +- .../lib/temporal/metadata/workflow_spec.rb | 5 ++ spec/unit/lib/temporal/metadata_spec.rb | 22 ------ .../testing/local_workflow_context_spec.rb | 2 +- .../lib/temporal/workflow/executor_spec.rb | 78 +++++++++++++++++++ 13 files changed, 126 insertions(+), 47 deletions(-) create mode 100644 spec/unit/lib/temporal/workflow/executor_spec.rb diff --git a/lib/temporal/metadata.rb b/lib/temporal/metadata.rb index e39f8da9..c467d25e 100644 --- a/lib/temporal/metadata.rb +++ b/lib/temporal/metadata.rb @@ -8,19 +8,16 @@ module Temporal module Metadata ACTIVITY_TYPE = :activity WORKFLOW_TASK_TYPE = :workflow_task - WORKFLOW_TYPE = :workflow class << self include Concerns::Payloads - def generate(type, data, namespace = nil) + def generate(type, data, namespace) case type when ACTIVITY_TYPE activity_metadata_from(data, namespace) when WORKFLOW_TASK_TYPE workflow_task_metadata_from(data, namespace) - when WORKFLOW_TYPE - workflow_metadata_from(data) else raise InternalError, 'Unsupported metadata type' end @@ -62,15 +59,6 @@ def workflow_task_metadata_from(task, namespace) workflow_name: task.workflow_type.name ) end - - def workflow_metadata_from(event) - Metadata::Workflow.new( - name: event.workflow_type.name, - run_id: event.original_execution_run_id, - attempt: event.attempt, - headers: headers(event.header&.fields) - ) - end end end end diff --git a/lib/temporal/metadata/workflow.rb b/lib/temporal/metadata/workflow.rb index 86d14de4..f70c7f2f 100644 --- a/lib/temporal/metadata/workflow.rb +++ b/lib/temporal/metadata/workflow.rb @@ -3,9 +3,11 @@ module Temporal module Metadata class Workflow < Base - attr_reader :name, :run_id, :attempt, :headers + attr_reader :namespace, :id, :name, :run_id, :attempt, :headers - def initialize(name:, run_id:, attempt:, headers: {}) + def initialize(namespace:, id:, name:, run_id:, attempt:, headers: {}) + @namespace = namespace + @id = id @name = name @run_id = run_id @attempt = attempt @@ -20,6 +22,8 @@ def workflow? def to_h { + 'namespace' => namespace, + 'workflow_id' => id, 'workflow_name' => name, 'workflow_run_id' => run_id, 'attempt' => attempt diff --git a/lib/temporal/testing/temporal_override.rb b/lib/temporal/testing/temporal_override.rb index de61b591..d4c58310 100644 --- a/lib/temporal/testing/temporal_override.rb +++ b/lib/temporal/testing/temporal_override.rb @@ -89,7 +89,12 @@ def start_locally(workflow, schedule, *input, **args) execution_options = ExecutionOptions.new(workflow, options) metadata = Metadata::Workflow.new( - name: workflow_id, run_id: run_id, attempt: 1, headers: execution_options.headers + namespace: execution_options.namespace, + id: workflow_id, + name: execution_options.name, + run_id: run_id, + attempt: 1, + headers: execution_options.headers ) context = Temporal::Testing::LocalWorkflowContext.new( execution, workflow_id, run_id, workflow.disabled_releases, metadata diff --git a/lib/temporal/testing/workflow_override.rb b/lib/temporal/testing/workflow_override.rb index a36e843e..eb9a9fd4 100644 --- a/lib/temporal/testing/workflow_override.rb +++ b/lib/temporal/testing/workflow_override.rb @@ -28,7 +28,11 @@ def execute_locally(*input) run_id = SecureRandom.uuid execution = WorkflowExecution.new metadata = Temporal::Metadata::Workflow.new( - name: workflow_id, run_id: run_id, attempt: 1 + namespace: nil, + id: workflow_id, + name: name, # Workflow class name + run_id: run_id, + attempt: 1 ) context = Temporal::Testing::LocalWorkflowContext.new( execution, workflow_id, run_id, disabled_releases, metadata diff --git a/lib/temporal/workflow/executor.rb b/lib/temporal/workflow/executor.rb index c81703cb..313e6977 100644 --- a/lib/temporal/workflow/executor.rb +++ b/lib/temporal/workflow/executor.rb @@ -4,14 +4,16 @@ require 'temporal/workflow/state_manager' require 'temporal/workflow/context' require 'temporal/workflow/history/event_target' +require 'temporal/metadata' module Temporal class Workflow class Executor - def initialize(workflow_class, history, config) + def initialize(workflow_class, history, metadata, config) @workflow_class = workflow_class @dispatcher = Dispatcher.new @state_manager = StateManager.new(dispatcher) + @metadata = metadata @history = history @config = config end @@ -32,15 +34,29 @@ def run private - attr_reader :workflow_class, :dispatcher, :state_manager, :history, :config + attr_reader :workflow_class, :dispatcher, :state_manager, :metadata, :history, :config - def execute_workflow(input, metadata) + def execute_workflow(input, workflow_started_event_attributes) + metadata = generate_workflow_metadata_from(workflow_started_event_attributes) context = Workflow::Context.new(state_manager, dispatcher, workflow_class, metadata, config) Fiber.new do workflow_class.execute_in_context(context, input) end.resume end + + # workflow_id and domain are confusingly not available on the WorkflowExecutionStartedEvent, + # so we have to fetch these from the DecisionTask's metadata + def generate_workflow_metadata_from(event_attributes) + Metadata::Workflow.new( + namespace: metadata.namespace, + id: metadata.workflow_id, + name: event_attributes.workflow_type.name, + run_id: event_attributes.original_execution_run_id, + attempt: event_attributes.attempt, + headers: event_attributes.header&.fields || {} + ) + end end end end diff --git a/lib/temporal/workflow/state_manager.rb b/lib/temporal/workflow/state_manager.rb index 693f1305..e88850fd 100644 --- a/lib/temporal/workflow/state_manager.rb +++ b/lib/temporal/workflow/state_manager.rb @@ -3,7 +3,6 @@ require 'temporal/workflow/command' require 'temporal/workflow/command_state_machine' require 'temporal/workflow/history/event_target' -require 'temporal/metadata' require 'temporal/concerns/payloads' require 'temporal/workflow/errors' @@ -106,7 +105,7 @@ def apply_event(event) History::EventTarget.workflow, 'started', from_payloads(event.attributes.input), - Metadata.generate(Metadata::WORKFLOW_TYPE, event.attributes) + event.attributes ) when 'WORKFLOW_EXECUTION_COMPLETED' diff --git a/lib/temporal/workflow/task_processor.rb b/lib/temporal/workflow/task_processor.rb index f2d0a3c2..1701e18d 100644 --- a/lib/temporal/workflow/task_processor.rb +++ b/lib/temporal/workflow/task_processor.rb @@ -32,7 +32,7 @@ def process history = fetch_full_history # TODO: For sticky workflows we need to cache the Executor instance - executor = Workflow::Executor.new(workflow_class, history, config) + executor = Workflow::Executor.new(workflow_class, history, metadata, config) commands = middleware_chain.invoke(metadata) do executor.run diff --git a/spec/fabricators/workflow_metadata_fabricator.rb b/spec/fabricators/workflow_metadata_fabricator.rb index 5a609bb8..f5393765 100644 --- a/spec/fabricators/workflow_metadata_fabricator.rb +++ b/spec/fabricators/workflow_metadata_fabricator.rb @@ -1,6 +1,8 @@ require 'securerandom' Fabricator(:workflow_metadata, from: :open_struct) do + namespace 'test-namespace' + id { SecureRandom.uuid } name 'TestWorkflow' run_id { SecureRandom.uuid } attempt 1 diff --git a/spec/unit/lib/temporal/activity/task_processor_spec.rb b/spec/unit/lib/temporal/activity/task_processor_spec.rb index bb5159c7..0a582883 100644 --- a/spec/unit/lib/temporal/activity/task_processor_spec.rb +++ b/spec/unit/lib/temporal/activity/task_processor_spec.rb @@ -14,7 +14,7 @@ input: config.converter.to_payloads(input) ) end - let(:metadata) { Temporal::Metadata.generate(Temporal::Metadata::ACTIVITY_TYPE, task) } + let(:metadata) { Temporal::Metadata.generate(Temporal::Metadata::ACTIVITY_TYPE, task, namespace) } let(:activity_name) { 'TestActivity' } let(:connection) { instance_double('Temporal::Connection::GRPC') } let(:middleware_chain) { Temporal::Middleware::Chain.new } diff --git a/spec/unit/lib/temporal/metadata/workflow_spec.rb b/spec/unit/lib/temporal/metadata/workflow_spec.rb index 6bbe3af6..562c3fb8 100644 --- a/spec/unit/lib/temporal/metadata/workflow_spec.rb +++ b/spec/unit/lib/temporal/metadata/workflow_spec.rb @@ -6,6 +6,8 @@ let(:args) { Fabricate(:workflow_metadata) } it 'sets the attributes' do + expect(subject.namespace).to eq(args.namespace) + expect(subject.id).to eq(args.id) expect(subject.name).to eq(args.name) expect(subject.run_id).to eq(args.run_id) expect(subject.attempt).to eq(args.attempt) @@ -25,6 +27,9 @@ it 'returns a hash' do expect(subject.to_h).to eq({ + 'namespace' => subject.namespace, + 'workflow_id' => subject.id, + 'workflow_id' => subject.id, 'attempt' => subject.attempt, 'workflow_name' => subject.name, 'workflow_run_id' => subject.run_id diff --git a/spec/unit/lib/temporal/metadata_spec.rb b/spec/unit/lib/temporal/metadata_spec.rb index 18134e26..f4c86df6 100644 --- a/spec/unit/lib/temporal/metadata_spec.rb +++ b/spec/unit/lib/temporal/metadata_spec.rb @@ -46,28 +46,6 @@ end end - context 'with workflow type' do - let(:type) { described_class::WORKFLOW_TYPE } - let(:data) { Fabricate(:api_workflow_execution_started_event_attributes) } - let(:namespace) { nil } - - it 'generates metadata' do - expect(subject.run_id).to eq(data.original_execution_run_id) - expect(subject.attempt).to eq(data.attempt) - expect(subject.headers).to eq({}) - end - - context 'with headers' do - let(:data) do - Fabricate(:api_workflow_execution_started_event_attributes, headers: { 'Foo' => 'Bar' }) - end - - it 'assigns headers' do - expect(subject.headers).to eq('Foo' => 'Bar') - end - end - end - context 'with unknown type' do let(:type) { :unknown } let(:data) { nil } diff --git a/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb b/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb index 29b4b2a3..70747841 100644 --- a/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb +++ b/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb @@ -12,7 +12,7 @@ workflow_id, run_id, [], - Temporal::Metadata::Workflow.new(name: workflow_id, run_id: run_id, attempt: 1) + Temporal::Metadata::Workflow.new(namespace: 'ruby-samples', id: workflow_id, name: 'HelloWorldWorkflow', run_id: run_id, attempt: 1) ) end let(:async_token) do diff --git a/spec/unit/lib/temporal/workflow/executor_spec.rb b/spec/unit/lib/temporal/workflow/executor_spec.rb new file mode 100644 index 00000000..a639bdca --- /dev/null +++ b/spec/unit/lib/temporal/workflow/executor_spec.rb @@ -0,0 +1,78 @@ +require 'temporal/workflow/executor' +require 'temporal/workflow/history' +require 'temporal/workflow' + +describe Temporal::Workflow::Executor do + subject { described_class.new(workflow, history, workflow_metadata, config) } + + let(:workflow_started_event) { Fabricate(:api_workflow_execution_started_event, event_id: 1) } + let(:history) do + Temporal::Workflow::History.new([ + workflow_started_event, + Fabricate(:api_workflow_task_scheduled_event, event_id: 2), + Fabricate(:api_workflow_task_started_event, event_id: 3), + Fabricate(:api_workflow_task_completed_event, event_id: 4) + ]) + end + let(:workflow) { TestWorkflow } + let(:workflow_metadata) { Fabricate(:workflow_metadata) } + let(:config) { Temporal::Configuration.new } + + class TestWorkflow < Temporal::Workflow + def execute + 'test' + end + end + + describe '#run' do + it 'runs a workflow' do + allow(workflow).to receive(:execute_in_context).and_call_original + + subject.run + + expect(workflow) + .to have_received(:execute_in_context) + .with( + an_instance_of(Temporal::Workflow::Context), + nil + ) + end + + it 'returns a complete workflow decision' do + decisions = subject.run + + expect(decisions.length).to eq(1) + + decision_id, decision = decisions.first + expect(decision_id).to eq(history.events.length + 1) + expect(decision).to be_an_instance_of(Temporal::Workflow::Command::CompleteWorkflow) + expect(decision.result).to eq('test') + end + + it 'generates workflow metadata' do + allow(Temporal::Metadata::Workflow).to receive(:new).and_call_original + payload = Temporal::Api::Common::V1::Payload.new( + metadata: { 'encoding' => 'xyz' }, + data: 'test'.b + ) + header = + Google::Protobuf::Map.new(:string, :message, Temporal::Api::Common::V1::Payload, { 'Foo' => payload }) + workflow_started_event.workflow_execution_started_event_attributes.header = + Fabricate(:api_header, fields: header) + + subject.run + + event_attributes = workflow_started_event.workflow_execution_started_event_attributes + expect(Temporal::Metadata::Workflow) + .to have_received(:new) + .with( + namespace: workflow_metadata.namespace, + id: workflow_metadata.workflow_id, + name: event_attributes.workflow_type.name, + run_id: event_attributes.original_execution_run_id, + attempt: event_attributes.attempt, + headers: header + ) + end + end +end \ No newline at end of file From 2c16ba14aded285e307a6b0a9d824b7c34f81f4e Mon Sep 17 00:00:00 2001 From: Anthony Dmitriyev Date: Wed, 3 Nov 2021 17:58:11 +0000 Subject: [PATCH 006/125] Explicit docker-compose project name (#114) --- examples/.env | 1 + 1 file changed, 1 insertion(+) create mode 100644 examples/.env diff --git a/examples/.env b/examples/.env new file mode 100644 index 00000000..d15cca49 --- /dev/null +++ b/examples/.env @@ -0,0 +1 @@ +COMPOSE_PROJECT_NAME=temporal-ruby-examples From 64dca95eb2bd2a0a99fcc893fc194dc72f0690aa Mon Sep 17 00:00:00 2001 From: Anthony Dmitriyev Date: Mon, 8 Nov 2021 12:43:33 +0000 Subject: [PATCH 007/125] Add YARD documentation for Temporal::Client (#113) * Add YARD documentation for Temporal::Client * Add yard gem * Fix @option tag * Typo fix --- README.md | 4 +- lib/temporal/client.rb | 116 ++++++++++++++++++++++++++++++++++++++--- temporal.gemspec | 1 + 3 files changed, 112 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index cc8b0441..4d59d255 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Ruby worker for Temporal +# Ruby SDK for Temporal [![Coverage Status](https://coveralls.io/repos/github/coinbase/temporal-ruby/badge.svg?branch=master)](https://coveralls.io/github/coinbase/temporal-ruby?branch=master) @@ -6,7 +6,7 @@ A pure Ruby library for defining and running Temporal workflows and activities. -To find more about Temporal please visit . +To find more about Temporal itself please visit . ## Getting Started diff --git a/lib/temporal/client.rb b/lib/temporal/client.rb index d2380855..b4d6d0b2 100644 --- a/lib/temporal/client.rb +++ b/lib/temporal/client.rb @@ -13,6 +13,23 @@ def initialize(config) @config = config end + # Start a workflow + # + # @param workflow [Temporal::Workflow, String] workflow class or name. When a workflow class + # is passed, its config (namespace, task_queue, timeouts, etc) will be used + # @param input [any] arguments to be passed to workflow's #execute method + # @param args [Hash] keyword arguments to be passed to workflow's #execute method + # @param options [Hash, nil] optional overrides + # @option options [String] :workflow_id + # @option options [Symbol] :workflow_id_reuse_policy check Temporal::Connection::GRPC::WORKFLOW_ID_REUSE_POLICY + # @option options [String] :name workflow name + # @option options [String] :namespace + # @option options [String] :task_queue + # @option options [Hash] :retry_policy check Temporal::RetryPolicy for available options + # @option options [Hash] :timeouts check Temporal::Configuration::DEFAULT_TIMEOUTS + # @option options [Hash] :headers + # + # @return [String] workflow's run ID def start_workflow(workflow, *input, **args) options = args.delete(:options) || {} input << args unless args.empty? @@ -37,6 +54,24 @@ def start_workflow(workflow, *input, **args) response.run_id end + # Schedule a workflow for a periodic cron-like execution + # + # @param workflow [Temporal::Workflow, String] workflow class or name. When a workflow class + # is passed, its config (namespace, task_queue, timeouts, etc) will be used + # @param cron_schedule [String] a cron-style schedule string + # @param input [any] arguments to be passed to workflow's #execute method + # @param args [Hash] keyword arguments to be pass to workflow's #execute method + # @param options [Hash, nil] optional overrides + # @option options [String] :workflow_id + # @option options [Symbol] :workflow_id_reuse_policy check Temporal::Connection::GRPC::WORKFLOW_ID_REUSE_POLICY + # @option options [String] :name workflow name + # @option options [String] :namespace + # @option options [String] :task_queue + # @option options [Hash] :retry_policy check Temporal::RetryPolicy for available options + # @option options [Hash] :timeouts check Temporal::Configuration::DEFAULT_TIMEOUTS + # @option options [Hash] :headers + # + # @return [String] workflow's run ID def schedule_workflow(workflow, cron_schedule, *input, **args) options = args.delete(:options) || {} input << args unless args.empty? @@ -64,10 +99,23 @@ def schedule_workflow(workflow, cron_schedule, *input, **args) response.run_id end + # Register a new Temporal namespace + # + # @param name [String] name of the new namespace + # @param description [String] optional namespace description def register_namespace(name, description = nil) connection.register_namespace(name: name, description: description) end + # Send a signal to a running workflow + # + # @param workflow [Temporal::Workflow, nil] workflow class or nil + # @param signal [String] name of the signal to send + # @param workflow_id [String] + # @param run_id [String] + # @param input [String, Array, nil] optional arguments for the signal + # @param namespace [String, nil] if nil, choose the one declared on the workflow class or the + # global default def signal_workflow(workflow, signal, workflow_id, run_id, input = nil, namespace: nil) execution_options = ExecutionOptions.new(workflow, {}, config.default_execution_options) @@ -80,15 +128,22 @@ def signal_workflow(workflow, signal, workflow_id, run_id, input = nil, namespac ) end - # Long polls for a workflow to be completed and returns whatever the execute function - # returned. This function times out after 30 seconds and throws Temporal::TimeoutError, + # Long polls for a workflow to be completed and returns workflow's return value. + # + # @note This function times out after 30 seconds and throws Temporal::TimeoutError, # not to be confused with Temporal::WorkflowTimedOut which reports that the workflow # itself timed out. - # run_id of nil: await the entire workflow completion. This can span multiple runs - # in the case where the workflow uses continue-as-new. - # timeout: seconds to wait for the result. This cannot be longer than 30 seconds because - # that is the maximum the server supports. - # namespace: if nil, choose the one declared on the Workflow, or the global default + # + # @param workflow [Temporal::Workflow, nil] workflow class or nil + # @param workflow_id [String] + # @param run_id [String, nil] awaits the entire workflow completion when nil. This can span + # multiple runs in the case where the workflow uses continue-as-new. + # @param timeout [Integer, nil] seconds to wait for the result. This cannot be longer than 30 + # seconds because that is the maximum the server supports. + # @param namespace [String, nil] if nil, choose the one declared on the workflow class or the + # global default + # + # @return workflow's return value def await_workflow_result(workflow, workflow_id:, run_id: nil, timeout: nil, namespace: nil) options = namespace ? {namespace: namespace} : {} execution_options = ExecutionOptions.new(workflow, options, config.default_execution_options) @@ -135,6 +190,21 @@ def await_workflow_result(workflow, workflow_id:, run_id: nil, timeout: nil, nam end end + # Reset a workflow + # + # @note More on resetting a workflow here — + # https://docs.temporal.io/docs/system-tools/tctl/#restart-reset-workflow + # + # @param namespace [String] + # @param workflow_id [String] + # @param run_id [String] + # @param strategy [Symbol, nil] one of the Temporal::ResetStrategy values or `nil` when + # passing a workflow_task_id + # @param workflow_task_id [Integer, nil] A specific event ID to reset to. The event has to + # be of a type WorkflowTaskCompleted, WorkflowTaskFailed or WorkflowTaskTimedOut + # @param reason [String] a reset reason to be recorded in workflow's history for reference + # + # @return [String] run_id of the new workflow execution def reset_workflow(namespace, workflow_id, run_id, strategy: nil, workflow_task_id: nil, reason: 'manual reset') # Pick default strategy for backwards-compatibility strategy ||= :last_workflow_task unless workflow_task_id @@ -157,6 +227,14 @@ def reset_workflow(namespace, workflow_id, run_id, strategy: nil, workflow_task_ response.run_id end + # Terminate a running workflow + # + # @param workflow_id [String] + # @param namespace [String, nil] use a default namespace when `nil` + # @param run_id [String, nil] + # @param reason [String, nil] a termination reason to be recorded in workflow's history + # for reference + # @param details [String, Array, nil] optional details to be stored in history def terminate_workflow(workflow_id, namespace: nil, run_id: nil, reason: nil, details: nil) namespace ||= Temporal.configuration.namespace @@ -169,6 +247,13 @@ def terminate_workflow(workflow_id, namespace: nil, run_id: nil, reason: nil, de ) end + # Fetch workflow's execution info + # + # @param namespace [String] + # @param workflow_id [String] + # @param run_id [String] + # + # @return [Temporal::Workflow::ExecutionInfo] an object containing workflow status and other info def fetch_workflow_execution_info(namespace, workflow_id, run_id) response = connection.describe_workflow_execution( namespace: namespace, @@ -179,6 +264,11 @@ def fetch_workflow_execution_info(namespace, workflow_id, run_id) Workflow::ExecutionInfo.generate_from(response.workflow_execution_info) end + # Manually complete an activity + # + # @param async_token [String] an encoded Temporal::Activity::AsyncToken + # @param result [String, Array, nil] activity's return value to be stored in history and + # passed back to a workflow def complete_activity(async_token, result = nil) details = Activity::AsyncToken.decode(async_token) @@ -191,6 +281,11 @@ def complete_activity(async_token, result = nil) ) end + # Manually fail an activity + # + # @param async_token [String] an encoded Temporal::Activity::AsyncToken + # @param exception [Exception] activity's failure exception to be stored in history and + # raised in a workflow def fail_activity(async_token, exception) details = Activity::AsyncToken.decode(async_token) @@ -203,6 +298,13 @@ def fail_activity(async_token, exception) ) end + # Fetch workflow's execution history + # + # @param namespace [String] + # @param workflow_id [String] + # @param run_id [String] + # + # @return [Temporal::Workflow::History] workflow's execution history def get_workflow_history(namespace:, workflow_id:, run_id:) history_response = connection.get_workflow_execution_history( namespace: namespace, diff --git a/temporal.gemspec b/temporal.gemspec index 59ef1192..c8589c9e 100644 --- a/temporal.gemspec +++ b/temporal.gemspec @@ -21,4 +21,5 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'rspec' spec.add_development_dependency 'fabrication' spec.add_development_dependency 'grpc-tools' + spec.add_development_dependency 'yard' end From 9a581cecba71170b54d1b3a87c3b34628a260a95 Mon Sep 17 00:00:00 2001 From: nagl-stripe <86737162+nagl-stripe@users.noreply.github.com> Date: Tue, 9 Nov 2021 10:20:32 -0800 Subject: [PATCH 008/125] Add signal arguments to start_workflow (support for signal_with_start) (#112) * Add signal arguments to start_workflow (to support signal_with_start) * Move signal arguments to the options hash * PR feedback * Fix merge error --- examples/bin/worker | 1 + .../integration/signal_with_start_spec.rb | 75 ++++++++++++++++++ .../workflows/signal_with_start_workflow.rb | 16 ++++ lib/temporal/client.rb | 63 +++++++++++---- lib/temporal/connection/grpc.rb | 48 +++++++++++- .../testing/local_workflow_context.rb | 6 +- lib/temporal/testing/temporal_override.rb | 5 ++ spec/unit/lib/temporal/client_spec.rb | 77 +++++++++++++++++++ spec/unit/lib/temporal/grpc_client_spec.rb | 37 +++++++++ .../testing/temporal_override_spec.rb | 8 ++ 10 files changed, 316 insertions(+), 20 deletions(-) create mode 100644 examples/spec/integration/signal_with_start_spec.rb create mode 100644 examples/workflows/signal_with_start_workflow.rb diff --git a/examples/bin/worker b/examples/bin/worker index 65828dfa..497eb9fc 100755 --- a/examples/bin/worker +++ b/examples/bin/worker @@ -39,6 +39,7 @@ worker.register_workflow(ReleaseWorkflow) worker.register_workflow(ResultWorkflow) worker.register_workflow(SerialHelloWorldWorkflow) worker.register_workflow(SideEffectWorkflow) +worker.register_workflow(SignalWithStartWorkflow) worker.register_workflow(SimpleTimerWorkflow) worker.register_workflow(TimeoutWorkflow) worker.register_workflow(TripBookingWorkflow) diff --git a/examples/spec/integration/signal_with_start_spec.rb b/examples/spec/integration/signal_with_start_spec.rb new file mode 100644 index 00000000..4af4f6f1 --- /dev/null +++ b/examples/spec/integration/signal_with_start_spec.rb @@ -0,0 +1,75 @@ +require 'workflows/signal_with_start_workflow' + +describe 'signal with start' do + + it 'signals at workflow start time' do + workflow_id = SecureRandom.uuid + run_id = Temporal.start_workflow( + SignalWithStartWorkflow, + 'signal_name', + 0.1, + options: { + workflow_id: workflow_id, + signal_name: 'signal_name', + signal_input: 'expected value', + } + ) + + result = Temporal.await_workflow_result( + SignalWithStartWorkflow, + workflow_id: workflow_id, + run_id: run_id, + ) + + expect(result).to eq('expected value') # the workflow should return the signal value + end + + it 'signals at workflow start time with name only' do + workflow_id = SecureRandom.uuid + run_id = Temporal.start_workflow( + SignalWithStartWorkflow, + 'signal_name', + 0.1, + options: { + workflow_id: workflow_id, + signal_name: 'signal_name', + } + ) + + result = Temporal.await_workflow_result( + SignalWithStartWorkflow, + workflow_id: workflow_id, + run_id: run_id, + ) + + expect(result).to eq(nil) # the workflow should return the signal value + end + + it 'does not launch a new workflow when signaling a running workflow through signal_with_start' do + workflow_id = SecureRandom.uuid + run_id = Temporal.start_workflow( + SignalWithStartWorkflow, + 'signal_name', + 10, + options: { + workflow_id: workflow_id, + signal_name: 'signal_name', + signal_input: 'expected value', + } + ) + + second_run_id = Temporal.start_workflow( + SignalWithStartWorkflow, + 'signal_name', + 0.1, + options: { + workflow_id: workflow_id, + signal_name: 'signal_name', + signal_input: 'expected value', + } + ) + + # If the run ids are the same, then we didn't start a new workflow + expect(second_run_id).to eq(run_id) + end +end diff --git a/examples/workflows/signal_with_start_workflow.rb b/examples/workflows/signal_with_start_workflow.rb new file mode 100644 index 00000000..dbcb186a --- /dev/null +++ b/examples/workflows/signal_with_start_workflow.rb @@ -0,0 +1,16 @@ +class SignalWithStartWorkflow < Temporal::Workflow + + def execute(expected_signal, sleep_for) + received = 'no signal received' + + workflow.on_signal do |signal, input| + if signal == expected_signal + received = input + end + end + + # Do something to get descheduled so the signal handler has a chance to run + workflow.sleep(sleep_for) + received + end +end diff --git a/lib/temporal/client.rb b/lib/temporal/client.rb index b4d6d0b2..e7e3c26c 100644 --- a/lib/temporal/client.rb +++ b/lib/temporal/client.rb @@ -13,7 +13,11 @@ def initialize(config) @config = config end - # Start a workflow + # Start a workflow with an optional signal + # + # If options[:signal_name] is specified, Temporal will atomically do one of: + # A) start a new workflow and signal it + # B) if workflow_id is specified and the workflow already exists, signal the existing workflow. # # @param workflow [Temporal::Workflow, String] workflow class or name. When a workflow class # is passed, its config (namespace, task_queue, timeouts, etc) will be used @@ -25,6 +29,9 @@ def initialize(config) # @option options [String] :name workflow name # @option options [String] :namespace # @option options [String] :task_queue + # @option options [String] :signal_name corresponds to the 'signal' argument to signal_workflow. Required if + # options[:signal_input] is specified. + # @option options [String, Array, nil] :signal_input corresponds to the 'input' argument to signal_workflow # @option options [Hash] :retry_policy check Temporal::RetryPolicy for available options # @option options [Hash] :timeouts check Temporal::Configuration::DEFAULT_TIMEOUTS # @option options [Hash] :headers @@ -34,22 +41,44 @@ def start_workflow(workflow, *input, **args) options = args.delete(:options) || {} input << args unless args.empty? + signal_name = options.delete(:signal_name) + signal_input = options.delete(:signal_input) + execution_options = ExecutionOptions.new(workflow, options, config.default_execution_options) workflow_id = options[:workflow_id] || SecureRandom.uuid - response = connection.start_workflow_execution( - namespace: execution_options.namespace, - workflow_id: workflow_id, - workflow_name: execution_options.name, - task_queue: execution_options.task_queue, - input: input, - execution_timeout: execution_options.timeouts[:execution], - # If unspecified, individual runs should have the full time for the execution (which includes retries). - run_timeout: execution_options.timeouts[:run] || execution_options.timeouts[:execution], - task_timeout: execution_options.timeouts[:task], - workflow_id_reuse_policy: options[:workflow_id_reuse_policy], - headers: execution_options.headers - ) + if signal_name.nil? && signal_input.nil? + response = connection.start_workflow_execution( + namespace: execution_options.namespace, + workflow_id: workflow_id, + workflow_name: execution_options.name, + task_queue: execution_options.task_queue, + input: input, + execution_timeout: execution_options.timeouts[:execution], + # If unspecified, individual runs should have the full time for the execution (which includes retries). + run_timeout: compute_run_timeout(execution_options), + task_timeout: execution_options.timeouts[:task], + workflow_id_reuse_policy: options[:workflow_id_reuse_policy], + headers: execution_options.headers + ) + else + raise ArgumentError, 'If signal_input is provided, you must also provide signal_name' if signal_name.nil? + + response = connection.signal_with_start_workflow_execution( + namespace: execution_options.namespace, + workflow_id: workflow_id, + workflow_name: execution_options.name, + task_queue: execution_options.task_queue, + input: input, + execution_timeout: execution_options.timeouts[:execution], + run_timeout: compute_run_timeout(execution_options), + task_timeout: execution_options.timeouts[:task], + workflow_id_reuse_policy: options[:workflow_id_reuse_policy], + headers: execution_options.headers, + signal_name: signal_name, + signal_input: signal_input + ) + end response.run_id end @@ -89,7 +118,7 @@ def schedule_workflow(workflow, cron_schedule, *input, **args) # Execution timeout is across all scheduled jobs, whereas run is for an individual run. # This default is here for backward compatibility. Certainly, the run timeout shouldn't be higher # than the execution timeout. - run_timeout: execution_options.timeouts[:run] || execution_options.timeouts[:execution], + run_timeout: compute_run_timeout(execution_options), task_timeout: execution_options.timeouts[:task], workflow_id_reuse_policy: options[:workflow_id_reuse_policy], headers: execution_options.headers, @@ -328,6 +357,10 @@ def connection @connection ||= Temporal::Connection.generate(config.for_connection) end + def compute_run_timeout(execution_options) + execution_options.timeouts[:run] || execution_options.timeouts[:execution] + end + def find_workflow_task(namespace, workflow_id, run_id, strategy) history = get_workflow_history( namespace: namespace, diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index 848d2de0..5b7e553b 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -292,8 +292,52 @@ def signal_workflow_execution(namespace:, workflow_id:, run_id:, signal:, input: client.signal_workflow_execution(request) end - def signal_with_start_workflow_execution - raise NotImplementedError + def signal_with_start_workflow_execution( + namespace:, + workflow_id:, + workflow_name:, + task_queue:, + input: nil, + execution_timeout:, + run_timeout:, + task_timeout:, + workflow_id_reuse_policy: nil, + headers: nil, + cron_schedule: nil, + signal_name:, + signal_input: + ) + request = Temporal::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionRequest.new( + identity: identity, + namespace: namespace, + workflow_type: Temporal::Api::Common::V1::WorkflowType.new( + name: workflow_name + ), + workflow_id: workflow_id, + task_queue: Temporal::Api::TaskQueue::V1::TaskQueue.new( + name: task_queue + ), + input: to_payloads(input), + workflow_execution_timeout: execution_timeout, + workflow_run_timeout: run_timeout, + workflow_task_timeout: task_timeout, + request_id: SecureRandom.uuid, + header: Temporal::Api::Common::V1::Header.new( + fields: headers + ), + cron_schedule: cron_schedule, + signal_name: signal_name, + signal_input: to_signal_payloads(signal_input) + ) + + if workflow_id_reuse_policy + policy = WORKFLOW_ID_REUSE_POLICY[workflow_id_reuse_policy] + raise Client::ArgumentError, 'Unknown workflow_id_reuse_policy specified' unless policy + + request.workflow_id_reuse_policy = policy + end + + client.signal_with_start_workflow_execution(request) end def reset_workflow_execution(namespace:, workflow_id:, run_id:, reason:, workflow_task_event_id:) diff --git a/lib/temporal/testing/local_workflow_context.rb b/lib/temporal/testing/local_workflow_context.rb index f9422af8..e4ded199 100644 --- a/lib/temporal/testing/local_workflow_context.rb +++ b/lib/temporal/testing/local_workflow_context.rb @@ -175,15 +175,15 @@ def now end def on_signal(&block) - raise NotImplementedError, 'not yet available for testing' + raise NotImplementedError, 'Signals are not available when Temporal::Testing.local! is on' end def cancel_activity(activity_id) - raise NotImplementedError, 'not yet available for testing' + raise NotImplementedError, 'Cancel is not available when Temporal::Testing.local! is on' end def cancel(target, cancelation_id) - raise NotImplementedError, 'not yet available for testing' + raise NotImplementedError, 'Cancel is not available when Temporal::Testing.local! is on' end private diff --git a/lib/temporal/testing/temporal_override.rb b/lib/temporal/testing/temporal_override.rb index d4c58310..825241be 100644 --- a/lib/temporal/testing/temporal_override.rb +++ b/lib/temporal/testing/temporal_override.rb @@ -73,6 +73,11 @@ def start_locally(workflow, schedule, *input, **args) options = args.delete(:options) || {} input << args unless args.empty? + # signals aren't supported at all, so let's prohibit start_workflow calls that try to signal + signal_name = options.delete(:signal_name) + signal_input = options.delete(:signal_input) + raise NotImplementedError, 'Signals are not available when Temporal::Testing.local! is on' if signal_name || signal_input + reuse_policy = options[:workflow_id_reuse_policy] || :allow_failed workflow_id = options[:workflow_id] || SecureRandom.uuid run_id = SecureRandom.uuid diff --git a/spec/unit/lib/temporal/client_spec.rb b/spec/unit/lib/temporal/client_spec.rb index 92ec094b..a0260f43 100644 --- a/spec/unit/lib/temporal/client_spec.rb +++ b/spec/unit/lib/temporal/client_spec.rb @@ -185,6 +185,83 @@ class TestStartWorkflow < Temporal::Workflow end end + describe '#start_workflow with a signal' do + let(:temporal_response) do + Temporal::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionResponse.new(run_id: 'xxx') + end + + before { allow(connection).to receive(:signal_with_start_workflow_execution).and_return(temporal_response) } + + def expect_signal_with_start(expected_arguments, expected_signal_argument) + expect(connection) + .to have_received(:signal_with_start_workflow_execution) + .with( + namespace: 'default-test-namespace', + workflow_id: an_instance_of(String), + workflow_name: 'TestStartWorkflow', + task_queue: 'default-test-task-queue', + input: expected_arguments, + task_timeout: Temporal.configuration.timeouts[:task], + run_timeout: Temporal.configuration.timeouts[:run], + execution_timeout: Temporal.configuration.timeouts[:execution], + workflow_id_reuse_policy: nil, + headers: {}, + signal_name: 'the question', + signal_input: expected_signal_argument, + ) + end + + it 'starts a workflow with a signal and no arguments' do + subject.start_workflow( + TestStartWorkflow, + options: { signal_name: 'the question' } + ) + + expect_signal_with_start([], nil) + end + + it 'starts a workflow with a signal and one scalar argument' do + signal_input = 'what do you get if you multiply six by nine?' + subject.start_workflow( + TestStartWorkflow, + 42, + options: { + signal_name: 'the question', + signal_input: signal_input, + } + ) + + expect_signal_with_start([42], signal_input) + end + + it 'starts a workflow with a signal and multiple arguments and signal_inputs' do + signal_input = ['what do you get', 'if you multiply six by nine?'] + subject.start_workflow( + TestStartWorkflow, + 42, + 43, + options: { + signal_name: 'the question', + # signals can't have multiple scalar args, but you can pass an array + signal_input: signal_input + } + ) + + expect_signal_with_start([42, 43], signal_input) + end + + it 'raises when signal_input is given but signal_name is not' do + expect do + subject.start_workflow( + TestStartWorkflow, + [42, 54], + [43, 55], + options: { signal_input: 'what do you get if you multiply six by nine?', } + ) + end.to raise_error(ArgumentError) + end + end + describe '#schedule_workflow' do let(:temporal_response) do Temporal::Api::WorkflowService::V1::StartWorkflowExecutionResponse.new(run_id: 'xxx') diff --git a/spec/unit/lib/temporal/grpc_client_spec.rb b/spec/unit/lib/temporal/grpc_client_spec.rb index bcc0458d..8349cc1e 100644 --- a/spec/unit/lib/temporal/grpc_client_spec.rb +++ b/spec/unit/lib/temporal/grpc_client_spec.rb @@ -34,6 +34,43 @@ end end end + + describe '#signal_with_start_workflow' do + let(:temporal_response) do + Temporal::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionResponse.new(run_id: 'xxx') + end + + before { allow(grpc_stub).to receive(:signal_with_start_workflow_execution).and_return(temporal_response) } + + it 'starts a workflow with a signal with scalar arguments' do + subject.signal_with_start_workflow_execution( + namespace: namespace, + workflow_id: workflow_id, + workflow_name: 'workflow_name', + task_queue: 'task_queue', + input: ['foo'], + execution_timeout: 1, + run_timeout: 2, + task_timeout: 3, + signal_name: 'the question', + signal_input: 'what do you get if you multiply six by nine?' + ) + + expect(grpc_stub).to have_received(:signal_with_start_workflow_execution) do |request| + expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionRequest) + expect(request.namespace).to eq(namespace) + expect(request.workflow_id).to eq(workflow_id) + expect(request.workflow_type.name).to eq('workflow_name') + expect(request.task_queue.name).to eq('task_queue') + expect(request.input.payloads[0].data).to eq('"foo"') + expect(request.workflow_execution_timeout.seconds).to eq(1) + expect(request.workflow_run_timeout.seconds).to eq(2) + expect(request.workflow_task_timeout.seconds).to eq(3) + expect(request.signal_name).to eq('the question') + expect(request.signal_input.payloads[0].data).to eq('"what do you get if you multiply six by nine?"') + end + end + end describe '#get_workflow_execution_history' do let(:response) do diff --git a/spec/unit/lib/temporal/testing/temporal_override_spec.rb b/spec/unit/lib/temporal/testing/temporal_override_spec.rb index 09e4eeee..42d72a9c 100644 --- a/spec/unit/lib/temporal/testing/temporal_override_spec.rb +++ b/spec/unit/lib/temporal/testing/temporal_override_spec.rb @@ -136,6 +136,14 @@ def execute .with(an_instance_of(Temporal::Testing::LocalWorkflowContext)) end + it 'explicitly does not support staring a workflow with a signal' do + expect { + client.start_workflow(TestTemporalOverrideWorkflow, options: { signal_name: 'breakme' }) + }.to raise_error(NotImplementedError) do |e| + expect(e.message).to eql("Signals are not available when Temporal::Testing.local! is on") + end + end + describe 'execution control' do subject do client.start_workflow( From 8c36d90577983a797eac7e287572fac431e0ffc9 Mon Sep 17 00:00:00 2001 From: jeffschoner-stripe <63118764+jeffschoner-stripe@users.noreply.github.com> Date: Fri, 12 Nov 2021 03:25:57 -0800 Subject: [PATCH 009/125] Extend #wait_for to take multiple futures and a condition block (#111) --- examples/bin/worker | 1 + .../integration/wait_for_workflow_spec.rb | 28 +++++++ examples/workflows/wait_for_workflow.rb | 80 +++++++++++++++++++ .../testing/local_workflow_context.rb | 13 ++- lib/temporal/workflow/context.rb | 48 ++++++++++- lib/temporal/workflow/dispatcher.rb | 1 + .../testing/local_workflow_context_spec.rb | 65 ++++++++++++++- .../lib/temporal/workflow/dispatcher_spec.rb | 17 ++++ 8 files changed, 243 insertions(+), 10 deletions(-) create mode 100644 examples/spec/integration/wait_for_workflow_spec.rb create mode 100644 examples/workflows/wait_for_workflow.rb diff --git a/examples/bin/worker b/examples/bin/worker index 497eb9fc..a119403b 100755 --- a/examples/bin/worker +++ b/examples/bin/worker @@ -43,6 +43,7 @@ worker.register_workflow(SignalWithStartWorkflow) worker.register_workflow(SimpleTimerWorkflow) worker.register_workflow(TimeoutWorkflow) worker.register_workflow(TripBookingWorkflow) +worker.register_workflow(WaitForWorkflow) worker.register_activity(AsyncActivity) worker.register_activity(EchoActivity) diff --git a/examples/spec/integration/wait_for_workflow_spec.rb b/examples/spec/integration/wait_for_workflow_spec.rb new file mode 100644 index 00000000..d5feeee6 --- /dev/null +++ b/examples/spec/integration/wait_for_workflow_spec.rb @@ -0,0 +1,28 @@ +require 'workflows/wait_for_workflow' + +describe WaitForWorkflow do + + it 'signals at workflow start time' do + workflow_id = SecureRandom.uuid + run_id = Temporal.start_workflow( + WaitForWorkflow, + 10, # number of echo activities to run + 2, # max activity parallelism + 'signal_name', + options: { workflow_id: workflow_id } + ) + + Temporal.signal_workflow(WaitForWorkflow, 'signal_name', workflow_id, run_id) + + result = Temporal.await_workflow_result( + WaitForWorkflow, + workflow_id: workflow_id, + run_id: run_id, + ) + + expect(result.length).to eq(3) + expect(result[:signal]).to eq(true) + expect(result[:timer]).to eq(true) + expect(result[:activity]).to eq(true) + end +end \ No newline at end of file diff --git a/examples/workflows/wait_for_workflow.rb b/examples/workflows/wait_for_workflow.rb new file mode 100644 index 00000000..226ac9c4 --- /dev/null +++ b/examples/workflows/wait_for_workflow.rb @@ -0,0 +1,80 @@ +require 'activities/echo_activity' +require 'activities/long_running_activity' + +# This example workflow exercises all three conditions that can change state that is being +# awaited upon: activity completion, sleep completion, signal receieved. +class WaitForWorkflow < Temporal::Workflow + def execute(total_echos, max_echos_at_once, expected_signal) + signals_received = {} + + workflow.on_signal do |signal, input| + signals_received[signal] = input + end + + workflow.wait_for do + workflow.logger.info("Awaiting #{expected_signal}, signals received so far: #{signals_received}") + signals_received.key?(expected_signal) + end + + # Run an activity but with a max time limit by starting a timer. This activity + # will not complete before the timer, which may result in a failed activity task after the + # workflow is completed. + long_running_future = LongRunningActivity.execute(15, 0.1) + timeout_timer = workflow.start_timer(1) + workflow.wait_for(timeout_timer, long_running_future) + + timer_beat_activity = timeout_timer.finished? && !long_running_future.finished? + + # This should not wait further. The first future has already finished, and therefore + # the second one should not be awaited upon. + long_timeout_timer = workflow.start_timer(15) + workflow.wait_for(timeout_timer, long_timeout_timer) + raise 'The workflow should not have waited for this timer to complete' if long_timeout_timer.finished? + + block_called = false + workflow.wait_for(timeout_timer) do + # This should never be called because the timeout_timer future was already + # finished before the wait was even called. + block_called = true + end + raise 'Block should not have been called' if block_called + + workflow.wait_for(long_timeout_timer) do + # This condition will immediately be true and not result in any waiting or dispatching + true + end + raise 'The workflow should not have waited for this timer to complete' if long_timeout_timer.finished? + + activity_futures = {} + echos_completed = 0 + + total_echos.times do |i| + workflow.wait_for do + workflow.logger.info("Activities in flight #{activity_futures.length}") + # Pause workflow until the number of active activity futures is less than 2. This + # will throttle new activities from being started, guaranteeing that only two of these + # activities are running at once. + activity_futures.length < max_echos_at_once + end + + future = EchoActivity.execute("hi #{i}") + activity_futures[i] = future + + future.done do + activity_futures.delete(i) + echos_completed += 1 + end + end + + workflow.wait_for do + workflow.logger.info("Waiting for queue to drain, size: #{activity_futures.length}") + activity_futures.empty? + end + + { + signal: signals_received.key?(expected_signal), + timer: timer_beat_activity, + activity: echos_completed == total_echos + } + end +end diff --git a/lib/temporal/testing/local_workflow_context.rb b/lib/temporal/testing/local_workflow_context.rb index e4ded199..5543452d 100644 --- a/lib/temporal/testing/local_workflow_context.rb +++ b/lib/temporal/testing/local_workflow_context.rb @@ -165,9 +165,16 @@ def wait_for_all(*futures) return end - def wait_for(future) - # Point of communication - Fiber.yield while !future.finished? + def wait_for(*futures, &unblock_condition) + if futures.empty? && unblock_condition.nil? + raise 'You must pass either a future or an unblock condition block to wait_for' + end + + while (futures.empty? || futures.none?(&:finished?)) && (!unblock_condition || !unblock_condition.call) + Fiber.yield + end + + return end def now diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index 709211c8..4b8e8cc6 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -215,14 +215,54 @@ def wait_for_all(*futures) return end - def wait_for(future) + # Block workflow progress until any future is finished or any unblock_condition + # block evaluates to true. + def wait_for(*futures, &unblock_condition) + if futures.empty? && unblock_condition.nil? + raise 'You must pass either a future or an unblock condition block to wait_for' + end + fiber = Fiber.current + should_yield = false + blocked = true + + if futures.any? + if futures.any?(&:finished?) + blocked = false + else + should_yield = true + futures.each do |future| + dispatcher.register_handler(future.target, Dispatcher::WILDCARD) do + if blocked && future.finished? + # Because this block can run for any dispatch, ensure the fiber is only + # resumed one time by checking if it's already been unblocked. + blocked = false + fiber.resume + end + end + end + end + end - dispatcher.register_handler(future.target, Dispatcher::WILDCARD) do - fiber.resume if future.finished? + if blocked && unblock_condition + if unblock_condition.call + blocked = false + should_yield = false + else + should_yield = true + + dispatcher.register_handler(Dispatcher::WILDCARD, Dispatcher::WILDCARD) do + # Because this block can run for any dispatch, ensure the fiber is only + # resumed one time by checking if it's already been unblocked. + if blocked && unblock_condition.call + blocked = false + fiber.resume + end + end + end end - Fiber.yield + Fiber.yield if should_yield return end diff --git a/lib/temporal/workflow/dispatcher.rb b/lib/temporal/workflow/dispatcher.rb index 55c581fb..03f11864 100644 --- a/lib/temporal/workflow/dispatcher.rb +++ b/lib/temporal/workflow/dispatcher.rb @@ -23,6 +23,7 @@ def dispatch(target, event_name, args = nil) def handlers_for(target, event_name) handlers[target] + .concat(handlers[WILDCARD]) .select { |(name, _)| name == event_name || name == WILDCARD } .map(&:last) end diff --git a/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb b/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb index 70747841..9fd322a3 100644 --- a/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb +++ b/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb @@ -120,10 +120,69 @@ def execute result = workflow_context.execute_activity!(TestActivity) expect(result).to eq('ok') end + + it 'can heartbeat' do + # Heartbeat doesn't do anything in local mode, but at least it can be called. + workflow_context.execute_activity!(TestHeartbeatingActivity) + end end - it 'can heartbeat' do - # Heartbeat doesn't do anything in local mode, but at least it can be called. - workflow_context.execute_activity!(TestHeartbeatingActivity) + describe '#wait_for' do + it 'await unblocks once condition changes' do + can_continue = false + exited = false + fiber = Fiber.new do + workflow_context.wait_for do + can_continue + end + + exited = true + end + + fiber.resume # start running + expect(exited).to eq(false) + + can_continue = true # change condition + fiber.resume # resume running after the Fiber.yield done in context.await + expect(exited).to eq(true) + end + + it 'condition or future unblocks' do + exited = false + + future = workflow_context.execute_activity(TestAsyncActivity) + + fiber = Fiber.new do + workflow_context.wait_for(future) do + false + end + + exited = true + end + + fiber.resume # start running + expect(exited).to eq(false) + + execution.complete_activity(async_token, 'async_ok') + + fiber.resume # resume running after the Fiber.yield done in context.await + expect(exited).to eq(true) + end + + it 'any future unblocks' do + exited = false + + async_future = workflow_context.execute_activity(TestAsyncActivity) + future = workflow_context.execute_activity(TestActivity) + future.wait + + fiber = Fiber.new do + workflow_context.wait_for(future, async_future) + exited = true + end + + fiber.resume # start running + expect(exited).to eq(true) + end end end diff --git a/spec/unit/lib/temporal/workflow/dispatcher_spec.rb b/spec/unit/lib/temporal/workflow/dispatcher_spec.rb index d5e008f8..c6a5f493 100644 --- a/spec/unit/lib/temporal/workflow/dispatcher_spec.rb +++ b/spec/unit/lib/temporal/workflow/dispatcher_spec.rb @@ -62,7 +62,24 @@ expect(handler_5).to have_received(:call) end + end + + context 'with WILDCARD target handler' do + let(:handler_6) { -> { 'sixth block' } } + before do + allow(handler_6).to receive(:call) + + subject.register_handler(described_class::WILDCARD, described_class::WILDCARD, &handler_6) + end + it 'calls the handler' do + subject.dispatch('target', 'completed') + + # Target handlers still invoked + expect(handler_1).to have_received(:call).ordered + expect(handler_4).to have_received(:call).ordered + expect(handler_6).to have_received(:call).ordered + end end end end From 388d9edc39862cca9f2078e0dfff9573fb64ee59 Mon Sep 17 00:00:00 2001 From: jeffschoner-stripe <63118764+jeffschoner-stripe@users.noreply.github.com> Date: Mon, 15 Nov 2021 02:42:36 -0800 Subject: [PATCH 010/125] Differentiate TARGET_WILDCARD and WILDCARD, allow comparison with EventTarget objects (#118) --- lib/temporal/workflow/context.rb | 2 +- lib/temporal/workflow/dispatcher.rb | 3 +- lib/temporal/workflow/history/event_target.rb | 2 +- .../lib/temporal/workflow/dispatcher_spec.rb | 34 ++++++++++++------- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index 4b8e8cc6..18ec3c05 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -251,7 +251,7 @@ def wait_for(*futures, &unblock_condition) else should_yield = true - dispatcher.register_handler(Dispatcher::WILDCARD, Dispatcher::WILDCARD) do + dispatcher.register_handler(Dispatcher::TARGET_WILDCARD, Dispatcher::WILDCARD) do # Because this block can run for any dispatch, ensure the fiber is only # resumed one time by checking if it's already been unblocked. if blocked && unblock_condition.call diff --git a/lib/temporal/workflow/dispatcher.rb b/lib/temporal/workflow/dispatcher.rb index 03f11864..2a768e54 100644 --- a/lib/temporal/workflow/dispatcher.rb +++ b/lib/temporal/workflow/dispatcher.rb @@ -2,6 +2,7 @@ module Temporal class Workflow class Dispatcher WILDCARD = '*'.freeze + TARGET_WILDCARD = '*'.freeze def initialize @handlers = Hash.new { |hash, key| hash[key] = [] } @@ -23,7 +24,7 @@ def dispatch(target, event_name, args = nil) def handlers_for(target, event_name) handlers[target] - .concat(handlers[WILDCARD]) + .concat(handlers[TARGET_WILDCARD]) .select { |(name, _)| name == event_name || name == WILDCARD } .map(&:last) end diff --git a/lib/temporal/workflow/history/event_target.rb b/lib/temporal/workflow/history/event_target.rb index a54dab55..96d5de93 100644 --- a/lib/temporal/workflow/history/event_target.rb +++ b/lib/temporal/workflow/history/event_target.rb @@ -61,7 +61,7 @@ def initialize(id, type) end def ==(other) - id == other.id && type == other.type + self.class == other.class && id == other.id && type == other.type end def eql?(other) diff --git a/spec/unit/lib/temporal/workflow/dispatcher_spec.rb b/spec/unit/lib/temporal/workflow/dispatcher_spec.rb index c6a5f493..43ccc8fc 100644 --- a/spec/unit/lib/temporal/workflow/dispatcher_spec.rb +++ b/spec/unit/lib/temporal/workflow/dispatcher_spec.rb @@ -1,13 +1,17 @@ require 'temporal/workflow/dispatcher' +require 'temporal/workflow/history/event_target' describe Temporal::Workflow::Dispatcher do + let(:target) { Temporal::Workflow::History::EventTarget.new(1, Temporal::Workflow::History::EventTarget::ACTIVITY_TYPE) } + let(:other_target) { Temporal::Workflow::History::EventTarget.new(2, Temporal::Workflow::History::EventTarget::TIMER_TYPE) } + describe '#register_handler' do it 'stores a given handler against the target' do block = -> { 'handler body' } - subject.register_handler('target', 'signaled', &block) + subject.register_handler(target, 'signaled', &block) - expect(subject.send(:handlers)).to include('target' => [['signaled', block]]) + expect(subject.send(:handlers)).to include(target => [['signaled', block]]) end end @@ -22,14 +26,14 @@ allow(handler).to receive(:call) end - subject.register_handler('target', 'completed', &handler_1) - subject.register_handler('other_target', 'completed', &handler_2) - subject.register_handler('target', 'failed', &handler_3) - subject.register_handler('target', 'completed', &handler_4) + subject.register_handler(target, 'completed', &handler_1) + subject.register_handler(other_target, 'completed', &handler_2) + subject.register_handler(target, 'failed', &handler_3) + subject.register_handler(target, 'completed', &handler_4) end it 'calls all matching handlers in the original order' do - subject.dispatch('target', 'completed') + subject.dispatch(target, 'completed') expect(handler_1).to have_received(:call).ordered expect(handler_4).to have_received(:call).ordered @@ -39,7 +43,7 @@ end it 'passes given arguments to the handlers' do - subject.dispatch('target', 'failed', ['TIME_OUT', 'Exceeded execution time']) + subject.dispatch(target, 'failed', ['TIME_OUT', 'Exceeded execution time']) expect(handler_3).to have_received(:call).with('TIME_OUT', 'Exceeded execution time') @@ -54,32 +58,36 @@ before do allow(handler_5).to receive(:call) - subject.register_handler('target', described_class::WILDCARD, &handler_5) + subject.register_handler(target, described_class::WILDCARD, &handler_5) end it 'calls the handler' do - subject.dispatch('target', 'completed') + subject.dispatch(target, 'completed') expect(handler_5).to have_received(:call) end end - context 'with WILDCARD target handler' do + context 'with TARGET_WILDCARD target handler' do let(:handler_6) { -> { 'sixth block' } } before do allow(handler_6).to receive(:call) - subject.register_handler(described_class::WILDCARD, described_class::WILDCARD, &handler_6) + subject.register_handler(described_class::TARGET_WILDCARD, described_class::WILDCARD, &handler_6) end it 'calls the handler' do - subject.dispatch('target', 'completed') + subject.dispatch(target, 'completed') # Target handlers still invoked expect(handler_1).to have_received(:call).ordered expect(handler_4).to have_received(:call).ordered expect(handler_6).to have_received(:call).ordered end + + it 'TARGET_WILDCARD can be compared to an EventTarget object' do + expect(target.eql?(described_class::TARGET_WILDCARD)).to be(false) + end end end end From be42f005690e630e039658c6fc8b998cf21556e8 Mon Sep 17 00:00:00 2001 From: Drew Hoskins <37816070+drewhoskins-stripe@users.noreply.github.com> Date: Wed, 17 Nov 2021 06:51:53 -0800 Subject: [PATCH 011/125] Turn off schedule_to_start activity timeout by default (#119) --- lib/temporal/configuration.rb | 8 ++++-- spec/unit/lib/temporal/configuration_spec.rb | 29 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 spec/unit/lib/temporal/configuration_spec.rb diff --git a/lib/temporal/configuration.rb b/lib/temporal/configuration.rb index 828c561f..8d36615c 100644 --- a/lib/temporal/configuration.rb +++ b/lib/temporal/configuration.rb @@ -14,6 +14,7 @@ class Configuration attr_writer :converter attr_accessor :connection_type, :host, :port, :logger, :metrics_adapter, :namespace, :task_queue, :headers + # See https://docs.temporal.io/blog/activity-timeouts/ for general docs. # We want an infinite execution timeout for cron schedules and other perpetual workflows. # We choose an 10-year execution timeout because that's the maximum the cassandra DB supports, # matching the go SDK, see https://github.com/temporalio/sdk-go/blob/d96130dad3d2bc189bc7626543bd5911cc07ff6d/internal/internal_workflow_testsuite.go#L68 @@ -21,9 +22,12 @@ class Configuration execution: 86_400 * 365 * 10, # End-to-end workflow time, including all recurrences if it's scheduled. # Time for a single run, excluding retries. Server defaults to execution timeout; we default here as well to be explicit. run: 86_400 * 365 * 10, - task: 10, # Workflow task processing time + # Workflow task processing time. Workflows should not use the network and should execute very quickly. + task: 10, schedule_to_close: nil, # End-to-end activity time (default: schedule_to_start + start_to_close) - schedule_to_start: 10, # Queue time for an activity + # Max queue time for an activity. Default: none. This is dangerous; most teams don't use. + # See # https://docs.temporal.io/blog/activity-timeouts/#schedule-to-start-timeout + schedule_to_start: nil, start_to_close: 30, # Time spent processing an activity heartbeat: nil # Max time between heartbeats (off by default) }.freeze diff --git a/spec/unit/lib/temporal/configuration_spec.rb b/spec/unit/lib/temporal/configuration_spec.rb new file mode 100644 index 00000000..7e083429 --- /dev/null +++ b/spec/unit/lib/temporal/configuration_spec.rb @@ -0,0 +1,29 @@ +require 'temporal/configuration' + +describe Temporal::Configuration do + describe '#initialize' do + it 'initializes proper default workflow timeouts' do + timeouts = subject.timeouts + + # By default, we don't ever want to timeout workflows, because workflows "always succeed" and + # they may be long-running + expect(timeouts[:execution]).to be >= 86_400 * 365 * 10 + expect(timeouts[:run]).to eq(timeouts[:execution]) + expect(timeouts[:task]).to eq(10) + end + + it 'initializes proper default activity timeouts' do + timeouts = subject.timeouts + + # Schedule to start timeouts are dangerous because there is no retry. + # https://docs.temporal.io/blog/activity-timeouts/#schedule-to-start-timeout recommends to use them rarely + expect(timeouts[:schedule_to_start]).to be(nil) + # We keep retrying until the workflow times out, by default + expect(timeouts[:schedule_to_close]).to be(nil) + # Activity invocations should be short-lived by default so they can be retried relatively quickly + expect(timeouts[:start_to_close]).to eq(30) + # No heartbeating for a default (short-lived) activity + expect(timeouts[:heartbeat]).to be(nil) + end + end +end \ No newline at end of file From 824652b811db848342d5c79bff700394d97a134d Mon Sep 17 00:00:00 2001 From: Anthony Dmitriyev Date: Wed, 17 Nov 2021 15:18:12 +0000 Subject: [PATCH 012/125] Separate options from keyword args in #start_workflow (#117) * Separate options from keyword args in #start_workflow * fixup! Separate options from keyword args in #start_workflow --- lib/temporal/client.rb | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/temporal/client.rb b/lib/temporal/client.rb index e7e3c26c..9971a37c 100644 --- a/lib/temporal/client.rb +++ b/lib/temporal/client.rb @@ -15,9 +15,8 @@ def initialize(config) # Start a workflow with an optional signal # - # If options[:signal_name] is specified, Temporal will atomically do one of: - # A) start a new workflow and signal it - # B) if workflow_id is specified and the workflow already exists, signal the existing workflow. + # If options[:signal_name] is specified, Temporal will atomically start a new workflow and + # signal it or signal a running workflow (matching a specified options[:workflow_id]) # # @param workflow [Temporal::Workflow, String] workflow class or name. When a workflow class # is passed, its config (namespace, task_queue, timeouts, etc) will be used @@ -37,8 +36,7 @@ def initialize(config) # @option options [Hash] :headers # # @return [String] workflow's run ID - def start_workflow(workflow, *input, **args) - options = args.delete(:options) || {} + def start_workflow(workflow, *input, options: {}, **args) input << args unless args.empty? signal_name = options.delete(:signal_name) @@ -101,8 +99,7 @@ def start_workflow(workflow, *input, **args) # @option options [Hash] :headers # # @return [String] workflow's run ID - def schedule_workflow(workflow, cron_schedule, *input, **args) - options = args.delete(:options) || {} + def schedule_workflow(workflow, cron_schedule, *input, options: {}, **args) input << args unless args.empty? execution_options = ExecutionOptions.new(workflow, options, config.default_execution_options) @@ -160,8 +157,8 @@ def signal_workflow(workflow, signal, workflow_id, run_id, input = nil, namespac # Long polls for a workflow to be completed and returns workflow's return value. # # @note This function times out after 30 seconds and throws Temporal::TimeoutError, - # not to be confused with Temporal::WorkflowTimedOut which reports that the workflow - # itself timed out. + # not to be confused with `Temporal::WorkflowTimedOut` which reports that the workflow + # itself timed out. # # @param workflow [Temporal::Workflow, nil] workflow class or nil # @param workflow_id [String] From 5648972af3bb14b97374a86aa186093ee1c8c3c8 Mon Sep 17 00:00:00 2001 From: jeffschoner-stripe <63118764+jeffschoner-stripe@users.noreply.github.com> Date: Sun, 21 Nov 2021 06:12:34 -0800 Subject: [PATCH 013/125] Surface additional workflow metadata on workflow context (#120) * Refactor metadata generation * Make task queue available on workflow metadata, add example test * Expose workflow start time metadata --- examples/bin/worker | 1 + .../integration/metadata_workflow_spec.rb | 54 +++++++++ examples/workflows/metadata_workflow.rb | 5 + lib/temporal/activity/task_processor.rb | 2 +- lib/temporal/metadata.rb | 53 ++++----- lib/temporal/metadata/workflow.rb | 10 +- lib/temporal/testing/temporal_override.rb | 2 + lib/temporal/testing/workflow_override.rb | 5 +- lib/temporal/workflow/executor.rb | 27 ++--- lib/temporal/workflow/state_manager.rb | 2 +- lib/temporal/workflow/task_processor.rb | 2 +- .../grpc/history_event_fabricator.rb | 11 +- ...ion_started_event_attributes_fabricator.rb | 1 + .../workflow_metadata_fabricator.rb | 2 + .../temporal/activity/task_processor_spec.rb | 6 +- .../lib/temporal/metadata/workflow_spec.rb | 5 +- spec/unit/lib/temporal/metadata_spec.rb | 105 ++++++++++-------- .../testing/local_workflow_context_spec.rb | 12 +- .../lib/temporal/workflow/executor_spec.rb | 8 +- 19 files changed, 208 insertions(+), 105 deletions(-) create mode 100644 examples/spec/integration/metadata_workflow_spec.rb create mode 100644 examples/workflows/metadata_workflow.rb diff --git a/examples/bin/worker b/examples/bin/worker index a119403b..14752ede 100755 --- a/examples/bin/worker +++ b/examples/bin/worker @@ -31,6 +31,7 @@ worker.register_workflow(HelloWorldWorkflow) worker.register_workflow(LocalHelloWorldWorkflow) worker.register_workflow(LongWorkflow) worker.register_workflow(LoopWorkflow) +worker.register_workflow(MetadataWorkflow) worker.register_workflow(ParentWorkflow) worker.register_workflow(ProcessFileWorkflow) worker.register_workflow(QuickTimeoutWorkflow) diff --git a/examples/spec/integration/metadata_workflow_spec.rb b/examples/spec/integration/metadata_workflow_spec.rb new file mode 100644 index 00000000..a7d8ee1b --- /dev/null +++ b/examples/spec/integration/metadata_workflow_spec.rb @@ -0,0 +1,54 @@ +require 'workflows/metadata_workflow' + +describe MetadataWorkflow do + subject { described_class } + + it 'gets task queue from running workflow' do + workflow_id = 'task-queue-' + SecureRandom.uuid + run_id = Temporal.start_workflow( + subject, + { options: { workflow_id: workflow_id } }, + ) + actual_result = Temporal.await_workflow_result( + subject, + workflow_id: workflow_id, + run_id: run_id, + ) + expect(actual_result.task_queue).to eq(Temporal.configuration.task_queue) + end + + it 'workflow can retrieve its headers' do + workflow_id = 'header_test_wf-' + SecureRandom.uuid + + run_id = Temporal.start_workflow( + MetadataWorkflow, + options: { + workflow_id: workflow_id, + headers: { 'foo' => Temporal.configuration.converter.to_payload('bar') }, + } + ) + + actual_result = Temporal.await_workflow_result( + MetadataWorkflow, + workflow_id: workflow_id, + run_id: run_id, + ) + expect(actual_result.headers).to eq({ 'foo' => 'bar' }) + end + + it 'workflow can retrieve its run started at' do + workflow_id = 'started_at_test_wf-' + SecureRandom.uuid + + run_id = Temporal.start_workflow( + MetadataWorkflow, + options: { workflow_id: workflow_id } + ) + + actual_result = Temporal.await_workflow_result( + MetadataWorkflow, + workflow_id: workflow_id, + run_id: run_id, + ) + expect(Time.now - actual_result.run_started_at).to be_between(0, 30) + end +end diff --git a/examples/workflows/metadata_workflow.rb b/examples/workflows/metadata_workflow.rb new file mode 100644 index 00000000..4cdfab59 --- /dev/null +++ b/examples/workflows/metadata_workflow.rb @@ -0,0 +1,5 @@ +class MetadataWorkflow < Temporal::Workflow + def execute + workflow.metadata + end +end \ No newline at end of file diff --git a/lib/temporal/activity/task_processor.rb b/lib/temporal/activity/task_processor.rb index f676cf21..6ad9cefd 100644 --- a/lib/temporal/activity/task_processor.rb +++ b/lib/temporal/activity/task_processor.rb @@ -14,7 +14,7 @@ class TaskProcessor def initialize(task, namespace, activity_lookup, middleware_chain, config) @task = task @namespace = namespace - @metadata = Metadata.generate(Metadata::ACTIVITY_TYPE, task, namespace) + @metadata = Metadata.generate_activity_metadata(task, namespace) @task_token = task.task_token @activity_name = task.activity_type.name @activity_class = activity_lookup.find(activity_name) diff --git a/lib/temporal/metadata.rb b/lib/temporal/metadata.rb index c467d25e..0617b784 100644 --- a/lib/temporal/metadata.rb +++ b/lib/temporal/metadata.rb @@ -6,34 +6,11 @@ module Temporal module Metadata - ACTIVITY_TYPE = :activity - WORKFLOW_TASK_TYPE = :workflow_task class << self include Concerns::Payloads - def generate(type, data, namespace) - case type - when ACTIVITY_TYPE - activity_metadata_from(data, namespace) - when WORKFLOW_TASK_TYPE - workflow_task_metadata_from(data, namespace) - else - raise InternalError, 'Unsupported metadata type' - end - end - - private - - def headers(fields) - result = {} - fields.each do |field, payload| - result[field] = from_payload(payload) - end - result - end - - def activity_metadata_from(task, namespace) + def generate_activity_metadata(task, namespace) Metadata::Activity.new( namespace: namespace, id: task.activity_id, @@ -48,7 +25,7 @@ def activity_metadata_from(task, namespace) ) end - def workflow_task_metadata_from(task, namespace) + def generate_workflow_task_metadata(task, namespace) Metadata::WorkflowTask.new( namespace: namespace, id: task.started_event_id, @@ -59,6 +36,32 @@ def workflow_task_metadata_from(task, namespace) workflow_name: task.workflow_type.name ) end + + # @param event [Temporal::Workflow::History::Event] Workflow started history event + # @param event [WorkflowExecutionStartedEventAttributes] :attributes + # @param task_metadata [Temporal::Metadata::WorkflowTask] workflow task metadata + def generate_workflow_metadata(event, task_metadata) + Metadata::Workflow.new( + name: event.attributes.workflow_type.name, + id: task_metadata.workflow_id, + run_id: event.attributes.original_execution_run_id, + attempt: event.attributes.attempt, + namespace: task_metadata.namespace, + task_queue: event.attributes.task_queue.name, + headers: headers(event.attributes.header&.fields), + run_started_at: event.timestamp, + ) + end + + private + + def headers(fields) + result = {} + fields.each do |field, payload| + result[field] = from_payload(payload) + end + result + end end end end diff --git a/lib/temporal/metadata/workflow.rb b/lib/temporal/metadata/workflow.rb index f70c7f2f..9e95acb1 100644 --- a/lib/temporal/metadata/workflow.rb +++ b/lib/temporal/metadata/workflow.rb @@ -3,15 +3,17 @@ module Temporal module Metadata class Workflow < Base - attr_reader :namespace, :id, :name, :run_id, :attempt, :headers + attr_reader :namespace, :id, :name, :run_id, :attempt, :task_queue, :headers, :run_started_at - def initialize(namespace:, id:, name:, run_id:, attempt:, headers: {}) + def initialize(namespace:, id:, name:, run_id:, attempt:, task_queue:, headers:, run_started_at:) @namespace = namespace @id = id @name = name @run_id = run_id @attempt = attempt + @task_queue = task_queue @headers = headers + @run_started_at = run_started_at freeze end @@ -26,7 +28,9 @@ def to_h 'workflow_id' => id, 'workflow_name' => name, 'workflow_run_id' => run_id, - 'attempt' => attempt + 'attempt' => attempt, + 'task_queue' => task_queue, + 'run_started_at' => run_started_at.to_f, } end end diff --git a/lib/temporal/testing/temporal_override.rb b/lib/temporal/testing/temporal_override.rb index 825241be..dd67be7f 100644 --- a/lib/temporal/testing/temporal_override.rb +++ b/lib/temporal/testing/temporal_override.rb @@ -99,6 +99,8 @@ def start_locally(workflow, schedule, *input, **args) name: execution_options.name, run_id: run_id, attempt: 1, + task_queue: execution_options.task_queue, + run_started_at: Time.now, headers: execution_options.headers ) context = Temporal::Testing::LocalWorkflowContext.new( diff --git a/lib/temporal/testing/workflow_override.rb b/lib/temporal/testing/workflow_override.rb index eb9a9fd4..dab7472b 100644 --- a/lib/temporal/testing/workflow_override.rb +++ b/lib/temporal/testing/workflow_override.rb @@ -32,7 +32,10 @@ def execute_locally(*input) id: workflow_id, name: name, # Workflow class name run_id: run_id, - attempt: 1 + attempt: 1, + task_queue: 'unit-test-task-queue', + headers: {}, + run_started_at: Time.now, ) context = Temporal::Testing::LocalWorkflowContext.new( execution, workflow_id, run_id, disabled_releases, metadata diff --git a/lib/temporal/workflow/executor.rb b/lib/temporal/workflow/executor.rb index 313e6977..78feb61b 100644 --- a/lib/temporal/workflow/executor.rb +++ b/lib/temporal/workflow/executor.rb @@ -9,12 +9,16 @@ module Temporal class Workflow class Executor - def initialize(workflow_class, history, metadata, config) + # @param workflow_class [Class] + # @param history [Workflow::History] + # @param task_metadata [Metadata::WorkflowTask] + # @param config [Configuration] + def initialize(workflow_class, history, task_metadata, config) @workflow_class = workflow_class @dispatcher = Dispatcher.new @state_manager = StateManager.new(dispatcher) - @metadata = metadata @history = history + @task_metadata = task_metadata @config = config end @@ -34,29 +38,16 @@ def run private - attr_reader :workflow_class, :dispatcher, :state_manager, :metadata, :history, :config + attr_reader :workflow_class, :dispatcher, :state_manager, :task_metadata, :history, :config - def execute_workflow(input, workflow_started_event_attributes) - metadata = generate_workflow_metadata_from(workflow_started_event_attributes) + def execute_workflow(input, workflow_started_event) + metadata = Metadata.generate_workflow_metadata(workflow_started_event, task_metadata) context = Workflow::Context.new(state_manager, dispatcher, workflow_class, metadata, config) Fiber.new do workflow_class.execute_in_context(context, input) end.resume end - - # workflow_id and domain are confusingly not available on the WorkflowExecutionStartedEvent, - # so we have to fetch these from the DecisionTask's metadata - def generate_workflow_metadata_from(event_attributes) - Metadata::Workflow.new( - namespace: metadata.namespace, - id: metadata.workflow_id, - name: event_attributes.workflow_type.name, - run_id: event_attributes.original_execution_run_id, - attempt: event_attributes.attempt, - headers: event_attributes.header&.fields || {} - ) - end end end end diff --git a/lib/temporal/workflow/state_manager.rb b/lib/temporal/workflow/state_manager.rb index e88850fd..7d515c06 100644 --- a/lib/temporal/workflow/state_manager.rb +++ b/lib/temporal/workflow/state_manager.rb @@ -105,7 +105,7 @@ def apply_event(event) History::EventTarget.workflow, 'started', from_payloads(event.attributes.input), - event.attributes + event, ) when 'WORKFLOW_EXECUTION_COMPLETED' diff --git a/lib/temporal/workflow/task_processor.rb b/lib/temporal/workflow/task_processor.rb index 1701e18d..63b4c76e 100644 --- a/lib/temporal/workflow/task_processor.rb +++ b/lib/temporal/workflow/task_processor.rb @@ -12,7 +12,7 @@ class TaskProcessor def initialize(task, namespace, workflow_lookup, middleware_chain, config) @task = task @namespace = namespace - @metadata = Metadata.generate(Metadata::WORKFLOW_TASK_TYPE, task, namespace) + @metadata = Metadata.generate_workflow_task_metadata(task, namespace) @task_token = task.task_token @workflow_name = task.workflow_type.name @workflow_class = workflow_lookup.find(workflow_name) diff --git a/spec/fabricators/grpc/history_event_fabricator.rb b/spec/fabricators/grpc/history_event_fabricator.rb index 0d7e9e48..a290ea0f 100644 --- a/spec/fabricators/grpc/history_event_fabricator.rb +++ b/spec/fabricators/grpc/history_event_fabricator.rb @@ -6,8 +6,15 @@ end Fabricator(:api_workflow_execution_started_event, from: :api_history_event) do + transient :headers event_type { Temporal::Api::Enums::V1::EventType::EVENT_TYPE_WORKFLOW_EXECUTION_STARTED } - workflow_execution_started_event_attributes do + event_time { Time.now } + workflow_execution_started_event_attributes do |attrs| + header_fields = (attrs[:headers] || {}).each_with_object({}) do |(field, value), h| + h[field] = Temporal.configuration.converter.to_payload(value) + end + header = Temporal::Api::Common::V1::Header.new(fields: header_fields) + Temporal::Api::History::V1::WorkflowExecutionStartedEventAttributes.new( workflow_type: Fabricate(:api_workflow_type), task_queue: Fabricate(:api_task_queue), @@ -19,7 +26,7 @@ first_execution_run_id: SecureRandom.uuid, retry_policy: nil, attempt: 0, - header: Fabricate(:api_header) + header: header, ) end end diff --git a/spec/fabricators/grpc/workflow_execution_started_event_attributes_fabricator.rb b/spec/fabricators/grpc/workflow_execution_started_event_attributes_fabricator.rb index a3e72609..0cc19e16 100644 --- a/spec/fabricators/grpc/workflow_execution_started_event_attributes_fabricator.rb +++ b/spec/fabricators/grpc/workflow_execution_started_event_attributes_fabricator.rb @@ -9,6 +9,7 @@ workflow_type { Fabricate(:api_workflow_type) } original_execution_run_id { SecureRandom.uuid } attempt 1 + task_queue { Fabricate(:api_task_queue) } header do |attrs| fields = (attrs[:headers] || {}).each_with_object({}) do |(field, value), h| h[field] = Temporal.configuration.converter.to_payload(value) diff --git a/spec/fabricators/workflow_metadata_fabricator.rb b/spec/fabricators/workflow_metadata_fabricator.rb index f5393765..2b72b9e8 100644 --- a/spec/fabricators/workflow_metadata_fabricator.rb +++ b/spec/fabricators/workflow_metadata_fabricator.rb @@ -6,5 +6,7 @@ name 'TestWorkflow' run_id { SecureRandom.uuid } attempt 1 + task_queue { Fabricate(:api_task_queue) } + run_started_at { Time.now } headers { {} } end diff --git a/spec/unit/lib/temporal/activity/task_processor_spec.rb b/spec/unit/lib/temporal/activity/task_processor_spec.rb index 0a582883..4cc18a5e 100644 --- a/spec/unit/lib/temporal/activity/task_processor_spec.rb +++ b/spec/unit/lib/temporal/activity/task_processor_spec.rb @@ -14,7 +14,7 @@ input: config.converter.to_payloads(input) ) end - let(:metadata) { Temporal::Metadata.generate(Temporal::Metadata::ACTIVITY_TYPE, task, namespace) } + let(:metadata) { Temporal::Metadata.generate_activity_metadata(task, namespace) } let(:activity_name) { 'TestActivity' } let(:connection) { instance_double('Temporal::Connection::GRPC') } let(:middleware_chain) { Temporal::Middleware::Chain.new } @@ -30,8 +30,8 @@ .with(config.for_connection) .and_return(connection) allow(Temporal::Metadata) - .to receive(:generate) - .with(Temporal::Metadata::ACTIVITY_TYPE, task, namespace) + .to receive(:generate_activity_metadata) + .with(task, namespace) .and_return(metadata) allow(Temporal::Activity::Context).to receive(:new).with(connection, metadata).and_return(context) diff --git a/spec/unit/lib/temporal/metadata/workflow_spec.rb b/spec/unit/lib/temporal/metadata/workflow_spec.rb index 562c3fb8..4cc6fb74 100644 --- a/spec/unit/lib/temporal/metadata/workflow_spec.rb +++ b/spec/unit/lib/temporal/metadata/workflow_spec.rb @@ -29,10 +29,11 @@ expect(subject.to_h).to eq({ 'namespace' => subject.namespace, 'workflow_id' => subject.id, - 'workflow_id' => subject.id, 'attempt' => subject.attempt, 'workflow_name' => subject.name, - 'workflow_run_id' => subject.run_id + 'workflow_run_id' => subject.run_id, + 'task_queue' => subject.task_queue, + 'run_started_at' => subject.run_started_at.to_f, }) end end diff --git a/spec/unit/lib/temporal/metadata_spec.rb b/spec/unit/lib/temporal/metadata_spec.rb index f4c86df6..7f5fb649 100644 --- a/spec/unit/lib/temporal/metadata_spec.rb +++ b/spec/unit/lib/temporal/metadata_spec.rb @@ -1,58 +1,75 @@ require 'temporal/metadata' describe Temporal::Metadata do - describe '.generate' do - subject { described_class.generate(type, data, namespace) } - - context 'with activity type' do - let(:type) { described_class::ACTIVITY_TYPE } - let(:data) { Fabricate(:api_activity_task) } - let(:namespace) { 'test-namespace' } - - it 'generates metadata' do - expect(subject.namespace).to eq(namespace) - expect(subject.id).to eq(data.activity_id) - expect(subject.name).to eq(data.activity_type.name) - expect(subject.task_token).to eq(data.task_token) - expect(subject.attempt).to eq(data.attempt) - expect(subject.workflow_run_id).to eq(data.workflow_execution.run_id) - expect(subject.workflow_id).to eq(data.workflow_execution.workflow_id) - expect(subject.workflow_name).to eq(data.workflow_type.name) - expect(subject.headers).to eq({}) - end + describe '.generate_activity_metadata' do + subject { described_class.generate_activity_metadata(data, namespace) } - context 'with headers' do - let(:data) { Fabricate(:api_activity_task, headers: { 'Foo' => 'Bar' }) } + let(:data) { Fabricate(:api_activity_task) } + let(:namespace) { 'test-namespace' } - it 'assigns headers' do - expect(subject.headers).to eq('Foo' => 'Bar') - end - end + it 'generates metadata' do + expect(subject.namespace).to eq(namespace) + expect(subject.id).to eq(data.activity_id) + expect(subject.name).to eq(data.activity_type.name) + expect(subject.task_token).to eq(data.task_token) + expect(subject.attempt).to eq(data.attempt) + expect(subject.workflow_run_id).to eq(data.workflow_execution.run_id) + expect(subject.workflow_id).to eq(data.workflow_execution.workflow_id) + expect(subject.workflow_name).to eq(data.workflow_type.name) + expect(subject.headers).to eq({}) end - context 'with workflow task type' do - let(:type) { described_class::WORKFLOW_TASK_TYPE } - let(:data) { Fabricate(:api_workflow_task) } - let(:namespace) { 'test-namespace' } - - it 'generates metadata' do - expect(subject.namespace).to eq(namespace) - expect(subject.id).to eq(data.started_event_id) - expect(subject.task_token).to eq(data.task_token) - expect(subject.attempt).to eq(data.attempt) - expect(subject.workflow_run_id).to eq(data.workflow_execution.run_id) - expect(subject.workflow_id).to eq(data.workflow_execution.workflow_id) - expect(subject.workflow_name).to eq(data.workflow_type.name) + context 'with headers' do + let(:data) { Fabricate(:api_activity_task, headers: { 'Foo' => 'Bar' }) } + + it 'assigns headers' do + expect(subject.headers).to eq('Foo' => 'Bar') end end + end - context 'with unknown type' do - let(:type) { :unknown } - let(:data) { nil } - let(:namespace) { nil } + describe '.generate_workflow_task_metadata' do + subject { described_class.generate_workflow_task_metadata(data, namespace) } + + let(:data) { Fabricate(:api_workflow_task) } + let(:namespace) { 'test-namespace' } + + it 'generates metadata' do + expect(subject.namespace).to eq(namespace) + expect(subject.id).to eq(data.started_event_id) + expect(subject.task_token).to eq(data.task_token) + expect(subject.attempt).to eq(data.attempt) + expect(subject.workflow_run_id).to eq(data.workflow_execution.run_id) + expect(subject.workflow_id).to eq(data.workflow_execution.workflow_id) + expect(subject.workflow_name).to eq(data.workflow_type.name) + end + end + + context '.generate_workflow_metadata' do + subject { described_class.generate_workflow_metadata(event, task_metadata) } + let(:event) { Temporal::Workflow::History::Event.new(Fabricate(:api_workflow_execution_started_event)) } + let(:task_metadata) { Fabricate(:workflow_task_metadata) } + let(:namespace) { nil } + + it 'generates metadata' do + expect(subject.run_id).to eq(event.attributes.original_execution_run_id) + expect(subject.id).to eq(task_metadata.workflow_id) + expect(subject.attempt).to eq(event.attributes.attempt) + expect(subject.headers).to eq({}) + expect(subject.namespace).to eq(task_metadata.namespace) + expect(subject.task_queue).to eq(event.attributes.task_queue.name) + expect(subject.run_started_at).to eq(event.timestamp) + end + + context 'with headers' do + let(:event) do + Temporal::Workflow::History::Event.new( + Fabricate(:api_workflow_execution_started_event, headers: { 'Foo' => 'Bar' }) + ) + end - it 'raises' do - expect { subject }.to raise_error(Temporal::InternalError, 'Unsupported metadata type') + it 'assigns headers' do + expect(subject.headers).to eq('Foo' => 'Bar') end end end diff --git a/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb b/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb index 9fd322a3..1e7b8270 100644 --- a/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb +++ b/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb @@ -6,13 +6,23 @@ let(:workflow_id) { 'workflow_id_1' } let(:run_id) { 'run_id_1' } let(:execution) { Temporal::Testing::WorkflowExecution.new } + let(:task_queue) { 'my_test_queue' } let(:workflow_context) do Temporal::Testing::LocalWorkflowContext.new( execution, workflow_id, run_id, [], - Temporal::Metadata::Workflow.new(namespace: 'ruby-samples', id: workflow_id, name: 'HelloWorldWorkflow', run_id: run_id, attempt: 1) + Temporal::Metadata::Workflow.new( + namespace: 'ruby-samples', + id: workflow_id, + name: 'HelloWorldWorkflow', + run_id: run_id, + attempt: 1, + task_queue: task_queue, + headers: {}, + run_started_at: Time.now, + ) ) end let(:async_token) do diff --git a/spec/unit/lib/temporal/workflow/executor_spec.rb b/spec/unit/lib/temporal/workflow/executor_spec.rb index a639bdca..dac7cbed 100644 --- a/spec/unit/lib/temporal/workflow/executor_spec.rb +++ b/spec/unit/lib/temporal/workflow/executor_spec.rb @@ -52,8 +52,8 @@ def execute it 'generates workflow metadata' do allow(Temporal::Metadata::Workflow).to receive(:new).and_call_original payload = Temporal::Api::Common::V1::Payload.new( - metadata: { 'encoding' => 'xyz' }, - data: 'test'.b + metadata: { 'encoding' => 'json/plain' }, + data: '"bar"'.b ) header = Google::Protobuf::Map.new(:string, :message, Temporal::Api::Common::V1::Payload, { 'Foo' => payload }) @@ -71,7 +71,9 @@ def execute name: event_attributes.workflow_type.name, run_id: event_attributes.original_execution_run_id, attempt: event_attributes.attempt, - headers: header + task_queue: event_attributes.task_queue.name, + run_started_at: workflow_started_event.event_time.to_time, + headers: {'Foo' => 'bar'} ) end end From 7d63692a4af0f408d000559895ed30ce4debbe37 Mon Sep 17 00:00:00 2001 From: jeffschoner-stripe <63118764+jeffschoner-stripe@users.noreply.github.com> Date: Sun, 28 Nov 2021 15:03:38 -0800 Subject: [PATCH 014/125] Add memos (#121) --- .../integration/await_workflow_result_spec.rb | 4 +- .../spec/integration/continue_as_new_spec.rb | 44 +++++++++++++++++++ .../integration/metadata_workflow_spec.rb | 39 +++++++++++++++- examples/workflows/loop_workflow.rb | 5 ++- examples/workflows/metadata_workflow.rb | 2 +- lib/temporal/client.rb | 7 ++- lib/temporal/concerns/payloads.rb | 8 ++++ lib/temporal/connection/grpc.rb | 16 +++++-- .../connection/serializer/continue_as_new.rb | 9 +++- .../serializer/start_child_workflow.rb | 9 +++- lib/temporal/execution_options.rb | 3 +- lib/temporal/metadata.rb | 15 ++----- lib/temporal/metadata/workflow.rb | 6 ++- lib/temporal/testing/temporal_override.rb | 2 + lib/temporal/testing/workflow_override.rb | 1 + lib/temporal/workflow/command.rb | 4 +- lib/temporal/workflow/context.rb | 9 +++- lib/temporal/workflow/execution_info.rb | 7 ++- spec/fabricators/grpc/memo_fabricator.rb | 7 +++ .../workflow_execution_info_fabricator.rb | 1 + .../workflow_metadata_fabricator.rb | 1 + spec/unit/lib/temporal/client_spec.rb | 25 +++++++---- .../serializer/continue_as_new_spec.rb | 18 ++++++-- spec/unit/lib/temporal/grpc_client_spec.rb | 3 +- .../lib/temporal/metadata/workflow_spec.rb | 1 + spec/unit/lib/temporal/metadata_spec.rb | 1 + .../testing/local_workflow_context_spec.rb | 1 + .../temporal/workflow/execution_info_spec.rb | 1 + .../lib/temporal/workflow/executor_spec.rb | 1 + 29 files changed, 207 insertions(+), 43 deletions(-) create mode 100644 examples/spec/integration/continue_as_new_spec.rb create mode 100644 spec/fabricators/grpc/memo_fabricator.rb diff --git a/examples/spec/integration/await_workflow_result_spec.rb b/examples/spec/integration/await_workflow_result_spec.rb index 3b4f1e77..bf48b7b1 100644 --- a/examples/spec/integration/await_workflow_result_spec.rb +++ b/examples/spec/integration/await_workflow_result_spec.rb @@ -95,7 +95,9 @@ run_id = Temporal.start_workflow( LoopWorkflow, 2, # it continues as new if this arg is > 1 - { options: { workflow_id: workflow_id } }, + options: { + workflow_id: workflow_id, + }, ) expect do diff --git a/examples/spec/integration/continue_as_new_spec.rb b/examples/spec/integration/continue_as_new_spec.rb new file mode 100644 index 00000000..bbeef5ce --- /dev/null +++ b/examples/spec/integration/continue_as_new_spec.rb @@ -0,0 +1,44 @@ +require 'workflows/loop_workflow' + +describe LoopWorkflow do + it 'workflow continues as new into a new run' do + workflow_id = SecureRandom.uuid + memo = { + 'my-memo' => 'foo', + } + run_id = Temporal.start_workflow( + LoopWorkflow, + 2, # it continues as new if this arg is > 1 + options: { + workflow_id: workflow_id, + memo: memo, + }, + ) + + # First run will throw because it continued as new + next_run_id = nil + expect do + Temporal.await_workflow_result( + LoopWorkflow, + workflow_id: workflow_id, + run_id: run_id, + ) + end.to raise_error(Temporal::WorkflowRunContinuedAsNew) do |error| + next_run_id = error.new_run_id + end + + expect(next_run_id).to_not eq(nil) + + # Second run will not throw because it returns rather than continues as new. + final_result = Temporal.await_workflow_result( + LoopWorkflow, + workflow_id: workflow_id, + run_id: next_run_id, + ) + + expect(final_result[:count]).to eq(1) + + # memo should be copied to the next run automatically + expect(final_result[:memo]).to eq(memo) + end +end diff --git a/examples/spec/integration/metadata_workflow_spec.rb b/examples/spec/integration/metadata_workflow_spec.rb index a7d8ee1b..0e3c4099 100644 --- a/examples/spec/integration/metadata_workflow_spec.rb +++ b/examples/spec/integration/metadata_workflow_spec.rb @@ -7,13 +7,15 @@ workflow_id = 'task-queue-' + SecureRandom.uuid run_id = Temporal.start_workflow( subject, - { options: { workflow_id: workflow_id } }, + options: { workflow_id: workflow_id } ) + actual_result = Temporal.await_workflow_result( subject, workflow_id: workflow_id, run_id: run_id, ) + expect(actual_result.task_queue).to eq(Temporal.configuration.task_queue) end @@ -51,4 +53,39 @@ ) expect(Time.now - actual_result.run_started_at).to be_between(0, 30) end + + it 'gets memo from workflow execution info' do + workflow_id = 'memo_execution_test_wf-' + SecureRandom.uuid + run_id = Temporal.start_workflow(subject, options: { workflow_id: workflow_id, memo: { 'foo' => 'bar' } }) + + actual_result = Temporal.await_workflow_result( + subject, + workflow_id: workflow_id, + run_id: run_id, + ) + expect(actual_result.memo['foo']).to eq('bar') + + expect(Temporal.fetch_workflow_execution_info( + 'ruby-samples', workflow_id, nil + ).memo).to eq({ 'foo' => 'bar' }) + end + + it 'gets memo from workflow context with no memo' do + workflow_id = 'memo_context_no_memo_test_wf-' + SecureRandom.uuid + + run_id = Temporal.start_workflow( + subject, + options: { workflow_id: workflow_id } + ) + + actual_result = Temporal.await_workflow_result( + subject, + workflow_id: workflow_id, + run_id: run_id, + ) + expect(actual_result.memo).to eq({}) + expect(Temporal.fetch_workflow_execution_info( + 'ruby-samples', workflow_id, nil + ).memo).to eq({}) + end end diff --git a/examples/workflows/loop_workflow.rb b/examples/workflows/loop_workflow.rb index 5b1f5bfd..e10a9b30 100644 --- a/examples/workflows/loop_workflow.rb +++ b/examples/workflows/loop_workflow.rb @@ -8,6 +8,9 @@ def execute(count) return workflow.continue_as_new(count - 1) end - return count + return { + count: count, + memo: workflow.metadata.memo, + } end end diff --git a/examples/workflows/metadata_workflow.rb b/examples/workflows/metadata_workflow.rb index 4cdfab59..62f61703 100644 --- a/examples/workflows/metadata_workflow.rb +++ b/examples/workflows/metadata_workflow.rb @@ -2,4 +2,4 @@ class MetadataWorkflow < Temporal::Workflow def execute workflow.metadata end -end \ No newline at end of file +end diff --git a/lib/temporal/client.rb b/lib/temporal/client.rb index 9971a37c..28336acf 100644 --- a/lib/temporal/client.rb +++ b/lib/temporal/client.rb @@ -57,7 +57,8 @@ def start_workflow(workflow, *input, options: {}, **args) run_timeout: compute_run_timeout(execution_options), task_timeout: execution_options.timeouts[:task], workflow_id_reuse_policy: options[:workflow_id_reuse_policy], - headers: execution_options.headers + headers: execution_options.headers, + memo: execution_options.memo, ) else raise ArgumentError, 'If signal_input is provided, you must also provide signal_name' if signal_name.nil? @@ -73,6 +74,7 @@ def start_workflow(workflow, *input, options: {}, **args) task_timeout: execution_options.timeouts[:task], workflow_id_reuse_policy: options[:workflow_id_reuse_policy], headers: execution_options.headers, + memo: execution_options.memo, signal_name: signal_name, signal_input: signal_input ) @@ -119,7 +121,8 @@ def schedule_workflow(workflow, cron_schedule, *input, options: {}, **args) task_timeout: execution_options.timeouts[:task], workflow_id_reuse_policy: options[:workflow_id_reuse_policy], headers: execution_options.headers, - cron_schedule: cron_schedule + cron_schedule: cron_schedule, + memo: execution_options.memo ) response.run_id diff --git a/lib/temporal/concerns/payloads.rb b/lib/temporal/concerns/payloads.rb index 49af8f3f..3be276d2 100644 --- a/lib/temporal/concerns/payloads.rb +++ b/lib/temporal/concerns/payloads.rb @@ -21,6 +21,10 @@ def from_signal_payloads(payloads) from_payloads(payloads)&.first end + def from_payload_map(payload_map) + payload_map.map { |key, value| [key, from_payload(value)] }.to_h + end + def to_payloads(data) payload_converter.to_payloads(data) end @@ -41,6 +45,10 @@ def to_signal_payloads(data) to_payloads([data]) end + def to_payload_map(data) + data.transform_values(&method(:to_payload)) + end + private def payload_converter diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index 5b7e553b..70987e1e 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -81,7 +81,8 @@ def start_workflow_execution( task_timeout:, workflow_id_reuse_policy: nil, headers: nil, - cron_schedule: nil + cron_schedule: nil, + memo: nil ) request = Temporal::Api::WorkflowService::V1::StartWorkflowExecutionRequest.new( identity: identity, @@ -101,7 +102,10 @@ def start_workflow_execution( header: Temporal::Api::Common::V1::Header.new( fields: headers ), - cron_schedule: cron_schedule + cron_schedule: cron_schedule, + memo: Temporal::Api::Common::V1::Memo.new( + fields: to_payload_map(memo || {}) + ) ) if workflow_id_reuse_policy @@ -305,7 +309,8 @@ def signal_with_start_workflow_execution( headers: nil, cron_schedule: nil, signal_name:, - signal_input: + signal_input:, + memo: nil ) request = Temporal::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionRequest.new( identity: identity, @@ -327,7 +332,10 @@ def signal_with_start_workflow_execution( ), cron_schedule: cron_schedule, signal_name: signal_name, - signal_input: to_signal_payloads(signal_input) + signal_input: to_signal_payloads(signal_input), + memo: Temporal::Api::Common::V1::Memo.new( + fields: to_payload_map(memo || {}) + ), ) if workflow_id_reuse_policy diff --git a/lib/temporal/connection/serializer/continue_as_new.rb b/lib/temporal/connection/serializer/continue_as_new.rb index 2d1e588c..db8259c6 100644 --- a/lib/temporal/connection/serializer/continue_as_new.rb +++ b/lib/temporal/connection/serializer/continue_as_new.rb @@ -19,7 +19,8 @@ def to_proto workflow_run_timeout: object.timeouts[:execution], workflow_task_timeout: object.timeouts[:task], retry_policy: Temporal::Connection::Serializer::RetryPolicy.new(object.retry_policy).to_proto, - header: serialize_headers(object.headers) + header: serialize_headers(object.headers), + memo: serialize_memo(object.memo) ) ) end @@ -31,6 +32,12 @@ def serialize_headers(headers) Temporal::Api::Common::V1::Header.new(fields: object.headers) end + + def serialize_memo(memo) + return unless memo + + Temporal::Api::Common::V1::Memo.new(fields: to_payload_map(memo)) + end end end end diff --git a/lib/temporal/connection/serializer/start_child_workflow.rb b/lib/temporal/connection/serializer/start_child_workflow.rb index 55312e50..ce1dc6ee 100644 --- a/lib/temporal/connection/serializer/start_child_workflow.rb +++ b/lib/temporal/connection/serializer/start_child_workflow.rb @@ -22,7 +22,8 @@ def to_proto workflow_run_timeout: object.timeouts[:run], workflow_task_timeout: object.timeouts[:task], retry_policy: Temporal::Connection::Serializer::RetryPolicy.new(object.retry_policy).to_proto, - header: serialize_headers(object.headers) + header: serialize_headers(object.headers), + memo: serialize_memo(object.memo), ) ) end @@ -34,6 +35,12 @@ def serialize_headers(headers) Temporal::Api::Common::V1::Header.new(fields: object.headers) end + + def serialize_memo(memo) + return unless memo + + Temporal::Api::Common::V1::Memo.new(fields: to_payload_map(memo)) + end end end end diff --git a/lib/temporal/execution_options.rb b/lib/temporal/execution_options.rb index 6fcaf371..182c4079 100644 --- a/lib/temporal/execution_options.rb +++ b/lib/temporal/execution_options.rb @@ -3,7 +3,7 @@ module Temporal class ExecutionOptions - attr_reader :name, :namespace, :task_queue, :retry_policy, :timeouts, :headers + attr_reader :name, :namespace, :task_queue, :retry_policy, :timeouts, :headers, :memo def initialize(object, options, defaults = nil) # Options are treated as overrides and take precedence @@ -13,6 +13,7 @@ def initialize(object, options, defaults = nil) @retry_policy = options[:retry_policy] || {} @timeouts = options[:timeouts] || {} @headers = options[:headers] || {} + @memo = options[:memo] || {} # For Temporal::Workflow and Temporal::Activity use defined values as the next option if has_executable_concern?(object) diff --git a/lib/temporal/metadata.rb b/lib/temporal/metadata.rb index 0617b784..df7165ea 100644 --- a/lib/temporal/metadata.rb +++ b/lib/temporal/metadata.rb @@ -20,7 +20,7 @@ def generate_activity_metadata(task, namespace) workflow_run_id: task.workflow_execution.run_id, workflow_id: task.workflow_execution.workflow_id, workflow_name: task.workflow_type.name, - headers: headers(task.header&.fields), + headers: from_payload_map(task.header&.fields || {}), heartbeat_details: from_details_payloads(task.heartbeat_details) ) end @@ -48,20 +48,11 @@ def generate_workflow_metadata(event, task_metadata) attempt: event.attributes.attempt, namespace: task_metadata.namespace, task_queue: event.attributes.task_queue.name, - headers: headers(event.attributes.header&.fields), + headers: from_payload_map(event.attributes.header&.fields || {}), run_started_at: event.timestamp, + memo: from_payload_map(event.attributes.memo&.fields || {}), ) end - - private - - def headers(fields) - result = {} - fields.each do |field, payload| - result[field] = from_payload(payload) - end - result - end end end end diff --git a/lib/temporal/metadata/workflow.rb b/lib/temporal/metadata/workflow.rb index 9e95acb1..f4715dde 100644 --- a/lib/temporal/metadata/workflow.rb +++ b/lib/temporal/metadata/workflow.rb @@ -3,9 +3,9 @@ module Temporal module Metadata class Workflow < Base - attr_reader :namespace, :id, :name, :run_id, :attempt, :task_queue, :headers, :run_started_at + attr_reader :namespace, :id, :name, :run_id, :attempt, :task_queue, :headers, :run_started_at, :memo - def initialize(namespace:, id:, name:, run_id:, attempt:, task_queue:, headers:, run_started_at:) + def initialize(namespace:, id:, name:, run_id:, attempt:, task_queue:, headers:, run_started_at:, memo:) @namespace = namespace @id = id @name = name @@ -14,6 +14,7 @@ def initialize(namespace:, id:, name:, run_id:, attempt:, task_queue:, headers:, @task_queue = task_queue @headers = headers @run_started_at = run_started_at + @memo = memo freeze end @@ -31,6 +32,7 @@ def to_h 'attempt' => attempt, 'task_queue' => task_queue, 'run_started_at' => run_started_at.to_f, + 'memo' => memo, } end end diff --git a/lib/temporal/testing/temporal_override.rb b/lib/temporal/testing/temporal_override.rb index dd67be7f..d44da24f 100644 --- a/lib/temporal/testing/temporal_override.rb +++ b/lib/temporal/testing/temporal_override.rb @@ -81,6 +81,7 @@ def start_locally(workflow, schedule, *input, **args) reuse_policy = options[:workflow_id_reuse_policy] || :allow_failed workflow_id = options[:workflow_id] || SecureRandom.uuid run_id = SecureRandom.uuid + memo = options[:memo] || {} if !allowed?(workflow_id, reuse_policy) raise Temporal::WorkflowExecutionAlreadyStartedFailure.new( @@ -101,6 +102,7 @@ def start_locally(workflow, schedule, *input, **args) attempt: 1, task_queue: execution_options.task_queue, run_started_at: Time.now, + memo: memo, headers: execution_options.headers ) context = Temporal::Testing::LocalWorkflowContext.new( diff --git a/lib/temporal/testing/workflow_override.rb b/lib/temporal/testing/workflow_override.rb index dab7472b..9d43f953 100644 --- a/lib/temporal/testing/workflow_override.rb +++ b/lib/temporal/testing/workflow_override.rb @@ -36,6 +36,7 @@ def execute_locally(*input) task_queue: 'unit-test-task-queue', headers: {}, run_started_at: Time.now, + memo: {}, ) context = Temporal::Testing::LocalWorkflowContext.new( execution, workflow_id, run_id, disabled_releases, metadata diff --git a/lib/temporal/workflow/command.rb b/lib/temporal/workflow/command.rb index 0f0d6ed9..8297abe9 100644 --- a/lib/temporal/workflow/command.rb +++ b/lib/temporal/workflow/command.rb @@ -3,8 +3,8 @@ class Workflow module Command # TODO: Move these classes into their own directories under workflow/command/* ScheduleActivity = Struct.new(:activity_type, :activity_id, :input, :namespace, :task_queue, :retry_policy, :timeouts, :headers, keyword_init: true) - StartChildWorkflow = Struct.new(:workflow_type, :workflow_id, :input, :namespace, :task_queue, :retry_policy, :timeouts, :headers, keyword_init: true) - ContinueAsNew = Struct.new(:workflow_type, :task_queue, :input, :timeouts, :retry_policy, :headers, keyword_init: true) + StartChildWorkflow = Struct.new(:workflow_type, :workflow_id, :input, :namespace, :task_queue, :retry_policy, :timeouts, :headers, :memo, keyword_init: true) + ContinueAsNew = Struct.new(:workflow_type, :task_queue, :input, :timeouts, :retry_policy, :headers, :memo, keyword_init: true) RequestActivityCancellation = Struct.new(:activity_id, keyword_init: true) RecordMarker = Struct.new(:name, :details, keyword_init: true) StartTimer = Struct.new(:timeout, :timer_id, keyword_init: true) diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index 18ec3c05..c06e4750 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -195,6 +195,12 @@ def continue_as_new(*input, **args) options = args.delete(:options) || {} input << args unless args.empty? + # If memo is not overridden, copy from current run + options_from_metadata = { + memo: metadata.memo, + } + options = options_from_metadata.merge(options) + execution_options = ExecutionOptions.new(workflow_class, options, config.default_execution_options) command = Command::ContinueAsNew.new( @@ -203,7 +209,8 @@ def continue_as_new(*input, **args) input: input, timeouts: execution_options.timeouts, retry_policy: execution_options.retry_policy, - headers: execution_options.headers + headers: execution_options.headers, + memo: execution_options.memo, ) schedule_command(command) completed! diff --git a/lib/temporal/workflow/execution_info.rb b/lib/temporal/workflow/execution_info.rb index 67cad260..46b8e4cb 100644 --- a/lib/temporal/workflow/execution_info.rb +++ b/lib/temporal/workflow/execution_info.rb @@ -1,6 +1,10 @@ +require 'temporal/concerns/payloads' + module Temporal class Workflow - class ExecutionInfo < Struct.new(:workflow, :workflow_id, :run_id, :start_time, :close_time, :status, :history_length, keyword_init: true) + class ExecutionInfo < Struct.new(:workflow, :workflow_id, :run_id, :start_time, :close_time, :status, :history_length, :memo, keyword_init: true) + extend Concerns::Payloads + RUNNING_STATUS = :RUNNING COMPLETED_STATUS = :COMPLETED FAILED_STATUS = :FAILED @@ -38,6 +42,7 @@ def self.generate_from(response) close_time: response.close_time&.to_time, status: API_STATUS_MAP.fetch(response.status), history_length: response.history_length, + memo: self.from_payload_map(response.memo.fields), ).freeze end diff --git a/spec/fabricators/grpc/memo_fabricator.rb b/spec/fabricators/grpc/memo_fabricator.rb new file mode 100644 index 00000000..6c9fd726 --- /dev/null +++ b/spec/fabricators/grpc/memo_fabricator.rb @@ -0,0 +1,7 @@ +Fabricator(:memo, from: Temporal::Api::Common::V1::Memo) do + fields do + Google::Protobuf::Map.new(:string, :message, Temporal::Api::Common::V1::Payload).tap do |m| + m['foo'] = Temporal.configuration.converter.to_payload('bar') + end + end +end diff --git a/spec/fabricators/grpc/workflow_execution_info_fabricator.rb b/spec/fabricators/grpc/workflow_execution_info_fabricator.rb index 296bde87..4f1d577e 100644 --- a/spec/fabricators/grpc/workflow_execution_info_fabricator.rb +++ b/spec/fabricators/grpc/workflow_execution_info_fabricator.rb @@ -5,4 +5,5 @@ close_time { Google::Protobuf::Timestamp.new.tap { |t| t.from_time(Time.now) } } status { Temporal::Api::Enums::V1::WorkflowExecutionStatus::WORKFLOW_EXECUTION_STATUS_COMPLETED } history_length { rand(100) } + memo { Fabricate(:memo) } end diff --git a/spec/fabricators/workflow_metadata_fabricator.rb b/spec/fabricators/workflow_metadata_fabricator.rb index 2b72b9e8..c32fd3e1 100644 --- a/spec/fabricators/workflow_metadata_fabricator.rb +++ b/spec/fabricators/workflow_metadata_fabricator.rb @@ -8,5 +8,6 @@ attempt 1 task_queue { Fabricate(:api_task_queue) } run_started_at { Time.now } + memo { {} } headers { {} } end diff --git a/spec/unit/lib/temporal/client_spec.rb b/spec/unit/lib/temporal/client_spec.rb index a0260f43..b52bd464 100644 --- a/spec/unit/lib/temporal/client_spec.rb +++ b/spec/unit/lib/temporal/client_spec.rb @@ -61,7 +61,8 @@ class TestStartWorkflow < Temporal::Workflow run_timeout: Temporal.configuration.timeouts[:run], execution_timeout: Temporal.configuration.timeouts[:execution], workflow_id_reuse_policy: nil, - headers: {} + headers: {}, + memo: {} ) end @@ -73,7 +74,8 @@ class TestStartWorkflow < Temporal::Workflow name: 'test-workflow', namespace: 'test-namespace', task_queue: 'test-task-queue', - headers: { 'Foo' => 'Bar' } + headers: { 'Foo' => 'Bar' }, + memo: { 'MemoKey1' => 'MemoValue1' } } ) @@ -89,7 +91,8 @@ class TestStartWorkflow < Temporal::Workflow run_timeout: Temporal.configuration.timeouts[:run], execution_timeout: Temporal.configuration.timeouts[:execution], workflow_id_reuse_policy: nil, - headers: { 'Foo' => 'Bar' } + headers: { 'Foo' => 'Bar' }, + memo: { 'MemoKey1' => 'MemoValue1' } ) end @@ -114,7 +117,8 @@ class TestStartWorkflow < Temporal::Workflow run_timeout: Temporal.configuration.timeouts[:run], execution_timeout: Temporal.configuration.timeouts[:execution], workflow_id_reuse_policy: nil, - headers: {} + headers: {}, + memo: {} ) end @@ -133,7 +137,8 @@ class TestStartWorkflow < Temporal::Workflow run_timeout: Temporal.configuration.timeouts[:run], execution_timeout: Temporal.configuration.timeouts[:execution], workflow_id_reuse_policy: nil, - headers: {} + headers: {}, + memo: {} ) end @@ -154,7 +159,8 @@ class TestStartWorkflow < Temporal::Workflow run_timeout: Temporal.configuration.timeouts[:run], execution_timeout: Temporal.configuration.timeouts[:execution], workflow_id_reuse_policy: :allow, - headers: {} + headers: {}, + memo: {} ) end end @@ -179,7 +185,8 @@ class TestStartWorkflow < Temporal::Workflow run_timeout: Temporal.configuration.timeouts[:run], execution_timeout: Temporal.configuration.timeouts[:execution], workflow_id_reuse_policy: nil, - headers: {} + headers: {}, + memo: {} ) end end @@ -206,6 +213,7 @@ def expect_signal_with_start(expected_arguments, expected_signal_argument) execution_timeout: Temporal.configuration.timeouts[:execution], workflow_id_reuse_policy: nil, headers: {}, + memo: {}, signal_name: 'the question', signal_input: expected_signal_argument, ) @@ -285,7 +293,8 @@ def expect_signal_with_start(expected_arguments, expected_signal_argument) run_timeout: Temporal.configuration.timeouts[:run], execution_timeout: Temporal.configuration.timeouts[:execution], workflow_id_reuse_policy: nil, - headers: {} + memo: {}, + headers: {}, ) end end diff --git a/spec/unit/lib/temporal/connection/serializer/continue_as_new_spec.rb b/spec/unit/lib/temporal/connection/serializer/continue_as_new_spec.rb index fbb00623..4a7e90eb 100644 --- a/spec/unit/lib/temporal/connection/serializer/continue_as_new_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/continue_as_new_spec.rb @@ -5,10 +5,11 @@ describe 'to_proto' do it 'produces a protobuf' do command = Temporal::Workflow::Command::ContinueAsNew.new( - workflow_type: 'Test', - task_queue: 'Test', + workflow_type: 'my-workflow-type', + task_queue: 'my-task-queue', input: ['one', 'two'], - timeouts: Temporal.configuration.timeouts + timeouts: Temporal.configuration.timeouts, + memo: {'foo-memo': 'baz'}, ) result = described_class.new(command).to_proto @@ -17,6 +18,17 @@ expect(result.command_type).to eql( :COMMAND_TYPE_CONTINUE_AS_NEW_WORKFLOW_EXECUTION ) + expect(result.continue_as_new_workflow_execution_command_attributes).not_to be_nil + attribs = result.continue_as_new_workflow_execution_command_attributes + + expect(attribs.workflow_type.name).to eq('my-workflow-type') + + expect(attribs.task_queue.name).to eq('my-task-queue') + + expect(attribs.input.payloads[0].data).to eq('"one"') + expect(attribs.input.payloads[1].data).to eq('"two"') + + expect(attribs.memo.fields['foo-memo'].data).to eq('"baz"') end end end diff --git a/spec/unit/lib/temporal/grpc_client_spec.rb b/spec/unit/lib/temporal/grpc_client_spec.rb index 8349cc1e..a168c359 100644 --- a/spec/unit/lib/temporal/grpc_client_spec.rb +++ b/spec/unit/lib/temporal/grpc_client_spec.rb @@ -27,7 +27,8 @@ task_queue: 'test', execution_timeout: 0, run_timeout: 0, - task_timeout: 0 + task_timeout: 0, + memo: {} ) end.to raise_error(Temporal::WorkflowExecutionAlreadyStartedFailure) do |e| expect(e.run_id).to eql('baaf1d86-4459-4ecd-a288-47aeae55245d') diff --git a/spec/unit/lib/temporal/metadata/workflow_spec.rb b/spec/unit/lib/temporal/metadata/workflow_spec.rb index 4cc6fb74..be3f50b9 100644 --- a/spec/unit/lib/temporal/metadata/workflow_spec.rb +++ b/spec/unit/lib/temporal/metadata/workflow_spec.rb @@ -34,6 +34,7 @@ 'workflow_run_id' => subject.run_id, 'task_queue' => subject.task_queue, 'run_started_at' => subject.run_started_at.to_f, + 'memo' => subject.memo, }) end end diff --git a/spec/unit/lib/temporal/metadata_spec.rb b/spec/unit/lib/temporal/metadata_spec.rb index 7f5fb649..cd21fb76 100644 --- a/spec/unit/lib/temporal/metadata_spec.rb +++ b/spec/unit/lib/temporal/metadata_spec.rb @@ -56,6 +56,7 @@ expect(subject.id).to eq(task_metadata.workflow_id) expect(subject.attempt).to eq(event.attributes.attempt) expect(subject.headers).to eq({}) + expect(subject.memo).to eq({}) expect(subject.namespace).to eq(task_metadata.namespace) expect(subject.task_queue).to eq(event.attributes.task_queue.name) expect(subject.run_started_at).to eq(event.timestamp) diff --git a/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb b/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb index 1e7b8270..9397ef88 100644 --- a/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb +++ b/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb @@ -22,6 +22,7 @@ task_queue: task_queue, headers: {}, run_started_at: Time.now, + memo: {}, ) ) end diff --git a/spec/unit/lib/temporal/workflow/execution_info_spec.rb b/spec/unit/lib/temporal/workflow/execution_info_spec.rb index fbac5ee0..a064e8ba 100644 --- a/spec/unit/lib/temporal/workflow/execution_info_spec.rb +++ b/spec/unit/lib/temporal/workflow/execution_info_spec.rb @@ -14,6 +14,7 @@ expect(subject.close_time).to be_a(Time) expect(subject.status).to eq(:COMPLETED) expect(subject.history_length).to eq(api_info.history_length) + expect(subject.memo).to eq({ 'foo' => 'bar' }) end it 'freezes the info' do diff --git a/spec/unit/lib/temporal/workflow/executor_spec.rb b/spec/unit/lib/temporal/workflow/executor_spec.rb index dac7cbed..a74c3387 100644 --- a/spec/unit/lib/temporal/workflow/executor_spec.rb +++ b/spec/unit/lib/temporal/workflow/executor_spec.rb @@ -73,6 +73,7 @@ def execute attempt: event_attributes.attempt, task_queue: event_attributes.task_queue.name, run_started_at: workflow_started_event.event_time.to_time, + memo: {}, headers: {'Foo' => 'bar'} ) end From 654cdad92ed0f3edd422cd76cf929bc1b71421fe Mon Sep 17 00:00:00 2001 From: Drew Hoskins <37816070+drewhoskins-stripe@users.noreply.github.com> Date: Tue, 30 Nov 2021 11:50:06 -0800 Subject: [PATCH 015/125] Add describe_namespace (#122) * Add describe_namespace * Feedback --- .../spec/integration/describe_namespace_spec.rb | 16 ++++++++++++++++ lib/temporal.rb | 1 + lib/temporal/client.rb | 7 +++++++ spec/unit/lib/temporal/client_spec.rb | 11 +++++++++++ spec/unit/lib/temporal_spec.rb | 4 ++++ 5 files changed, 39 insertions(+) create mode 100644 examples/spec/integration/describe_namespace_spec.rb diff --git a/examples/spec/integration/describe_namespace_spec.rb b/examples/spec/integration/describe_namespace_spec.rb new file mode 100644 index 00000000..86501f7b --- /dev/null +++ b/examples/spec/integration/describe_namespace_spec.rb @@ -0,0 +1,16 @@ +require 'temporal/errors' + +describe 'Temporal.describe_namespace' do + it 'returns a value' do + description = 'Namespace for temporal-ruby integration test' + begin + Temporal.register_namespace('a_test_namespace', description) + rescue Temporal::NamespaceAlreadyExistsFailure + end + result = Temporal.describe_namespace('a_test_namespace') + expect(result).to be_an_instance_of(Temporal::Api::WorkflowService::V1::DescribeNamespaceResponse) + expect(result.namespace_info.name).to eq('a_test_namespace') + expect(result.namespace_info.state).to eq(:NAMESPACE_STATE_REGISTERED) + expect(result.namespace_info.description).to eq(description) + end +end diff --git a/lib/temporal.rb b/lib/temporal.rb index e580192f..f02d6ec9 100644 --- a/lib/temporal.rb +++ b/lib/temporal.rb @@ -17,6 +17,7 @@ module Temporal :start_workflow, :schedule_workflow, :register_namespace, + :describe_namespace, :signal_workflow, :await_workflow_result, :reset_workflow, diff --git a/lib/temporal/client.rb b/lib/temporal/client.rb index 28336acf..2fcea794 100644 --- a/lib/temporal/client.rb +++ b/lib/temporal/client.rb @@ -136,6 +136,13 @@ def register_namespace(name, description = nil) connection.register_namespace(name: name, description: description) end + # Fetches metadata for a namespace. + # @param name [String] name of the namespace + # @return [Hash] info deserialized from Temporal::Api::WorkflowService::V1::DescribeNamespaceResponse + def describe_namespace(name) + connection.describe_namespace(name: name) + end + # Send a signal to a running workflow # # @param workflow [Temporal::Workflow, nil] workflow class or nil diff --git a/spec/unit/lib/temporal/client_spec.rb b/spec/unit/lib/temporal/client_spec.rb index b52bd464..68e5076a 100644 --- a/spec/unit/lib/temporal/client_spec.rb +++ b/spec/unit/lib/temporal/client_spec.rb @@ -319,6 +319,17 @@ def expect_signal_with_start(expected_arguments, expected_signal_argument) end end + describe '#describe_namespace' do + before { allow(connection).to receive(:describe_namespace).and_return(Temporal::Api::WorkflowService::V1::DescribeNamespaceResponse.new) } + + it 'passes the namespace to the connection' do + result = subject.describe_namespace('new-namespace') + + expect(connection) + .to have_received(:describe_namespace) + .with(name: 'new-namespace') + end + end describe '#signal_workflow' do before { allow(connection).to receive(:signal_workflow_execution).and_return(nil) } diff --git a/spec/unit/lib/temporal_spec.rb b/spec/unit/lib/temporal_spec.rb index 0d9daefd..47ccd73d 100644 --- a/spec/unit/lib/temporal_spec.rb +++ b/spec/unit/lib/temporal_spec.rb @@ -28,6 +28,10 @@ describe '.register_namespace' do it_behaves_like 'a forwarded method', :register_namespace, 'test-namespace', 'This is a test namespace' end + + describe '.describe_namespace' do + it_behaves_like 'a forwarded method', :describe_namespace, 'test-namespace' + end describe '.signal_workflow' do it_behaves_like 'a forwarded method', :signal_workflow, 'TestWorkflow', 'TST_SIGNAL', 'x', 'y' From 27ac01429aad5d81f9dd0ae1ded748a94388ed13 Mon Sep 17 00:00:00 2001 From: jeffschoner-stripe <63118764+jeffschoner-stripe@users.noreply.github.com> Date: Sun, 5 Dec 2021 06:40:07 -0800 Subject: [PATCH 016/125] Improve header serialization and propagation (#124) --- examples/spec/integration/continue_as_new_spec.rb | 7 ++++++- .../spec/integration/metadata_workflow_spec.rb | 2 +- examples/workflows/loop_workflow.rb | 1 + lib/temporal/connection/grpc.rb | 14 ++++++++++++-- .../connection/serializer/continue_as_new.rb | 2 +- .../connection/serializer/start_child_workflow.rb | 2 +- lib/temporal/workflow/context.rb | 3 ++- .../connection/serializer/continue_as_new_spec.rb | 2 ++ 8 files changed, 26 insertions(+), 7 deletions(-) diff --git a/examples/spec/integration/continue_as_new_spec.rb b/examples/spec/integration/continue_as_new_spec.rb index bbeef5ce..74b0771e 100644 --- a/examples/spec/integration/continue_as_new_spec.rb +++ b/examples/spec/integration/continue_as_new_spec.rb @@ -6,12 +6,16 @@ memo = { 'my-memo' => 'foo', } + headers = { + 'my-header' => 'bar', + } run_id = Temporal.start_workflow( LoopWorkflow, 2, # it continues as new if this arg is > 1 options: { workflow_id: workflow_id, memo: memo, + headers: headers, }, ) @@ -38,7 +42,8 @@ expect(final_result[:count]).to eq(1) - # memo should be copied to the next run automatically + # memo and headers should be copied to the next run automatically expect(final_result[:memo]).to eq(memo) + expect(final_result[:headers]).to eq(headers) end end diff --git a/examples/spec/integration/metadata_workflow_spec.rb b/examples/spec/integration/metadata_workflow_spec.rb index 0e3c4099..ac828e01 100644 --- a/examples/spec/integration/metadata_workflow_spec.rb +++ b/examples/spec/integration/metadata_workflow_spec.rb @@ -26,7 +26,7 @@ MetadataWorkflow, options: { workflow_id: workflow_id, - headers: { 'foo' => Temporal.configuration.converter.to_payload('bar') }, + headers: { 'foo' => 'bar' }, } ) diff --git a/examples/workflows/loop_workflow.rb b/examples/workflows/loop_workflow.rb index e10a9b30..b99408f4 100644 --- a/examples/workflows/loop_workflow.rb +++ b/examples/workflows/loop_workflow.rb @@ -11,6 +11,7 @@ def execute(count) return { count: count, memo: workflow.metadata.memo, + headers: workflow.metadata.headers, } end end diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index 70987e1e..b2ce0350 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -100,7 +100,7 @@ def start_workflow_execution( workflow_task_timeout: task_timeout, request_id: SecureRandom.uuid, header: Temporal::Api::Common::V1::Header.new( - fields: headers + fields: to_payload_map(headers || {}) ), cron_schedule: cron_schedule, memo: Temporal::Api::Common::V1::Memo.new( @@ -312,6 +312,16 @@ def signal_with_start_workflow_execution( signal_input:, memo: nil ) + proto_header_fields = if headers.nil? + to_payload_map({}) + elsif headers.class == Hash + to_payload_map(headers) + else + # Preserve backward compatability for headers specified using proto objects + warn '[DEPRECATION] Specify headers using a hash rather than protobuf objects' + headers + end + request = Temporal::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionRequest.new( identity: identity, namespace: namespace, @@ -328,7 +338,7 @@ def signal_with_start_workflow_execution( workflow_task_timeout: task_timeout, request_id: SecureRandom.uuid, header: Temporal::Api::Common::V1::Header.new( - fields: headers + fields: proto_header_fields, ), cron_schedule: cron_schedule, signal_name: signal_name, diff --git a/lib/temporal/connection/serializer/continue_as_new.rb b/lib/temporal/connection/serializer/continue_as_new.rb index db8259c6..357f0008 100644 --- a/lib/temporal/connection/serializer/continue_as_new.rb +++ b/lib/temporal/connection/serializer/continue_as_new.rb @@ -30,7 +30,7 @@ def to_proto def serialize_headers(headers) return unless headers - Temporal::Api::Common::V1::Header.new(fields: object.headers) + Temporal::Api::Common::V1::Header.new(fields: to_payload_map(headers)) end def serialize_memo(memo) diff --git a/lib/temporal/connection/serializer/start_child_workflow.rb b/lib/temporal/connection/serializer/start_child_workflow.rb index ce1dc6ee..5f6b350d 100644 --- a/lib/temporal/connection/serializer/start_child_workflow.rb +++ b/lib/temporal/connection/serializer/start_child_workflow.rb @@ -33,7 +33,7 @@ def to_proto def serialize_headers(headers) return unless headers - Temporal::Api::Common::V1::Header.new(fields: object.headers) + Temporal::Api::Common::V1::Header.new(fields: to_payload_map(headers)) end def serialize_memo(memo) diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index c06e4750..d88d8f30 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -195,9 +195,10 @@ def continue_as_new(*input, **args) options = args.delete(:options) || {} input << args unless args.empty? - # If memo is not overridden, copy from current run + # If memo or headers are not overridden, use those from the current run options_from_metadata = { memo: metadata.memo, + headers: metadata.headers, } options = options_from_metadata.merge(options) diff --git a/spec/unit/lib/temporal/connection/serializer/continue_as_new_spec.rb b/spec/unit/lib/temporal/connection/serializer/continue_as_new_spec.rb index 4a7e90eb..18de4355 100644 --- a/spec/unit/lib/temporal/connection/serializer/continue_as_new_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/continue_as_new_spec.rb @@ -9,6 +9,7 @@ task_queue: 'my-task-queue', input: ['one', 'two'], timeouts: Temporal.configuration.timeouts, + headers: {'foo-header': 'bar'}, memo: {'foo-memo': 'baz'}, ) @@ -28,6 +29,7 @@ expect(attribs.input.payloads[0].data).to eq('"one"') expect(attribs.input.payloads[1].data).to eq('"two"') + expect(attribs.header.fields['foo-header'].data).to eq('"bar"') expect(attribs.memo.fields['foo-memo'].data).to eq('"baz"') end end From 9e367d1bed244ece386fb98883ef04f6dc4e23ae Mon Sep 17 00:00:00 2001 From: Anthony Dmitriyev Date: Thu, 9 Dec 2021 18:13:05 +0000 Subject: [PATCH 017/125] [Fix] Non-started activity cancellation (#125) * Fix event target map entry for ACTIVITY_CANCELED event * Fix cancellation of a non-started activity * fixup! Fix event target map entry for ACTIVITY_CANCELED event --- .../integration/activity_cancellation_spec.rb | 39 +++++++++++++++++++ examples/workflows/long_workflow.rb | 2 +- lib/temporal/errors.rb | 3 ++ lib/temporal/workflow/history/event_target.rb | 2 +- lib/temporal/workflow/state_manager.rb | 2 +- .../grpc/history_event_fabricator.rb | 26 +++++++++++++ .../workflow/history/event_target_spec.rb | 16 ++++++++ 7 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 examples/spec/integration/activity_cancellation_spec.rb diff --git a/examples/spec/integration/activity_cancellation_spec.rb b/examples/spec/integration/activity_cancellation_spec.rb new file mode 100644 index 00000000..0d551a19 --- /dev/null +++ b/examples/spec/integration/activity_cancellation_spec.rb @@ -0,0 +1,39 @@ +require 'workflows/long_workflow' + +describe 'Activity cancellation' do + let(:workflow_id) { SecureRandom.uuid } + + it 'cancels a running activity' do + run_id = Temporal.start_workflow(LongWorkflow, options: { workflow_id: workflow_id }) + + # Signal workflow after starting, allowing it to schedule the first activity + sleep 0.5 + Temporal.signal_workflow(LongWorkflow, :CANCEL, workflow_id, run_id) + + result = Temporal.await_workflow_result( + LongWorkflow, + workflow_id: workflow_id, + run_id: run_id, + ) + + expect(result).to be_a(LongRunningActivity::Canceled) + expect(result.message).to eq('cancel activity request received') + end + + it 'cancels a non-started activity' do + # Workflow is started with a signal which will cancel an activity before it has started + run_id = Temporal.start_workflow(LongWorkflow, options: { + workflow_id: workflow_id, + signal_name: :CANCEL + }) + + result = Temporal.await_workflow_result( + LongWorkflow, + workflow_id: workflow_id, + run_id: run_id, + ) + + expect(result).to be_a(Temporal::ActivityCanceled) + expect(result.message).to eq('ACTIVITY_ID_NOT_STARTED') + end +end diff --git a/examples/workflows/long_workflow.rb b/examples/workflows/long_workflow.rb index 3682fad9..c4b4682f 100644 --- a/examples/workflows/long_workflow.rb +++ b/examples/workflows/long_workflow.rb @@ -9,6 +9,6 @@ def execute(cycles = 10, interval = 1) future.cancel end - future.wait + future.get end end diff --git a/lib/temporal/errors.rb b/lib/temporal/errors.rb index 5730224c..2e7f4ebe 100644 --- a/lib/temporal/errors.rb +++ b/lib/temporal/errors.rb @@ -19,6 +19,9 @@ class TimeoutError < ClientError; end # with the intent to propagate to a workflow class ActivityException < ClientError; end + # Represents cancellation of a non-started activity + class ActivityCanceled < ActivityException; end + class ActivityNotRegistered < ClientError; end class WorkflowNotRegistered < ClientError; end diff --git a/lib/temporal/workflow/history/event_target.rb b/lib/temporal/workflow/history/event_target.rb index 96d5de93..8f605a01 100644 --- a/lib/temporal/workflow/history/event_target.rb +++ b/lib/temporal/workflow/history/event_target.rb @@ -19,7 +19,7 @@ class UnexpectedEventType < InternalError; end # NOTE: The order is important, first prefix match wins (will be a longer match) TARGET_TYPES = { - 'ACTIVITY_TASK_CANCEL' => CANCEL_ACTIVITY_REQUEST_TYPE, + 'ACTIVITY_TASK_CANCEL_REQUESTED' => CANCEL_ACTIVITY_REQUEST_TYPE, 'ACTIVITY_TASK' => ACTIVITY_TYPE, 'REQUEST_CANCEL_ACTIVITY_TASK' => CANCEL_ACTIVITY_REQUEST_TYPE, 'TIMER_CANCELED' => CANCEL_TIMER_REQUEST_TYPE, diff --git a/lib/temporal/workflow/state_manager.rb b/lib/temporal/workflow/state_manager.rb index 7d515c06..ac4c1846 100644 --- a/lib/temporal/workflow/state_manager.rb +++ b/lib/temporal/workflow/state_manager.rb @@ -162,7 +162,7 @@ def apply_event(event) when 'ACTIVITY_TASK_CANCELED' state_machine.cancel - dispatch(target, 'failed', Temporal::Workflow::Errors.generate_error(event.attributes.failure)) + dispatch(target, 'failed', Temporal::ActivityCanceled.new(from_details_payloads(event.attributes.details))) when 'TIMER_STARTED' state_machine.start diff --git a/spec/fabricators/grpc/history_event_fabricator.rb b/spec/fabricators/grpc/history_event_fabricator.rb index a290ea0f..9e8538eb 100644 --- a/spec/fabricators/grpc/history_event_fabricator.rb +++ b/spec/fabricators/grpc/history_event_fabricator.rb @@ -1,5 +1,9 @@ require 'securerandom' +class TestSerializer + extend Temporal::Concerns::Payloads +end + Fabricator(:api_history_event, from: Temporal::Api::History::V1::HistoryEvent) do event_id { 1 } event_time { Time.now } @@ -122,6 +126,28 @@ end end +Fabricator(:api_activity_task_canceled_event, from: :api_history_event) do + event_type { Temporal::Api::Enums::V1::EventType::EVENT_TYPE_ACTIVITY_TASK_CANCELED } + activity_task_canceled_event_attributes do |attrs| + Temporal::Api::History::V1::ActivityTaskCanceledEventAttributes.new( + details: TestSerializer.to_details_payloads('ACTIVITY_ID_NOT_STARTED'), + scheduled_event_id: attrs[:event_id] - 2, + started_event_id: nil, + identity: 'test-worker@test-host' + ) + end +end + +Fabricator(:api_activity_task_cancel_requested_event, from: :api_history_event) do + event_type { Temporal::Api::Enums::V1::EventType::EVENT_TYPE_ACTIVITY_TASK_CANCEL_REQUESTED } + activity_task_cancel_requested_event_attributes do |attrs| + Temporal::Api::History::V1::ActivityTaskCancelRequestedEventAttributes.new( + scheduled_event_id: attrs[:event_id] - 1, + workflow_task_completed_event_id: attrs[:event_id] - 2, + ) + end +end + Fabricator(:api_timer_started_event, from: :api_history_event) do event_type { Temporal::Api::Enums::V1::EventType::EVENT_TYPE_TIMER_STARTED } timer_started_event_attributes do |attrs| diff --git a/spec/unit/lib/temporal/workflow/history/event_target_spec.rb b/spec/unit/lib/temporal/workflow/history/event_target_spec.rb index 717c4572..2f2c80bd 100644 --- a/spec/unit/lib/temporal/workflow/history/event_target_spec.rb +++ b/spec/unit/lib/temporal/workflow/history/event_target_spec.rb @@ -21,5 +21,21 @@ expect(subject.type).to eq(described_class::CANCEL_TIMER_REQUEST_TYPE) end end + + context 'when event is ACTIVITY_CANCELED' do + let(:raw_event) { Fabricate(:api_activity_task_canceled_event) } + + it 'sets type to activity' do + expect(subject.type).to eq(described_class::ACTIVITY_TYPE) + end + end + + context 'when event is ACTIVITY_TASK_CANCEL_REQUESTED' do + let(:raw_event) { Fabricate(:api_activity_task_cancel_requested_event) } + + it 'sets type to cancel_activity_request' do + expect(subject.type).to eq(described_class::CANCEL_ACTIVITY_REQUEST_TYPE) + end + end end end From 965ef0d37497a859ff57c2228badcb672036e264 Mon Sep 17 00:00:00 2001 From: Anthony Dmitriyev Date: Thu, 23 Dec 2021 16:43:59 +0000 Subject: [PATCH 018/125] [Fix] Workflow scheduling using a String for a workflow name (#128) --- .../spec/integration/start_workflow_spec.rb | 36 +++++++++++++++++++ lib/temporal/execution_options.rb | 4 ++- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 examples/spec/integration/start_workflow_spec.rb diff --git a/examples/spec/integration/start_workflow_spec.rb b/examples/spec/integration/start_workflow_spec.rb new file mode 100644 index 00000000..2380ade0 --- /dev/null +++ b/examples/spec/integration/start_workflow_spec.rb @@ -0,0 +1,36 @@ +require 'workflows/hello_world_workflow' + +describe 'Temporal.start_workflow' do + let(:workflow_id) { SecureRandom.uuid } + + it 'starts a workflow using a class reference' do + run_id = Temporal.start_workflow(HelloWorldWorkflow, 'Test', options: { + workflow_id: workflow_id + }) + + result = Temporal.await_workflow_result( + HelloWorldWorkflow, + workflow_id: workflow_id, + run_id: run_id + ) + + expect(result).to eq('Hello World, Test') + end + + it 'starts a workflow using a string reference' do + run_id = Temporal.start_workflow('HelloWorldWorkflow', 'Test', options: { + workflow_id: workflow_id, + namespace: Temporal.configuration.namespace, + task_queue: Temporal.configuration.task_queue + }) + + result = Temporal.await_workflow_result( + 'HelloWorldWorkflow', + workflow_id: workflow_id, + run_id: run_id, + namespace: Temporal.configuration.namespace + ) + + expect(result).to eq('Hello World, Test') + end +end diff --git a/lib/temporal/execution_options.rb b/lib/temporal/execution_options.rb index 182c4079..b1cfc94d 100644 --- a/lib/temporal/execution_options.rb +++ b/lib/temporal/execution_options.rb @@ -49,7 +49,9 @@ def task_list private def has_executable_concern?(object) - object.singleton_class.included_modules.include?(Concerns::Executable) + # NOTE: When object is a String .dup is needed since Object#singleton_class mutates + # it and screws up C extension class detection (used by Protobufs) + object.dup.singleton_class.included_modules.include?(Concerns::Executable) rescue TypeError false end From 265edd3bdc697d6c83b96db826f923ad7a061ddc Mon Sep 17 00:00:00 2001 From: jeffschoner-stripe <63118764+jeffschoner-stripe@users.noreply.github.com> Date: Tue, 4 Jan 2022 02:42:23 -0800 Subject: [PATCH 019/125] Fix describe_namespace integration spec to use the default namespace (#129) It can take some time between registering a namespace, and that namespace being available to describe. Moreover, the test as originally written can depend on the behavior of a previous run. The description passed on initial namespace registration will remain on the namespace indefinitely. If this value is changed in code, subsequent tests will fail when the description does not match. Using ruby-samples (or whatever name has been overridden in an environment variable) ensures the same behavior each run and is not subject to the possible delay of creating a new namespace since this namespace must already exist to be able to start the test workers. --- examples/spec/helpers.rb | 4 ++++ .../spec/integration/describe_namespace_spec.rb | 15 +++++++++------ .../spec/integration/metadata_workflow_spec.rb | 6 +++--- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/examples/spec/helpers.rb b/examples/spec/helpers.rb index 4d6d9a20..b5f32504 100644 --- a/examples/spec/helpers.rb +++ b/examples/spec/helpers.rb @@ -33,4 +33,8 @@ def fetch_history(workflow_id, run_id, options = {}) }.merge(options) ) end + + def integration_spec_namespace + ENV.fetch('TEMPORAL_NAMESPACE', 'ruby-samples') + end end diff --git a/examples/spec/integration/describe_namespace_spec.rb b/examples/spec/integration/describe_namespace_spec.rb index 86501f7b..d042190e 100644 --- a/examples/spec/integration/describe_namespace_spec.rb +++ b/examples/spec/integration/describe_namespace_spec.rb @@ -1,16 +1,19 @@ require 'temporal/errors' -describe 'Temporal.describe_namespace' do +describe 'Temporal.describe_namespace', :integration do it 'returns a value' do - description = 'Namespace for temporal-ruby integration test' + namespace = integration_spec_namespace + rescued = false begin - Temporal.register_namespace('a_test_namespace', description) + Temporal.register_namespace(namespace) rescue Temporal::NamespaceAlreadyExistsFailure + rescued = true end - result = Temporal.describe_namespace('a_test_namespace') + expect(rescued).to eq(true) + result = Temporal.describe_namespace(namespace) expect(result).to be_an_instance_of(Temporal::Api::WorkflowService::V1::DescribeNamespaceResponse) - expect(result.namespace_info.name).to eq('a_test_namespace') + expect(result.namespace_info.name).to eq(namespace) expect(result.namespace_info.state).to eq(:NAMESPACE_STATE_REGISTERED) - expect(result.namespace_info.description).to eq(description) + expect(result.namespace_info.description).to_not eq(nil) end end diff --git a/examples/spec/integration/metadata_workflow_spec.rb b/examples/spec/integration/metadata_workflow_spec.rb index ac828e01..508c3af8 100644 --- a/examples/spec/integration/metadata_workflow_spec.rb +++ b/examples/spec/integration/metadata_workflow_spec.rb @@ -1,6 +1,6 @@ require 'workflows/metadata_workflow' -describe MetadataWorkflow do +describe MetadataWorkflow, :integration do subject { described_class } it 'gets task queue from running workflow' do @@ -66,7 +66,7 @@ expect(actual_result.memo['foo']).to eq('bar') expect(Temporal.fetch_workflow_execution_info( - 'ruby-samples', workflow_id, nil + integration_spec_namespace, workflow_id, nil ).memo).to eq({ 'foo' => 'bar' }) end @@ -85,7 +85,7 @@ ) expect(actual_result.memo).to eq({}) expect(Temporal.fetch_workflow_execution_info( - 'ruby-samples', workflow_id, nil + integration_spec_namespace, workflow_id, nil ).memo).to eq({}) end end From f764e21c9a35f343481e2e3d43ca3d979d965485 Mon Sep 17 00:00:00 2001 From: nagl-stripe <86737162+nagl-stripe@users.noreply.github.com> Date: Mon, 10 Jan 2022 03:55:28 -0800 Subject: [PATCH 020/125] Make error deserialization more resilient (#127) --- lib/temporal/workflow/errors.rb | 3 +- .../unit/lib/temporal/workflow/errors_spec.rb | 39 ++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/lib/temporal/workflow/errors.rb b/lib/temporal/workflow/errors.rb index d7c294f5..cea7f471 100644 --- a/lib/temporal/workflow/errors.rb +++ b/lib/temporal/workflow/errors.rb @@ -25,7 +25,7 @@ def self.generate_error(failure, default_exception_class = StandardError) begin exception = exception_class.new(message) - rescue ArgumentError => deserialization_error + rescue => deserialization_error # We don't currently support serializing/deserializing exceptions with more than one argument. message = "#{exception_class}: #{message}" exception = default_exception_class.new(message) @@ -33,6 +33,7 @@ def self.generate_error(failure, default_exception_class = StandardError) "Could not instantiate original error. Defaulting to StandardError.", { original_error: failure.application_failure_info.type, + instantiation_error_class: deserialization_error.class.to_s, instantiation_error_message: deserialization_error.message, }, ) diff --git a/spec/unit/lib/temporal/workflow/errors_spec.rb b/spec/unit/lib/temporal/workflow/errors_spec.rb index a9e053f6..05d5f82a 100644 --- a/spec/unit/lib/temporal/workflow/errors_spec.rb +++ b/spec/unit/lib/temporal/workflow/errors_spec.rb @@ -4,6 +4,15 @@ class ErrorWithTwoArgs < StandardError def initialize(message, another_argument); end end +class ErrorThatRaisesInInitialize < StandardError + def initialize(message) + # This class simulates an error class that has bugs in its initialize method, or where + # the arg isn't a string. It raises the sort of TypeError that would happen if you wrote + # 1 + message + raise TypeError.new("String can't be coerced into Integer") + end +end + class SomeError < StandardError; end describe Temporal::Workflow::Errors do @@ -51,7 +60,7 @@ class SomeError < StandardError; end end - it "falls back to StandardError when the client can't initialize the error class" do + it "falls back to StandardError when the client can't initialize the error class due to arity" do allow(Temporal.logger).to receive(:error) message = "An error message" @@ -73,10 +82,38 @@ class SomeError < StandardError; end 'Could not instantiate original error. Defaulting to StandardError.', { original_error: "ErrorWithTwoArgs", + instantiation_error_class: "ArgumentError", instantiation_error_message: "wrong number of arguments (given 1, expected 2)", }, ) end + it "falls back to StandardError when the client can't initialize the error class when initialize doesn't take a string" do + allow(Temporal.logger).to receive(:error) + + message = "An error message" + stack_trace = ["a fake backtrace"] + failure = Fabricate( + :api_application_failure, + message: message, + backtrace: stack_trace, + error_class: ErrorThatRaisesInInitialize.to_s, + ) + + e = Temporal::Workflow::Errors.generate_error(failure) + expect(e).to be_a(StandardError) + expect(e.message).to eq("ErrorThatRaisesInInitialize: An error message") + expect(e.backtrace).to eq(stack_trace) + expect(Temporal.logger) + .to have_received(:error) + .with( + 'Could not instantiate original error. Defaulting to StandardError.', + { + original_error: "ErrorThatRaisesInInitialize", + instantiation_error_class: "TypeError", + instantiation_error_message: "String can't be coerced into Integer", + }, + ) + end end end From 6adb4e4322d2ae2aa341693582950c415ed86657 Mon Sep 17 00:00:00 2001 From: Chuck Remes Date: Wed, 12 Jan 2022 16:21:47 -0600 Subject: [PATCH 021/125] Modify the Getting Started section so that it works - removed metacharacters from the shell commands so they can be copy/pasted directly - added require 'temporal-ruby' for each example that needs it - made it clear that the worker and workflow both need access to the same configuration step, so it should be its own file - made it clear the worker and workflow files need to be executed in their own terminals/shells --- README.md | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 4d59d255..13e6ef56 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ To find more about Temporal itself please visit . Clone this repository: ```sh -> git clone git@github.com:coinbase/temporal-ruby.git +git clone git@github.com:coinbase/temporal-ruby.git ``` Include this gem to your `Gemfile`: @@ -26,6 +26,7 @@ gem 'temporal-ruby', github: 'coinbase/temporal-ruby' Define an activity: ```ruby +require 'temporal-ruby' class HelloActivity < Temporal::Activity def execute(name) puts "Hello #{name}!" @@ -49,37 +50,41 @@ class HelloWorldWorkflow < Temporal::Workflow end ``` -Configure your Temporal connection: +Configure your Temporal connection and register the namespace with the Temporal service: ```ruby +require 'temporal-ruby' Temporal.configure do |config| config.host = 'localhost' config.port = 7233 config.namespace = 'ruby-samples' config.task_queue = 'hello-world' end -``` - -Register namespace with the Temporal service: -```ruby -Temporal.register_namespace('ruby-samples', 'A safe space for playing with Temporal Ruby') +begin + Temporal.register_namespace('ruby-samples', 'A safe space for playing with Temporal Ruby') +rescue Temporal::NamespaceAlreadyExistsFailure + nil # service was already registered +end ``` -Configure and start your worker process: + +Configure and start your worker process in a terminal shell: ```ruby +require 'path/to/configuration' require 'temporal/worker' worker = Temporal::Worker.new worker.register_workflow(HelloWorldWorkflow) worker.register_activity(HelloActivity) -worker.start +worker.start # runs forever ``` -And finally start your workflow: +And finally start your workflow in another terminal shell: ```ruby +require 'path/to/configuration' require 'path/to/hello_world_workflow' Temporal.start_workflow(HelloWorldWorkflow) @@ -97,12 +102,16 @@ available, make sure to check them out. ## Installing dependencies Temporal service handles all the persistence, fault tolerance and coordination of your workflows and -activities. To set it up locally, download and boot the Docker Compose file from the official repo: +activities. To set it up locally, download and boot the Docker Compose file from the official repo. +The Docker Compose file forwards all ports to your localhost so you can interact with +the containers easily from your shells. + +Run: ```sh -> curl -O https://raw.githubusercontent.com/temporalio/docker-compose/main/docker-compose.yml +curl -O https://raw.githubusercontent.com/temporalio/docker-compose/main/docker-compose.yml -> docker-compose up +docker-compose up ``` ## Workflows From 31d22ced3a7f664f5f0a613a9dd64e39feaaea4a Mon Sep 17 00:00:00 2001 From: Christopher Brown Date: Wed, 19 Jan 2022 12:13:50 -0800 Subject: [PATCH 022/125] add namespace to emitted metrics --- lib/temporal/activity/task_processor.rb | 4 ++-- lib/temporal/workflow/task_processor.rb | 4 ++-- spec/unit/lib/temporal/activity/task_processor_spec.rb | 8 ++++---- spec/unit/lib/temporal/workflow/task_processor_spec.rb | 8 ++++---- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/temporal/activity/task_processor.rb b/lib/temporal/activity/task_processor.rb index 6ad9cefd..ac064cde 100644 --- a/lib/temporal/activity/task_processor.rb +++ b/lib/temporal/activity/task_processor.rb @@ -26,7 +26,7 @@ def process start_time = Time.now Temporal.logger.debug("Processing Activity task", metadata.to_h) - Temporal.metrics.timing('activity_task.queue_time', queue_time_ms, activity: activity_name) + Temporal.metrics.timing('activity_task.queue_time', queue_time_ms, activity: activity_name, namespace: namespace) context = Activity::Context.new(connection, metadata) @@ -46,7 +46,7 @@ def process respond_failed(error) ensure time_diff_ms = ((Time.now - start_time) * 1000).round - Temporal.metrics.timing('activity_task.latency', time_diff_ms, activity: activity_name) + Temporal.metrics.timing('activity_task.latency', time_diff_ms, activity: activity_name, namespace: namespace) Temporal.logger.debug("Activity task processed", metadata.to_h.merge(execution_time: time_diff_ms)) end diff --git a/lib/temporal/workflow/task_processor.rb b/lib/temporal/workflow/task_processor.rb index 63b4c76e..ddb10e77 100644 --- a/lib/temporal/workflow/task_processor.rb +++ b/lib/temporal/workflow/task_processor.rb @@ -24,7 +24,7 @@ def process start_time = Time.now Temporal.logger.debug("Processing Workflow task", metadata.to_h) - Temporal.metrics.timing('workflow_task.queue_time', queue_time_ms, workflow: workflow_name) + Temporal.metrics.timing('workflow_task.queue_time', queue_time_ms, workflow: workflow_name, namespace: namespace) if !workflow_class raise Temporal::WorkflowNotRegistered, 'Workflow is not registered with this worker' @@ -45,7 +45,7 @@ def process fail_task(error) ensure time_diff_ms = ((Time.now - start_time) * 1000).round - Temporal.metrics.timing('workflow_task.latency', time_diff_ms, workflow: workflow_name) + Temporal.metrics.timing('workflow_task.latency', time_diff_ms, workflow: workflow_name, namespace: namespace) Temporal.logger.debug("Workflow task processed", metadata.to_h.merge(execution_time: time_diff_ms)) end diff --git a/spec/unit/lib/temporal/activity/task_processor_spec.rb b/spec/unit/lib/temporal/activity/task_processor_spec.rb index 4cc18a5e..427ac289 100644 --- a/spec/unit/lib/temporal/activity/task_processor_spec.rb +++ b/spec/unit/lib/temporal/activity/task_processor_spec.rb @@ -125,7 +125,7 @@ expect(Temporal.metrics) .to have_received(:timing) - .with('activity_task.queue_time', an_instance_of(Integer), activity: activity_name) + .with('activity_task.queue_time', an_instance_of(Integer), activity: activity_name, namespace: namespace) end it 'sends latency metric' do @@ -133,7 +133,7 @@ expect(Temporal.metrics) .to have_received(:timing) - .with('activity_task.latency', an_instance_of(Integer), activity: activity_name) + .with('activity_task.latency', an_instance_of(Integer), activity: activity_name, namespace: namespace) end context 'with async activity' do @@ -203,7 +203,7 @@ expect(Temporal.metrics) .to have_received(:timing) - .with('activity_task.queue_time', an_instance_of(Integer), activity: activity_name) + .with('activity_task.queue_time', an_instance_of(Integer), activity: activity_name, namespace: namespace) end it 'sends latency metric' do @@ -211,7 +211,7 @@ expect(Temporal.metrics) .to have_received(:timing) - .with('activity_task.latency', an_instance_of(Integer), activity: activity_name) + .with('activity_task.latency', an_instance_of(Integer), activity: activity_name, namespace: namespace) end context 'with ScriptError exception' do diff --git a/spec/unit/lib/temporal/workflow/task_processor_spec.rb b/spec/unit/lib/temporal/workflow/task_processor_spec.rb index 695b3da6..987aacfa 100644 --- a/spec/unit/lib/temporal/workflow/task_processor_spec.rb +++ b/spec/unit/lib/temporal/workflow/task_processor_spec.rb @@ -105,7 +105,7 @@ expect(Temporal.metrics) .to have_received(:timing) - .with('workflow_task.queue_time', an_instance_of(Integer), workflow: workflow_name) + .with('workflow_task.queue_time', an_instance_of(Integer), workflow: workflow_name, namespace: namespace) end it 'sends latency metric' do @@ -113,7 +113,7 @@ expect(Temporal.metrics) .to have_received(:timing) - .with('workflow_task.latency', an_instance_of(Integer), workflow: workflow_name) + .with('workflow_task.latency', an_instance_of(Integer), workflow: workflow_name, namespace: namespace) end end @@ -170,7 +170,7 @@ expect(Temporal.metrics) .to have_received(:timing) - .with('workflow_task.queue_time', an_instance_of(Integer), workflow: workflow_name) + .with('workflow_task.queue_time', an_instance_of(Integer), workflow: workflow_name, namespace: namespace) end it 'sends latency metric' do @@ -178,7 +178,7 @@ expect(Temporal.metrics) .to have_received(:timing) - .with('workflow_task.latency', an_instance_of(Integer), workflow: workflow_name) + .with('workflow_task.latency', an_instance_of(Integer), workflow: workflow_name, namespace: namespace) end end From 534d2a6dc8a4f2886eb641c67668a61b9630715d Mon Sep 17 00:00:00 2001 From: Christopher Brown Date: Mon, 31 Jan 2022 10:43:28 -0800 Subject: [PATCH 023/125] emit the workflow name tag during activity processing --- lib/temporal/activity/task_processor.rb | 4 ++-- spec/unit/lib/temporal/activity/task_processor_spec.rb | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/temporal/activity/task_processor.rb b/lib/temporal/activity/task_processor.rb index ac064cde..7e13325a 100644 --- a/lib/temporal/activity/task_processor.rb +++ b/lib/temporal/activity/task_processor.rb @@ -26,7 +26,7 @@ def process start_time = Time.now Temporal.logger.debug("Processing Activity task", metadata.to_h) - Temporal.metrics.timing('activity_task.queue_time', queue_time_ms, activity: activity_name, namespace: namespace) + Temporal.metrics.timing('activity_task.queue_time', queue_time_ms, activity: activity_name, namespace: namespace, workflow: metadata.workflow_name) context = Activity::Context.new(connection, metadata) @@ -46,7 +46,7 @@ def process respond_failed(error) ensure time_diff_ms = ((Time.now - start_time) * 1000).round - Temporal.metrics.timing('activity_task.latency', time_diff_ms, activity: activity_name, namespace: namespace) + Temporal.metrics.timing('activity_task.latency', time_diff_ms, activity: activity_name, namespace: namespace, workflow: metadata.workflow_name) Temporal.logger.debug("Activity task processed", metadata.to_h.merge(execution_time: time_diff_ms)) end diff --git a/spec/unit/lib/temporal/activity/task_processor_spec.rb b/spec/unit/lib/temporal/activity/task_processor_spec.rb index 427ac289..173fcd65 100644 --- a/spec/unit/lib/temporal/activity/task_processor_spec.rb +++ b/spec/unit/lib/temporal/activity/task_processor_spec.rb @@ -15,6 +15,7 @@ ) end let(:metadata) { Temporal::Metadata.generate_activity_metadata(task, namespace) } + let(:workflow_name) { task.workflow_type.name } let(:activity_name) { 'TestActivity' } let(:connection) { instance_double('Temporal::Connection::GRPC') } let(:middleware_chain) { Temporal::Middleware::Chain.new } @@ -125,7 +126,7 @@ expect(Temporal.metrics) .to have_received(:timing) - .with('activity_task.queue_time', an_instance_of(Integer), activity: activity_name, namespace: namespace) + .with('activity_task.queue_time', an_instance_of(Integer), activity: activity_name, namespace: namespace, workflow: workflow_name) end it 'sends latency metric' do @@ -133,7 +134,7 @@ expect(Temporal.metrics) .to have_received(:timing) - .with('activity_task.latency', an_instance_of(Integer), activity: activity_name, namespace: namespace) + .with('activity_task.latency', an_instance_of(Integer), activity: activity_name, namespace: namespace, workflow: workflow_name) end context 'with async activity' do @@ -203,7 +204,7 @@ expect(Temporal.metrics) .to have_received(:timing) - .with('activity_task.queue_time', an_instance_of(Integer), activity: activity_name, namespace: namespace) + .with('activity_task.queue_time', an_instance_of(Integer), activity: activity_name, namespace: namespace, workflow: workflow_name) end it 'sends latency metric' do @@ -211,7 +212,7 @@ expect(Temporal.metrics) .to have_received(:timing) - .with('activity_task.latency', an_instance_of(Integer), activity: activity_name, namespace: namespace) + .with('activity_task.latency', an_instance_of(Integer), activity: activity_name, namespace: namespace, workflow: workflow_name) end context 'with ScriptError exception' do From 0d7e637ba96ef01ebeb128a3d911468231e76db8 Mon Sep 17 00:00:00 2001 From: Christopher Brown Date: Mon, 31 Jan 2022 10:58:25 -0800 Subject: [PATCH 024/125] fix typo --- lib/temporal/workflow/poller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/temporal/workflow/poller.rb b/lib/temporal/workflow/poller.rb index c0e7b950..691dcfd1 100644 --- a/lib/temporal/workflow/poller.rb +++ b/lib/temporal/workflow/poller.rb @@ -63,7 +63,7 @@ def poll_loop time_diff_ms = ((Time.now - last_poll_time) * 1000).round Temporal.metrics.timing('workflow_poller.time_since_last_poll', time_diff_ms, metrics_tags) - Temporal.logger.debug("Polling Worklow task queue", { namespace: namespace, task_queue: task_queue }) + Temporal.logger.debug("Polling workflow task queue", { namespace: namespace, task_queue: task_queue }) task = poll_for_task last_poll_time = Time.now From e249d63cdbdd898ff410c267e8ce51286db982e4 Mon Sep 17 00:00:00 2001 From: calum-stripe <98350978+calum-stripe@users.noreply.github.com> Date: Mon, 14 Feb 2022 13:03:36 -0800 Subject: [PATCH 025/125] Added list_namespace to client and fixed gRPC request (#137) --- .../spec/integration/list_namespaces_spec.rb | 6 +++++ lib/temporal.rb | 1 + lib/temporal/client.rb | 8 ++++++ lib/temporal/connection/grpc.rb | 4 +-- spec/unit/lib/temporal/grpc_client_spec.rb | 26 +++++++++++++++++++ 5 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 examples/spec/integration/list_namespaces_spec.rb diff --git a/examples/spec/integration/list_namespaces_spec.rb b/examples/spec/integration/list_namespaces_spec.rb new file mode 100644 index 00000000..d22ed82c --- /dev/null +++ b/examples/spec/integration/list_namespaces_spec.rb @@ -0,0 +1,6 @@ +describe 'Temporal.list_namespaces', :integration do + it 'returns the correct values' do + result = Temporal.list_namespaces(page_size: 100) + expect(result).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListNamespacesResponse) + end +end diff --git a/lib/temporal.rb b/lib/temporal.rb index f02d6ec9..2a1e4695 100644 --- a/lib/temporal.rb +++ b/lib/temporal.rb @@ -18,6 +18,7 @@ module Temporal :schedule_workflow, :register_namespace, :describe_namespace, + :list_namespaces, :signal_workflow, :await_workflow_result, :reset_workflow, diff --git a/lib/temporal/client.rb b/lib/temporal/client.rb index 2fcea794..421155c2 100644 --- a/lib/temporal/client.rb +++ b/lib/temporal/client.rb @@ -143,6 +143,14 @@ def describe_namespace(name) connection.describe_namespace(name: name) end + # Fetches all the namespaces. + # + # @param page_size [Integer] number of namespace results to return per page. + # @param next_page_token [String] a optional pagination token returned by a previous list_namespaces call + def list_namespaces(page_size:, next_page_token: "") + connection.list_namespaces(page_size: page_size, next_page_token: next_page_token) + end + # Send a signal to a running workflow # # @param workflow [Temporal::Workflow, nil] workflow class or nil diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index b2ce0350..b8922699 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -50,8 +50,8 @@ def describe_namespace(name:) client.describe_namespace(request) end - def list_namespaces(page_size:) - request = Temporal::Api::WorkflowService::V1::ListNamespacesRequest.new(pageSize: page_size) + def list_namespaces(page_size:, next_page_token: "") + request = Temporal::Api::WorkflowService::V1::ListNamespacesRequest.new(page_size: page_size, next_page_token: next_page_token) client.list_namespaces(request) end diff --git a/spec/unit/lib/temporal/grpc_client_spec.rb b/spec/unit/lib/temporal/grpc_client_spec.rb index a168c359..580a0c68 100644 --- a/spec/unit/lib/temporal/grpc_client_spec.rb +++ b/spec/unit/lib/temporal/grpc_client_spec.rb @@ -73,6 +73,32 @@ end end + describe "#list_namespaces" do + let (:response) do + Temporal::Api::WorkflowService::V1::ListNamespacesResponse.new( + namespaces: [Temporal::Api::WorkflowService::V1::DescribeNamespaceResponse.new], + next_page_token: "" + ) + end + + before { allow(grpc_stub).to receive(:list_namespaces).and_return(response) } + + it 'calls GRPC service with supplied arguments' do + next_page_token = "next-page-token-id" + + subject.list_namespaces( + page_size: 10, + next_page_token: next_page_token, + ) + + expect(grpc_stub).to have_received(:list_namespaces) do |request| + expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListNamespacesRequest) + expect(request.page_size).to eq(10) + expect(request.next_page_token).to eq(next_page_token) + end + end + end + describe '#get_workflow_execution_history' do let(:response) do Temporal::Api::WorkflowService::V1::GetWorkflowExecutionHistoryResponse.new( From 175ffe4a5ef4ad2b9a80bd87a852685b46c0a104 Mon Sep 17 00:00:00 2001 From: Chuck Remes <37843211+chuckremes2@users.noreply.github.com> Date: Mon, 14 Feb 2022 15:12:15 -0600 Subject: [PATCH 026/125] Add support for SignalExternalWorkflow (#134) * initial work to support SignalExternalWorkflow * define the serializer and hook it up * stub in what I think is the correct work for each event type * some fixes per antstorm advice * initial attempt at integration test * docs on testing and an improvement to existing test * encode the signal payload using correct helper * return a Future and fulfill it correctly upon completion * get the \*event_id from the right field in the command structure * modify test to verify the signal is only received once * test for failure to deliver a signal to external workflow * do not discard the failure command otherwise non-deterministic * simplify test workflow by eliminating unnecessary timer * oops, had double call to #schedule_command so signals were sent twice * edit description of example * split to separate files and improve test coverage * change method signature for consistency and a few other cleanups * oops, fix EventType name to match correct constant --- examples/README.md | 20 ++++- examples/bin/worker | 2 + .../wait_for_external_signal_workflow_spec.rb | 82 +++++++++++++++++++ .../send_signal_to_external_workflow.rb | 17 ++++ .../wait_for_external_signal_workflow.rb | 22 +++++ lib/temporal/connection/serializer.rb | 4 +- .../serializer/signal_external_workflow.rb | 33 ++++++++ lib/temporal/workflow/command.rb | 5 +- lib/temporal/workflow/context.rb | 46 +++++++++++ lib/temporal/workflow/history/event.rb | 4 +- lib/temporal/workflow/state_manager.rb | 19 ++++- 11 files changed, 243 insertions(+), 11 deletions(-) create mode 100644 examples/spec/integration/wait_for_external_signal_workflow_spec.rb create mode 100644 examples/workflows/send_signal_to_external_workflow.rb create mode 100644 examples/workflows/wait_for_external_signal_workflow.rb create mode 100644 lib/temporal/connection/serializer/signal_external_workflow.rb diff --git a/examples/README.md b/examples/README.md index 72697ed6..bd9e064a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,7 +7,7 @@ To try these out you need to have a running Temporal service ([setup instruction Install all the gem dependencies by running: ```sh -> bundle install +bundle install ``` Modify the `init.rb` file to point to your Temporal cluster. @@ -15,11 +15,25 @@ Modify the `init.rb` file to point to your Temporal cluster. Start a worker process: ```sh -> bin/worker +bin/worker ``` Use this command to trigger one of the example workflows from the `workflows` directory: ```sh -> bin/trigger NAME_OF_THE_WORKFLOW [argument_1, argument_2, ...] +bin/trigger NAME_OF_THE_WORKFLOW [argument_1, argument_2, ...] ``` +## Testing + +To run tests, make sure the temporal server and the worker process are already running: +```shell +docker-compose up +bin/worker +``` +To execute the tests, run: +```shell +bundle exec rspec +``` +To add a new test that uses a new workflow or new activity, make sure to register those new +workflows and activities by modifying the `bin/worker` file and adding them there. After any +changes to that file, restart the worker process to pick up the new registrations. diff --git a/examples/bin/worker b/examples/bin/worker index 14752ede..174806e8 100755 --- a/examples/bin/worker +++ b/examples/bin/worker @@ -45,6 +45,8 @@ worker.register_workflow(SimpleTimerWorkflow) worker.register_workflow(TimeoutWorkflow) worker.register_workflow(TripBookingWorkflow) worker.register_workflow(WaitForWorkflow) +worker.register_workflow(WaitForExternalSignalWorkflow) +worker.register_workflow(SendSignalToExternalWorkflow) worker.register_activity(AsyncActivity) worker.register_activity(EchoActivity) diff --git a/examples/spec/integration/wait_for_external_signal_workflow_spec.rb b/examples/spec/integration/wait_for_external_signal_workflow_spec.rb new file mode 100644 index 00000000..35c1fd36 --- /dev/null +++ b/examples/spec/integration/wait_for_external_signal_workflow_spec.rb @@ -0,0 +1,82 @@ +require 'workflows/wait_for_external_signal_workflow' +require 'workflows/send_signal_to_external_workflow' + +describe WaitForExternalSignalWorkflow do + let(:signal_name) { "signal_name" } + let(:receiver_workflow_id) { SecureRandom.uuid } + let(:sender_workflow_id) { SecureRandom.uuid } + + context 'when the workflows succeed then' do + it 'receives signal from an external workflow only once' do + run_id = Temporal.start_workflow( + WaitForExternalSignalWorkflow, + signal_name, + options: {workflow_id: receiver_workflow_id} + ) + + Temporal.start_workflow( + SendSignalToExternalWorkflow, + signal_name, + receiver_workflow_id + ) + + result = Temporal.await_workflow_result( + WaitForExternalSignalWorkflow, + workflow_id: receiver_workflow_id, + run_id: run_id, + ) + + expect(result).to eq( + { + received: { + signal_name => ["arg1", "arg2"] + }, + counts: { + signal_name => 1 + } + } + ) + end + + it 'returns :success to the sending workflow' do + Temporal.start_workflow( + WaitForExternalSignalWorkflow, + signal_name, + options: {workflow_id: receiver_workflow_id} + ) + + run_id = Temporal.start_workflow( + SendSignalToExternalWorkflow, + signal_name, + receiver_workflow_id, + options: {workflow_id: sender_workflow_id} + ) + + result = Temporal.await_workflow_result( + SendSignalToExternalWorkflow, + workflow_id: sender_workflow_id, + run_id: run_id, + ) + + expect(result).to eq(:success) + end + end + + context 'when the workflows fail' do + it 'correctly handles failure to deliver' do + run_id = Temporal.start_workflow( + SendSignalToExternalWorkflow, + signal_name, + receiver_workflow_id, + options: {workflow_id: sender_workflow_id}) + + result = Temporal.await_workflow_result( + SendSignalToExternalWorkflow, + workflow_id: sender_workflow_id, + run_id: run_id, + ) + + expect(result).to eq(:failed) + end + end +end diff --git a/examples/workflows/send_signal_to_external_workflow.rb b/examples/workflows/send_signal_to_external_workflow.rb new file mode 100644 index 00000000..c4d560e4 --- /dev/null +++ b/examples/workflows/send_signal_to_external_workflow.rb @@ -0,0 +1,17 @@ +# Sends +signal_name+ to the +target_workflow+ from within a workflow. +# This is different than using the Client#send_signal method which is +# for signaling a workflow *from outside* any workflow. +# +# Returns :success or :failed +# +class SendSignalToExternalWorkflow < Temporal::Workflow + def execute(signal_name, target_workflow) + logger.info("Send a signal to an external workflow") + future = workflow.signal_external_workflow(WaitForExternalSignalWorkflow, signal_name, target_workflow, nil, ["arg1", "arg2"]) + @status = nil + future.done { @status = :success } + future.failed { @status = :failed } + future.get + @status + end +end diff --git a/examples/workflows/wait_for_external_signal_workflow.rb b/examples/workflows/wait_for_external_signal_workflow.rb new file mode 100644 index 00000000..03986309 --- /dev/null +++ b/examples/workflows/wait_for_external_signal_workflow.rb @@ -0,0 +1,22 @@ +# One workflow sends a signal to another workflow. Can be used to implement +# the synchronous-proxy pattern (see Go samples) +# +class WaitForExternalSignalWorkflow < Temporal::Workflow + def execute(expected_signal) + signals_received = {} + signal_counts = Hash.new { |h,k| h[k] = 0 } + + workflow.on_signal do |signal, input| + workflow.logger.info("Received signal name #{signal}, with input #{input.inspect}") + signals_received[signal] = input + signal_counts[signal] += 1 + end + + workflow.wait_for do + workflow.logger.info("Awaiting #{expected_signal}, signals received so far: #{signals_received}") + signals_received.key?(expected_signal) + end + + { received: signals_received, counts: signal_counts } + end +end diff --git a/lib/temporal/connection/serializer.rb b/lib/temporal/connection/serializer.rb index 98ce71b4..b6da64b3 100644 --- a/lib/temporal/connection/serializer.rb +++ b/lib/temporal/connection/serializer.rb @@ -8,6 +8,7 @@ require 'temporal/connection/serializer/complete_workflow' require 'temporal/connection/serializer/continue_as_new' require 'temporal/connection/serializer/fail_workflow' +require 'temporal/connection/serializer/signal_external_workflow' module Temporal module Connection @@ -21,7 +22,8 @@ module Serializer Workflow::Command::CancelTimer => Serializer::CancelTimer, Workflow::Command::CompleteWorkflow => Serializer::CompleteWorkflow, Workflow::Command::ContinueAsNew => Serializer::ContinueAsNew, - Workflow::Command::FailWorkflow => Serializer::FailWorkflow + Workflow::Command::FailWorkflow => Serializer::FailWorkflow, + Workflow::Command::SignalExternalWorkflow => Serializer::SignalExternalWorkflow }.freeze def self.serialize(object) diff --git a/lib/temporal/connection/serializer/signal_external_workflow.rb b/lib/temporal/connection/serializer/signal_external_workflow.rb new file mode 100644 index 00000000..91907edd --- /dev/null +++ b/lib/temporal/connection/serializer/signal_external_workflow.rb @@ -0,0 +1,33 @@ +require 'temporal/connection/serializer/base' +require 'temporal/concerns/payloads' + +module Temporal + module Connection + module Serializer + class SignalExternalWorkflow < Base + include Concerns::Payloads + + def to_proto + Temporal::Api::Command::V1::Command.new( + command_type: Temporal::Api::Enums::V1::CommandType::COMMAND_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION, + signal_external_workflow_execution_command_attributes: + Temporal::Api::Command::V1::SignalExternalWorkflowExecutionCommandAttributes.new( + namespace: object.namespace, + execution: serialize_execution(object.execution), + signal_name: object.signal_name, + input: to_signal_payloads(object.input), + control: "", # deprecated + child_workflow_only: object.child_workflow_only + ) + ) + end + + private + + def serialize_execution(execution) + Temporal::Api::Common::V1::WorkflowExecution.new(workflow_id: execution[:workflow_id], run_id: execution[:run_id]) + end + end + end + end +end diff --git a/lib/temporal/workflow/command.rb b/lib/temporal/workflow/command.rb index 8297abe9..c02bc8d6 100644 --- a/lib/temporal/workflow/command.rb +++ b/lib/temporal/workflow/command.rb @@ -11,6 +11,7 @@ module Command CancelTimer = Struct.new(:timer_id, keyword_init: true) CompleteWorkflow = Struct.new(:result, keyword_init: true) FailWorkflow = Struct.new(:exception, keyword_init: true) + SignalExternalWorkflow = Struct.new(:namespace, :execution, :signal_name, :input, :child_workflow_only, keyword_init: true) # only these commands are supported right now SCHEDULE_ACTIVITY_TYPE = :schedule_activity @@ -21,6 +22,7 @@ module Command CANCEL_TIMER_TYPE = :cancel_timer COMPLETE_WORKFLOW_TYPE = :complete_workflow FAIL_WORKFLOW_TYPE = :fail_workflow + SIGNAL_EXTERNAL_WORKFLOW_TYPE = :signal_external_workflow COMMAND_CLASS_MAP = { SCHEDULE_ACTIVITY_TYPE => ScheduleActivity, @@ -30,7 +32,8 @@ module Command START_TIMER_TYPE => StartTimer, CANCEL_TIMER_TYPE => CancelTimer, COMPLETE_WORKFLOW_TYPE => CompleteWorkflow, - FAIL_WORKFLOW_TYPE => FailWorkflow + FAIL_WORKFLOW_TYPE => FailWorkflow, + SIGNAL_EXTERNAL_WORKFLOW_TYPE => SignalExternalWorkflow }.freeze def self.generate(type, **args) diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index d88d8f30..7a07d4fc 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -304,6 +304,52 @@ def cancel(target, cancelation_id) end end + # Send a signal from inside a workflow to another workflow. Not to be confused with + # Client#signal_workflow which sends a signal from outside a workflow to a workflow. + # + # @param workflow [Temporal::Workflow, nil] workflow class or nil + # @param signal [String] name of the signal to send + # @param workflow_id [String] + # @param run_id [String] + # @param input [String, Array, nil] optional arguments for the signal + # @param namespace [String, nil] if nil, choose the one declared on the workflow class or the + # global default + # @param child_workflow_only [Boolean] indicates whether the signal should only be delivered to a + # child workflow; defaults to false + # + # @return [Future] future + def signal_external_workflow(workflow, signal, workflow_id, run_id = nil, input = nil, namespace: nil, child_workflow_only: false) + options ||= {} + + execution_options = ExecutionOptions.new(workflow, {}, config.default_execution_options) + + command = Command::SignalExternalWorkflow.new( + namespace: namespace || execution_options.namespace, + execution: { + workflow_id: workflow_id, + run_id: run_id + }, + signal_name: signal, + input: input, + child_workflow_only: child_workflow_only + ) + + target, cancelation_id = schedule_command(command) + future = Future.new(target, self, cancelation_id: cancelation_id) + + dispatcher.register_handler(target, 'completed') do |result| + future.set(result) + future.success_callbacks.each { |callback| call_in_fiber(callback, result) } + end + + dispatcher.register_handler(target, 'failed') do |exception| + future.fail(exception) + future.failure_callbacks.each { |callback| call_in_fiber(callback, exception) } + end + + future + end + private attr_reader :state_manager, :dispatcher, :workflow_class diff --git a/lib/temporal/workflow/history/event.rb b/lib/temporal/workflow/history/event.rb index be389636..9bf90927 100644 --- a/lib/temporal/workflow/history/event.rb +++ b/lib/temporal/workflow/history/event.rb @@ -10,9 +10,7 @@ class Event ACTIVITY_TASK_CANCELED TIMER_FIRED REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION_FAILED - SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_FAILED EXTERNAL_WORKFLOW_EXECUTION_CANCEL_REQUESTED - EXTERNAL_WORKFLOW_EXECUTION_SIGNALED UPSERT_WORKFLOW_SEARCH_ATTRIBUTES ].freeze @@ -48,7 +46,7 @@ def originating_event_id 1 # fixed id for everything related to current workflow when *EVENT_TYPES attributes.scheduled_event_id - when *CHILD_WORKFLOW_EVENTS + when *CHILD_WORKFLOW_EVENTS, 'EXTERNAL_WORKFLOW_EXECUTION_SIGNALED', 'SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_FAILED' attributes.initiated_event_id else id diff --git a/lib/temporal/workflow/state_manager.rb b/lib/temporal/workflow/state_manager.rb index ac4c1846..22bbd4c0 100644 --- a/lib/temporal/workflow/state_manager.rb +++ b/lib/temporal/workflow/state_manager.rb @@ -241,13 +241,24 @@ def apply_event(event) # todo when 'SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_INITIATED' - # todo + # Temporal Server will try to Signal the targeted Workflow + # Contains the Signal name, as well as a Signal payload + # The workflow that sends the signal creates this event in its log; the + # receiving workflow records WORKFLOW_EXECUTION_SIGNALED on reception + state_machine.start + discard_command(target) when 'SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_FAILED' - # todo + # Temporal Server cannot Signal the targeted Workflow + # Usually because the Workflow could not be found + state_machine.fail + dispatch(target, 'failed', 'StandardError', event.attributes.cause) when 'EXTERNAL_WORKFLOW_EXECUTION_SIGNALED' - # todo + # Temporal Server has successfully Signaled the targeted Workflow + # Return the result to the Future waiting on this + state_machine.complete + dispatch(target, 'completed') when 'UPSERT_WORKFLOW_SEARCH_ATTRIBUTES' # todo @@ -274,6 +285,8 @@ def event_target_from(command_id, command) History::EventTarget::WORKFLOW_TYPE when Command::StartChildWorkflow History::EventTarget::CHILD_WORKFLOW_TYPE + when Command::SignalExternalWorkflow + History::EventTarget::EXTERNAL_WORKFLOW_TYPE end History::EventTarget.new(command_id, target_type) From 427f105e81813463a96e186b39d82e0797d2d6de Mon Sep 17 00:00:00 2001 From: Drew Hoskins <37816070+drewhoskins-stripe@users.noreply.github.com> Date: Mon, 14 Feb 2022 13:15:20 -0800 Subject: [PATCH 027/125] Reject commands issued after workflow completion (#135) * Validate commands issued after workflow completion * polish * Unit test * Improve efficiency * wordsmith --- examples/bin/worker | 1 + .../invalid_continue_as_new_workflow.rb | 17 ++++++++ lib/temporal/errors.rb | 8 ++++ lib/temporal/workflow/state_manager.rb | 22 +++++++++++ .../temporal/workflow/state_manager_spec.rb | 39 +++++++++++++++++++ 5 files changed, 87 insertions(+) create mode 100644 examples/workflows/invalid_continue_as_new_workflow.rb create mode 100644 spec/unit/lib/temporal/workflow/state_manager_spec.rb diff --git a/examples/bin/worker b/examples/bin/worker index 174806e8..b4e55d23 100755 --- a/examples/bin/worker +++ b/examples/bin/worker @@ -28,6 +28,7 @@ worker.register_workflow(CheckWorkflow) worker.register_workflow(FailingActivitiesWorkflow) worker.register_workflow(FailingWorkflow) worker.register_workflow(HelloWorldWorkflow) +worker.register_workflow(InvalidContinueAsNewWorkflow) worker.register_workflow(LocalHelloWorldWorkflow) worker.register_workflow(LongWorkflow) worker.register_workflow(LoopWorkflow) diff --git a/examples/workflows/invalid_continue_as_new_workflow.rb b/examples/workflows/invalid_continue_as_new_workflow.rb new file mode 100644 index 00000000..99b52d98 --- /dev/null +++ b/examples/workflows/invalid_continue_as_new_workflow.rb @@ -0,0 +1,17 @@ +require 'activities/hello_world_activity' + +# If you run this, you'll get a WorkflowAlreadyCompletingError because after the +# continue_as_new, we try to do something else. +class InvalidContinueAsNewWorkflow < Temporal::Workflow + timeouts execution: 20 + + def execute + future = HelloWorldActivity.execute('Alice') + workflow.sleep(1) + workflow.continue_as_new + # Doing anything after continue_as_new (or any workflow completion) is illegal + future.done do + HelloWorldActivity.execute('Bob') + end + end +end diff --git a/lib/temporal/errors.rb b/lib/temporal/errors.rb index 2e7f4ebe..5047cc90 100644 --- a/lib/temporal/errors.rb +++ b/lib/temporal/errors.rb @@ -47,6 +47,13 @@ def initialize(new_run_id:) end end + # Once the workflow succeeds, fails, or continues as new, you can't issue any other commands such as + # scheduling an activity. This error is thrown if you try, before we report completion back to the server. + # This could happen due to activity futures that aren't awaited before the workflow closes, + # calling workflow.continue_as_new, workflow.complete, or workflow.fail in the middle of your workflow code, + # or an internal framework bug. + class WorkflowAlreadyCompletingError < InternalError; end + class WorkflowExecutionAlreadyStartedFailure < ApiError attr_reader :run_id @@ -62,4 +69,5 @@ class NamespaceAlreadyExistsFailure < ApiError; end class CancellationAlreadyRequestedFailure < ApiError; end class QueryFailedFailure < ApiError; end class UnexpectedResponse < ApiError; end + end diff --git a/lib/temporal/workflow/state_manager.rb b/lib/temporal/workflow/state_manager.rb index 22bbd4c0..da78d0cc 100644 --- a/lib/temporal/workflow/state_manager.rb +++ b/lib/temporal/workflow/state_manager.rb @@ -56,6 +56,7 @@ def schedule(command) state_machine = command_tracker[command_id] state_machine.requested if state_machine.state == CommandStateMachine::NEW_STATE + validate_append_command(command) commands << [command_id, command] return [event_target_from(command_id, command), cancelation_id] @@ -94,6 +95,27 @@ def next_event_id @last_event_id += 1 end + def validate_append_command(command) + return if commands.last.nil? + _, previous_command = commands.last + case previous_command + when Command::CompleteWorkflow, Command::FailWorkflow, Command::ContinueAsNew + context_string = case previous_command + when Command::CompleteWorkflow + "The workflow completed" + when Command::FailWorkflow + "The workflow failed" + when Command::ContinueAsNew + "The workflow continued as new" + end + raise Temporal::WorkflowAlreadyCompletingError.new( + "You cannot do anything in a Workflow after it completes. #{context_string}, "\ + "but then it sent a new command: #{command.class}. This can happen, for example, if you've "\ + "not waited for all of your Activity futures before finishing the Workflow." + ) + end + end + def apply_event(event) state_machine = command_tracker[event.originating_event_id] target = History::EventTarget.from_event(event) diff --git a/spec/unit/lib/temporal/workflow/state_manager_spec.rb b/spec/unit/lib/temporal/workflow/state_manager_spec.rb new file mode 100644 index 00000000..57745b71 --- /dev/null +++ b/spec/unit/lib/temporal/workflow/state_manager_spec.rb @@ -0,0 +1,39 @@ +require 'temporal/workflow' +require 'temporal/workflow/dispatcher' +require 'temporal/workflow/state_manager' +require 'temporal/errors' + +describe Temporal::Workflow::StateManager do + + describe '#schedule' do + class MyWorkflow < Temporal::Workflow; end + + # These are all "terminal" commands + [ + Temporal::Workflow::Command::ContinueAsNew.new( + workflow_type: MyWorkflow, + task_queue: 'dummy', + ), + Temporal::Workflow::Command::FailWorkflow.new( + exception: StandardError.new('dummy'), + ), + Temporal::Workflow::Command::CompleteWorkflow.new( + result: 5, + ), + ].each do |terminal_command| + it "fails to validate if #{terminal_command.class} is not the last command scheduled" do + state_manager = described_class.new(Temporal::Workflow::Dispatcher.new) + + next_command = Temporal::Workflow::Command::RecordMarker.new( + name: Temporal::Workflow::StateManager::RELEASE_MARKER, + details: 'dummy', + ) + + state_manager.schedule(terminal_command) + expect do + state_manager.schedule(next_command) + end.to raise_error(Temporal::WorkflowAlreadyCompletingError) + end + end + end +end \ No newline at end of file From fd445bd298a4dec69410208eea22bf8f4acdbaf1 Mon Sep 17 00:00:00 2001 From: Christopher Vanderschuere Date: Mon, 21 Feb 2022 15:00:32 -0800 Subject: [PATCH 028/125] Populate namespace field to all workflow/activity task RPCs (#139) Related to https://github.com/temporalio/api/pull/101 and https://github.com/temporalio/api/pull/102 --- lib/temporal/activity/task_processor.rb | 6 +++--- lib/temporal/connection/grpc.rb | 18 ++++++++++++------ lib/temporal/workflow/task_processor.rb | 3 ++- .../temporal/activity/task_processor_spec.rb | 11 +++++++++-- .../temporal/workflow/task_processor_spec.rb | 4 +++- 5 files changed, 29 insertions(+), 13 deletions(-) diff --git a/lib/temporal/activity/task_processor.rb b/lib/temporal/activity/task_processor.rb index 7e13325a..27ee7ba4 100644 --- a/lib/temporal/activity/task_processor.rb +++ b/lib/temporal/activity/task_processor.rb @@ -54,7 +54,7 @@ def process attr_reader :task, :namespace, :task_token, :activity_name, :activity_class, :middleware_chain, :metadata, :config - + def connection @connection ||= Temporal::Connection.generate(config.for_connection) end @@ -71,7 +71,7 @@ def respond_completed(result) Temporal.logger.debug("Failed to report activity task completion, retrying", metadata.to_h) end Temporal::Connection::Retryer.with_retries(on_retry: log_retry) do - connection.respond_activity_task_completed(task_token: task_token, result: result) + connection.respond_activity_task_completed(namespace: namespace, task_token: task_token, result: result) end rescue StandardError => error Temporal.logger.error("Unable to complete Activity", metadata.to_h.merge(error: error.inspect)) @@ -85,7 +85,7 @@ def respond_failed(error) Temporal.logger.debug("Failed to report activity task failure, retrying", metadata.to_h) end Temporal::Connection::Retryer.with_retries(on_retry: log_retry) do - connection.respond_activity_task_failed(task_token: task_token, exception: error) + connection.respond_activity_task_failed(namespace: namespace, task_token: task_token, exception: error) end rescue StandardError => error Temporal.logger.error("Unable to fail Activity task", metadata.to_h.merge(error: error.inspect)) diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index b8922699..0e2e0e7c 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -174,8 +174,9 @@ def poll_workflow_task_queue(namespace:, task_queue:) poll_request.execute end - def respond_workflow_task_completed(task_token:, commands:) + def respond_workflow_task_completed(namespace:, task_token:, commands:) request = Temporal::Api::WorkflowService::V1::RespondWorkflowTaskCompletedRequest.new( + namespace: namespace, identity: identity, task_token: task_token, commands: Array(commands).map { |(_, command)| Serializer.serialize(command) } @@ -183,8 +184,9 @@ def respond_workflow_task_completed(task_token:, commands:) client.respond_workflow_task_completed(request) end - def respond_workflow_task_failed(task_token:, cause:, exception: nil) + def respond_workflow_task_failed(namespace:, task_token:, cause:, exception: nil) request = Temporal::Api::WorkflowService::V1::RespondWorkflowTaskFailedRequest.new( + namespace: namespace, identity: identity, task_token: task_token, cause: cause, @@ -210,8 +212,9 @@ def poll_activity_task_queue(namespace:, task_queue:) poll_request.execute end - def record_activity_task_heartbeat(task_token:, details: nil) + def record_activity_task_heartbeat(namespace:, task_token:, details: nil) request = Temporal::Api::WorkflowService::V1::RecordActivityTaskHeartbeatRequest.new( + namespace: namespace, task_token: task_token, details: to_details_payloads(details), identity: identity @@ -223,8 +226,9 @@ def record_activity_task_heartbeat_by_id raise NotImplementedError end - def respond_activity_task_completed(task_token:, result:) + def respond_activity_task_completed(namespace:, task_token:, result:) request = Temporal::Api::WorkflowService::V1::RespondActivityTaskCompletedRequest.new( + namespace: namespace, identity: identity, task_token: task_token, result: to_result_payloads(result), @@ -244,8 +248,9 @@ def respond_activity_task_completed_by_id(namespace:, activity_id:, workflow_id: client.respond_activity_task_completed_by_id(request) end - def respond_activity_task_failed(task_token:, exception:) + def respond_activity_task_failed(namespace:, task_token:, exception:) request = Temporal::Api::WorkflowService::V1::RespondActivityTaskFailedRequest.new( + namespace: namespace, identity: identity, task_token: task_token, failure: Serializer::Failure.new(exception).to_proto @@ -265,8 +270,9 @@ def respond_activity_task_failed_by_id(namespace:, activity_id:, workflow_id:, r client.respond_activity_task_failed_by_id(request) end - def respond_activity_task_canceled(task_token:, details: nil) + def respond_activity_task_canceled(namespace:, task_token:, details: nil) request = Temporal::Api::WorkflowService::V1::RespondActivityTaskCanceledRequest.new( + namespace: namespace, task_token: task_token, details: to_details_payloads(details), identity: identity diff --git a/lib/temporal/workflow/task_processor.rb b/lib/temporal/workflow/task_processor.rb index ddb10e77..ce271e4e 100644 --- a/lib/temporal/workflow/task_processor.rb +++ b/lib/temporal/workflow/task_processor.rb @@ -90,7 +90,7 @@ def fetch_full_history def complete_task(commands) Temporal.logger.info("Workflow task completed", metadata.to_h) - connection.respond_workflow_task_completed(task_token: task_token, commands: commands) + connection.respond_workflow_task_completed(namespace: namespace, task_token: task_token, commands: commands) end def fail_task(error) @@ -103,6 +103,7 @@ def fail_task(error) return if task.attempt > MAX_FAILED_ATTEMPTS connection.respond_workflow_task_failed( + namespace: namespace, task_token: task_token, cause: Temporal::Api::Enums::V1::WorkflowTaskFailedCause::WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE, exception: error diff --git a/spec/unit/lib/temporal/activity/task_processor_spec.rb b/spec/unit/lib/temporal/activity/task_processor_spec.rb index 173fcd65..87688a20 100644 --- a/spec/unit/lib/temporal/activity/task_processor_spec.rb +++ b/spec/unit/lib/temporal/activity/task_processor_spec.rb @@ -54,6 +54,7 @@ expect(connection) .to have_received(:respond_activity_task_failed) .with( + namespace: namespace, task_token: task.task_token, exception: an_instance_of(Temporal::ActivityNotRegistered) ) @@ -110,7 +111,7 @@ expect(connection) .to have_received(:respond_activity_task_completed) - .with(task_token: task.task_token, result: 'result') + .with(namespace: namespace, task_token: task.task_token, result: 'result') end it 'ignores connection exception' do @@ -171,6 +172,7 @@ expect(connection) .to have_received(:respond_activity_task_failed) .with( + namespace: namespace, task_token: task.task_token, exception: exception ) @@ -224,6 +226,7 @@ expect(connection) .to have_received(:respond_activity_task_failed) .with( + namespace: namespace, task_token: task.task_token, exception: exception ) @@ -248,7 +251,11 @@ expect(connection) .to have_received(:respond_activity_task_failed) - .with(task_token: task.task_token, exception: exception) + .with( + namespace: namespace, + task_token: task.task_token, + exception: exception + ) end end end diff --git a/spec/unit/lib/temporal/workflow/task_processor_spec.rb b/spec/unit/lib/temporal/workflow/task_processor_spec.rb index 987aacfa..188b3a90 100644 --- a/spec/unit/lib/temporal/workflow/task_processor_spec.rb +++ b/spec/unit/lib/temporal/workflow/task_processor_spec.rb @@ -89,7 +89,7 @@ expect(connection) .to have_received(:respond_workflow_task_completed) - .with(task_token: task.task_token, commands: commands) + .with(namespace: namespace, task_token: task.task_token, commands: commands) end it 'ignores connection exception' do @@ -128,6 +128,7 @@ expect(connection) .to have_received(:respond_workflow_task_failed) .with( + namespace: namespace, task_token: task.task_token, cause: Temporal::Api::Enums::V1::WorkflowTaskFailedCause::WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE, exception: exception @@ -216,6 +217,7 @@ expect(connection) .to have_received(:respond_workflow_task_failed) .with( + namespace: namespace, task_token: task.task_token, cause: Temporal::Api::Enums::V1::WorkflowTaskFailedCause::WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE, exception: an_instance_of(Temporal::UnexpectedResponse) From 48f185e64589f56f4731013b6b4a6d52bfc231a3 Mon Sep 17 00:00:00 2001 From: Dave Willett Date: Mon, 28 Feb 2022 04:14:19 -0800 Subject: [PATCH 029/125] [Fix] Add missing namespace arg for heartbeat (#146) * Add missing namespace arg for heartbeat, small helper refactor * Return the appropriate workflow_id that is used --- examples/spec/helpers.rb | 12 ++++-------- .../spec/integration/activity_cancellation_spec.rb | 9 +++------ lib/temporal/activity/context.rb | 2 +- spec/unit/lib/temporal/activity/context_spec.rb | 4 ++-- 4 files changed, 10 insertions(+), 17 deletions(-) diff --git a/examples/spec/helpers.rb b/examples/spec/helpers.rb index b5f32504..18810d5e 100644 --- a/examples/spec/helpers.rb +++ b/examples/spec/helpers.rb @@ -2,14 +2,10 @@ module Helpers def run_workflow(workflow, *input, **args) - workflow_id = SecureRandom.uuid - run_id = Temporal.start_workflow( - workflow, - *input, - **args.merge(options: { workflow_id: workflow_id }) - ) + args[:options] = { workflow_id: SecureRandom.uuid }.merge(args[:options] || {}) + run_id = Temporal.start_workflow(workflow, *input, **args) - return workflow_id, run_id + [args[:options][:workflow_id], run_id] end def wait_for_workflow_completion(workflow_id, run_id) @@ -25,7 +21,7 @@ def wait_for_workflow_completion(workflow_id, run_id) def fetch_history(workflow_id, run_id, options = {}) connection = Temporal.send(:default_client).send(:connection) - result = connection.get_workflow_execution_history( + connection.get_workflow_execution_history( { namespace: Temporal.configuration.namespace, workflow_id: workflow_id, diff --git a/examples/spec/integration/activity_cancellation_spec.rb b/examples/spec/integration/activity_cancellation_spec.rb index 0d551a19..ca39d639 100644 --- a/examples/spec/integration/activity_cancellation_spec.rb +++ b/examples/spec/integration/activity_cancellation_spec.rb @@ -1,10 +1,8 @@ require 'workflows/long_workflow' -describe 'Activity cancellation' do - let(:workflow_id) { SecureRandom.uuid } - +describe 'Activity cancellation', :integration do it 'cancels a running activity' do - run_id = Temporal.start_workflow(LongWorkflow, options: { workflow_id: workflow_id }) + workflow_id, run_id = run_workflow(LongWorkflow) # Signal workflow after starting, allowing it to schedule the first activity sleep 0.5 @@ -22,8 +20,7 @@ it 'cancels a non-started activity' do # Workflow is started with a signal which will cancel an activity before it has started - run_id = Temporal.start_workflow(LongWorkflow, options: { - workflow_id: workflow_id, + workflow_id, run_id = run_workflow(LongWorkflow, options: { signal_name: :CANCEL }) diff --git a/lib/temporal/activity/context.rb b/lib/temporal/activity/context.rb index 2c65c5af..a5515142 100644 --- a/lib/temporal/activity/context.rb +++ b/lib/temporal/activity/context.rb @@ -32,7 +32,7 @@ def async_token def heartbeat(details = nil) logger.debug("Activity heartbeat", metadata.to_h) - connection.record_activity_task_heartbeat(task_token: task_token, details: details) + connection.record_activity_task_heartbeat(namespace: metadata.namespace, task_token: task_token, details: details) end def heartbeat_details diff --git a/spec/unit/lib/temporal/activity/context_spec.rb b/spec/unit/lib/temporal/activity/context_spec.rb index 97ea4fc4..df0ff740 100644 --- a/spec/unit/lib/temporal/activity/context_spec.rb +++ b/spec/unit/lib/temporal/activity/context_spec.rb @@ -17,7 +17,7 @@ expect(client) .to have_received(:record_activity_task_heartbeat) - .with(task_token: metadata.task_token, details: nil) + .with(namespace: metadata.namespace, task_token: metadata.task_token, details: nil) end it 'records heartbeat with details' do @@ -25,7 +25,7 @@ expect(client) .to have_received(:record_activity_task_heartbeat) - .with(task_token: metadata.task_token, details: { foo: :bar }) + .with(namespace: metadata.namespace, task_token: metadata.task_token, details: { foo: :bar }) end end From 69f90a3a77b35c3e25b23cf778d2c4d0b3ccc4a5 Mon Sep 17 00:00:00 2001 From: Drew Hoskins <37816070+drewhoskins-stripe@users.noreply.github.com> Date: Mon, 28 Feb 2022 14:40:13 -0800 Subject: [PATCH 030/125] Implement upsert_search_attributes (#145) * [woflo] upsert_search_attributes * Move context validators * tests * Feedback, take 2 --- examples/bin/worker | 3 +- .../upsert_search_attributes_spec.rb | 36 +++++++++++++++++ .../upsert_search_attributes_workflow.rb | 17 ++++++++ lib/temporal/connection/serializer.rb | 4 +- .../serializer/upsert_search_attributes.rb | 24 +++++++++++ .../testing/local_workflow_context.rb | 8 ++++ lib/temporal/workflow/command.rb | 5 ++- lib/temporal/workflow/context.rb | 13 ++++++ lib/temporal/workflow/context_validators.rb | 22 ++++++++++ lib/temporal/workflow/execution_info.rb | 3 +- lib/temporal/workflow/history/event_target.rb | 3 +- lib/temporal/workflow/state_manager.rb | 4 +- .../grpc/search_attributes_fabricator.rb | 7 ++++ .../workflow_execution_info_fabricator.rb | 1 + .../upsert_search_attributes_spec.rb | 36 +++++++++++++++++ .../testing/local_workflow_context_spec.rb | 24 +++++++++++ .../lib/temporal/workflow/context_spec.rb | 40 +++++++++++++++++++ 17 files changed, 244 insertions(+), 6 deletions(-) create mode 100644 examples/spec/integration/upsert_search_attributes_spec.rb create mode 100644 examples/workflows/upsert_search_attributes_workflow.rb create mode 100644 lib/temporal/connection/serializer/upsert_search_attributes.rb create mode 100644 lib/temporal/workflow/context_validators.rb create mode 100644 spec/fabricators/grpc/search_attributes_fabricator.rb create mode 100644 spec/unit/lib/temporal/connection/serializer/upsert_search_attributes_spec.rb create mode 100644 spec/unit/lib/temporal/workflow/context_spec.rb diff --git a/examples/bin/worker b/examples/bin/worker index b4e55d23..968c991e 100755 --- a/examples/bin/worker +++ b/examples/bin/worker @@ -39,15 +39,16 @@ worker.register_workflow(QuickTimeoutWorkflow) worker.register_workflow(RandomlyFailingWorkflow) worker.register_workflow(ReleaseWorkflow) worker.register_workflow(ResultWorkflow) +worker.register_workflow(SendSignalToExternalWorkflow) worker.register_workflow(SerialHelloWorldWorkflow) worker.register_workflow(SideEffectWorkflow) worker.register_workflow(SignalWithStartWorkflow) worker.register_workflow(SimpleTimerWorkflow) worker.register_workflow(TimeoutWorkflow) worker.register_workflow(TripBookingWorkflow) +worker.register_workflow(UpsertSearchAttributesWorkflow) worker.register_workflow(WaitForWorkflow) worker.register_workflow(WaitForExternalSignalWorkflow) -worker.register_workflow(SendSignalToExternalWorkflow) worker.register_activity(AsyncActivity) worker.register_activity(EchoActivity) diff --git a/examples/spec/integration/upsert_search_attributes_spec.rb b/examples/spec/integration/upsert_search_attributes_spec.rb new file mode 100644 index 00000000..135c3611 --- /dev/null +++ b/examples/spec/integration/upsert_search_attributes_spec.rb @@ -0,0 +1,36 @@ +require 'workflows/upsert_search_attributes_workflow' + +describe 'Temporal::Workflow::Context.upsert_search_attributes', :integration do + it 'can upsert a search attribute and then retrieve it' do + workflow_id = 'upsert_search_attributes_test_wf-' + SecureRandom.uuid + + expected_attributes = { + 'CustomStringField' => 'moo', + 'CustomBoolField' => true, + 'CustomDoubleField' => 3.14, + 'CustomIntField' => 0, + } + + run_id = Temporal.start_workflow( + UpsertSearchAttributesWorkflow, + *expected_attributes.values, + options: { + workflow_id: workflow_id, + }, + ) + + added_attributes = Temporal.await_workflow_result( + UpsertSearchAttributesWorkflow, + workflow_id: workflow_id, + run_id: run_id, + ) + expect(added_attributes).to eq(expected_attributes) + + execution_info = Temporal.fetch_workflow_execution_info( + integration_spec_namespace, + workflow_id, + nil + ) + expect(execution_info.search_attributes).to eq(expected_attributes) + end +end \ No newline at end of file diff --git a/examples/workflows/upsert_search_attributes_workflow.rb b/examples/workflows/upsert_search_attributes_workflow.rb new file mode 100644 index 00000000..ccdf8392 --- /dev/null +++ b/examples/workflows/upsert_search_attributes_workflow.rb @@ -0,0 +1,17 @@ +class UpsertSearchAttributesWorkflow < Temporal::Workflow + def execute(string_value, bool_value, float_value, int_value) + # These are included in the default temporal docker setup. + # Run tctl admin cluster get-search-attributes to list the options and + # See https://docs.temporal.io/docs/tctl/how-to-add-a-custom-search-attribute-to-a-cluster-using-tctl + # for instructions on adding them. + attributes = { + 'CustomStringField' => string_value, + 'CustomBoolField' => bool_value, + 'CustomDoubleField' => float_value, + 'CustomIntField' => int_value, + } + workflow.upsert_search_attributes(attributes) + + attributes + end +end diff --git a/lib/temporal/connection/serializer.rb b/lib/temporal/connection/serializer.rb index b6da64b3..6343cb01 100644 --- a/lib/temporal/connection/serializer.rb +++ b/lib/temporal/connection/serializer.rb @@ -9,6 +9,7 @@ require 'temporal/connection/serializer/continue_as_new' require 'temporal/connection/serializer/fail_workflow' require 'temporal/connection/serializer/signal_external_workflow' +require 'temporal/connection/serializer/upsert_search_attributes' module Temporal module Connection @@ -23,7 +24,8 @@ module Serializer Workflow::Command::CompleteWorkflow => Serializer::CompleteWorkflow, Workflow::Command::ContinueAsNew => Serializer::ContinueAsNew, Workflow::Command::FailWorkflow => Serializer::FailWorkflow, - Workflow::Command::SignalExternalWorkflow => Serializer::SignalExternalWorkflow + Workflow::Command::SignalExternalWorkflow => Serializer::SignalExternalWorkflow, + Workflow::Command::UpsertSearchAttributes => Serializer::UpsertSearchAttributes, }.freeze def self.serialize(object) diff --git a/lib/temporal/connection/serializer/upsert_search_attributes.rb b/lib/temporal/connection/serializer/upsert_search_attributes.rb new file mode 100644 index 00000000..c11a8a0a --- /dev/null +++ b/lib/temporal/connection/serializer/upsert_search_attributes.rb @@ -0,0 +1,24 @@ +require 'temporal/connection/serializer/base' +require 'temporal/concerns/payloads' + +module Temporal + module Connection + module Serializer + class UpsertSearchAttributes < Base + include Concerns::Payloads + + def to_proto + Temporal::Api::Command::V1::Command.new( + command_type: Temporal::Api::Enums::V1::CommandType::COMMAND_TYPE_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES, + upsert_workflow_search_attributes_command_attributes: + Temporal::Api::Command::V1::UpsertWorkflowSearchAttributesCommandAttributes.new( + search_attributes: Temporal::Api::Common::V1::SearchAttributes.new( + indexed_fields: to_payload_map(object.search_attributes || {}) + ), + ) + ) + end + end + end + end +end diff --git a/lib/temporal/testing/local_workflow_context.rb b/lib/temporal/testing/local_workflow_context.rb index 5543452d..ccea6ea1 100644 --- a/lib/temporal/testing/local_workflow_context.rb +++ b/lib/temporal/testing/local_workflow_context.rb @@ -5,6 +5,7 @@ require 'temporal/metadata/activity' require 'temporal/workflow/future' require 'temporal/workflow/history/event_target' +require 'temporal/workflow/context_validators' module Temporal module Testing @@ -193,6 +194,13 @@ def cancel(target, cancelation_id) raise NotImplementedError, 'Cancel is not available when Temporal::Testing.local! is on' end + def upsert_search_attributes(search_attributes) + Temporal::Workflow::Context::Validators.validate_search_attributes(search_attributes) + + # We no-op in local testing mode since there is no search functionality. We don't fail because we + # don't want to block workflows testing other aspects. + end + private attr_reader :execution, :run_id, :workflow_id, :disabled_releases diff --git a/lib/temporal/workflow/command.rb b/lib/temporal/workflow/command.rb index c02bc8d6..8bf36f75 100644 --- a/lib/temporal/workflow/command.rb +++ b/lib/temporal/workflow/command.rb @@ -12,6 +12,7 @@ module Command CompleteWorkflow = Struct.new(:result, keyword_init: true) FailWorkflow = Struct.new(:exception, keyword_init: true) SignalExternalWorkflow = Struct.new(:namespace, :execution, :signal_name, :input, :child_workflow_only, keyword_init: true) + UpsertSearchAttributes = Struct.new(:search_attributes, keyword_init: true) # only these commands are supported right now SCHEDULE_ACTIVITY_TYPE = :schedule_activity @@ -23,6 +24,7 @@ module Command COMPLETE_WORKFLOW_TYPE = :complete_workflow FAIL_WORKFLOW_TYPE = :fail_workflow SIGNAL_EXTERNAL_WORKFLOW_TYPE = :signal_external_workflow + UPSERT_SEARCH_ATTRIBUTES_TYPE = :upsert_search_attributes COMMAND_CLASS_MAP = { SCHEDULE_ACTIVITY_TYPE => ScheduleActivity, @@ -33,7 +35,8 @@ module Command CANCEL_TIMER_TYPE => CancelTimer, COMPLETE_WORKFLOW_TYPE => CompleteWorkflow, FAIL_WORKFLOW_TYPE => FailWorkflow, - SIGNAL_EXTERNAL_WORKFLOW_TYPE => SignalExternalWorkflow + SIGNAL_EXTERNAL_WORKFLOW_TYPE => SignalExternalWorkflow, + UPSERT_SEARCH_ATTRIBUTES_TYPE => UpsertSearchAttributes, }.freeze def self.generate(type, **args) diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index 7a07d4fc..e9bf18e6 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -5,6 +5,7 @@ require 'temporal/thread_local_context' require 'temporal/workflow/history/event_target' require 'temporal/workflow/command' +require 'temporal/workflow/context_validators' require 'temporal/workflow/future' require 'temporal/workflow/replay_aware_logger' require 'temporal/workflow/state_manager' @@ -350,6 +351,18 @@ def signal_external_workflow(workflow, signal, workflow_id, run_id = nil, input future end + # @param search_attributes [Hash] + # replaces or adds the values of your custom search attributes specified during a workflow's execution. + # To use this your server must support ElasticSearch, and the attributes must be pre-configured + # See https://docs.temporal.io/docs/concepts/what-is-a-search-attribute/ + def upsert_search_attributes(search_attributes) + Validators.validate_search_attributes(search_attributes) + command = Command::UpsertSearchAttributes.new( + search_attributes: search_attributes + ) + schedule_command(command) + end + private attr_reader :state_manager, :dispatcher, :workflow_class diff --git a/lib/temporal/workflow/context_validators.rb b/lib/temporal/workflow/context_validators.rb new file mode 100644 index 00000000..a1527214 --- /dev/null +++ b/lib/temporal/workflow/context_validators.rb @@ -0,0 +1,22 @@ + +module Temporal + class Workflow + class Context + # Shared between Context and LocalWorkflowContext so we can do the same validations in test and production. + module Validators + + def self.validate_search_attributes(search_attributes) + if search_attributes.nil? + raise ArgumentError, 'search_attributes cannot be nil' + end + if !search_attributes.is_a?(Hash) + raise ArgumentError, "for search_attributes, expecting a Hash, not #{search_attributes.class}" + end + if search_attributes.empty? + raise ArgumentError, "Cannot upsert an empty hash for search_attributes, as this would do nothing." + end + end + end + end + end +end diff --git a/lib/temporal/workflow/execution_info.rb b/lib/temporal/workflow/execution_info.rb index 46b8e4cb..f47d3fcf 100644 --- a/lib/temporal/workflow/execution_info.rb +++ b/lib/temporal/workflow/execution_info.rb @@ -2,7 +2,7 @@ module Temporal class Workflow - class ExecutionInfo < Struct.new(:workflow, :workflow_id, :run_id, :start_time, :close_time, :status, :history_length, :memo, keyword_init: true) + class ExecutionInfo < Struct.new(:workflow, :workflow_id, :run_id, :start_time, :close_time, :status, :history_length, :memo, :search_attributes, keyword_init: true) extend Concerns::Payloads RUNNING_STATUS = :RUNNING @@ -43,6 +43,7 @@ def self.generate_from(response) status: API_STATUS_MAP.fetch(response.status), history_length: response.history_length, memo: self.from_payload_map(response.memo.fields), + search_attributes: self.from_payload_map(response.search_attributes.indexed_fields), ).freeze end diff --git a/lib/temporal/workflow/history/event_target.rb b/lib/temporal/workflow/history/event_target.rb index 8f605a01..d054947f 100644 --- a/lib/temporal/workflow/history/event_target.rb +++ b/lib/temporal/workflow/history/event_target.rb @@ -16,6 +16,7 @@ class UnexpectedEventType < InternalError; end CANCEL_EXTERNAL_WORKFLOW_REQUEST_TYPE = :cancel_external_workflow_request WORKFLOW_TYPE = :workflow CANCEL_WORKFLOW_REQUEST_TYPE = :cancel_workflow_request + UPSERT_SEARCH_ATTRIBUTES_REQUEST_TYPE = :upsert_search_attributes_request # NOTE: The order is important, first prefix match wins (will be a longer match) TARGET_TYPES = { @@ -32,7 +33,7 @@ class UnexpectedEventType < InternalError; end 'SIGNAL_EXTERNAL_WORKFLOW_EXECUTION' => EXTERNAL_WORKFLOW_TYPE, 'EXTERNAL_WORKFLOW_EXECUTION_CANCEL' => CANCEL_EXTERNAL_WORKFLOW_REQUEST_TYPE, 'REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION' => CANCEL_EXTERNAL_WORKFLOW_REQUEST_TYPE, - 'UPSERT_WORKFLOW_SEARCH_ATTRIBUTES' => WORKFLOW_TYPE, + 'UPSERT_WORKFLOW_SEARCH_ATTRIBUTES' => UPSERT_SEARCH_ATTRIBUTES_REQUEST_TYPE, 'WORKFLOW_EXECUTION_CANCEL' => CANCEL_WORKFLOW_REQUEST_TYPE, 'WORKFLOW_EXECUTION' => WORKFLOW_TYPE, }.freeze diff --git a/lib/temporal/workflow/state_manager.rb b/lib/temporal/workflow/state_manager.rb index da78d0cc..1c9ede81 100644 --- a/lib/temporal/workflow/state_manager.rb +++ b/lib/temporal/workflow/state_manager.rb @@ -283,7 +283,7 @@ def apply_event(event) dispatch(target, 'completed') when 'UPSERT_WORKFLOW_SEARCH_ATTRIBUTES' - # todo + # no need to track state; this is just a synchronous API call. else raise UnsupportedEvent, event.type @@ -307,6 +307,8 @@ def event_target_from(command_id, command) History::EventTarget::WORKFLOW_TYPE when Command::StartChildWorkflow History::EventTarget::CHILD_WORKFLOW_TYPE + when Command::UpsertSearchAttributes + History::EventTarget::UPSERT_SEARCH_ATTRIBUTES_REQUEST_TYPE when Command::SignalExternalWorkflow History::EventTarget::EXTERNAL_WORKFLOW_TYPE end diff --git a/spec/fabricators/grpc/search_attributes_fabricator.rb b/spec/fabricators/grpc/search_attributes_fabricator.rb new file mode 100644 index 00000000..f201abd7 --- /dev/null +++ b/spec/fabricators/grpc/search_attributes_fabricator.rb @@ -0,0 +1,7 @@ +Fabricator(:search_attributes, from: Temporal::Api::Common::V1::SearchAttributes) do + indexed_fields do + Google::Protobuf::Map.new(:string, :message, Temporal::Api::Common::V1::Payload).tap do |m| + m['foo'] = Temporal.configuration.converter.to_payload('bar') + end + end +end diff --git a/spec/fabricators/grpc/workflow_execution_info_fabricator.rb b/spec/fabricators/grpc/workflow_execution_info_fabricator.rb index 4f1d577e..32683abd 100644 --- a/spec/fabricators/grpc/workflow_execution_info_fabricator.rb +++ b/spec/fabricators/grpc/workflow_execution_info_fabricator.rb @@ -6,4 +6,5 @@ status { Temporal::Api::Enums::V1::WorkflowExecutionStatus::WORKFLOW_EXECUTION_STATUS_COMPLETED } history_length { rand(100) } memo { Fabricate(:memo) } + search_attributes { Fabricate(:search_attributes) } end diff --git a/spec/unit/lib/temporal/connection/serializer/upsert_search_attributes_spec.rb b/spec/unit/lib/temporal/connection/serializer/upsert_search_attributes_spec.rb new file mode 100644 index 00000000..21816b6c --- /dev/null +++ b/spec/unit/lib/temporal/connection/serializer/upsert_search_attributes_spec.rb @@ -0,0 +1,36 @@ +require 'securerandom' +require 'time' +require 'temporal/connection/serializer/upsert_search_attributes' +require 'temporal/workflow/command' + +class TestDeserializer + extend Temporal::Concerns::Payloads +end + +describe Temporal::Connection::Serializer::UpsertSearchAttributes do + it 'produces a protobuf that round-trips' do + expected_attributes = { + 'CustomStringField' => 'moo', + 'CustomBoolField' => true, + 'CustomDoubleField' => 3.14, + 'CustomIntField' => 0, + 'CustomKeywordField' => SecureRandom.uuid, + 'CustomDatetimeField' => Time.now.to_i + } + + command = Temporal::Workflow::Command::UpsertSearchAttributes.new( + search_attributes: expected_attributes + ) + + result = described_class.new(command).to_proto + expect(result).to be_an_instance_of(Temporal::Api::Command::V1::Command) + expect(result.command_type).to eql( + :COMMAND_TYPE_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES + ) + command_attributes = result.upsert_workflow_search_attributes_command_attributes + expect(command_attributes).not_to be_nil + actual_attributes = TestDeserializer.from_payload_map(command_attributes&.search_attributes&.indexed_fields) + expect(actual_attributes).to eql(expected_attributes) + + end +end diff --git a/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb b/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb index 9397ef88..f036b3b2 100644 --- a/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb +++ b/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb @@ -195,5 +195,29 @@ def execute fiber.resume # start running expect(exited).to eq(true) end + + describe '#upsert_search_attributes' do + it 'can be run' do + workflow_context.upsert_search_attributes({'CustomKeywordField' => 'moo'}) + end + + it 'does not accept nil' do + expect do + workflow_context.upsert_search_attributes(nil) + end.to raise_error(ArgumentError, 'search_attributes cannot be nil') + end + + it 'requires a hash' do + expect do + workflow_context.upsert_search_attributes(['array_not_supported']) + end.to raise_error(ArgumentError, 'for search_attributes, expecting a Hash, not Array') + end + + it 'requires a non-empty hash' do + expect do + workflow_context.upsert_search_attributes({}) + end.to raise_error(ArgumentError, 'Cannot upsert an empty hash for search_attributes, as this would do nothing.') + end + end end end diff --git a/spec/unit/lib/temporal/workflow/context_spec.rb b/spec/unit/lib/temporal/workflow/context_spec.rb new file mode 100644 index 00000000..b55e4a56 --- /dev/null +++ b/spec/unit/lib/temporal/workflow/context_spec.rb @@ -0,0 +1,40 @@ +require 'temporal/workflow' +require 'temporal/workflow/context' + +class MyTestWorkflow < Temporal::Workflow; end + +describe Temporal::Workflow::Context do + let(:state_manager) { instance_double('Temporal::Workflow::StateManager') } + let(:dispatcher) { instance_double('Temporal::Workflow::Dispatcher') } + let(:metadata) { instance_double('Temporal::Metadata::Workflow') } + let(:workflow_context) { + Temporal::Workflow::Context.new(state_manager, dispatcher, MyTestWorkflow, metadata, Temporal.configuration) + } + + describe '#upsert_search_attributes' do + it 'does not accept nil' do + expect do + workflow_context.upsert_search_attributes(nil) + end.to raise_error(ArgumentError, 'search_attributes cannot be nil') + end + + it 'requires a hash' do + expect do + workflow_context.upsert_search_attributes(['array_not_supported']) + end.to raise_error(ArgumentError, 'for search_attributes, expecting a Hash, not Array') + end + + it 'requires a non-empty hash' do + expect do + workflow_context.upsert_search_attributes({}) + end.to raise_error(ArgumentError, 'Cannot upsert an empty hash for search_attributes, as this would do nothing.') + end + + it 'creates a command to execute the request' do + expect(state_manager).to receive(:schedule) + .with an_instance_of(Temporal::Workflow::Command::UpsertSearchAttributes) + workflow_context.upsert_search_attributes({'CustomIntField' => 5}) + end + + end +end From bfd52508e9704b0a12619c49abab2bf1cdc16166 Mon Sep 17 00:00:00 2001 From: Drew Hoskins <37816070+drewhoskins-stripe@users.noreply.github.com> Date: Wed, 2 Mar 2022 04:34:34 -0800 Subject: [PATCH 031/125] Improvements for upsert_search_attributes (#147) * Explain how to pass indexed times into upsert_search_attributes * Support fetch_workflow_execution_info of search attributes in local mode * ISO8601 UTC; Allow passing in a Time.now --- .../upsert_search_attributes_spec.rb | 12 ++++---- .../upsert_search_attributes_workflow.rb | 7 +++-- .../testing/local_workflow_context.rb | 8 ++---- lib/temporal/testing/temporal_override.rb | 2 +- lib/temporal/testing/workflow_execution.rb | 7 ++++- lib/temporal/workflow/context.rb | 19 +++++++++---- ...ntext_validators.rb => context_helpers.rb} | 14 ++++++++-- .../testing/local_workflow_context_spec.rb | 10 ++++++- .../testing/temporal_override_spec.rb | 28 ++++++++++++++++++- .../lib/temporal/workflow/context_spec.rb | 12 +++++++- 10 files changed, 93 insertions(+), 26 deletions(-) rename lib/temporal/workflow/{context_validators.rb => context_helpers.rb} (63%) diff --git a/examples/spec/integration/upsert_search_attributes_spec.rb b/examples/spec/integration/upsert_search_attributes_spec.rb index 135c3611..05d6f71a 100644 --- a/examples/spec/integration/upsert_search_attributes_spec.rb +++ b/examples/spec/integration/upsert_search_attributes_spec.rb @@ -1,7 +1,8 @@ require 'workflows/upsert_search_attributes_workflow' +require 'time' -describe 'Temporal::Workflow::Context.upsert_search_attributes', :integration do - it 'can upsert a search attribute and then retrieve it' do +describe 'Temporal::Workflow::Context.upsert_search_attributes', :integration do + it 'can upsert a search attribute and then retrieve it' do workflow_id = 'upsert_search_attributes_test_wf-' + SecureRandom.uuid expected_attributes = { @@ -9,6 +10,7 @@ 'CustomBoolField' => true, 'CustomDoubleField' => 3.14, 'CustomIntField' => 0, + 'CustomDatetimeField' => Time.now.utc.iso8601, } run_id = Temporal.start_workflow( @@ -27,10 +29,10 @@ expect(added_attributes).to eq(expected_attributes) execution_info = Temporal.fetch_workflow_execution_info( - integration_spec_namespace, - workflow_id, + integration_spec_namespace, + workflow_id, nil ) expect(execution_info.search_attributes).to eq(expected_attributes) end -end \ No newline at end of file +end diff --git a/examples/workflows/upsert_search_attributes_workflow.rb b/examples/workflows/upsert_search_attributes_workflow.rb index ccdf8392..26f168f8 100644 --- a/examples/workflows/upsert_search_attributes_workflow.rb +++ b/examples/workflows/upsert_search_attributes_workflow.rb @@ -1,17 +1,18 @@ class UpsertSearchAttributesWorkflow < Temporal::Workflow - def execute(string_value, bool_value, float_value, int_value) + # time_value example: use this format: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ") + def execute(string_value, bool_value, float_value, int_value, time_value) # These are included in the default temporal docker setup. # Run tctl admin cluster get-search-attributes to list the options and # See https://docs.temporal.io/docs/tctl/how-to-add-a-custom-search-attribute-to-a-cluster-using-tctl # for instructions on adding them. - attributes = { + attributes = { 'CustomStringField' => string_value, 'CustomBoolField' => bool_value, 'CustomDoubleField' => float_value, 'CustomIntField' => int_value, + 'CustomDatetimeField' => time_value, } workflow.upsert_search_attributes(attributes) - attributes end end diff --git a/lib/temporal/testing/local_workflow_context.rb b/lib/temporal/testing/local_workflow_context.rb index ccea6ea1..4f5dbb67 100644 --- a/lib/temporal/testing/local_workflow_context.rb +++ b/lib/temporal/testing/local_workflow_context.rb @@ -5,7 +5,7 @@ require 'temporal/metadata/activity' require 'temporal/workflow/future' require 'temporal/workflow/history/event_target' -require 'temporal/workflow/context_validators' +require 'temporal/workflow/context_helpers' module Temporal module Testing @@ -195,10 +195,8 @@ def cancel(target, cancelation_id) end def upsert_search_attributes(search_attributes) - Temporal::Workflow::Context::Validators.validate_search_attributes(search_attributes) - - # We no-op in local testing mode since there is no search functionality. We don't fail because we - # don't want to block workflows testing other aspects. + search_attributes = Temporal::Workflow::Context::Helpers.process_search_attributes(search_attributes) + execution.upsert_search_attributes(search_attributes) end private diff --git a/lib/temporal/testing/temporal_override.rb b/lib/temporal/testing/temporal_override.rb index d44da24f..7be2ff0c 100644 --- a/lib/temporal/testing/temporal_override.rb +++ b/lib/temporal/testing/temporal_override.rb @@ -31,7 +31,6 @@ def schedule_workflow(workflow, cron_schedule, *input, **args) def fetch_workflow_execution_info(_namespace, workflow_id, run_id) return super if Temporal::Testing.disabled? - execution = executions[[workflow_id, run_id]] Workflow::ExecutionInfo.new( @@ -42,6 +41,7 @@ def fetch_workflow_execution_info(_namespace, workflow_id, run_id) close_time: nil, status: execution.status, history_length: nil, + search_attributes: execution.search_attributes, ).freeze end diff --git a/lib/temporal/testing/workflow_execution.rb b/lib/temporal/testing/workflow_execution.rb index ada7d0e8..e2ce5ca2 100644 --- a/lib/temporal/testing/workflow_execution.rb +++ b/lib/temporal/testing/workflow_execution.rb @@ -3,11 +3,12 @@ module Temporal module Testing class WorkflowExecution - attr_reader :status + attr_reader :status, :search_attributes def initialize @status = Workflow::ExecutionInfo::RUNNING_STATUS @futures = FutureRegistry.new + @search_attributes = {} end def run(&block) @@ -36,6 +37,10 @@ def fail_activity(token, exception) resume end + def upsert_search_attributes(search_attributes) + @search_attributes.merge!(search_attributes) + end + private attr_reader :fiber, :futures diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index e9bf18e6..dd28fdbf 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -5,7 +5,7 @@ require 'temporal/thread_local_context' require 'temporal/workflow/history/event_target' require 'temporal/workflow/command' -require 'temporal/workflow/context_validators' +require 'temporal/workflow/context_helpers' require 'temporal/workflow/future' require 'temporal/workflow/replay_aware_logger' require 'temporal/workflow/state_manager' @@ -351,16 +351,25 @@ def signal_external_workflow(workflow, signal, workflow_id, run_id = nil, input future end - # @param search_attributes [Hash] - # replaces or adds the values of your custom search attributes specified during a workflow's execution. - # To use this your server must support ElasticSearch, and the attributes must be pre-configured + # Replaces or adds the values of your custom search attributes specified during a workflow's execution. + # To use this your server must support Elasticsearch, and the attributes must be pre-configured # See https://docs.temporal.io/docs/concepts/what-is-a-search-attribute/ + # + # @param search_attributes [Hash] + # If an attribute is registered as a Datetime, you can pass in a Time: e.g. + # workflow.now + # or as a string in UTC ISO-8601 format: + # workflow.now.utc.iso8601 + # It would look like: "2022-03-01T17:39:06Z" + # @return [Hash] the search attributes after any preprocessing. + # def upsert_search_attributes(search_attributes) - Validators.validate_search_attributes(search_attributes) + search_attributes = Helpers.process_search_attributes(search_attributes) command = Command::UpsertSearchAttributes.new( search_attributes: search_attributes ) schedule_command(command) + search_attributes end private diff --git a/lib/temporal/workflow/context_validators.rb b/lib/temporal/workflow/context_helpers.rb similarity index 63% rename from lib/temporal/workflow/context_validators.rb rename to lib/temporal/workflow/context_helpers.rb index a1527214..0ecc345f 100644 --- a/lib/temporal/workflow/context_validators.rb +++ b/lib/temporal/workflow/context_helpers.rb @@ -1,11 +1,11 @@ - +require 'time' module Temporal class Workflow class Context # Shared between Context and LocalWorkflowContext so we can do the same validations in test and production. - module Validators + module Helpers - def self.validate_search_attributes(search_attributes) + def self.process_search_attributes(search_attributes) if search_attributes.nil? raise ArgumentError, 'search_attributes cannot be nil' end @@ -15,6 +15,14 @@ def self.validate_search_attributes(search_attributes) if search_attributes.empty? raise ArgumentError, "Cannot upsert an empty hash for search_attributes, as this would do nothing." end + search_attributes.transform_values do |attribute| + if attribute.is_a?(Time) + # The server expects UTC times in the standard format. + attribute.utc.iso8601 + else + attribute + end + end end end end diff --git a/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb b/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb index f036b3b2..a62d866a 100644 --- a/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb +++ b/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb @@ -1,6 +1,7 @@ require 'temporal/testing' require 'temporal/workflow' require 'temporal/api/errordetails/v1/message_pb' +require 'time' describe Temporal::Testing::LocalWorkflowContext do let(:workflow_id) { 'workflow_id_1' } @@ -206,7 +207,7 @@ def execute workflow_context.upsert_search_attributes(nil) end.to raise_error(ArgumentError, 'search_attributes cannot be nil') end - + it 'requires a hash' do expect do workflow_context.upsert_search_attributes(['array_not_supported']) @@ -218,6 +219,13 @@ def execute workflow_context.upsert_search_attributes({}) end.to raise_error(ArgumentError, 'Cannot upsert an empty hash for search_attributes, as this would do nothing.') end + + it 'converts a Time to the ISO8601 UTC format expected by the Temporal server' do + time = Time.now + expect( + workflow_context.upsert_search_attributes({'CustomDatetimeField' => time}) + ).to eq({ 'CustomDatetimeField' => time.utc.iso8601 }) + end end end end diff --git a/spec/unit/lib/temporal/testing/temporal_override_spec.rb b/spec/unit/lib/temporal/testing/temporal_override_spec.rb index 42d72a9c..9047150a 100644 --- a/spec/unit/lib/temporal/testing/temporal_override_spec.rb +++ b/spec/unit/lib/temporal/testing/temporal_override_spec.rb @@ -13,6 +13,15 @@ class TestTemporalOverrideWorkflow < Temporal::Workflow def execute; end end + class UpsertSearchAttributesWorkflow < Temporal::Workflow + namespace 'default-namespace' + task_queue 'default-task-queue' + + def execute + workflow.upsert_search_attributes('CustomIntField' => 5) + end + end + context 'when testing mode is disabled' do describe 'Temporal.start_workflow' do let(:connection) { instance_double('Temporal::Connection::GRPC') } @@ -139,7 +148,7 @@ def execute it 'explicitly does not support staring a workflow with a signal' do expect { client.start_workflow(TestTemporalOverrideWorkflow, options: { signal_name: 'breakme' }) - }.to raise_error(NotImplementedError) do |e| + }.to raise_error(NotImplementedError) do |e| expect(e.message).to eql("Signals are not available when Temporal::Testing.local! is on") end end @@ -260,6 +269,23 @@ def execute end end end + + describe 'Temporal.fetch_workflow_execution_info' do + it 'retrieves search attributes' do + workflow_id = 'upsert_search_attributes_test_wf-' + SecureRandom.uuid + + run_id = client.start_workflow( + UpsertSearchAttributesWorkflow, + options: { + workflow_id: workflow_id, + }, + ) + + info = client.fetch_workflow_execution_info('default-namespace', workflow_id, run_id) + expect(info.search_attributes).to eq({'CustomIntField' => 5}) + end + + end end end end diff --git a/spec/unit/lib/temporal/workflow/context_spec.rb b/spec/unit/lib/temporal/workflow/context_spec.rb index b55e4a56..24768d82 100644 --- a/spec/unit/lib/temporal/workflow/context_spec.rb +++ b/spec/unit/lib/temporal/workflow/context_spec.rb @@ -1,5 +1,6 @@ require 'temporal/workflow' require 'temporal/workflow/context' +require 'time' class MyTestWorkflow < Temporal::Workflow; end @@ -33,8 +34,17 @@ class MyTestWorkflow < Temporal::Workflow; end it 'creates a command to execute the request' do expect(state_manager).to receive(:schedule) .with an_instance_of(Temporal::Workflow::Command::UpsertSearchAttributes) - workflow_context.upsert_search_attributes({'CustomIntField' => 5}) + workflow_context.upsert_search_attributes({ 'CustomIntField' => 5 }) end + it 'converts a Time to the ISO8601 UTC format expected by the Temporal server' do + time = Time.now + allow(state_manager).to receive(:schedule) + .with an_instance_of(Temporal::Workflow::Command::UpsertSearchAttributes) + + expect( + workflow_context.upsert_search_attributes({'CustomDatetimeField' => time}) + ).to eq({ 'CustomDatetimeField' => time.utc.iso8601 }) + end end end From 7422a3838a3012d3011530fb7e55015a34d091d6 Mon Sep 17 00:00:00 2001 From: Drew Hoskins <37816070+drewhoskins-stripe@users.noreply.github.com> Date: Thu, 3 Mar 2022 19:04:43 -0800 Subject: [PATCH 032/125] Remove error logspew when workers shutdown (#148) * Remove confusing errors on worker shutdown * Bring workflow poller shutdown message to parity with activity one --- lib/temporal/activity/poller.rb | 3 +++ lib/temporal/workflow/poller.rb | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/temporal/activity/poller.rb b/lib/temporal/activity/poller.rb index 4157f0f3..bc01290f 100644 --- a/lib/temporal/activity/poller.rb +++ b/lib/temporal/activity/poller.rb @@ -75,6 +75,9 @@ def poll_loop def poll_for_task connection.poll_activity_task_queue(namespace: namespace, task_queue: task_queue) + rescue ::GRPC::Cancelled + # We're shutting down and we've already reported that in the logs + nil rescue StandardError => error Temporal.logger.error("Unable to poll activity task queue", { namespace: namespace, task_queue: task_queue, error: error.inspect }) diff --git a/lib/temporal/workflow/poller.rb b/lib/temporal/workflow/poller.rb index 691dcfd1..cf557b79 100644 --- a/lib/temporal/workflow/poller.rb +++ b/lib/temporal/workflow/poller.rb @@ -1,3 +1,4 @@ +require 'grpc/errors' require 'temporal/connection' require 'temporal/thread_pool' require 'temporal/middleware/chain' @@ -28,7 +29,7 @@ def start def stop_polling @shutting_down = true - Temporal.logger.info('Shutting down a workflow poller') + Temporal.logger.info('Shutting down a workflow poller', { namespace: namespace, task_queue: task_queue }) end def cancel_pending_requests @@ -75,6 +76,9 @@ def poll_loop def poll_for_task connection.poll_workflow_task_queue(namespace: namespace, task_queue: task_queue) + rescue ::GRPC::Cancelled + # We're shutting down and we've already reported that in the logs + nil rescue StandardError => error Temporal.logger.error("Unable to poll Workflow task queue", { namespace: namespace, task_queue: task_queue, error: error.inspect }) Temporal::ErrorHandler.handle(error, config) From 5b2d7e4fdbf951794f96f45b4526e1f66803a876 Mon Sep 17 00:00:00 2001 From: Christopher Vanderschuere Date: Fri, 4 Mar 2022 08:07:03 -0800 Subject: [PATCH 033/125] Added stub for signal_external_workflow (#142) --- lib/temporal/testing/local_workflow_context.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/temporal/testing/local_workflow_context.rb b/lib/temporal/testing/local_workflow_context.rb index 4f5dbb67..1c0cdd07 100644 --- a/lib/temporal/testing/local_workflow_context.rb +++ b/lib/temporal/testing/local_workflow_context.rb @@ -194,6 +194,10 @@ def cancel(target, cancelation_id) raise NotImplementedError, 'Cancel is not available when Temporal::Testing.local! is on' end + def signal_external_workflow(workflow, signal, workflow_id, run_id = nil, input = nil, namespace: nil, child_workflow_only: false) + raise NotImplementedError, 'Signals are not available when Temporal::Testing.local! is on' + end + def upsert_search_attributes(search_attributes) search_attributes = Temporal::Workflow::Context::Helpers.process_search_attributes(search_attributes) execution.upsert_search_attributes(search_attributes) From 6f094b8efc0f4560db7bc200499b447306079119 Mon Sep 17 00:00:00 2001 From: Anthony Dmitriyev Date: Fri, 4 Mar 2022 18:35:17 +0000 Subject: [PATCH 034/125] [Fix] Spec issues with Ruby 3 (#155) * Fix specs issues with Ruby 3 * Update containers --- .circleci/config.yml | 4 ++-- examples/spec/helpers.rb | 13 ++++++------- .../spec/integration/await_workflow_result_spec.rb | 10 +++++----- .../call_failing_activity_workflow_spec.rb | 2 +- .../spec/integration/terminate_workflow_spec.rb | 2 +- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index cd84ef34..b23efce7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,7 +5,7 @@ orbs: jobs: test_gem: docker: - - image: circleci/ruby:2.6.3-stretch-node + - image: cimg/ruby:3.0.3 executor: ruby/default steps: - checkout @@ -16,7 +16,7 @@ jobs: test_examples: docker: - - image: circleci/ruby:2.6.3-stretch-node + - image: cimg/ruby:3.0.3 - image: circleci/postgres:alpine name: postgres environment: diff --git a/examples/spec/helpers.rb b/examples/spec/helpers.rb index 18810d5e..f2e614e4 100644 --- a/examples/spec/helpers.rb +++ b/examples/spec/helpers.rb @@ -20,14 +20,13 @@ def wait_for_workflow_completion(workflow_id, run_id) def fetch_history(workflow_id, run_id, options = {}) connection = Temporal.send(:default_client).send(:connection) + options = { + namespace: Temporal.configuration.namespace, + workflow_id: workflow_id, + run_id: run_id, + }.merge(options) - connection.get_workflow_execution_history( - { - namespace: Temporal.configuration.namespace, - workflow_id: workflow_id, - run_id: run_id, - }.merge(options) - ) + connection.get_workflow_execution_history(**options) end def integration_spec_namespace diff --git a/examples/spec/integration/await_workflow_result_spec.rb b/examples/spec/integration/await_workflow_result_spec.rb index bf48b7b1..d4d4977d 100644 --- a/examples/spec/integration/await_workflow_result_spec.rb +++ b/examples/spec/integration/await_workflow_result_spec.rb @@ -10,7 +10,7 @@ run_id = Temporal.start_workflow( ResultWorkflow, expected_result, - { options: { workflow_id: workflow_id } }, + options: { workflow_id: workflow_id }, ) actual_result = Temporal.await_workflow_result( ResultWorkflow, @@ -26,7 +26,7 @@ first_run_id = Temporal.start_workflow( ResultWorkflow, expected_first_result, - { options: { workflow_id: workflow_id } }, + options: { workflow_id: workflow_id }, ) actual_first_result = Temporal.await_workflow_result( ResultWorkflow, @@ -38,7 +38,7 @@ Temporal.start_workflow( ResultWorkflow, expected_second_result, - { options: { workflow_id: workflow_id } }, + options: { workflow_id: workflow_id }, ) actual_second_result = Temporal.await_workflow_result( ResultWorkflow, @@ -59,7 +59,7 @@ workflow_id = SecureRandom.uuid run_id = Temporal.start_workflow( FailingWorkflow, - { options: { workflow_id: workflow_id } }, + options: { workflow_id: workflow_id }, ) expect do @@ -78,7 +78,7 @@ workflow_id = SecureRandom.uuid run_id = Temporal.start_workflow( QuickTimeoutWorkflow, - { options: { workflow_id: workflow_id } }, + options: { workflow_id: workflow_id }, ) expect do diff --git a/examples/spec/integration/call_failing_activity_workflow_spec.rb b/examples/spec/integration/call_failing_activity_workflow_spec.rb index eef65424..c39853a6 100644 --- a/examples/spec/integration/call_failing_activity_workflow_spec.rb +++ b/examples/spec/integration/call_failing_activity_workflow_spec.rb @@ -9,7 +9,7 @@ class TestDeserializer it 'correctly re-raises an activity-thrown exception in the workflow' do workflow_id = SecureRandom.uuid expected_message = "a failure message" - Temporal.start_workflow(described_class, expected_message, { options: { workflow_id: workflow_id } }) + Temporal.start_workflow(described_class, expected_message, options: { workflow_id: workflow_id }) expect do Temporal.await_workflow_result(described_class, workflow_id: workflow_id) end.to raise_error(FailingActivity::MyError, "a failure message") diff --git a/examples/spec/integration/terminate_workflow_spec.rb b/examples/spec/integration/terminate_workflow_spec.rb index 4e6137a7..19c88cdd 100644 --- a/examples/spec/integration/terminate_workflow_spec.rb +++ b/examples/spec/integration/terminate_workflow_spec.rb @@ -7,7 +7,7 @@ TimeoutWorkflow, 1, # sleep long enough to be sure I can cancel in time. 1, - { options: { workflow_id: workflow_id } }, + options: { workflow_id: workflow_id }, ) Temporal.terminate_workflow(workflow_id) From a769b1fe8f77ada9d229d2bff30549added73516 Mon Sep 17 00:00:00 2001 From: Drew Hoskins <37816070+drewhoskins-stripe@users.noreply.github.com> Date: Fri, 4 Mar 2022 10:35:35 -0800 Subject: [PATCH 035/125] Fix bugs with workflow.upsert_search_attributes (#154) --- examples/workflows/upsert_search_attributes_workflow.rb | 3 +++ lib/temporal/workflow/history/event.rb | 1 - lib/temporal/workflow/state_manager.rb | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/workflows/upsert_search_attributes_workflow.rb b/examples/workflows/upsert_search_attributes_workflow.rb index 26f168f8..9b69f25f 100644 --- a/examples/workflows/upsert_search_attributes_workflow.rb +++ b/examples/workflows/upsert_search_attributes_workflow.rb @@ -1,3 +1,4 @@ +require 'activities/hello_world_activity' class UpsertSearchAttributesWorkflow < Temporal::Workflow # time_value example: use this format: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ") def execute(string_value, bool_value, float_value, int_value, time_value) @@ -13,6 +14,8 @@ def execute(string_value, bool_value, float_value, int_value, time_value) 'CustomDatetimeField' => time_value, } workflow.upsert_search_attributes(attributes) + + HelloWorldActivity.execute!("Moon") attributes end end diff --git a/lib/temporal/workflow/history/event.rb b/lib/temporal/workflow/history/event.rb index 9bf90927..562fd018 100644 --- a/lib/temporal/workflow/history/event.rb +++ b/lib/temporal/workflow/history/event.rb @@ -11,7 +11,6 @@ class Event TIMER_FIRED REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION_FAILED EXTERNAL_WORKFLOW_EXECUTION_CANCEL_REQUESTED - UPSERT_WORKFLOW_SEARCH_ATTRIBUTES ].freeze CHILD_WORKFLOW_EVENTS = %w[ diff --git a/lib/temporal/workflow/state_manager.rb b/lib/temporal/workflow/state_manager.rb index 1c9ede81..8c991b08 100644 --- a/lib/temporal/workflow/state_manager.rb +++ b/lib/temporal/workflow/state_manager.rb @@ -284,6 +284,7 @@ def apply_event(event) when 'UPSERT_WORKFLOW_SEARCH_ATTRIBUTES' # no need to track state; this is just a synchronous API call. + discard_command(target) else raise UnsupportedEvent, event.type From d5f3918edfa1bc1dcf43cdeaa4361dba860afaa4 Mon Sep 17 00:00:00 2001 From: DeRauk Gibble Date: Fri, 4 Mar 2022 14:16:24 -0500 Subject: [PATCH 036/125] List filter workflow executions (#151) * List & filter workflow executions * Remove testing script * Fix specs * Add closed? convenience method and tests Co-authored-by: DeRauk Gibble --- lib/temporal.rb | 4 +- lib/temporal/client.rb | 48 ++++++ lib/temporal/connection/grpc.rb | 67 +++++++- lib/temporal/testing/temporal_override.rb | 11 +- lib/temporal/testing/workflow_execution.rb | 7 +- lib/temporal/workflow/execution_info.rb | 45 ++---- lib/temporal/workflow/status.rb | 24 +++ .../workflow_execution_info_fabricator.rb | 6 +- spec/unit/lib/temporal/client_spec.rb | 147 ++++++++++++++++- spec/unit/lib/temporal/grpc_client_spec.rb | 150 ++++++++++++++++++ .../testing/temporal_override_spec.rb | 18 +-- .../temporal/workflow/execution_info_spec.rb | 33 +++- 12 files changed, 502 insertions(+), 58 deletions(-) create mode 100644 lib/temporal/workflow/status.rb diff --git a/lib/temporal.rb b/lib/temporal.rb index 2a1e4695..0b95a882 100644 --- a/lib/temporal.rb +++ b/lib/temporal.rb @@ -25,7 +25,9 @@ module Temporal :terminate_workflow, :fetch_workflow_execution_info, :complete_activity, - :fail_activity + :fail_activity, + :list_open_workflow_executions, + :list_closed_workflow_executions class << self def configure(&block) diff --git a/lib/temporal/client.rb b/lib/temporal/client.rb index 421155c2..5e622743 100644 --- a/lib/temporal/client.rb +++ b/lib/temporal/client.rb @@ -5,6 +5,7 @@ require 'temporal/workflow' require 'temporal/workflow/history' require 'temporal/workflow/execution_info' +require 'temporal/workflow/status' require 'temporal/reset_strategy' module Temporal @@ -359,6 +360,18 @@ def get_workflow_history(namespace:, workflow_id:, run_id:) Workflow::History.new(history_response.history.events) end + def list_open_workflow_executions(namespace, from, to = Time.now, filter: {}) + validate_filter(filter, :workflow, :workflow_id) + + fetch_executions(:open, { namespace: namespace, from: from, to: to }.merge(filter)) + end + + def list_closed_workflow_executions(namespace, from, to = Time.now, filter: {}) + validate_filter(filter, :status, :workflow, :workflow_id) + + fetch_executions(:closed, { namespace: namespace, from: from, to: to }.merge(filter)) + end + class ResultConverter extend Concerns::Payloads end @@ -402,5 +415,40 @@ def find_workflow_task(namespace, workflow_id, run_id, strategy) raise ArgumentError, 'Unsupported reset strategy' end end + def validate_filter(filter, *allowed_filters) + if (filter.keys - allowed_filters).length > 0 + raise ArgumentError, "Allowed filters are: #{allowed_filters}" + end + + raise ArgumentError, 'Only one filter is allowed' if filter.size > 1 + end + + def fetch_executions(status, request_options) + api_method = + if status == :open + :list_open_workflow_executions + else + :list_closed_workflow_executions + end + + executions = [] + next_page_token = nil + + loop do + response = connection.public_send( + api_method, + **request_options.merge(next_page_token: next_page_token) + ) + + executions += Array(response.executions) + next_page_token = response.next_page_token + + break if next_page_token.to_s.empty? + end + + executions.map do |raw_execution| + Temporal::Workflow::ExecutionInfo.generate_from(raw_execution) + end + end end end diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index 0e2e0e7c..af5ef156 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -1,10 +1,13 @@ require 'grpc' +require 'time' require 'google/protobuf/well_known_types' require 'securerandom' +require 'gen/temporal/api/filter/v1/message_pb' +require 'gen/temporal/api/workflowservice/v1/service_services_pb' +require 'gen/temporal/api/enums/v1/workflow_pb' require 'temporal/connection/errors' require 'temporal/connection/serializer' require 'temporal/connection/serializer/failure' -require 'gen/temporal/api/workflowservice/v1/service_services_pb' require 'temporal/concerns/payloads' module Temporal @@ -23,12 +26,17 @@ class GRPC close: Temporal::Api::Enums::V1::HistoryEventFilterType::HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT, }.freeze - def initialize(host, port, identity) + DEFAULT_OPTIONS = { + max_page_size: 100 + }.freeze + + def initialize(host, port, identity, options = {}) @url = "#{host}:#{port}" @identity = identity @poll = true @poll_mutex = Mutex.new @poll_request = nil + @options = DEFAULT_OPTIONS.merge(options) end def register_namespace(name:, description: nil, global: false, retention_period: 10) @@ -398,12 +406,29 @@ def terminate_workflow_execution( client.terminate_workflow_execution(request) end - def list_open_workflow_executions - raise NotImplementedError + def list_open_workflow_executions(namespace:, from:, to:, next_page_token: nil, workflow_id: nil, workflow: nil) + request = Temporal::Api::WorkflowService::V1::ListOpenWorkflowExecutionsRequest.new( + namespace: namespace, + maximum_page_size: options[:max_page_size], + next_page_token: next_page_token, + start_time_filter: serialize_time_filter(from, to), + execution_filter: serialize_execution_filter(workflow_id), + type_filter: serialize_type_filter(workflow) + ) + client.list_open_workflow_executions(request) end - def list_closed_workflow_executions - raise NotImplementedError + def list_closed_workflow_executions(namespace:, from:, to:, next_page_token: nil, workflow_id: nil, workflow: nil, status: nil) + request = Temporal::Api::WorkflowService::V1::ListClosedWorkflowExecutionsRequest.new( + namespace: namespace, + maximum_page_size: options[:max_page_size], + next_page_token: next_page_token, + start_time_filter: serialize_time_filter(from, to), + execution_filter: serialize_execution_filter(workflow_id), + type_filter: serialize_type_filter(workflow), + status_filter: serialize_status_filter(status) + ) + client.list_closed_workflow_executions(request) end def list_workflow_executions @@ -470,7 +495,7 @@ def cancel_polling_request private - attr_reader :url, :identity, :poll_mutex, :poll_request + attr_reader :url, :identity, :options, :poll_mutex, :poll_request def client @client ||= Temporal::Api::WorkflowService::V1::WorkflowService::Stub.new( @@ -483,6 +508,34 @@ def client def can_poll? @poll end + + def serialize_time_filter(from, to) + Temporal::Api::Filter::V1::StartTimeFilter.new( + earliest_time: from&.to_time, + latest_time: to&.to_time + ) + end + + def serialize_execution_filter(value) + return unless value + + Temporal::Api::Filter::V1::WorkflowExecutionFilter.new(workflow_id: value) + end + + def serialize_type_filter(value) + return unless value + + Temporal::Api::Filter::V1::WorkflowTypeFilter.new(name: value) + end + + def serialize_status_filter(value) + return unless value + + sym = Temporal::Workflow::Status::API_STATUS_MAP.invert[value] + status = Temporal::Api::Enums::V1::WorkflowExecutionStatus.resolve(sym) + + Temporal::Api::Filter::V1::StatusFilter.new(status: status) + end end end end diff --git a/lib/temporal/testing/temporal_override.rb b/lib/temporal/testing/temporal_override.rb index 7be2ff0c..1e76cce7 100644 --- a/lib/temporal/testing/temporal_override.rb +++ b/lib/temporal/testing/temporal_override.rb @@ -1,6 +1,7 @@ require 'securerandom' require 'temporal/activity/async_token' require 'temporal/workflow/execution_info' +require 'temporal/workflow/status' require 'temporal/testing/workflow_execution' require 'temporal/testing/local_workflow_context' @@ -147,14 +148,14 @@ def previous_run_id(workflow_id) def disallowed_statuses_for(reuse_policy) case reuse_policy when :allow_failed - [Workflow::ExecutionInfo::RUNNING_STATUS, Workflow::ExecutionInfo::COMPLETED_STATUS] + [Workflow::Status::RUNNING, Workflow::Status::COMPLETED] when :allow - [Workflow::ExecutionInfo::RUNNING_STATUS] + [Workflow::Status::RUNNING] when :reject [ - Workflow::ExecutionInfo::RUNNING_STATUS, - Workflow::ExecutionInfo::FAILED_STATUS, - Workflow::ExecutionInfo::COMPLETED_STATUS + Workflow::Status::RUNNING, + Workflow::Status::FAILED, + Workflow::Status::COMPLETED ] end end diff --git a/lib/temporal/testing/workflow_execution.rb b/lib/temporal/testing/workflow_execution.rb index e2ce5ca2..8e42518b 100644 --- a/lib/temporal/testing/workflow_execution.rb +++ b/lib/temporal/testing/workflow_execution.rb @@ -1,4 +1,5 @@ require 'temporal/testing/future_registry' +require 'temporal/workflow/status' module Temporal module Testing @@ -6,7 +7,7 @@ class WorkflowExecution attr_reader :status, :search_attributes def initialize - @status = Workflow::ExecutionInfo::RUNNING_STATUS + @status = Workflow::Status::RUNNING @futures = FutureRegistry.new @search_attributes = {} end @@ -18,9 +19,9 @@ def run(&block) def resume fiber.resume - @status = Workflow::ExecutionInfo::COMPLETED_STATUS unless fiber.alive? + @status = Workflow::Status::COMPLETED unless fiber.alive? rescue StandardError - @status = Workflow::ExecutionInfo::FAILED_STATUS + @status = Workflow::Status::FAILED end def register_future(token, future) diff --git a/lib/temporal/workflow/execution_info.rb b/lib/temporal/workflow/execution_info.rb index f47d3fcf..76f46ff6 100644 --- a/lib/temporal/workflow/execution_info.rb +++ b/lib/temporal/workflow/execution_info.rb @@ -1,37 +1,20 @@ require 'temporal/concerns/payloads' +require 'temporal/workflow/status' module Temporal class Workflow class ExecutionInfo < Struct.new(:workflow, :workflow_id, :run_id, :start_time, :close_time, :status, :history_length, :memo, :search_attributes, keyword_init: true) extend Concerns::Payloads - RUNNING_STATUS = :RUNNING - COMPLETED_STATUS = :COMPLETED - FAILED_STATUS = :FAILED - CANCELED_STATUS = :CANCELED - TERMINATED_STATUS = :TERMINATED - CONTINUED_AS_NEW_STATUS = :CONTINUED_AS_NEW - TIMED_OUT_STATUS = :TIMED_OUT - - API_STATUS_MAP = { - WORKFLOW_EXECUTION_STATUS_RUNNING: RUNNING_STATUS, - WORKFLOW_EXECUTION_STATUS_COMPLETED: COMPLETED_STATUS, - WORKFLOW_EXECUTION_STATUS_FAILED: FAILED_STATUS, - WORKFLOW_EXECUTION_STATUS_CANCELED: CANCELED_STATUS, - WORKFLOW_EXECUTION_STATUS_TERMINATED: TERMINATED_STATUS, - WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW: CONTINUED_AS_NEW_STATUS, - WORKFLOW_EXECUTION_STATUS_TIMED_OUT: TIMED_OUT_STATUS - }.freeze - - VALID_STATUSES = [ - RUNNING_STATUS, - COMPLETED_STATUS, - FAILED_STATUS, - CANCELED_STATUS, - TERMINATED_STATUS, - CONTINUED_AS_NEW_STATUS, - TIMED_OUT_STATUS - ].freeze + STATUSES = [ + Temporal::Workflow::Status::RUNNING, + Temporal::Workflow::Status::COMPLETED, + Temporal::Workflow::Status::FAILED, + Temporal::Workflow::Status::CANCELED, + Temporal::Workflow::Status::TERMINATED, + Temporal::Workflow::Status::CONTINUED_AS_NEW, + Temporal::Workflow::Status::TIMED_OUT, + ] def self.generate_from(response) new( @@ -40,18 +23,22 @@ def self.generate_from(response) run_id: response.execution.run_id, start_time: response.start_time&.to_time, close_time: response.close_time&.to_time, - status: API_STATUS_MAP.fetch(response.status), + status: Temporal::Workflow::Status::API_STATUS_MAP.fetch(response.status), history_length: response.history_length, memo: self.from_payload_map(response.memo.fields), search_attributes: self.from_payload_map(response.search_attributes.indexed_fields), ).freeze end - VALID_STATUSES.each do |status| + STATUSES.each do |status| define_method("#{status.downcase}?") do self.status == status end end + + def closed? + !running? + end end end end diff --git a/lib/temporal/workflow/status.rb b/lib/temporal/workflow/status.rb new file mode 100644 index 00000000..dd3875ab --- /dev/null +++ b/lib/temporal/workflow/status.rb @@ -0,0 +1,24 @@ +module Temporal + class Workflow + module Status + RUNNING = :RUNNING + COMPLETED = :COMPLETED + FAILED = :FAILED + CANCELED = :CANCELED + TERMINATED = :TERMINATED + CONTINUED_AS_NEW = :CONTINUED_AS_NEW + TIMED_OUT = :TIMED_OUT + + + API_STATUS_MAP = { + WORKFLOW_EXECUTION_STATUS_RUNNING: RUNNING, + WORKFLOW_EXECUTION_STATUS_COMPLETED: COMPLETED, + WORKFLOW_EXECUTION_STATUS_FAILED: FAILED, + WORKFLOW_EXECUTION_STATUS_CANCELED: CANCELED, + WORKFLOW_EXECUTION_STATUS_TERMINATED: TERMINATED, + WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW: CONTINUED_AS_NEW, + WORKFLOW_EXECUTION_STATUS_TIMED_OUT: TIMED_OUT + }.freeze + end + end +end \ No newline at end of file diff --git a/spec/fabricators/grpc/workflow_execution_info_fabricator.rb b/spec/fabricators/grpc/workflow_execution_info_fabricator.rb index 32683abd..cbd6a916 100644 --- a/spec/fabricators/grpc/workflow_execution_info_fabricator.rb +++ b/spec/fabricators/grpc/workflow_execution_info_fabricator.rb @@ -1,6 +1,8 @@ Fabricator(:api_workflow_execution_info, from: Temporal::Api::Workflow::V1::WorkflowExecutionInfo) do - execution { Fabricate(:api_workflow_execution) } - type { Fabricate(:api_workflow_type) } + transient :workflow_id, :workflow + + execution { |attrs| Fabricate(:api_workflow_execution, workflow_id: attrs[:workflow_id]) } + type { |attrs| Fabricate(:api_workflow_type, name: attrs[:workflow]) } start_time { Google::Protobuf::Timestamp.new.tap { |t| t.from_time(Time.now) } } close_time { Google::Protobuf::Timestamp.new.tap { |t| t.from_time(Time.now) } } status { Temporal::Api::Enums::V1::WorkflowExecutionStatus::WORKFLOW_EXECUTION_STATUS_COMPLETED } diff --git a/spec/unit/lib/temporal/client_spec.rb b/spec/unit/lib/temporal/client_spec.rb index 68e5076a..321207f7 100644 --- a/spec/unit/lib/temporal/client_spec.rb +++ b/spec/unit/lib/temporal/client_spec.rb @@ -697,7 +697,7 @@ class NamespacedWorkflow < Temporal::Workflow workflow_execution_info: api_info ) end - let(:api_info) { Fabricate(:api_workflow_execution_info) } + let(:api_info) { Fabricate(:api_workflow_execution_info, workflow: 'TestWorkflow', workflow_id: '') } before { allow(connection).to receive(:describe_workflow_execution).and_return(response) } @@ -776,4 +776,149 @@ class NamespacedWorkflow < Temporal::Workflow end end end + + describe '#list_open_workflow_executions' do + let(:from) { Time.now - 600 } + let(:now) { Time.now } + let(:api_execution_info) do + Fabricate(:api_workflow_execution_info, workflow: 'TestWorkflow', workflow_id: '') + end + let(:response) do + Temporal::Api::WorkflowService::V1::ListOpenWorkflowExecutionsResponse.new( + executions: [api_execution_info], + next_page_token: '' + ) + end + + before do + allow(Time).to receive(:now).and_return(now) + allow(connection) + .to receive(:list_open_workflow_executions) + .and_return(response) + end + + it 'returns a list of executions' do + executions = subject.list_open_workflow_executions(namespace, from) + + expect(executions.length).to eq(1) + expect(executions.first).to be_an_instance_of(Temporal::Workflow::ExecutionInfo) + end + + context 'when history is paginated' do + let(:response_1) do + Temporal::Api::WorkflowService::V1::ListOpenWorkflowExecutionsResponse.new( + executions: [api_execution_info], + next_page_token: 'a' + ) + end + let(:response_2) do + Temporal::Api::WorkflowService::V1::ListOpenWorkflowExecutionsResponse.new( + executions: [api_execution_info], + next_page_token: 'b' + ) + end + let(:response_3) do + Temporal::Api::WorkflowService::V1::ListOpenWorkflowExecutionsResponse.new( + executions: [api_execution_info], + next_page_token: '' + ) + end + + before do + allow(connection) + .to receive(:list_open_workflow_executions) + .and_return(response_1, response_2, response_3) + end + + it 'calls the API 3 times' do + subject.list_open_workflow_executions(namespace, from) + + expect(connection).to have_received(:list_open_workflow_executions).exactly(3).times + + expect(connection) + .to have_received(:list_open_workflow_executions) + .with(namespace: namespace, from: from, to: now, next_page_token: nil) + .once + + expect(connection) + .to have_received(:list_open_workflow_executions) + .with(namespace: namespace, from: from, to: now, next_page_token: 'a') + .once + + expect(connection) + .to have_received(:list_open_workflow_executions) + .with(namespace: namespace, from: from, to: now, next_page_token: 'b') + .once + end + + it 'returns a list of executions' do + executions = subject.list_open_workflow_executions(namespace, from) + + expect(executions.length).to eq(3) + executions.each do |execution| + expect(execution).to be_an_instance_of(Temporal::Workflow::ExecutionInfo) + end + end + end + + context 'when given unsupported filter' do + let(:filter) { { foo: :bar } } + + it 'raises ArgumentError' do + expect do + subject.list_open_workflow_executions(namespace, from, filter: filter) + end.to raise_error(ArgumentError, 'Allowed filters are: [:workflow, :workflow_id]') + end + end + + context 'when given multiple filters' do + let(:filter) { { workflow: 'TestWorkflow', workflow_id: 'xxx' } } + + it 'raises ArgumentError' do + expect do + subject.list_open_workflow_executions(namespace, from, filter: filter) + end.to raise_error(ArgumentError, 'Only one filter is allowed') + end + end + + context 'when called without filters' do + it 'makes a request' do + subject.list_open_workflow_executions(namespace, from) + + expect(connection) + .to have_received(:list_open_workflow_executions) + .with(namespace: namespace, from: from, to: now, next_page_token: nil) + end + end + + context 'when called with :to' do + it 'makes a request' do + subject.list_open_workflow_executions(namespace, from, now - 10) + + expect(connection) + .to have_received(:list_open_workflow_executions) + .with(namespace: namespace, from: from, to: now - 10, next_page_token: nil) + end + end + + context 'when called with a :workflow filter' do + it 'makes a request' do + subject.list_open_workflow_executions(namespace, from, filter: { workflow: 'TestWorkflow' }) + + expect(connection) + .to have_received(:list_open_workflow_executions) + .with(namespace: namespace, from: from, to: now, next_page_token: nil, workflow: 'TestWorkflow') + end + end + + context 'when called with a :workflow_id filter' do + it 'makes a request' do + subject.list_open_workflow_executions(namespace, from, filter: { workflow_id: 'xxx' }) + + expect(connection) + .to have_received(:list_open_workflow_executions) + .with(namespace: namespace, from: from, to: now, next_page_token: nil, workflow_id: 'xxx') + end + end + end end diff --git a/spec/unit/lib/temporal/grpc_client_spec.rb b/spec/unit/lib/temporal/grpc_client_spec.rb index 580a0c68..70a7885d 100644 --- a/spec/unit/lib/temporal/grpc_client_spec.rb +++ b/spec/unit/lib/temporal/grpc_client_spec.rb @@ -194,5 +194,155 @@ end end end + + describe '#list_open_workflow_executions' do + let(:namespace) { 'test-namespace' } + let(:from) { Time.now - 600 } + let(:to) { Time.now } + let(:args) { { namespace: namespace, from: from, to: to } } + let(:temporal_response) do + Temporal::Api::WorkflowService::V1::ListOpenWorkflowExecutionsResponse.new(executions: [], next_page_token: '') + end + + before do + allow(grpc_stub).to receive(:list_open_workflow_executions).and_return(temporal_response) + end + + it 'makes an API request' do + subject.list_open_workflow_executions(**args) + + expect(grpc_stub).to have_received(:list_open_workflow_executions) do |request| + expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListOpenWorkflowExecutionsRequest) + expect(request.maximum_page_size).to eq(described_class::DEFAULT_OPTIONS[:max_page_size]) + expect(request.next_page_token).to eq('') + expect(request.start_time_filter).to be_an_instance_of(Temporal::Api::Filter::V1::StartTimeFilter) + expect(request.start_time_filter.earliest_time.to_time) + .to eq(from) + expect(request.start_time_filter.latest_time.to_time) + .to eq(to) + expect(request.execution_filter).to eq(nil) + expect(request.type_filter).to eq(nil) + end + end + + context 'when next_page_token is supplied' do + it 'makes an API request' do + subject.list_open_workflow_executions(**args.merge(next_page_token: 'x')) + + expect(grpc_stub).to have_received(:list_open_workflow_executions) do |request| + expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListOpenWorkflowExecutionsRequest) + expect(request.next_page_token).to eq('x') + end + end + end + + context 'when workflow_id is supplied' do + it 'makes an API request' do + subject.list_open_workflow_executions(**args.merge(workflow_id: 'xxx')) + + expect(grpc_stub).to have_received(:list_open_workflow_executions) do |request| + expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListOpenWorkflowExecutionsRequest) + expect(request.execution_filter) + .to be_an_instance_of(Temporal::Api::Filter::V1::WorkflowExecutionFilter) + expect(request.execution_filter.workflow_id).to eq('xxx') + end + end + end + + context 'when workflow is supplied' do + it 'makes an API request' do + subject.list_open_workflow_executions(**args.merge(workflow: 'TestWorkflow')) + + expect(grpc_stub).to have_received(:list_open_workflow_executions) do |request| + expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListOpenWorkflowExecutionsRequest) + expect(request.type_filter).to be_an_instance_of(Temporal::Api::Filter::V1::WorkflowTypeFilter) + expect(request.type_filter.name).to eq('TestWorkflow') + end + end + end + end + + describe '#list_closed_workflow_executions' do + let(:namespace) { 'test-namespace' } + let(:from) { Time.now - 600 } + let(:to) { Time.now } + let(:args) { { namespace: namespace, from: from, to: to } } + let(:temporal_response) do + Temporal::Api::WorkflowService::V1::ListClosedWorkflowExecutionsResponse.new(executions: [], next_page_token: '') + end + + before do + allow(grpc_stub).to receive(:list_closed_workflow_executions).and_return(temporal_response) + end + + it 'makes an API request' do + subject.list_closed_workflow_executions(**args) + + expect(grpc_stub).to have_received(:list_closed_workflow_executions) do |request| + expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListClosedWorkflowExecutionsRequest) + expect(request.maximum_page_size).to eq(described_class::DEFAULT_OPTIONS[:max_page_size]) + expect(request.next_page_token).to eq('') + expect(request.start_time_filter).to be_an_instance_of(Temporal::Api::Filter::V1::StartTimeFilter) + expect(request.start_time_filter.earliest_time.to_time) + .to eq(from) + expect(request.start_time_filter.latest_time.to_time) + .to eq(to) + expect(request.execution_filter).to eq(nil) + expect(request.type_filter).to eq(nil) + expect(request.status_filter).to eq(nil) + end + end + + context 'when next_page_token is supplied' do + it 'makes an API request' do + subject.list_closed_workflow_executions(**args.merge(next_page_token: 'x')) + + expect(grpc_stub).to have_received(:list_closed_workflow_executions) do |request| + expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListClosedWorkflowExecutionsRequest) + expect(request.next_page_token).to eq('x') + end + end + end + + context 'when status is supplied' do + let(:api_completed_status) { Temporal::Api::Enums::V1::WorkflowExecutionStatus::WORKFLOW_EXECUTION_STATUS_COMPLETED } + + it 'makes an API request' do + subject.list_closed_workflow_executions( + **args.merge(status: Temporal::Workflow::Status::COMPLETED) + ) + + expect(grpc_stub).to have_received(:list_closed_workflow_executions) do |request| + expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListClosedWorkflowExecutionsRequest) + expect(request.status_filter).to eq(Temporal::Api::Filter::V1::StatusFilter.new(status: api_completed_status)) + end + end + end + + context 'when workflow_id is supplied' do + it 'makes an API request' do + subject.list_closed_workflow_executions(**args.merge(workflow_id: 'xxx')) + + expect(grpc_stub).to have_received(:list_closed_workflow_executions) do |request| + expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListClosedWorkflowExecutionsRequest) + expect(request.execution_filter) + .to be_an_instance_of(Temporal::Api::Filter::V1::WorkflowExecutionFilter) + expect(request.execution_filter.workflow_id).to eq('xxx') + end + end + end + + context 'when workflow is supplied' do + it 'makes an API request' do + subject.list_closed_workflow_executions(**args.merge(workflow: 'TestWorkflow')) + + expect(grpc_stub).to have_received(:list_closed_workflow_executions) do |request| + expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListClosedWorkflowExecutionsRequest) + expect(request.type_filter).to be_an_instance_of(Temporal::Api::Filter::V1::WorkflowTypeFilter) + expect(request.type_filter.name).to eq('TestWorkflow') + end + end + end + end end end diff --git a/spec/unit/lib/temporal/testing/temporal_override_spec.rb b/spec/unit/lib/temporal/testing/temporal_override_spec.rb index 9047150a..9280efb5 100644 --- a/spec/unit/lib/temporal/testing/temporal_override_spec.rb +++ b/spec/unit/lib/temporal/testing/temporal_override_spec.rb @@ -183,7 +183,7 @@ def execute end context 'when workflow is started' do - let(:status) { Temporal::Workflow::ExecutionInfo::RUNNING_STATUS } + let(:status) { Temporal::Workflow::Status::RUNNING } it 'raises error' do expect { subject }.to raise_error(error_class) { |e| expect(e.run_id).to eql(run_id) } @@ -191,7 +191,7 @@ def execute end context 'when workflow has completed' do - let(:status) { Temporal::Workflow::ExecutionInfo::COMPLETED_STATUS } + let(:status) { Temporal::Workflow::Status::COMPLETED } it 'raises error' do expect { subject }.to raise_error(error_class) { |e| expect(e.run_id).to eql(run_id) } @@ -199,7 +199,7 @@ def execute end context 'when workflow has failed' do - let(:status) { Temporal::Workflow::ExecutionInfo::FAILED_STATUS } + let(:status) { Temporal::Workflow::Status::FAILED } it { is_expected.to be_a(String) } end @@ -215,7 +215,7 @@ def execute end context 'when workflow is started' do - let(:status) { Temporal::Workflow::ExecutionInfo::RUNNING_STATUS } + let(:status) { Temporal::Workflow::Status::RUNNING } it 'raises error' do expect { subject }.to raise_error(error_class) { |e| expect(e.run_id).to eql(run_id) } @@ -223,13 +223,13 @@ def execute end context 'when workflow has completed' do - let(:status) { Temporal::Workflow::ExecutionInfo::COMPLETED_STATUS } + let(:status) { Temporal::Workflow::Status::COMPLETED } it { is_expected.to be_a(String) } end context 'when workflow has failed' do - let(:status) { Temporal::Workflow::ExecutionInfo::FAILED_STATUS } + let(:status) { Temporal::Workflow::Status::FAILED } it { is_expected.to be_a(String) } end @@ -245,7 +245,7 @@ def execute end context 'when workflow is started' do - let(:status) { Temporal::Workflow::ExecutionInfo::RUNNING_STATUS } + let(:status) { Temporal::Workflow::Status::RUNNING } it 'raises error' do expect { subject }.to raise_error(error_class) { |e| expect(e.run_id).to eql(run_id) } @@ -253,7 +253,7 @@ def execute end context 'when workflow has completed' do - let(:status) { Temporal::Workflow::ExecutionInfo::COMPLETED_STATUS } + let(:status) { Temporal::Workflow::Status::COMPLETED } it 'raises error' do expect { subject }.to raise_error(error_class) { |e| expect(e.run_id).to eql(run_id) } @@ -261,7 +261,7 @@ def execute end context 'when workflow has failed' do - let(:status) { Temporal::Workflow::ExecutionInfo::FAILED_STATUS } + let(:status) { Temporal::Workflow::Status::FAILED } it 'raises error' do expect { subject }.to raise_error(error_class) { |e| expect(e.run_id).to eql(run_id) } diff --git a/spec/unit/lib/temporal/workflow/execution_info_spec.rb b/spec/unit/lib/temporal/workflow/execution_info_spec.rb index a064e8ba..4074a3d6 100644 --- a/spec/unit/lib/temporal/workflow/execution_info_spec.rb +++ b/spec/unit/lib/temporal/workflow/execution_info_spec.rb @@ -2,7 +2,7 @@ describe Temporal::Workflow::ExecutionInfo do subject { described_class.generate_from(api_info) } - let(:api_info) { Fabricate(:api_workflow_execution_info) } + let(:api_info) { Fabricate(:api_workflow_execution_info, workflow: 'TestWorkflow', workflow_id: '') } describe '.generate_for' do @@ -26,6 +26,8 @@ let(:api_info) do Fabricate( :api_workflow_execution_info, + workflow: 'TestWorkflow', + workflow_id: '', status: Temporal::Api::Enums::V1::WorkflowExecutionStatus::WORKFLOW_EXECUTION_STATUS_TERMINATED ) end @@ -51,4 +53,33 @@ expect(subject).not_to be_timed_out end end + + describe '#closed?' do + Temporal::Workflow::Status::API_STATUS_MAP.keys.select { |x| x != :WORKFLOW_EXECUTION_STATUS_RUNNING }.each do |status| + context "when status is #{status}" do + let(:api_info) do + Fabricate( + :api_workflow_execution_info, + workflow: 'TestWorkflow', + workflow_id: '', + status: Temporal::Api::Enums::V1::WorkflowExecutionStatus.resolve(status) + ) + end + it { is_expected.to be_closed } + end + end + + context "when status is RUNNING" do + let(:api_info) do + Fabricate( + :api_workflow_execution_info, + workflow: 'TestWorkflow', + workflow_id: '', + status: Temporal::Api::Enums::V1::WorkflowExecutionStatus.resolve(:WORKFLOW_EXECUTION_STATUS_RUNNING) + ) + end + + it { is_expected.not_to be_closed } + end + end end From 9f4ee9d49dfbaa24f6f78e46582bc1d0fb5f5837 Mon Sep 17 00:00:00 2001 From: Drew Hoskins <37816070+drewhoskins-stripe@users.noreply.github.com> Date: Mon, 7 Mar 2022 17:56:48 -0800 Subject: [PATCH 037/125] Fix upsert_search_attributes (#158) --- examples/workflows/upsert_search_attributes_workflow.rb | 8 +++++++- lib/temporal/workflow/history.rb | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/examples/workflows/upsert_search_attributes_workflow.rb b/examples/workflows/upsert_search_attributes_workflow.rb index 9b69f25f..434d2c87 100644 --- a/examples/workflows/upsert_search_attributes_workflow.rb +++ b/examples/workflows/upsert_search_attributes_workflow.rb @@ -14,8 +14,14 @@ def execute(string_value, bool_value, float_value, int_value, time_value) 'CustomDatetimeField' => time_value, } workflow.upsert_search_attributes(attributes) + # The following lines are extra complexity to test if upsert_search_attributes is tracked properly in the internal + # state machine. + future = HelloWorldActivity.execute("Moon") - HelloWorldActivity.execute!("Moon") + name = workflow.side_effect { SecureRandom.uuid } + workflow.wait_for_all(future) + + HelloWorldActivity.execute!(name) attributes end end diff --git a/lib/temporal/workflow/history.rb b/lib/temporal/workflow/history.rb index ad188a77..e11ea9b2 100644 --- a/lib/temporal/workflow/history.rb +++ b/lib/temporal/workflow/history.rb @@ -56,6 +56,7 @@ def next_window REQUEST_CANCEL_ACTIVITY_TASK_FAILED REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION_INITIATED MARKER_RECORDED + UPSERT_WORKFLOW_SEARCH_ATTRIBUTES ].freeze attr_reader :iterator From 0cfd4b3e281bf88efc339afbf4579a3c1cd1ba3f Mon Sep 17 00:00:00 2001 From: aryak-stripe <97758420+aryak-stripe@users.noreply.github.com> Date: Mon, 7 Mar 2022 18:19:58 -0800 Subject: [PATCH 038/125] expose memo to child workflow execution (#160) --- lib/temporal/workflow/context.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index dd28fdbf..e71e19f6 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -112,7 +112,8 @@ def execute_workflow(workflow_class, *input, **args) task_queue: execution_options.task_queue, retry_policy: execution_options.retry_policy, timeouts: execution_options.timeouts, - headers: execution_options.headers + headers: execution_options.headers, + memo: execution_options.memo, ) target, cancelation_id = schedule_command(command) From 1e299895ef610a987cd0db5d2855786139ffb178 Mon Sep 17 00:00:00 2001 From: calum-stripe <98350978+calum-stripe@users.noreply.github.com> Date: Fri, 11 Mar 2022 16:03:48 -0800 Subject: [PATCH 039/125] Added fix for nil search attributes when listing workflows (#161) * added fix for nil search attributes * added unit test * updated unit test * added expect to be nil * added {} default --- lib/temporal/workflow/execution_info.rb | 3 ++- spec/unit/lib/temporal/workflow/execution_info_spec.rb | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/temporal/workflow/execution_info.rb b/lib/temporal/workflow/execution_info.rb index 76f46ff6..88d27cd7 100644 --- a/lib/temporal/workflow/execution_info.rb +++ b/lib/temporal/workflow/execution_info.rb @@ -17,6 +17,7 @@ class ExecutionInfo < Struct.new(:workflow, :workflow_id, :run_id, :start_time, ] def self.generate_from(response) + search_attributes = response.search_attributes.nil? ? {} : self.from_payload_map(response.search_attributes.indexed_fields) new( workflow: response.type.name, workflow_id: response.execution.workflow_id, @@ -26,7 +27,7 @@ def self.generate_from(response) status: Temporal::Workflow::Status::API_STATUS_MAP.fetch(response.status), history_length: response.history_length, memo: self.from_payload_map(response.memo.fields), - search_attributes: self.from_payload_map(response.search_attributes.indexed_fields), + search_attributes: search_attributes, ).freeze end diff --git a/spec/unit/lib/temporal/workflow/execution_info_spec.rb b/spec/unit/lib/temporal/workflow/execution_info_spec.rb index 4074a3d6..f5cf1f1b 100644 --- a/spec/unit/lib/temporal/workflow/execution_info_spec.rb +++ b/spec/unit/lib/temporal/workflow/execution_info_spec.rb @@ -20,6 +20,13 @@ it 'freezes the info' do expect(subject).to be_frozen end + + it 'deserializes if search_attributes is nil' do + api_info.search_attributes = nil + + result = described_class.generate_from(api_info) + expect(result.search_attributes).to eq({}) + end end describe 'statuses' do From e85d4016c2f7e736cdfd9137783d8a3baa9c0495 Mon Sep 17 00:00:00 2001 From: Drew Hoskins <37816070+drewhoskins-stripe@users.noreply.github.com> Date: Sun, 13 Mar 2022 17:17:23 -0700 Subject: [PATCH 040/125] Make it easier to debug converter_spec failure. (#153) --- examples/spec/integration/converter_spec.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/spec/integration/converter_spec.rb b/examples/spec/integration/converter_spec.rb index ce1ea66a..bc3f78a6 100644 --- a/examples/spec/integration/converter_spec.rb +++ b/examples/spec/integration/converter_spec.rb @@ -1,5 +1,6 @@ require 'workflows/hello_world_workflow' require 'lib/cryptconverter' +require 'grpc/errors' describe 'Converter', :integration do around(:each) do |example| @@ -23,7 +24,11 @@ it 'can encrypt payloads' do workflow_id, run_id = run_workflow(HelloWorldWorkflow, 'Tom') - wait_for_workflow_completion(workflow_id, run_id) + begin + wait_for_workflow_completion(workflow_id, run_id) + rescue GRPC::DeadlineExceeded + raise "Encrypted-payload workflow didn't run. Make sure you run USE_ENCRYPTION=1 ./bin/worker and try again." + end result = fetch_history(workflow_id, run_id) From 37a2c4fca613ffdcda4ed1a3fb3e35740c7b3c26 Mon Sep 17 00:00:00 2001 From: nagl-stripe <86737162+nagl-stripe@users.noreply.github.com> Date: Wed, 16 Mar 2022 18:39:14 -0700 Subject: [PATCH 041/125] Expose scheduled_time and current_attempt_scheduled_time on activity metadata (#164) * Expose scheduled_time and current_attempt_scheduled_time on activity metadata * Change field name, use a Time * Fix comment, fix mising .to_time --- lib/temporal/metadata.rb | 4 +++- lib/temporal/metadata/activity.rb | 10 +++++++--- lib/temporal/testing/local_workflow_context.rb | 8 ++++++-- spec/fabricators/activity_metadata_fabricator.rb | 2 ++ spec/fabricators/grpc/activity_task_fabricator.rb | 2 ++ spec/unit/lib/temporal/metadata/activity_spec.rb | 6 +++++- 6 files changed, 25 insertions(+), 7 deletions(-) diff --git a/lib/temporal/metadata.rb b/lib/temporal/metadata.rb index df7165ea..4bcfade8 100644 --- a/lib/temporal/metadata.rb +++ b/lib/temporal/metadata.rb @@ -21,7 +21,9 @@ def generate_activity_metadata(task, namespace) workflow_id: task.workflow_execution.workflow_id, workflow_name: task.workflow_type.name, headers: from_payload_map(task.header&.fields || {}), - heartbeat_details: from_details_payloads(task.heartbeat_details) + heartbeat_details: from_details_payloads(task.heartbeat_details), + scheduled_at: task.scheduled_time.to_time, + current_attempt_scheduled_at: task.current_attempt_scheduled_time.to_time ) end diff --git a/lib/temporal/metadata/activity.rb b/lib/temporal/metadata/activity.rb index c7e7d814..bdca3366 100644 --- a/lib/temporal/metadata/activity.rb +++ b/lib/temporal/metadata/activity.rb @@ -3,9 +3,9 @@ module Temporal module Metadata class Activity < Base - attr_reader :namespace, :id, :name, :task_token, :attempt, :workflow_run_id, :workflow_id, :workflow_name, :headers, :heartbeat_details + attr_reader :namespace, :id, :name, :task_token, :attempt, :workflow_run_id, :workflow_id, :workflow_name, :headers, :heartbeat_details, :scheduled_at, :current_attempt_scheduled_at - def initialize(namespace:, id:, name:, task_token:, attempt:, workflow_run_id:, workflow_id:, workflow_name:, headers: {}, heartbeat_details:) + def initialize(namespace:, id:, name:, task_token:, attempt:, workflow_run_id:, workflow_id:, workflow_name:, headers: {}, heartbeat_details:, scheduled_at:, current_attempt_scheduled_at:) @namespace = namespace @id = id @name = name @@ -16,6 +16,8 @@ def initialize(namespace:, id:, name:, task_token:, attempt:, workflow_run_id:, @workflow_name = workflow_name @headers = headers @heartbeat_details = heartbeat_details + @scheduled_at = scheduled_at + @current_attempt_scheduled_at = current_attempt_scheduled_at freeze end @@ -32,7 +34,9 @@ def to_h 'workflow_run_id' => workflow_run_id, 'activity_id' => id, 'activity_name' => name, - 'attempt' => attempt + 'attempt' => attempt, + 'scheduled_at' => scheduled_at.to_s, + 'current_attempt_scheduled_at' => current_attempt_scheduled_at.to_s, } end end diff --git a/lib/temporal/testing/local_workflow_context.rb b/lib/temporal/testing/local_workflow_context.rb index 1c0cdd07..a115eb26 100644 --- a/lib/temporal/testing/local_workflow_context.rb +++ b/lib/temporal/testing/local_workflow_context.rb @@ -60,7 +60,9 @@ def execute_activity(activity_class, *input, **args) workflow_id: workflow_id, workflow_name: nil, # not yet used, but will be in the future headers: execution_options.headers, - heartbeat_details: nil + heartbeat_details: nil, + scheduled_at: Time.now, + current_attempt_scheduled_at: Time.now, ) context = LocalActivityContext.new(metadata) @@ -108,7 +110,9 @@ def execute_local_activity(activity_class, *input, **args) workflow_id: workflow_id, workflow_name: nil, # not yet used, but will be in the future headers: execution_options.headers, - heartbeat_details: nil + heartbeat_details: nil, + scheduled_at: Time.now, + current_attempt_scheduled_at: Time.now, ) context = LocalActivityContext.new(metadata) diff --git a/spec/fabricators/activity_metadata_fabricator.rb b/spec/fabricators/activity_metadata_fabricator.rb index 06409da6..5fd34e3a 100644 --- a/spec/fabricators/activity_metadata_fabricator.rb +++ b/spec/fabricators/activity_metadata_fabricator.rb @@ -11,4 +11,6 @@ workflow_name 'TestWorkflow' headers { {} } heartbeat_details nil + scheduled_at { Time.now } + current_attempt_scheduled_at { Time.now } end diff --git a/spec/fabricators/grpc/activity_task_fabricator.rb b/spec/fabricators/grpc/activity_task_fabricator.rb index b6fc43fc..8e940a96 100644 --- a/spec/fabricators/grpc/activity_task_fabricator.rb +++ b/spec/fabricators/grpc/activity_task_fabricator.rb @@ -11,6 +11,8 @@ workflow_execution { Fabricate(:api_workflow_execution) } current_attempt_scheduled_time { Google::Protobuf::Timestamp.new.tap { |t| t.from_time(Time.now) } } started_time { Google::Protobuf::Timestamp.new.tap { |t| t.from_time(Time.now) } } + scheduled_time { Google::Protobuf::Timestamp.new.tap { |t| t.from_time(Time.now) } } + current_attempt_scheduled_time { Google::Protobuf::Timestamp.new.tap { |t| t.from_time(Time.now) } } header do |attrs| fields = (attrs[:headers] || {}).each_with_object({}) do |(field, value), h| h[field] = Temporal.configuration.converter.to_payload(value) diff --git a/spec/unit/lib/temporal/metadata/activity_spec.rb b/spec/unit/lib/temporal/metadata/activity_spec.rb index 4df742be..a07a67a8 100644 --- a/spec/unit/lib/temporal/metadata/activity_spec.rb +++ b/spec/unit/lib/temporal/metadata/activity_spec.rb @@ -16,6 +16,8 @@ expect(subject.workflow_name).to eq(args.workflow_name) expect(subject.headers).to eq(args.headers) expect(subject.heartbeat_details).to eq(args.heartbeat_details) + expect(subject.scheduled_at).to eq(args.scheduled_at) + expect(subject.current_attempt_scheduled_at).to eq(args.current_attempt_scheduled_at) end it { is_expected.to be_frozen } @@ -36,7 +38,9 @@ 'namespace' => subject.namespace, 'workflow_id' => subject.workflow_id, 'workflow_name' => subject.workflow_name, - 'workflow_run_id' => subject.workflow_run_id + 'workflow_run_id' => subject.workflow_run_id, + 'scheduled_at' => subject.scheduled_at.to_s, + 'current_attempt_scheduled_at' => subject.current_attempt_scheduled_at.to_s, }) end end From 4ce0748fd97a4017f95f330dec0c50caeb6ebf1c Mon Sep 17 00:00:00 2001 From: aryak-stripe <97758420+aryak-stripe@users.noreply.github.com> Date: Wed, 16 Mar 2022 18:39:36 -0700 Subject: [PATCH 042/125] child workflow execution parent close policy (#163) * Implement ParentClosePolicy for child workflows * Add e2e test for child workflow execution * move serialization logic farther down the stack * Refactor serialize_parent_close_policy; add unit tests * Remove unused unit test stub --- examples/bin/worker | 2 + .../integration/parent_close_workflow_spec.rb | 55 +++++++++++++++++++ examples/workflows/parent_close_workflow.rb | 12 ++++ examples/workflows/slow_child_workflow.rb | 9 +++ .../serializer/start_child_workflow.rb | 17 ++++++ lib/temporal/workflow/command.rb | 2 +- lib/temporal/workflow/context.rb | 2 + .../serializer/start_child_workflow_spec.rb | 48 ++++++++++++++++ 8 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 examples/spec/integration/parent_close_workflow_spec.rb create mode 100644 examples/workflows/parent_close_workflow.rb create mode 100644 examples/workflows/slow_child_workflow.rb create mode 100644 spec/unit/lib/temporal/connection/serializer/start_child_workflow_spec.rb diff --git a/examples/bin/worker b/examples/bin/worker index 968c991e..435e418e 100755 --- a/examples/bin/worker +++ b/examples/bin/worker @@ -33,6 +33,7 @@ worker.register_workflow(LocalHelloWorldWorkflow) worker.register_workflow(LongWorkflow) worker.register_workflow(LoopWorkflow) worker.register_workflow(MetadataWorkflow) +worker.register_workflow(ParentCloseWorkflow) worker.register_workflow(ParentWorkflow) worker.register_workflow(ProcessFileWorkflow) worker.register_workflow(QuickTimeoutWorkflow) @@ -44,6 +45,7 @@ worker.register_workflow(SerialHelloWorldWorkflow) worker.register_workflow(SideEffectWorkflow) worker.register_workflow(SignalWithStartWorkflow) worker.register_workflow(SimpleTimerWorkflow) +worker.register_workflow(SlowChildWorkflow) worker.register_workflow(TimeoutWorkflow) worker.register_workflow(TripBookingWorkflow) worker.register_workflow(UpsertSearchAttributesWorkflow) diff --git a/examples/spec/integration/parent_close_workflow_spec.rb b/examples/spec/integration/parent_close_workflow_spec.rb new file mode 100644 index 00000000..e307dbd8 --- /dev/null +++ b/examples/spec/integration/parent_close_workflow_spec.rb @@ -0,0 +1,55 @@ +require 'workflows/parent_close_workflow' + +describe ParentCloseWorkflow, :integration do + subject { described_class } + + it 'SlowChildWorkflow terminates if parent_close_policy is TERMINATE' do + workflow_id = 'parent_close_test_wf-' + SecureRandom.uuid + child_workflow_id = 'slow_child_test_wf-' + SecureRandom.uuid + + run_id = Temporal.start_workflow( + ParentCloseWorkflow, + child_workflow_id, + :terminate, + options: { workflow_id: workflow_id } + ) + + Temporal.await_workflow_result( + ParentCloseWorkflow, + workflow_id: workflow_id, + run_id: run_id, + ) + + expect do + Temporal.await_workflow_result( + SlowChildWorkflow, + workflow_id: child_workflow_id, + ) + end.to raise_error(Temporal::WorkflowTerminated) + end + + it 'SlowChildWorkflow completes if parent_close_policy is ABANDON' do + workflow_id = 'parent_close_test_wf-' + SecureRandom.uuid + child_workflow_id = 'slow_child_test_wf-' + SecureRandom.uuid + + run_id = Temporal.start_workflow( + ParentCloseWorkflow, + child_workflow_id, + :abandon, + options: { workflow_id: workflow_id } + ) + + Temporal.await_workflow_result( + ParentCloseWorkflow, + workflow_id: workflow_id, + run_id: run_id, + ) + + result = Temporal.await_workflow_result( + SlowChildWorkflow, + workflow_id: child_workflow_id, + ) + + expect(result).to eq('slow child ran') + end +end diff --git a/examples/workflows/parent_close_workflow.rb b/examples/workflows/parent_close_workflow.rb new file mode 100644 index 00000000..1b7b670c --- /dev/null +++ b/examples/workflows/parent_close_workflow.rb @@ -0,0 +1,12 @@ +require 'workflows/slow_child_workflow' + +class ParentCloseWorkflow < Temporal::Workflow + def execute(child_workflow_id, parent_close_policy) + options = { workflow_id: child_workflow_id, parent_close_policy: parent_close_policy } + + SlowChildWorkflow.execute(1, options: options) + workflow.sleep(0.1) # Make sure the child workflow is scheduled before we exit. + + return + end +end diff --git a/examples/workflows/slow_child_workflow.rb b/examples/workflows/slow_child_workflow.rb new file mode 100644 index 00000000..8de8e3cd --- /dev/null +++ b/examples/workflows/slow_child_workflow.rb @@ -0,0 +1,9 @@ +class SlowChildWorkflow < Temporal::Workflow + def execute(delay) + if delay.positive? + workflow.sleep(delay) + end + + return 'slow child ran' + end +end diff --git a/lib/temporal/connection/serializer/start_child_workflow.rb b/lib/temporal/connection/serializer/start_child_workflow.rb index 5f6b350d..3952ce97 100644 --- a/lib/temporal/connection/serializer/start_child_workflow.rb +++ b/lib/temporal/connection/serializer/start_child_workflow.rb @@ -8,6 +8,12 @@ module Serializer class StartChildWorkflow < Base include Concerns::Payloads + PARENT_CLOSE_POLICY = { + terminate: Temporal::Api::Enums::V1::ParentClosePolicy::PARENT_CLOSE_POLICY_TERMINATE, + abandon: Temporal::Api::Enums::V1::ParentClosePolicy::PARENT_CLOSE_POLICY_ABANDON, + request_cancel: Temporal::Api::Enums::V1::ParentClosePolicy::PARENT_CLOSE_POLICY_REQUEST_CANCEL, + }.freeze + def to_proto Temporal::Api::Command::V1::Command.new( command_type: Temporal::Api::Enums::V1::CommandType::COMMAND_TYPE_START_CHILD_WORKFLOW_EXECUTION, @@ -22,6 +28,7 @@ def to_proto workflow_run_timeout: object.timeouts[:run], workflow_task_timeout: object.timeouts[:task], retry_policy: Temporal::Connection::Serializer::RetryPolicy.new(object.retry_policy).to_proto, + parent_close_policy: serialize_parent_close_policy(object.parent_close_policy), header: serialize_headers(object.headers), memo: serialize_memo(object.memo), ) @@ -41,6 +48,16 @@ def serialize_memo(memo) Temporal::Api::Common::V1::Memo.new(fields: to_payload_map(memo)) end + + def serialize_parent_close_policy(parent_close_policy) + return unless parent_close_policy + + unless PARENT_CLOSE_POLICY.key? parent_close_policy + raise ArgumentError, "Unknown parent_close_policy '#{parent_close_policy}' specified" + end + + PARENT_CLOSE_POLICY[parent_close_policy] + end end end end diff --git a/lib/temporal/workflow/command.rb b/lib/temporal/workflow/command.rb index 8bf36f75..52208307 100644 --- a/lib/temporal/workflow/command.rb +++ b/lib/temporal/workflow/command.rb @@ -3,7 +3,7 @@ class Workflow module Command # TODO: Move these classes into their own directories under workflow/command/* ScheduleActivity = Struct.new(:activity_type, :activity_id, :input, :namespace, :task_queue, :retry_policy, :timeouts, :headers, keyword_init: true) - StartChildWorkflow = Struct.new(:workflow_type, :workflow_id, :input, :namespace, :task_queue, :retry_policy, :timeouts, :headers, :memo, keyword_init: true) + StartChildWorkflow = Struct.new(:workflow_type, :workflow_id, :input, :namespace, :task_queue, :retry_policy, :parent_close_policy, :timeouts, :headers, :memo, keyword_init: true) ContinueAsNew = Struct.new(:workflow_type, :task_queue, :input, :timeouts, :retry_policy, :headers, :memo, keyword_init: true) RequestActivityCancellation = Struct.new(:activity_id, keyword_init: true) RecordMarker = Struct.new(:name, :details, keyword_init: true) diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index e71e19f6..e92e1393 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -102,6 +102,7 @@ def execute_workflow(workflow_class, *input, **args) options = args.delete(:options) || {} input << args unless args.empty? + parent_close_policy = options.delete(:parent_close_policy) execution_options = ExecutionOptions.new(workflow_class, options, config.default_execution_options) command = Command::StartChildWorkflow.new( @@ -111,6 +112,7 @@ def execute_workflow(workflow_class, *input, **args) namespace: execution_options.namespace, task_queue: execution_options.task_queue, retry_policy: execution_options.retry_policy, + parent_close_policy: parent_close_policy, timeouts: execution_options.timeouts, headers: execution_options.headers, memo: execution_options.memo, diff --git a/spec/unit/lib/temporal/connection/serializer/start_child_workflow_spec.rb b/spec/unit/lib/temporal/connection/serializer/start_child_workflow_spec.rb new file mode 100644 index 00000000..5e3278b4 --- /dev/null +++ b/spec/unit/lib/temporal/connection/serializer/start_child_workflow_spec.rb @@ -0,0 +1,48 @@ +require 'temporal/connection/errors' +require 'temporal/workflow/command' +require 'temporal/connection/serializer/start_child_workflow' + +describe Temporal::Connection::Serializer::StartChildWorkflow do + let(:example_command) do + Temporal::Workflow::Command::StartChildWorkflow.new( + workflow_id: SecureRandom.uuid, + workflow_type: '', + input: nil, + namespace: '', + task_queue: '', + retry_policy: nil, + timeouts: { execution: 1, run: 1, task: 1 }, + headers: nil, + memo: {}, + ) + end + + describe 'to_proto' do + it 'raises an error if an invalid parent_close_policy is specified' do + command = example_command + command.parent_close_policy = :invalid + + expect do + described_class.new(command).to_proto + end.to raise_error(Temporal::Connection::ArgumentError) do |e| + expect(e.message).to eq("Unknown parent_close_policy '#{command.parent_close_policy}' specified") + end + end + + { + nil => :PARENT_CLOSE_POLICY_UNSPECIFIED, + :terminate => :PARENT_CLOSE_POLICY_TERMINATE, + :abandon => :PARENT_CLOSE_POLICY_ABANDON, + :request_cancel => :PARENT_CLOSE_POLICY_REQUEST_CANCEL, + }.each do |policy_name, expected_parent_close_policy| + it "successfully resolves a parent_close_policy of #{policy_name}" do + command = example_command + command.parent_close_policy = policy_name + + result = described_class.new(command).to_proto + attribs = result.start_child_workflow_execution_command_attributes + expect(attribs.parent_close_policy).to eq(expected_parent_close_policy) + end + end + end +end From f02fec17045d860303835f47bcd56ee366b9c9fa Mon Sep 17 00:00:00 2001 From: calum-stripe <98350978+calum-stripe@users.noreply.github.com> Date: Wed, 16 Mar 2022 18:40:36 -0700 Subject: [PATCH 043/125] Updating register namespace (#149) * updated register namespace to accept new params * examples * fixed namespace test * empty * updated unit tests * removed unncessary code * updated seconds * fixed nits * updated sleep to 0.5 * updated unit tests and nits * fixed unit tests * added link to comment * updated nits * nit --- .../integration/register_namespace_spec.rb | 36 +++++++++++++++++++ lib/temporal/client.rb | 7 ++-- lib/temporal/connection/grpc.rb | 9 ++--- spec/unit/lib/temporal/client_spec.rb | 4 +-- 4 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 examples/spec/integration/register_namespace_spec.rb diff --git a/examples/spec/integration/register_namespace_spec.rb b/examples/spec/integration/register_namespace_spec.rb new file mode 100644 index 00000000..5380895b --- /dev/null +++ b/examples/spec/integration/register_namespace_spec.rb @@ -0,0 +1,36 @@ +describe 'Temporal.register_namespace' do + it 'can register a new namespace' do + # have to generate a new namespace on each run because currently can't delete namespaces + name = "test_namespace_#{SecureRandom.uuid}" + description = 'this is the description' + retention_period = 30 + data = { test: 'value' } + + Temporal.register_namespace(name, description, retention_period: retention_period, data: data) + + # fetch the namespace from Temporal and check it exists and has the correct settings + # (need to wait a few seconds for temporal to catch up so try a few times) + attempts = 0 + while attempts < 30 do + attempts += 1 + + begin + result = Temporal.describe_namespace(name) + + expect(result.namespace_info.name).to eq(name) + expect(result.namespace_info.data).to eq(data) + expect(result.config.workflow_execution_retention_ttl.seconds).to eq(retention_period * 24 * 60 * 60) + break + rescue GRPC::NotFound + sleep 0.5 + end + end + end + + it 'errors if attempting to register a namespace with the same name' do + name = "test_namespace_#{SecureRandom.uuid}" + Temporal.register_namespace(name) + + expect {Temporal.register_namespace(name)}.to raise_error(Temporal::NamespaceAlreadyExistsFailure, 'Namespace already exists.') + end +end diff --git a/lib/temporal/client.rb b/lib/temporal/client.rb index 5e622743..c1e9a2da 100644 --- a/lib/temporal/client.rb +++ b/lib/temporal/client.rb @@ -133,8 +133,11 @@ def schedule_workflow(workflow, cron_schedule, *input, options: {}, **args) # # @param name [String] name of the new namespace # @param description [String] optional namespace description - def register_namespace(name, description = nil) - connection.register_namespace(name: name, description: description) + # @param is_global [Boolean] used to distinguish local namespaces from global namespaces (https://docs.temporal.io/docs/server/namespaces/#global-namespaces) + # @param retention_period [Int] optional value which specifies how long Temporal will keep workflows after completing + # @param data [Hash] optional key-value map for any customized purpose that can be retreived with describe_namespace + def register_namespace(name, description = nil, is_global: false, retention_period: 10, data: nil) + connection.register_namespace(name: name, description: description, is_global: is_global, retention_period: retention_period, data: data) end # Fetches metadata for a namespace. diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index af5ef156..9e27a10e 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -39,14 +39,15 @@ def initialize(host, port, identity, options = {}) @options = DEFAULT_OPTIONS.merge(options) end - def register_namespace(name:, description: nil, global: false, retention_period: 10) + def register_namespace(name:, description: nil, is_global: false, retention_period: 10, data: nil) request = Temporal::Api::WorkflowService::V1::RegisterNamespaceRequest.new( namespace: name, description: description, - is_global_namespace: global, + is_global_namespace: is_global, workflow_execution_retention_period: Google::Protobuf::Duration.new( - seconds: retention_period * 24 * 60 * 60 - ) + seconds: (retention_period * 24 * 60 * 60).to_i + ), + data: data, ) client.register_namespace(request) rescue ::GRPC::AlreadyExists => e diff --git a/spec/unit/lib/temporal/client_spec.rb b/spec/unit/lib/temporal/client_spec.rb index 321207f7..3872c8d2 100644 --- a/spec/unit/lib/temporal/client_spec.rb +++ b/spec/unit/lib/temporal/client_spec.rb @@ -307,7 +307,7 @@ def expect_signal_with_start(expected_arguments, expected_signal_argument) expect(connection) .to have_received(:register_namespace) - .with(name: 'new-namespace', description: nil) + .with(name: 'new-namespace', description: nil, is_global: false, data: nil, retention_period: 10) end it 'registers namespace with the specified name and description' do @@ -315,7 +315,7 @@ def expect_signal_with_start(expected_arguments, expected_signal_argument) expect(connection) .to have_received(:register_namespace) - .with(name: 'new-namespace', description: 'namespace description') + .with(name: 'new-namespace', description: 'namespace description', is_global: false, data: nil, retention_period: 10) end end From 25a666fe53e5286df33463e69f974de509979230 Mon Sep 17 00:00:00 2001 From: Arya Kumar Date: Wed, 23 Mar 2022 08:55:08 -0700 Subject: [PATCH 044/125] Expose wait_for_start for child workflow execution (#167) * Expose wait_for_start for child workflow execution * Address review feedback Co-authored-by: Arya Kumar --- examples/workflows/parent_close_workflow.rb | 8 ++++---- lib/temporal/workflow/context.rb | 7 +++++++ lib/temporal/workflow/state_manager.rb | 1 + 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/examples/workflows/parent_close_workflow.rb b/examples/workflows/parent_close_workflow.rb index 1b7b670c..355bc7ca 100644 --- a/examples/workflows/parent_close_workflow.rb +++ b/examples/workflows/parent_close_workflow.rb @@ -2,11 +2,11 @@ class ParentCloseWorkflow < Temporal::Workflow def execute(child_workflow_id, parent_close_policy) - options = { workflow_id: child_workflow_id, parent_close_policy: parent_close_policy } - + options = { + workflow_id: child_workflow_id, + parent_close_policy: parent_close_policy, + } SlowChildWorkflow.execute(1, options: options) - workflow.sleep(0.1) # Make sure the child workflow is scheduled before we exit. - return end end diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index e92e1393..ac4454ce 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -131,6 +131,13 @@ def execute_workflow(workflow_class, *input, **args) future.failure_callbacks.each { |callback| call_in_fiber(callback, exception) } end + # Temporal docs say that we *must* wait for the child to get spawned: + child_workflow_started = false + dispatcher.register_handler(target, 'started') do + child_workflow_started = true + end + wait_for { child_workflow_started } + future end diff --git a/lib/temporal/workflow/state_manager.rb b/lib/temporal/workflow/state_manager.rb index 8c991b08..c54def51 100644 --- a/lib/temporal/workflow/state_manager.rb +++ b/lib/temporal/workflow/state_manager.rb @@ -241,6 +241,7 @@ def apply_event(event) dispatch(target, 'failed', 'StandardError', from_payloads(event.attributes.cause)) when 'CHILD_WORKFLOW_EXECUTION_STARTED' + dispatch(target, 'started') state_machine.start when 'CHILD_WORKFLOW_EXECUTION_COMPLETED' From 06b99972a21f77737cf3769a45a87faee90efbf5 Mon Sep 17 00:00:00 2001 From: Chuck Remes <37843211+chuckremes2@users.noreply.github.com> Date: Wed, 23 Mar 2022 10:55:30 -0500 Subject: [PATCH 045/125] Synchronous-proxy example in Ruby (#138) * initial commit; broken and a bit messy with logging * save current code while I go work on adding SignalExternalWorkflow support * initial work to support SignalExternalWorkflow * define the serializer and hook it up * stub in what I think is the correct work for each event type * some fixes per antstorm advice * initial attempt at integration test * docs on testing and an improvement to existing test * encode the signal payload using correct helper * return a Future and fulfill it correctly upon completion * get the \*event_id from the right field in the command structure * modify test to verify the signal is only received once * test for failure to deliver a signal to external workflow * do not discard the failure command otherwise non-deterministic * simplify test workflow by eliminating unnecessary timer * oops, had double call to #schedule_command so signals were sent twice * edit description of example * split to separate files and improve test coverage * change method signature for consistency and a few other cleanups * oops, fix EventType name to match correct constant * cleanup to work with new #signal_external_workflow call * improve readme * add custom errors and set to nonretryable for upstream error handling * finish error handling logic * improve how errors are propagated, detected, and logged * improve the README to explain the pattern and give another example * small spelling fix * make code more idiomatic for Ruby * make Ruby a bit more idiomatic * switch to CAP_SNAKECASE for constants * remove task_queue setting since default is set in configuration.rb * remove unused argument * remove antiquated error handling and reraise exceptions instead * loop using retry until no exceptions are received --- examples/synchronous-proxy/README.md | 77 ++++++++++ examples/synchronous-proxy/activities.rb | 57 ++++++++ examples/synchronous-proxy/configuration.rb | 17 +++ examples/synchronous-proxy/flow.png | Bin 0 -> 109760 bytes .../synchronous-proxy/proxy/communications.rb | 112 +++++++++++++++ examples/synchronous-proxy/ui/main.rb | 68 +++++++++ examples/synchronous-proxy/worker/worker.rb | 15 ++ examples/synchronous-proxy/workflows.rb | 133 ++++++++++++++++++ 8 files changed, 479 insertions(+) create mode 100644 examples/synchronous-proxy/README.md create mode 100644 examples/synchronous-proxy/activities.rb create mode 100644 examples/synchronous-proxy/configuration.rb create mode 100644 examples/synchronous-proxy/flow.png create mode 100644 examples/synchronous-proxy/proxy/communications.rb create mode 100644 examples/synchronous-proxy/ui/main.rb create mode 100644 examples/synchronous-proxy/worker/worker.rb create mode 100644 examples/synchronous-proxy/workflows.rb diff --git a/examples/synchronous-proxy/README.md b/examples/synchronous-proxy/README.md new file mode 100644 index 00000000..67010958 --- /dev/null +++ b/examples/synchronous-proxy/README.md @@ -0,0 +1,77 @@ +# Purpose + +This pattern is used when a non-workflow process needs to advance a workflow state +machine from its initial state to its terminal state. It does this by adding input +data to the workflow (via Signals) and receiving new information back from the +workflow (when a secondary proxy workflow exits and returns a value). + +The only way to add information to a workflow is via a Signal. + +There are two ways to +get information out of a workflow. One, the workflow has a Query handler and can respond +to queries. However, this is limited in that Queries may not modify the state of the +workflow itself. Two, the workflow can exit and return a result to its caller. This +second approach is leveraged by the pattern to get information back from the primary +workflow. This information could be used to determine branching behavior for the +non-workflow caller. + +The flow of calls is outlined in the diagram below. + +![Flow Diagram](flow.png) + +# Explanation + +The primary use-case for this pattern is for a non-workflow process to *send and receive* data +to and from a workflow. Note that a Temporal client may send a signal to a workflow but the +information transfer is one-way (i.e. fire and forget). There is no mechanism for a workflow +to send a signal to a non-workflow. A keen observer would note that a Query can be used to +ask for information, however a Query is supposed to be idempotent and *should not cause any +state change* in the workflow itself. Also, Queries imply polling for a result which is slow +and inefficient. Therefore, it is not a mechanism for sending new information +into a workflow and receiving a response. + +So, the non-workflow process can communicate to a workflow by: + +a) Starting that workflow, and + +b) Communicating with the workflow by creating proxy workflows to signal the main workflow and +then block for a response. When these proxy workflows exit, they can return the response to the +caller. + +In the real world, this pattern could be utilized for managing an interaction via a series of +web pages. Imagine that a user lands on a home page and clicks a link to apply for a library +card. The link hits the web application's controller and can now start the +`ApplyForLibraryCardWorkflow`. The workflow ID could be returned back in a response to the caller +as a session value, for example. + +On the next page, the user can fill out the application for the library card by providing their +name, address, and phone number. Upon submission of this form (via POST), the web application +controller can 1) lookup the associated workflow from the session, and 2) create the +`SubmitPersonalDetailsWorkflow` workflow and pass in the form data. This workflow packages up +the data and signals it to the `ApplyForLibraryCardWorkflow` and waits for a response via another +signal. The main workflow applies the appropriate business logic to the payload and advances its +state. It then signals back to the proxy workflow the result of its work and then blocks to +await new data. + +Depending on the response from the `ApplyForLibraryCardWorkflow`, the controller can render a page +to continue the application or ask for the user to correct some bad input. + +Continue and repeat this action via the web application controller(s) as it moves the user +through the entire library card application journey. By its nature, web applications are all stateless +and asynchronous, so the state and behavior are encapsulated by the workflow and its associated +activity outcomes. The only state outside of the workflow that the web application cares about is the +session information so it can match the user back to the correct workflow. + +# Execution + +Open two shells / terminal windows. In one, execute: +```shell +ruby worker/worker.rb +``` +In the second, execute: +```shell +ruby ui/main.rb +``` +In the shell running `ui` it will ask a series of questions. Answer the questions and the +program will send the appropriate signals around to complete the process. Upon completion it +prints a success message. diff --git a/examples/synchronous-proxy/activities.rb b/examples/synchronous-proxy/activities.rb new file mode 100644 index 00000000..766a6bff --- /dev/null +++ b/examples/synchronous-proxy/activities.rb @@ -0,0 +1,57 @@ +module SynchronousProxy + class RegisterEmailActivity < Temporal::Activity + def execute(email) + logger.info "activity: registered email #{email}" + nil + end + end + + class ValidateSizeActivity < Temporal::Activity + InvalidSize = Class.new(StandardError) + + retry_policy( + interval: 1, + backoff: 1, + max_attempts: 3, + non_retriable_errors: [InvalidSize]) + + def execute(size) + logger.info "activity: validate size #{size}" + return nil if TShirtSizes.include?(size) + + raise InvalidSize.new("#{size} is not a valid size choice.") + end + end + + class ValidateColorActivity < Temporal::Activity + InvalidColor = Class.new(StandardError) + + retry_policy( + interval: 1, + backoff: 1, + max_attempts: 3, + non_retriable_errors: [InvalidColor]) + + def execute(color) + logger.info "activity: validate color #{color}" + return nil if TShirtColors.include?(color) + + raise InvalidColor.new("#{color} is not a valid color choice.") + end + end + + class ScheduleDeliveryActivity < Temporal::Activity + def execute(order) + delivery_date = Time.now + (2 * 60 * 60 * 24) + logger.info "activity: scheduled delivery for order #{order} at #{delivery_date}" + delivery_date + end + end + + class SendDeliveryEmailActivity < Temporal::Activity + def execute(order, order_id, delivery_date) + logger.info "email to: #{order.email}, order: #{order}, scheduled delivery: #{delivery_date}" + nil + end + end +end diff --git a/examples/synchronous-proxy/configuration.rb b/examples/synchronous-proxy/configuration.rb new file mode 100644 index 00000000..09f179b9 --- /dev/null +++ b/examples/synchronous-proxy/configuration.rb @@ -0,0 +1,17 @@ +require 'bundler' +Bundler.require :default + +require 'temporal' + +Temporal.configure do |config| + config.host = 'localhost' + config.port = 7233 + config.namespace = 'ruby-samples' + config.task_queue = 'ui-driven' +end + +begin + Temporal.register_namespace('ruby-samples', 'A safe space for playing with Temporal Ruby') +rescue Temporal::NamespaceAlreadyExistsFailure + nil # service was already registered +end diff --git a/examples/synchronous-proxy/flow.png b/examples/synchronous-proxy/flow.png new file mode 100644 index 0000000000000000000000000000000000000000..90b9b0f4cdbe8d5a60a4f2d4d1cef46d5bf33b58 GIT binary patch literal 109760 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 q6zZThL=NG<|FYko&UVTBn8 e + STDERR.puts e.message + retry + end + + sequence_no += 1 + begin + color = prompt_and_read_input("Please enter your required tshirt color:") + status = update_order(random_id: random_id, sequence_no: sequence_no, order_id: status.order_id, stage: SynchronousProxy::ColorStage, value: color) + puts "status #{status.inspect}" + rescue SynchronousProxy::ValidateColorActivity::InvalidColor => e + STDERR.puts e.message + retry + end + + puts "Thanks for your order!" + puts "You will receive an email with shipping details shortly" + puts "Exiting at #{Time.now}" + end + + def create_order(random_id, sequence_no) + w_id = "new-tshirt-order-#{random_id}-#{sequence_no}" + workflow_options = {workflow_id: w_id} + Temporal.start_workflow(SynchronousProxy::OrderWorkflow, options: workflow_options) + status = SynchronousProxy::OrderStatus.new + status.order_id = w_id + status + end + + def update_order(random_id:, sequence_no:, order_id:, stage:, value:) + w_id = "update_#{stage}_#{random_id}-#{sequence_no}" + workflow_options = {workflow_id: w_id} + run_id = Temporal.start_workflow(SynchronousProxy::UpdateOrderWorkflow, order_id, stage, value, options: workflow_options) + Temporal.await_workflow_result(SynchronousProxy::UpdateOrderWorkflow, workflow_id: w_id, run_id: run_id) + end + + def prompt_and_read_input(prompt) + print(prompt + " ") + gets.chomp + end + end + end +end + +if $0 == __FILE__ + SynchronousProxy::UI::Main.new.run +end diff --git a/examples/synchronous-proxy/worker/worker.rb b/examples/synchronous-proxy/worker/worker.rb new file mode 100644 index 00000000..6694db37 --- /dev/null +++ b/examples/synchronous-proxy/worker/worker.rb @@ -0,0 +1,15 @@ +require_relative "../configuration" +require_relative "../workflows" +require_relative "../activities" +require 'temporal/worker' + +worker = Temporal::Worker.new +worker.register_workflow(SynchronousProxy::OrderWorkflow) +worker.register_workflow(SynchronousProxy::UpdateOrderWorkflow) +worker.register_workflow(SynchronousProxy::ShippingWorkflow) +worker.register_activity(SynchronousProxy::RegisterEmailActivity) +worker.register_activity(SynchronousProxy::ValidateSizeActivity) +worker.register_activity(SynchronousProxy::ValidateColorActivity) +worker.register_activity(SynchronousProxy::ScheduleDeliveryActivity) +worker.register_activity(SynchronousProxy::SendDeliveryEmailActivity) +worker.start diff --git a/examples/synchronous-proxy/workflows.rb b/examples/synchronous-proxy/workflows.rb new file mode 100644 index 00000000..f122a6e2 --- /dev/null +++ b/examples/synchronous-proxy/workflows.rb @@ -0,0 +1,133 @@ +require_relative "proxy/communications" +require_relative "activities" + +module SynchronousProxy + RegisterStage = "register".freeze + SizeStage = "size".freeze + ColorStage = "color".freeze + ShippingStage = "shipping".freeze + + TShirtSizes = ["small", "medium", "large"] + TShirtColors = ["red", "blue", "black"] + + OrderStatus = Struct.new(:order_id, :stage, keyword_init: true) + TShirtOrder = Struct.new(:email, :size, :color) do + def to_s + "size: #{size}, color: #{color}" + end + end + + class OrderWorkflow < Temporal::Workflow + include Proxy::Communications # defines #receive_request, #receive_response, #send_error_response, #send_request, and #send_response + + timeouts start_to_close: 60 + + def execute + order = TShirtOrder.new + setup_signal_handler + + # Loop until we receive a valid email + loop do + signal_detail = receive_request("email_payload") + source_id, email = signal_detail.calling_workflow_id, signal_detail.value + future = RegisterEmailActivity.execute(email) + + future.failed do |exception| + send_error_response(source_id, exception) + logger.warn "RegisterEmailActivity returned an error, loop back to top" + end + + future.done do + order.email = email + send_response(source_id, SizeStage, "") + end + + future.get + break unless future.failed? + end + + # Loop until we receive a valid size + loop do + signal_detail = receive_request("size_payload") + source_id, size = signal_detail.calling_workflow_id, signal_detail.value + future = ValidateSizeActivity.execute(size) + + future.failed do |exception| + send_error_response(source_id, exception) + logger.warn "ValidateSizeActivity returned an error, loop back to top" + end + + future.done do + order.size = size + logger.info "ValidateSizeActivity succeeded, progress to next stage" + send_response(source_id, ColorStage, "") + end + + future.get # block waiting for response + break unless future.failed? + end + + # Loop until we receive a valid color + loop do + signal_detail = receive_request("color_payload") + source_id, color = signal_detail.calling_workflow_id, signal_detail.value + future = ValidateColorActivity.execute(color) + + future.failed do |exception| + send_error_response(source_id, exception) + logger.warn "ValidateColorActivity returned an error, loop back to top" + end + + future.done do + order.color = color + logger.info "ValidateColorActivity succeeded, progress to next stage" + send_response(source_id, ShippingStage, "") + end + + future.get # block waiting for response + break unless future.failed? + end + + # #execute_workflow! blocks until child workflow exits with a result + workflow.execute_workflow!(SynchronousProxy::ShippingWorkflow, order, workflow.metadata.id) + nil + end + end + + class UpdateOrderWorkflow < Temporal::Workflow + include Proxy::Communications + timeouts start_to_close: 60 + + def execute(order_workflow_id, stage, value) + w_id = workflow.metadata.id + setup_signal_handler + status = OrderStatus.new(order_id: order_workflow_id, stage: stage) + signal_workflow_execution_response = send_request(order_workflow_id, stage, value) + + signal_details = receive_response("#{stage}_stage_payload") + logger.warn "UpdateOrderWorkflow received signal_details #{signal_details.inspect}, error? #{signal_details.error?}" + raise signal_details.value.class, signal_details.value.message if signal_details.error? + + status.stage = signal_details.key # next stage + status + end + end + + class ShippingWorkflow < Temporal::Workflow + timeouts run: 60 + + def execute(order, order_workflow_id) + future = ScheduleDeliveryActivity.execute(order_workflow_id) + + future.failed do |exception| + logger.warn "ShippingWorkflow, ScheduleDelivery failed" + end + + future.done do |delivery_date| + SendDeliveryEmailActivity.execute!(order, order_workflow_id, delivery_date) + end + + future.get + end + end +end From 79c0b3679b414fcedc9f48c73eea832c73c06b36 Mon Sep 17 00:00:00 2001 From: aryak-stripe <97758420+aryak-stripe@users.noreply.github.com> Date: Mon, 28 Mar 2022 16:07:51 -0700 Subject: [PATCH 046/125] Add parent_run_id, parent_id to workflow metadata (#169) --- examples/spec/integration/parent_close_workflow_spec.rb | 2 +- examples/workflows/slow_child_workflow.rb | 2 +- lib/temporal/metadata.rb | 2 ++ lib/temporal/metadata/workflow.rb | 8 ++++++-- lib/temporal/testing/temporal_override.rb | 2 ++ lib/temporal/testing/workflow_override.rb | 2 ++ spec/fabricators/workflow_metadata_fabricator.rb | 2 ++ spec/unit/lib/temporal/metadata/workflow_spec.rb | 2 ++ .../lib/temporal/testing/local_workflow_context_spec.rb | 2 ++ spec/unit/lib/temporal/workflow/executor_spec.rb | 2 ++ 10 files changed, 22 insertions(+), 4 deletions(-) diff --git a/examples/spec/integration/parent_close_workflow_spec.rb b/examples/spec/integration/parent_close_workflow_spec.rb index e307dbd8..44f9348f 100644 --- a/examples/spec/integration/parent_close_workflow_spec.rb +++ b/examples/spec/integration/parent_close_workflow_spec.rb @@ -50,6 +50,6 @@ workflow_id: child_workflow_id, ) - expect(result).to eq('slow child ran') + expect(result).to eq({ parent_workflow_id: workflow_id }) end end diff --git a/examples/workflows/slow_child_workflow.rb b/examples/workflows/slow_child_workflow.rb index 8de8e3cd..332b9a33 100644 --- a/examples/workflows/slow_child_workflow.rb +++ b/examples/workflows/slow_child_workflow.rb @@ -4,6 +4,6 @@ def execute(delay) workflow.sleep(delay) end - return 'slow child ran' + return { parent_workflow_id: workflow.metadata.parent_id } end end diff --git a/lib/temporal/metadata.rb b/lib/temporal/metadata.rb index 4bcfade8..f5649672 100644 --- a/lib/temporal/metadata.rb +++ b/lib/temporal/metadata.rb @@ -47,6 +47,8 @@ def generate_workflow_metadata(event, task_metadata) name: event.attributes.workflow_type.name, id: task_metadata.workflow_id, run_id: event.attributes.original_execution_run_id, + parent_id: event.attributes.parent_workflow_execution&.workflow_id, + parent_run_id: event.attributes.parent_workflow_execution&.run_id, attempt: event.attributes.attempt, namespace: task_metadata.namespace, task_queue: event.attributes.task_queue.name, diff --git a/lib/temporal/metadata/workflow.rb b/lib/temporal/metadata/workflow.rb index f4715dde..e912391d 100644 --- a/lib/temporal/metadata/workflow.rb +++ b/lib/temporal/metadata/workflow.rb @@ -3,13 +3,15 @@ module Temporal module Metadata class Workflow < Base - attr_reader :namespace, :id, :name, :run_id, :attempt, :task_queue, :headers, :run_started_at, :memo + attr_reader :namespace, :id, :name, :run_id, :parent_id, :parent_run_id, :attempt, :task_queue, :headers, :run_started_at, :memo - def initialize(namespace:, id:, name:, run_id:, attempt:, task_queue:, headers:, run_started_at:, memo:) + def initialize(namespace:, id:, name:, run_id:, parent_id:, parent_run_id:, attempt:, task_queue:, headers:, run_started_at:, memo:) @namespace = namespace @id = id @name = name @run_id = run_id + @parent_id = parent_id + @parent_run_id = parent_run_id @attempt = attempt @task_queue = task_queue @headers = headers @@ -29,6 +31,8 @@ def to_h 'workflow_id' => id, 'workflow_name' => name, 'workflow_run_id' => run_id, + 'parent_workflow_id' => parent_id, + 'parent_workflow_run_id' => parent_run_id, 'attempt' => attempt, 'task_queue' => task_queue, 'run_started_at' => run_started_at.to_f, diff --git a/lib/temporal/testing/temporal_override.rb b/lib/temporal/testing/temporal_override.rb index 1e76cce7..1fae6c36 100644 --- a/lib/temporal/testing/temporal_override.rb +++ b/lib/temporal/testing/temporal_override.rb @@ -100,6 +100,8 @@ def start_locally(workflow, schedule, *input, **args) id: workflow_id, name: execution_options.name, run_id: run_id, + parent_id: nil, + parent_run_id: nil, attempt: 1, task_queue: execution_options.task_queue, run_started_at: Time.now, diff --git a/lib/temporal/testing/workflow_override.rb b/lib/temporal/testing/workflow_override.rb index 9d43f953..45e989d4 100644 --- a/lib/temporal/testing/workflow_override.rb +++ b/lib/temporal/testing/workflow_override.rb @@ -32,6 +32,8 @@ def execute_locally(*input) id: workflow_id, name: name, # Workflow class name run_id: run_id, + parent_id: nil, + parent_run_id: nil, attempt: 1, task_queue: 'unit-test-task-queue', headers: {}, diff --git a/spec/fabricators/workflow_metadata_fabricator.rb b/spec/fabricators/workflow_metadata_fabricator.rb index c32fd3e1..3f3a1b7a 100644 --- a/spec/fabricators/workflow_metadata_fabricator.rb +++ b/spec/fabricators/workflow_metadata_fabricator.rb @@ -5,6 +5,8 @@ id { SecureRandom.uuid } name 'TestWorkflow' run_id { SecureRandom.uuid } + parent_id { nil } + parent_run_id { nil } attempt 1 task_queue { Fabricate(:api_task_queue) } run_started_at { Time.now } diff --git a/spec/unit/lib/temporal/metadata/workflow_spec.rb b/spec/unit/lib/temporal/metadata/workflow_spec.rb index be3f50b9..0ad4af48 100644 --- a/spec/unit/lib/temporal/metadata/workflow_spec.rb +++ b/spec/unit/lib/temporal/metadata/workflow_spec.rb @@ -32,6 +32,8 @@ 'attempt' => subject.attempt, 'workflow_name' => subject.name, 'workflow_run_id' => subject.run_id, + 'parent_workflow_id' => nil, + 'parent_workflow_run_id' => nil, 'task_queue' => subject.task_queue, 'run_started_at' => subject.run_started_at.to_f, 'memo' => subject.memo, diff --git a/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb b/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb index a62d866a..ae8eca8c 100644 --- a/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb +++ b/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb @@ -19,6 +19,8 @@ id: workflow_id, name: 'HelloWorldWorkflow', run_id: run_id, + parent_id: nil, + parent_run_id: nil, attempt: 1, task_queue: task_queue, headers: {}, diff --git a/spec/unit/lib/temporal/workflow/executor_spec.rb b/spec/unit/lib/temporal/workflow/executor_spec.rb index a74c3387..bc9828da 100644 --- a/spec/unit/lib/temporal/workflow/executor_spec.rb +++ b/spec/unit/lib/temporal/workflow/executor_spec.rb @@ -70,6 +70,8 @@ def execute id: workflow_metadata.workflow_id, name: event_attributes.workflow_type.name, run_id: event_attributes.original_execution_run_id, + parent_id: nil, + parent_run_id: nil, attempt: event_attributes.attempt, task_queue: event_attributes.task_queue.name, run_started_at: workflow_started_event.event_time.to_time, From 243c6a72af7a4f2066edc3c3d85ff83cad6b992a Mon Sep 17 00:00:00 2001 From: Chuck Remes <37843211+chuckremes2@users.noreply.github.com> Date: Thu, 7 Apr 2022 13:27:29 -0500 Subject: [PATCH 047/125] Named signal handlers (#157) * tests for named signal handlers * support for named signal handlers * improve tests and disallow duplicate registration of named handlers * extra work to make sure we only match non-nil handler_name * change behavior * remove unused constant; update docs; rename method * verify named handler and default are both always called * test for proper dispatcher behavior with named and defaults * correct tests to expect new behavior * change logging so named handler is easier to pick out * fix signal event registration to use special naming and a WILDCARD * restore original simple API * construct proper signal event name before dispatching; fix method signatures * fix unit tests * use 'signaled' as base event and dispatch twice to get named and catch-all * switch to dedicated Signal target --- examples/bin/worker | 1 + .../integration/named_signal_handler_spec.rb | 84 +++++++++++++++++++ .../wait_for_named_signal_workflow.rb | 27 ++++++ lib/temporal/workflow/context.rb | 24 ++++-- lib/temporal/workflow/dispatcher.rb | 26 +++++- lib/temporal/workflow/signal.rb | 5 ++ lib/temporal/workflow/state_manager.rb | 5 +- .../lib/temporal/workflow/dispatcher_spec.rb | 75 ++++++++++++++++- 8 files changed, 234 insertions(+), 13 deletions(-) create mode 100644 examples/spec/integration/named_signal_handler_spec.rb create mode 100644 examples/workflows/wait_for_named_signal_workflow.rb create mode 100644 lib/temporal/workflow/signal.rb diff --git a/examples/bin/worker b/examples/bin/worker index 435e418e..aa56bf0c 100755 --- a/examples/bin/worker +++ b/examples/bin/worker @@ -51,6 +51,7 @@ worker.register_workflow(TripBookingWorkflow) worker.register_workflow(UpsertSearchAttributesWorkflow) worker.register_workflow(WaitForWorkflow) worker.register_workflow(WaitForExternalSignalWorkflow) +worker.register_workflow(WaitForNamedSignalWorkflow) worker.register_activity(AsyncActivity) worker.register_activity(EchoActivity) diff --git a/examples/spec/integration/named_signal_handler_spec.rb b/examples/spec/integration/named_signal_handler_spec.rb new file mode 100644 index 00000000..aa5fc559 --- /dev/null +++ b/examples/spec/integration/named_signal_handler_spec.rb @@ -0,0 +1,84 @@ +require 'workflows/wait_for_named_signal_workflow' + +describe WaitForNamedSignalWorkflow, :integration do + let(:receiver_workflow_id) { SecureRandom.uuid } + + context 'when the signal is named' do + let(:arg1) { "arg1" } + let(:arg2) { 7890.1234 } + + context 'and the workflow has a named signal handler matching the signal name' do + let(:signal_name) { "NamedSignal" } + + it 'receives the signal in its named handler' do + _, run_id = run_workflow(WaitForNamedSignalWorkflow, signal_name, options: { workflow_id: receiver_workflow_id}) + + Temporal.signal_workflow(WaitForNamedSignalWorkflow, signal_name, receiver_workflow_id, run_id, [arg1, arg2]) + + result = Temporal.await_workflow_result( + WaitForNamedSignalWorkflow, + workflow_id: receiver_workflow_id, + run_id: run_id, + ) + + expect(result[:received]).to include({signal_name => [arg1, arg2]}) + expect(result[:counts]).to include({signal_name => 1}) + expect(result).to eq( + { + received: { + signal_name => [arg1, arg2], + 'catch-all' => [arg1, arg2] + }, + counts: { + signal_name => 1, + 'catch-all' => 1 + } + } + ) + + end + + it 'receives the signal in its catch-all signal handler' do + _, run_id = run_workflow(WaitForNamedSignalWorkflow, signal_name, options: { workflow_id: receiver_workflow_id}) + + Temporal.signal_workflow(WaitForNamedSignalWorkflow, signal_name, receiver_workflow_id, run_id, [arg1, arg2]) + + result = Temporal.await_workflow_result( + WaitForNamedSignalWorkflow, + workflow_id: receiver_workflow_id, + run_id: run_id, + ) + + expect(result[:received]).to include({"catch-all" => [arg1, arg2]}) + expect(result[:counts]).to include({"catch-all" => 1}) + end + end + + context 'and the workflow does NOT have a named signal handler matching the signal name' do + let(:signal_name) { 'doesNOTmatchAsignalHandler' } + + it 'receives the signal in its catch-all signal handler' do + _, run_id = run_workflow(WaitForNamedSignalWorkflow, signal_name, options: { workflow_id: receiver_workflow_id}) + + Temporal.signal_workflow(WaitForNamedSignalWorkflow, signal_name, receiver_workflow_id, run_id, [arg1, arg2]) + + result = Temporal.await_workflow_result( + WaitForNamedSignalWorkflow, + workflow_id: receiver_workflow_id, + run_id: run_id, + ) + + expect(result).to eq( + { + received: { + 'catch-all' => [arg1, arg2] + }, + counts: { + 'catch-all' => 1 + } + } + ) + end + end + end +end diff --git a/examples/workflows/wait_for_named_signal_workflow.rb b/examples/workflows/wait_for_named_signal_workflow.rb new file mode 100644 index 00000000..9f715a2a --- /dev/null +++ b/examples/workflows/wait_for_named_signal_workflow.rb @@ -0,0 +1,27 @@ +# Can receive signals to its named signal handler. If a signal doesn't match the +# named handler's signature, then it matches the catch-all signal handler +# +class WaitForNamedSignalWorkflow < Temporal::Workflow + def execute(expected_signal) + signals_received = {} + signal_counts = Hash.new { |h,k| h[k] = 0 } + + # catch-all handler + workflow.on_signal do |signal, input| + workflow.logger.info("Received signal name as #{signal}, with input #{input.inspect}") + signals_received['catch-all'] = input + signal_counts['catch-all'] += 1 + end + + workflow.on_signal('NamedSignal') do |input| + workflow.logger.info("Received signal name -NamedSignal-, with input #{input.inspect}") + signals_received['NamedSignal'] = input + signal_counts['NamedSignal'] += 1 + end + + timeout_timer = workflow.start_timer(1) + workflow.wait_for(timeout_timer) + + { received: signals_received, counts: signal_counts } + end +end diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index ac4454ce..7414a845 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -9,6 +9,7 @@ require 'temporal/workflow/future' require 'temporal/workflow/replay_aware_logger' require 'temporal/workflow/state_manager' +require 'temporal/workflow/signal' # This context class is available in the workflow implementation # and provides context and methods for interacting with Temporal @@ -290,11 +291,24 @@ def now state_manager.local_time end - def on_signal(&block) - target = History::EventTarget.workflow - - dispatcher.register_handler(target, 'signaled') do |signal, input| - call_in_fiber(block, signal, input) + # Define a signal handler to receive signals onto the workflow. When + # +name+ is defined, this creates a named signal handler which will be + # invoked whenever a signal named +name+ is received. A handler without + # a set name (defaults to nil) will be the default handler and will receive + # all signals that do not match a named signal handler. + # + # @param signal_name [String, Symbol, nil] an optional signal name; converted to a String + def on_signal(signal_name=nil, &block) + if signal_name + target = Signal.new(signal_name) + dispatcher.register_handler(target, 'signaled') do |_, input| + # do not pass signal name when triggering a named handler + call_in_fiber(block, input) + end + else + dispatcher.register_handler(Dispatcher::TARGET_WILDCARD, 'signaled') do |signal, input| + call_in_fiber(block, signal, input) + end end end diff --git a/lib/temporal/workflow/dispatcher.rb b/lib/temporal/workflow/dispatcher.rb index 2a768e54..58b32f46 100644 --- a/lib/temporal/workflow/dispatcher.rb +++ b/lib/temporal/workflow/dispatcher.rb @@ -1,15 +1,30 @@ module Temporal class Workflow + # This provides a generic event dispatcher mechanism. There are two main entry + # points to this class, #register_handler and #dispatch. + # + # A handler may be associated with a specific event name so when that event occurs + # elsewhere in the system we may dispatch the event and execute the handler. + # We *always* execute the handler associated with the event_name. + # + # Optionally, we may register a named handler that is triggered when an event _and + # an optional handler_name key_ are provided. In this situation, we dispatch to both + # the handler associated to event_name+handler_name and to the handler associated with + # the event_name. The order of this dispatch is not guaranteed. + # class Dispatcher WILDCARD = '*'.freeze TARGET_WILDCARD = '*'.freeze + EventStruct = Struct.new(:event_name, :handler) + def initialize @handlers = Hash.new { |hash, key| hash[key] = [] } end def register_handler(target, event_name, &handler) - handlers[target] << [event_name, handler] + handlers[target] << EventStruct.new(event_name, handler) + self end def dispatch(target, event_name, args = nil) @@ -25,8 +40,13 @@ def dispatch(target, event_name, args = nil) def handlers_for(target, event_name) handlers[target] .concat(handlers[TARGET_WILDCARD]) - .select { |(name, _)| name == event_name || name == WILDCARD } - .map(&:last) + .select { |event_struct| match?(event_struct, event_name) } + .map(&:handler) + end + + def match?(event_struct, event_name) + event_struct.event_name == event_name || + event_struct.event_name == WILDCARD end end end diff --git a/lib/temporal/workflow/signal.rb b/lib/temporal/workflow/signal.rb new file mode 100644 index 00000000..e31d77fc --- /dev/null +++ b/lib/temporal/workflow/signal.rb @@ -0,0 +1,5 @@ +module Temporal + class Workflow + Signal = Struct.new(:signal_name) + end +end diff --git a/lib/temporal/workflow/state_manager.rb b/lib/temporal/workflow/state_manager.rb index c54def51..cd25dc70 100644 --- a/lib/temporal/workflow/state_manager.rb +++ b/lib/temporal/workflow/state_manager.rb @@ -5,6 +5,7 @@ require 'temporal/workflow/history/event_target' require 'temporal/concerns/payloads' require 'temporal/workflow/errors' +require 'temporal/workflow/signal' module Temporal class Workflow @@ -224,7 +225,9 @@ def apply_event(event) handle_marker(event.id, event.attributes.marker_name, from_details_payloads(event.attributes.details['data'])) when 'WORKFLOW_EXECUTION_SIGNALED' - dispatch(target, 'signaled', event.attributes.signal_name, from_signal_payloads(event.attributes.input)) + # relies on Signal#== for matching in Dispatcher + signal_target = Signal.new(event.attributes.signal_name) + dispatch(signal_target, 'signaled', event.attributes.signal_name, from_signal_payloads(event.attributes.input)) when 'WORKFLOW_EXECUTION_TERMINATED' # todo diff --git a/spec/unit/lib/temporal/workflow/dispatcher_spec.rb b/spec/unit/lib/temporal/workflow/dispatcher_spec.rb index 43ccc8fc..6e1ae8d6 100644 --- a/spec/unit/lib/temporal/workflow/dispatcher_spec.rb +++ b/spec/unit/lib/temporal/workflow/dispatcher_spec.rb @@ -6,12 +6,56 @@ let(:other_target) { Temporal::Workflow::History::EventTarget.new(2, Temporal::Workflow::History::EventTarget::TIMER_TYPE) } describe '#register_handler' do - it 'stores a given handler against the target' do - block = -> { 'handler body' } + let(:block) { -> { 'handler body' } } + let(:event_name) { 'signaled' } + let(:dispatcher) { subject.register_handler(target, event_name, &block) } + let(:handlers) { dispatcher.send(:handlers) } - subject.register_handler(target, 'signaled', &block) + context 'with default handler_name' do + let(:handler_name) { nil } - expect(subject.send(:handlers)).to include(target => [['signaled', block]]) + it 'stores the target' do + expect(handlers.key?(target)).to be true + end + + it 'stores the target and handler once' do + expect(handlers[target]).to be_kind_of(Array) + expect(handlers[target].count).to eq 1 + end + + it 'associates the event name with the target' do + event = handlers[target].first + expect(event.event_name).to eq(event_name) + end + + it 'associates the handler with the target' do + event = handlers[target].first + expect(event.handler).to eq(block) + end + end + + context 'with a specific handler_name' do + let(:handler_name) { 'specific name' } + let(:event_name) { "signaled:#{handler_name}" } + + it 'stores the target' do + expect(handlers.key?(target)).to be true + end + + it 'stores the target and handler once' do + expect(handlers[target]).to be_kind_of(Array) + expect(handlers[target].count).to eq 1 + end + + it 'associates the event name and handler name with the target' do + event = handlers[target].first + expect(event.event_name).to eq(event_name) + end + + it 'associates the handler with the target' do + event = handlers[target].first + expect(event.handler).to eq(block) + end end end @@ -89,5 +133,28 @@ expect(target.eql?(described_class::TARGET_WILDCARD)).to be(false) end end + + context 'with a named handler' do + let(:handler_7) { -> { 'seventh block' } } + let(:handler_name) { 'specific name' } + before do + allow(handler_7).to receive(:call) + + subject.register_handler(target, 'completed', &handler_7) + end + + it 'calls the named handler and the default' do + subject.dispatch(target, 'completed', handler_name: handler_name) + + # the parent context "before" block registers the handlers with the target + # so look there for why handlers 1 and 4 are also called + expect(handler_7).to have_received(:call) + expect(handler_1).to have_received(:call) + expect(handler_4).to have_received(:call) + + expect(handler_2).not_to have_received(:call) + expect(handler_3).not_to have_received(:call) + end + end end end From 93c71021e09982b97ecbd615666176e8b846d52e Mon Sep 17 00:00:00 2001 From: Drew Hoskins <37816070+drewhoskins-stripe@users.noreply.github.com> Date: Tue, 12 Apr 2022 06:51:47 -0700 Subject: [PATCH 048/125] Better error message for NonDeterministicWorkflowError (#171) * Better error message for NonDeterministicWorkflowError * s/code_/replay_/ * Improve error message * Remove dangling ) --- lib/temporal/workflow/state_manager.rb | 77 ++++++++++++++------------ 1 file changed, 42 insertions(+), 35 deletions(-) diff --git a/lib/temporal/workflow/state_manager.rb b/lib/temporal/workflow/state_manager.rb index cd25dc70..92e1ddd9 100644 --- a/lib/temporal/workflow/state_manager.rb +++ b/lib/temporal/workflow/state_manager.rb @@ -119,7 +119,7 @@ def validate_append_command(command) def apply_event(event) state_machine = command_tracker[event.originating_event_id] - target = History::EventTarget.from_event(event) + history_target = History::EventTarget.from_event(event) case event.type when 'WORKFLOW_EXECUTION_STARTED' @@ -157,53 +157,53 @@ def apply_event(event) when 'ACTIVITY_TASK_SCHEDULED' state_machine.schedule - discard_command(target) + discard_command(history_target) when 'ACTIVITY_TASK_STARTED' state_machine.start when 'ACTIVITY_TASK_COMPLETED' state_machine.complete - dispatch(target, 'completed', from_result_payloads(event.attributes.result)) + dispatch(history_target, 'completed', from_result_payloads(event.attributes.result)) when 'ACTIVITY_TASK_FAILED' state_machine.fail - dispatch(target, 'failed', Temporal::Workflow::Errors.generate_error(event.attributes.failure, ActivityException)) + dispatch(history_target, 'failed', Temporal::Workflow::Errors.generate_error(event.attributes.failure, ActivityException)) when 'ACTIVITY_TASK_TIMED_OUT' state_machine.time_out - dispatch(target, 'failed', Temporal::Workflow::Errors.generate_error(event.attributes.failure)) + dispatch(history_target, 'failed', Temporal::Workflow::Errors.generate_error(event.attributes.failure)) when 'ACTIVITY_TASK_CANCEL_REQUESTED' state_machine.requested - discard_command(target) + discard_command(history_target) when 'REQUEST_CANCEL_ACTIVITY_TASK_FAILED' state_machine.fail - discard_command(target) - dispatch(target, 'failed', event.attributes.cause, nil) + discard_command(history_target) + dispatch(history_target, 'failed', event.attributes.cause, nil) when 'ACTIVITY_TASK_CANCELED' state_machine.cancel - dispatch(target, 'failed', Temporal::ActivityCanceled.new(from_details_payloads(event.attributes.details))) + dispatch(history_target, 'failed', Temporal::ActivityCanceled.new(from_details_payloads(event.attributes.details))) when 'TIMER_STARTED' state_machine.start - discard_command(target) + discard_command(history_target) when 'TIMER_FIRED' state_machine.complete - dispatch(target, 'fired') + dispatch(history_target, 'fired') when 'CANCEL_TIMER_FAILED' state_machine.failed - discard_command(target) - dispatch(target, 'failed', event.attributes.cause, nil) + discard_command(history_target) + dispatch(history_target, 'failed', event.attributes.cause, nil) when 'TIMER_CANCELED' state_machine.cancel - discard_command(target) - dispatch(target, 'canceled') + discard_command(history_target) + dispatch(history_target, 'canceled') when 'WORKFLOW_EXECUTION_CANCEL_REQUESTED' # todo @@ -237,31 +237,31 @@ def apply_event(event) when 'START_CHILD_WORKFLOW_EXECUTION_INITIATED' state_machine.schedule - discard_command(target) + discard_command(history_target) when 'START_CHILD_WORKFLOW_EXECUTION_FAILED' state_machine.fail - dispatch(target, 'failed', 'StandardError', from_payloads(event.attributes.cause)) + dispatch(history_target, 'failed', 'StandardError', from_payloads(event.attributes.cause)) when 'CHILD_WORKFLOW_EXECUTION_STARTED' - dispatch(target, 'started') + dispatch(history_target, 'started') state_machine.start when 'CHILD_WORKFLOW_EXECUTION_COMPLETED' state_machine.complete - dispatch(target, 'completed', from_result_payloads(event.attributes.result)) + dispatch(history_target, 'completed', from_result_payloads(event.attributes.result)) when 'CHILD_WORKFLOW_EXECUTION_FAILED' state_machine.fail - dispatch(target, 'failed', Temporal::Workflow::Errors.generate_error(event.attributes.failure)) + dispatch(history_target, 'failed', Temporal::Workflow::Errors.generate_error(event.attributes.failure)) when 'CHILD_WORKFLOW_EXECUTION_CANCELED' state_machine.cancel - dispatch(target, 'failed', Temporal::Workflow::Errors.generate_error(event.attributes.failure)) + dispatch(history_target, 'failed', Temporal::Workflow::Errors.generate_error(event.attributes.failure)) when 'CHILD_WORKFLOW_EXECUTION_TIMED_OUT' state_machine.time_out - dispatch(target, 'failed', Temporal::Workflow::Errors.generate_error(event.attributes.failure)) + dispatch(history_target, 'failed', Temporal::Workflow::Errors.generate_error(event.attributes.failure)) when 'CHILD_WORKFLOW_EXECUTION_TERMINATED' # todo @@ -272,23 +272,23 @@ def apply_event(event) # The workflow that sends the signal creates this event in its log; the # receiving workflow records WORKFLOW_EXECUTION_SIGNALED on reception state_machine.start - discard_command(target) + discard_command(history_target) when 'SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_FAILED' # Temporal Server cannot Signal the targeted Workflow # Usually because the Workflow could not be found state_machine.fail - dispatch(target, 'failed', 'StandardError', event.attributes.cause) + dispatch(history_target, 'failed', 'StandardError', event.attributes.cause) when 'EXTERNAL_WORKFLOW_EXECUTION_SIGNALED' # Temporal Server has successfully Signaled the targeted Workflow # Return the result to the Future waiting on this state_machine.complete - dispatch(target, 'completed') + dispatch(history_target, 'completed') when 'UPSERT_WORKFLOW_SEARCH_ATTRIBUTES' # no need to track state; this is just a synchronous API call. - discard_command(target) + discard_command(history_target) else raise UnsupportedEvent, event.type @@ -321,21 +321,28 @@ def event_target_from(command_id, command) History::EventTarget.new(command_id, target_type) end - def dispatch(target, name, *attributes) - dispatcher.dispatch(target, name, attributes) + def dispatch(history_target, name, *attributes) + dispatcher.dispatch(history_target, name, attributes) end - def discard_command(target) + NONDETERMINISM_ERROR_SUGGESTION = + 'Likely, either you have made a version-unsafe change to your workflow or have non-deterministic '\ + 'behavior in your workflow. See https://docs.temporal.io/docs/java/versioning/#introduction-to-versioning.'.freeze + + def discard_command(history_target) # Pop the first command from the list, it is expected to match - existing_command_id, existing_command = commands.shift + replay_command_id, replay_command = commands.shift - if !existing_command_id - raise NonDeterministicWorkflowError, "A command #{target} was not scheduled upon replay" + if !replay_command_id + raise NonDeterministicWorkflowError, + "A command in the history of previous executions, #{history_target}, was not scheduled upon replay. " + NONDETERMINISM_ERROR_SUGGESTION end - existing_target = event_target_from(existing_command_id, existing_command) - if target != existing_target - raise NonDeterministicWorkflowError, "Unexpected command #{existing_target} (expected #{target})" + replay_target = event_target_from(replay_command_id, replay_command) + if history_target != replay_target + raise NonDeterministicWorkflowError, + "Unexpected command. The replaying code is issuing: #{replay_target}, "\ + "but the history of previous executions recorded: #{history_target}. " + NONDETERMINISM_ERROR_SUGGESTION end end From 398fd24b0c99dc9b59076d82e38f14f27d1279e8 Mon Sep 17 00:00:00 2001 From: Dave Willett Date: Fri, 15 Apr 2022 08:34:14 -0700 Subject: [PATCH 049/125] Support for invoking and processing queries (#141) * Support for invoking and processing queries, WIP * Catch-all query handler support, feedback changes Made a handful of changes on approach from the initial spike. This is operating under an assumption that the added EventTarget type for query is a valid approach * Fixes for on_query interface, clean up workflow and spec * Fix method signature on testing context * Move catch-all handler back to block Also adding a second targeted query handler to spec * Use nil workflow class in test case * Updates to remove catch all handling, add query reject handling * More concise when no status returned from server * More consistent raise message style * Add test for reject condition not met * Simplify legacy handling and use serializers for query protos * Add specs for the new changes * Test query result & freeze them * Implement QueryRegistry * Swap Context#query_handlers with QueryRegistry * Add a spec for Workflow::Context * Rename QueryFailedFailure error to QueryFailed * Small cleanup items * Update readme Co-authored-by: antstorm --- README.md | 2 +- examples/bin/query | 14 ++ examples/bin/worker | 1 + .../spec/integration/query_workflow_spec.rb | 54 +++++ examples/workflows/query_workflow.rb | 36 ++++ lib/temporal.rb | 5 +- lib/temporal/client.rb | 25 ++- lib/temporal/concerns/payloads.rb | 8 + lib/temporal/connection/grpc.rb | 68 ++++++- lib/temporal/connection/serializer.rb | 5 + .../connection/serializer/query_answer.rb | 19 ++ .../connection/serializer/query_failure.rb | 16 ++ lib/temporal/errors.rb | 2 +- lib/temporal/metadata.rb | 3 +- .../testing/local_workflow_context.rb | 6 +- lib/temporal/workflow/context.rb | 13 +- lib/temporal/workflow/executor.rb | 26 ++- lib/temporal/workflow/query_registry.rb | 33 +++ lib/temporal/workflow/query_result.rb | 16 ++ lib/temporal/workflow/task_processor.rb | 59 +++++- rbi/temporal-ruby.rbi | 2 +- .../grpc/workflow_query_fabricator.rb | 4 + .../grpc/workflow_task_fabricator.rb | 1 + .../serializer/query_answer_spec.rb | 23 +++ .../serializer/query_failure_spec.rb | 19 ++ .../{grpc_client_spec.rb => grpc_spec.rb} | 120 ++++++++++- .../temporal/metadata/workflow_task_spec.rb | 2 +- .../lib/temporal/workflow/context_spec.rb | 28 ++- .../lib/temporal/workflow/executor_spec.rb | 36 +++- .../temporal/workflow/query_registry_spec.rb | 67 +++++++ .../temporal/workflow/query_result_spec.rb | 25 +++ .../temporal/workflow/task_processor_spec.rb | 189 ++++++++++++++---- 32 files changed, 848 insertions(+), 79 deletions(-) create mode 100755 examples/bin/query create mode 100644 examples/spec/integration/query_workflow_spec.rb create mode 100644 examples/workflows/query_workflow.rb create mode 100644 lib/temporal/connection/serializer/query_answer.rb create mode 100644 lib/temporal/connection/serializer/query_failure.rb create mode 100644 lib/temporal/workflow/query_registry.rb create mode 100644 lib/temporal/workflow/query_result.rb create mode 100644 spec/fabricators/grpc/workflow_query_fabricator.rb create mode 100644 spec/unit/lib/temporal/connection/serializer/query_answer_spec.rb create mode 100644 spec/unit/lib/temporal/connection/serializer/query_failure_spec.rb rename spec/unit/lib/temporal/{grpc_client_spec.rb => grpc_spec.rb} (75%) create mode 100644 spec/unit/lib/temporal/workflow/query_registry_spec.rb create mode 100644 spec/unit/lib/temporal/workflow/query_result_spec.rb diff --git a/README.md b/README.md index 13e6ef56..838fda7a 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ Besides calling activities workflows can: - Use timers - Receive signals - Execute other (child) workflows -- Respond to queries [not yet implemented] +- Respond to queries ## Activities diff --git a/examples/bin/query b/examples/bin/query new file mode 100755 index 00000000..c0e7f719 --- /dev/null +++ b/examples/bin/query @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby +require_relative '../init' + +Dir[File.expand_path('../workflows/*.rb', __dir__)].each { |f| require f } + +workflow_class_name, workflow_id, run_id, query, args = ARGV +workflow_class = Object.const_get(workflow_class_name) + +if ![workflow_class, workflow_id, run_id, query].all? + fail 'Wrong arguments, use `bin/query WORKFLOW WORKFLOW_ID RUN_ID QUERY [ARGS]`' +end + +result = Temporal.query_workflow(workflow_class, query, workflow_id, run_id, args) +puts result.inspect diff --git a/examples/bin/worker b/examples/bin/worker index aa56bf0c..c9c78145 100755 --- a/examples/bin/worker +++ b/examples/bin/worker @@ -36,6 +36,7 @@ worker.register_workflow(MetadataWorkflow) worker.register_workflow(ParentCloseWorkflow) worker.register_workflow(ParentWorkflow) worker.register_workflow(ProcessFileWorkflow) +worker.register_workflow(QueryWorkflow) worker.register_workflow(QuickTimeoutWorkflow) worker.register_workflow(RandomlyFailingWorkflow) worker.register_workflow(ReleaseWorkflow) diff --git a/examples/spec/integration/query_workflow_spec.rb b/examples/spec/integration/query_workflow_spec.rb new file mode 100644 index 00000000..93812635 --- /dev/null +++ b/examples/spec/integration/query_workflow_spec.rb @@ -0,0 +1,54 @@ +require 'workflows/query_workflow' +require 'temporal/errors' + +describe QueryWorkflow, :integration do + subject { described_class } + + it 'returns the correct result for the queries' do + workflow_id, run_id = run_workflow(described_class) + + # Query with nil workflow class + expect(Temporal.query_workflow(nil, 'state', workflow_id, run_id)) + .to eq 'started' + + # Query with arbitrary args + expect(Temporal.query_workflow(described_class, 'state', workflow_id, run_id, + 'upcase', 'ignored', 'reverse')) + .to eq 'DETRATS' + + # Query with no args + expect(Temporal.query_workflow(described_class, 'signal_count', workflow_id, run_id)) + .to eq 0 + + # Query with unregistered handler + expect { Temporal.query_workflow(described_class, 'unknown_query', workflow_id, run_id) } + .to raise_error(Temporal::QueryFailed, 'Workflow did not register a handler for unknown_query') + + Temporal.signal_workflow(described_class, 'make_progress', workflow_id, run_id) + + # Query for updated signal_count with an unsatisfied reject condition + expect(Temporal.query_workflow(described_class, 'signal_count', workflow_id, run_id, query_reject_condition: :not_open)) + .to eq 1 + + Temporal.signal_workflow(described_class, 'finish', workflow_id, run_id) + wait_for_workflow_completion(workflow_id, run_id) + + # Repeating original query scenarios above, expecting updated state and signal results + expect(Temporal.query_workflow(nil, 'state', workflow_id, run_id)) + .to eq 'finished' + + expect(Temporal.query_workflow(described_class, 'state', workflow_id, run_id, + 'upcase', 'ignored', 'reverse')) + .to eq 'DEHSINIF' + + expect(Temporal.query_workflow(described_class, 'signal_count', workflow_id, run_id)) + .to eq 2 + + expect { Temporal.query_workflow(described_class, 'unknown_query', workflow_id, run_id) } + .to raise_error(Temporal::QueryFailed, 'Workflow did not register a handler for unknown_query') + + # Now that the workflow is completed, test a query with a reject condition satisfied + expect { Temporal.query_workflow(described_class, 'state', workflow_id, run_id, query_reject_condition: :not_open) } + .to raise_error(Temporal::QueryFailed, 'Query rejected: status WORKFLOW_EXECUTION_STATUS_COMPLETED') + end +end diff --git a/examples/workflows/query_workflow.rb b/examples/workflows/query_workflow.rb new file mode 100644 index 00000000..47650ca4 --- /dev/null +++ b/examples/workflows/query_workflow.rb @@ -0,0 +1,36 @@ +class QueryWorkflow < Temporal::Workflow + attr_reader :state, :signal_count, :last_signal_received + + def execute + @state = "started" + @signal_count = 0 + @last_signal_received = nil + + workflow.on_query("state") { |*args| apply_transforms(state, args) } + workflow.on_query("signal_count") { signal_count } + + workflow.on_signal do |signal| + @signal_count += 1 + @last_signal_received = signal + end + + workflow.wait_for { last_signal_received == "finish" } + @state = "finished" + + { + signal_count: signal_count, + last_signal_received: last_signal_received, + final_state: state + } + end + + private + + def apply_transforms(value, transforms) + return value if value.nil? || transforms.empty? + transforms.inject(value) do |memo, input| + next memo unless memo.respond_to?(input) + memo.public_send(input) + end + end +end diff --git a/lib/temporal.rb b/lib/temporal.rb index 0b95a882..7f62be78 100644 --- a/lib/temporal.rb +++ b/lib/temporal.rb @@ -20,6 +20,7 @@ module Temporal :describe_namespace, :list_namespaces, :signal_workflow, + :query_workflow, :await_workflow_result, :reset_workflow, :terminate_workflow, @@ -48,11 +49,11 @@ def metrics end private - + def default_client @default_client ||= Client.new(config) end - + def config @config ||= Configuration.new end diff --git a/lib/temporal/client.rb b/lib/temporal/client.rb index c1e9a2da..6aede5a5 100644 --- a/lib/temporal/client.rb +++ b/lib/temporal/client.rb @@ -176,6 +176,29 @@ def signal_workflow(workflow, signal, workflow_id, run_id, input = nil, namespac ) end + # Issue a query against a running workflow + # + # @param workflow [Temporal::Workflow, nil] workflow class or nil + # @param query [String] name of the query to issue + # @param workflow_id [String] + # @param run_id [String] + # @param args [String, Array, nil] optional arguments for the query + # @param namespace [String, nil] if nil, choose the one declared on the workflow class or the + # global default + # @param query_reject_condition [Symbol] check Temporal::Connection::GRPC::QUERY_REJECT_CONDITION + def query_workflow(workflow, query, workflow_id, run_id, *args, namespace: nil, query_reject_condition: nil) + execution_options = ExecutionOptions.new(workflow, {}, config.default_execution_options) + + connection.query_workflow( + namespace: namespace || execution_options.namespace, + workflow_id: workflow_id, + run_id: run_id, + query: query, + args: args, + query_reject_condition: query_reject_condition + ) + end + # Long polls for a workflow to be completed and returns workflow's return value. # # @note This function times out after 30 seconds and throws Temporal::TimeoutError, @@ -207,7 +230,7 @@ def await_workflow_result(workflow, workflow_id:, run_id: nil, timeout: nil, nam timeout: timeout || max_timeout, ) rescue GRPC::DeadlineExceeded => e - message = if timeout + message = if timeout "Timed out after your specified limit of timeout: #{timeout} seconds" else "Timed out after #{max_timeout} seconds, which is the maximum supported amount." diff --git a/lib/temporal/concerns/payloads.rb b/lib/temporal/concerns/payloads.rb index 3be276d2..ad703542 100644 --- a/lib/temporal/concerns/payloads.rb +++ b/lib/temporal/concerns/payloads.rb @@ -21,6 +21,10 @@ def from_signal_payloads(payloads) from_payloads(payloads)&.first end + def from_query_payloads(payloads) + from_payloads(payloads)&.first + end + def from_payload_map(payload_map) payload_map.map { |key, value| [key, from_payload(value)] }.to_h end @@ -45,6 +49,10 @@ def to_signal_payloads(data) to_payloads([data]) end + def to_query_payloads(data) + to_payloads([data]) + end + def to_payload_map(data) data.transform_values(&method(:to_payload)) end diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index 9e27a10e..747c13a8 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -26,6 +26,12 @@ class GRPC close: Temporal::Api::Enums::V1::HistoryEventFilterType::HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT, }.freeze + QUERY_REJECT_CONDITION = { + none: Temporal::Api::Enums::V1::QueryRejectCondition::QUERY_REJECT_CONDITION_NONE, + not_open: Temporal::Api::Enums::V1::QueryRejectCondition::QUERY_REJECT_CONDITION_NOT_OPEN, + not_completed_cleanly: Temporal::Api::Enums::V1::QueryRejectCondition::QUERY_REJECT_CONDITION_NOT_COMPLETED_CLEANLY + }.freeze + DEFAULT_OPTIONS = { max_page_size: 100 }.freeze @@ -142,7 +148,7 @@ def get_workflow_execution_history( event_type: :all, timeout: nil ) - if wait_for_new_event + if wait_for_new_event if timeout.nil? # This is an internal error. Wrappers should enforce this. raise "You must specify a timeout when wait_for_new_event = true." @@ -183,13 +189,28 @@ def poll_workflow_task_queue(namespace:, task_queue:) poll_request.execute end - def respond_workflow_task_completed(namespace:, task_token:, commands:) + def respond_query_task_completed(namespace:, task_token:, query_result:) + query_result_proto = Serializer.serialize(query_result) + request = Temporal::Api::WorkflowService::V1::RespondQueryTaskCompletedRequest.new( + task_token: task_token, + namespace: namespace, + completed_type: query_result_proto.result_type, + query_result: query_result_proto.answer, + error_message: query_result_proto.error_message, + ) + + client.respond_query_task_completed(request) + end + + def respond_workflow_task_completed(namespace:, task_token:, commands:, query_results: {}) request = Temporal::Api::WorkflowService::V1::RespondWorkflowTaskCompletedRequest.new( namespace: namespace, identity: identity, task_token: task_token, - commands: Array(commands).map { |(_, command)| Serializer.serialize(command) } + commands: Array(commands).map { |(_, command)| Serializer.serialize(command) }, + query_results: query_results.transform_values { |value| Serializer.serialize(value) } ) + client.respond_workflow_task_completed(request) end @@ -452,16 +473,43 @@ def get_search_attributes raise NotImplementedError end - def respond_query_task_completed - raise NotImplementedError - end - def reset_sticky_task_queue raise NotImplementedError end - def query_workflow - raise NotImplementedError + def query_workflow(namespace:, workflow_id:, run_id:, query:, args: nil, query_reject_condition: nil) + request = Temporal::Api::WorkflowService::V1::QueryWorkflowRequest.new( + namespace: namespace, + execution: Temporal::Api::Common::V1::WorkflowExecution.new( + workflow_id: workflow_id, + run_id: run_id + ), + query: Temporal::Api::Query::V1::WorkflowQuery.new( + query_type: query, + query_args: to_query_payloads(args) + ) + ) + if query_reject_condition + condition = QUERY_REJECT_CONDITION[query_reject_condition] + raise Client::ArgumentError, 'Unknown query_reject_condition specified' unless condition + + request.query_reject_condition = condition + end + + begin + response = client.query_workflow(request) + rescue ::GRPC::InvalidArgument => e + raise Temporal::QueryFailed, e.details + end + + if response.query_rejected + rejection_status = response.query_rejected.status || 'not specified by server' + raise Temporal::QueryFailed, "Query rejected: status #{rejection_status}" + elsif !response.query_result + raise Temporal::QueryFailed, 'Invalid response from server' + else + from_query_payloads(response.query_result) + end end def describe_workflow_execution(namespace:, workflow_id:, run_id:) @@ -534,7 +582,7 @@ def serialize_status_filter(value) sym = Temporal::Workflow::Status::API_STATUS_MAP.invert[value] status = Temporal::Api::Enums::V1::WorkflowExecutionStatus.resolve(sym) - + Temporal::Api::Filter::V1::StatusFilter.new(status: status) end end diff --git a/lib/temporal/connection/serializer.rb b/lib/temporal/connection/serializer.rb index 6343cb01..46070c66 100644 --- a/lib/temporal/connection/serializer.rb +++ b/lib/temporal/connection/serializer.rb @@ -1,4 +1,5 @@ require 'temporal/workflow/command' +require 'temporal/workflow/query_result' require 'temporal/connection/serializer/schedule_activity' require 'temporal/connection/serializer/start_child_workflow' require 'temporal/connection/serializer/request_activity_cancellation' @@ -10,6 +11,8 @@ require 'temporal/connection/serializer/fail_workflow' require 'temporal/connection/serializer/signal_external_workflow' require 'temporal/connection/serializer/upsert_search_attributes' +require 'temporal/connection/serializer/query_answer' +require 'temporal/connection/serializer/query_failure' module Temporal module Connection @@ -26,6 +29,8 @@ module Serializer Workflow::Command::FailWorkflow => Serializer::FailWorkflow, Workflow::Command::SignalExternalWorkflow => Serializer::SignalExternalWorkflow, Workflow::Command::UpsertSearchAttributes => Serializer::UpsertSearchAttributes, + Workflow::QueryResult::Answer => Serializer::QueryAnswer, + Workflow::QueryResult::Failure => Serializer::QueryFailure, }.freeze def self.serialize(object) diff --git a/lib/temporal/connection/serializer/query_answer.rb b/lib/temporal/connection/serializer/query_answer.rb new file mode 100644 index 00000000..7f28ec06 --- /dev/null +++ b/lib/temporal/connection/serializer/query_answer.rb @@ -0,0 +1,19 @@ +require 'temporal/connection/serializer/base' +require 'temporal/concerns/payloads' + +module Temporal + module Connection + module Serializer + class QueryAnswer < Base + include Concerns::Payloads + + def to_proto + Temporal::Api::Query::V1::WorkflowQueryResult.new( + result_type: Temporal::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_ANSWERED, + answer: to_query_payloads(object.result) + ) + end + end + end + end +end diff --git a/lib/temporal/connection/serializer/query_failure.rb b/lib/temporal/connection/serializer/query_failure.rb new file mode 100644 index 00000000..0a2fca21 --- /dev/null +++ b/lib/temporal/connection/serializer/query_failure.rb @@ -0,0 +1,16 @@ +require 'temporal/connection/serializer/base' + +module Temporal + module Connection + module Serializer + class QueryFailure < Base + def to_proto + Temporal::Api::Query::V1::WorkflowQueryResult.new( + result_type: Temporal::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_FAILED, + error_message: object.error.message + ) + end + end + end + end +end diff --git a/lib/temporal/errors.rb b/lib/temporal/errors.rb index 5047cc90..6d49bef2 100644 --- a/lib/temporal/errors.rb +++ b/lib/temporal/errors.rb @@ -67,7 +67,7 @@ class ClientVersionNotSupportedFailure < ApiError; end class FeatureVersionNotSupportedFailure < ApiError; end class NamespaceAlreadyExistsFailure < ApiError; end class CancellationAlreadyRequestedFailure < ApiError; end - class QueryFailedFailure < ApiError; end + class QueryFailed < ApiError; end class UnexpectedResponse < ApiError; end end diff --git a/lib/temporal/metadata.rb b/lib/temporal/metadata.rb index f5649672..5cfb7a00 100644 --- a/lib/temporal/metadata.rb +++ b/lib/temporal/metadata.rb @@ -27,6 +27,8 @@ def generate_activity_metadata(task, namespace) ) end + # @param task [Temporal::Api::WorkflowService::V1::PollWorkflowTaskQueueResponse] + # @param namespace [String] def generate_workflow_task_metadata(task, namespace) Metadata::WorkflowTask.new( namespace: namespace, @@ -40,7 +42,6 @@ def generate_workflow_task_metadata(task, namespace) end # @param event [Temporal::Workflow::History::Event] Workflow started history event - # @param event [WorkflowExecutionStartedEventAttributes] :attributes # @param task_metadata [Temporal::Metadata::WorkflowTask] workflow task metadata def generate_workflow_metadata(event, task_metadata) Metadata::Workflow.new( diff --git a/lib/temporal/testing/local_workflow_context.rb b/lib/temporal/testing/local_workflow_context.rb index a115eb26..1f200e84 100644 --- a/lib/temporal/testing/local_workflow_context.rb +++ b/lib/temporal/testing/local_workflow_context.rb @@ -186,10 +186,14 @@ def now Time.now end - def on_signal(&block) + def on_signal(signal_name = nil, &block) raise NotImplementedError, 'Signals are not available when Temporal::Testing.local! is on' end + def on_query(query, &block) + raise NotImplementedError, 'Queries are not available when Temporal::Testing.local! is on' + end + def cancel_activity(activity_id) raise NotImplementedError, 'Cancel is not available when Temporal::Testing.local! is on' end diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index 7414a845..74722f00 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -19,9 +19,10 @@ class Workflow class Context attr_reader :metadata, :config - def initialize(state_manager, dispatcher, workflow_class, metadata, config) + def initialize(state_manager, dispatcher, workflow_class, metadata, config, query_registry) @state_manager = state_manager @dispatcher = dispatcher + @query_registry = query_registry @workflow_class = workflow_class @metadata = metadata @completed = false @@ -298,7 +299,7 @@ def now # all signals that do not match a named signal handler. # # @param signal_name [String, Symbol, nil] an optional signal name; converted to a String - def on_signal(signal_name=nil, &block) + def on_signal(signal_name = nil, &block) if signal_name target = Signal.new(signal_name) dispatcher.register_handler(target, 'signaled') do |_, input| @@ -312,6 +313,10 @@ def on_signal(signal_name=nil, &block) end end + def on_query(query, &block) + query_registry.register(query, &block) + end + def cancel_activity(activity_id) command = Command::RequestActivityCancellation.new(activity_id: activity_id) @@ -344,8 +349,6 @@ def cancel(target, cancelation_id) # # @return [Future] future def signal_external_workflow(workflow, signal, workflow_id, run_id = nil, input = nil, namespace: nil, child_workflow_only: false) - options ||= {} - execution_options = ExecutionOptions.new(workflow, {}, config.default_execution_options) command = Command::SignalExternalWorkflow.new( @@ -398,7 +401,7 @@ def upsert_search_attributes(search_attributes) private - attr_reader :state_manager, :dispatcher, :workflow_class + attr_reader :state_manager, :dispatcher, :workflow_class, :query_registry def completed! @completed = true diff --git a/lib/temporal/workflow/executor.rb b/lib/temporal/workflow/executor.rb index 78feb61b..546fe7f6 100644 --- a/lib/temporal/workflow/executor.rb +++ b/lib/temporal/workflow/executor.rb @@ -1,6 +1,7 @@ require 'fiber' require 'temporal/workflow/dispatcher' +require 'temporal/workflow/query_registry' require 'temporal/workflow/state_manager' require 'temporal/workflow/context' require 'temporal/workflow/history/event_target' @@ -16,6 +17,7 @@ class Executor def initialize(workflow_class, history, task_metadata, config) @workflow_class = workflow_class @dispatcher = Dispatcher.new + @query_registry = QueryRegistry.new @state_manager = StateManager.new(dispatcher) @history = history @task_metadata = task_metadata @@ -36,13 +38,33 @@ def run return state_manager.commands end + # Process queries using the pre-registered query handlers + # + # @note this method is expected to be executed after the history has + # been fully replayed (by invoking the #run method) + # + # @param queries [Hash] + # + # @return [Hash] + def process_queries(queries = {}) + queries.transform_values(&method(:process_query)) + end + private - attr_reader :workflow_class, :dispatcher, :state_manager, :task_metadata, :history, :config + attr_reader :workflow_class, :dispatcher, :query_registry, :state_manager, :task_metadata, :history, :config + + def process_query(query) + result = query_registry.handle(query.query_type, query.query_args) + + QueryResult.answer(result) + rescue StandardError => error + QueryResult.failure(error) + end def execute_workflow(input, workflow_started_event) metadata = Metadata.generate_workflow_metadata(workflow_started_event, task_metadata) - context = Workflow::Context.new(state_manager, dispatcher, workflow_class, metadata, config) + context = Workflow::Context.new(state_manager, dispatcher, workflow_class, metadata, config, query_registry) Fiber.new do workflow_class.execute_in_context(context, input) diff --git a/lib/temporal/workflow/query_registry.rb b/lib/temporal/workflow/query_registry.rb new file mode 100644 index 00000000..babdda66 --- /dev/null +++ b/lib/temporal/workflow/query_registry.rb @@ -0,0 +1,33 @@ +require 'temporal/errors' + +module Temporal + class Workflow + class QueryRegistry + def initialize + @handlers = {} + end + + def register(type, &handler) + if handlers.key?(type) + warn "[NOTICE] Overwriting a query handler for #{type}" + end + + handlers[type] = handler + end + + def handle(type, args = nil) + handler = handlers[type] + + unless handler + raise Temporal::QueryFailed, "Workflow did not register a handler for #{type}" + end + + handler.call(*args) + end + + private + + attr_reader :handlers + end + end +end diff --git a/lib/temporal/workflow/query_result.rb b/lib/temporal/workflow/query_result.rb new file mode 100644 index 00000000..a4d0401e --- /dev/null +++ b/lib/temporal/workflow/query_result.rb @@ -0,0 +1,16 @@ +module Temporal + class Workflow + module QueryResult + Answer = Struct.new(:result) + Failure = Struct.new(:error) + + def self.answer(result) + Answer.new(result).freeze + end + + def self.failure(error) + Failure.new(error).freeze + end + end + end +end diff --git a/lib/temporal/workflow/task_processor.rb b/lib/temporal/workflow/task_processor.rb index ce271e4e..4b80918a 100644 --- a/lib/temporal/workflow/task_processor.rb +++ b/lib/temporal/workflow/task_processor.rb @@ -7,7 +7,20 @@ module Temporal class Workflow class TaskProcessor + Query = Struct.new(:query) do + include Concerns::Payloads + + def query_type + query.query_type + end + + def query_args + from_query_payloads(query.query_args) + end + end + MAX_FAILED_ATTEMPTS = 1 + LEGACY_QUERY_KEY = :legacy_query def initialize(task, namespace, workflow_lookup, middleware_chain, config) @task = task @@ -38,7 +51,13 @@ def process executor.run end - complete_task(commands) + query_results = executor.process_queries(parse_queries) + + if legacy_query_task? + complete_query(query_results[LEGACY_QUERY_KEY]) + else + complete_task(commands, query_results) + end rescue StandardError => error Temporal::ErrorHandler.handle(error, config, metadata: metadata) @@ -87,10 +106,44 @@ def fetch_full_history Workflow::History.new(events) end - def complete_task(commands) + def legacy_query_task? + !!task.query + end + + def parse_queries + # Support for deprecated query style + if legacy_query_task? + { LEGACY_QUERY_KEY => Query.new(task.query) } + else + task.queries.each_with_object({}) do |(query_id, query), result| + result[query_id] = Query.new(query) + end + end + end + + def complete_task(commands, query_results) Temporal.logger.info("Workflow task completed", metadata.to_h) - connection.respond_workflow_task_completed(namespace: namespace, task_token: task_token, commands: commands) + connection.respond_workflow_task_completed( + namespace: namespace, + task_token: task_token, + commands: commands, + query_results: query_results + ) + end + + def complete_query(result) + Temporal.logger.info("Workflow Query task completed", metadata.to_h) + + connection.respond_query_task_completed( + namespace: namespace, + task_token: task_token, + query_result: result + ) + rescue StandardError => error + Temporal.logger.error("Unable to complete a query", metadata.to_h.merge(error: error.inspect)) + + Temporal::ErrorHandler.handle(error, config, metadata: metadata) end def fail_task(error) diff --git a/rbi/temporal-ruby.rbi b/rbi/temporal-ruby.rbi index b7da9d12..cdcda078 100644 --- a/rbi/temporal-ruby.rbi +++ b/rbi/temporal-ruby.rbi @@ -39,5 +39,5 @@ module Temporal class FeatureVersionNotSupportedFailure; end class NamespaceAlreadyExistsFailure; end class CancellationAlreadyRequestedFailure; end - class QueryFailedFailure; end + class QueryFailed; end end diff --git a/spec/fabricators/grpc/workflow_query_fabricator.rb b/spec/fabricators/grpc/workflow_query_fabricator.rb new file mode 100644 index 00000000..dcabbb24 --- /dev/null +++ b/spec/fabricators/grpc/workflow_query_fabricator.rb @@ -0,0 +1,4 @@ +Fabricator(:api_workflow_query, from: Temporal::Api::Query::V1::WorkflowQuery) do + query_type { 'state' } + query_args { Temporal.configuration.converter.to_payloads(['']) } +end diff --git a/spec/fabricators/grpc/workflow_task_fabricator.rb b/spec/fabricators/grpc/workflow_task_fabricator.rb index a699428a..d1470c04 100644 --- a/spec/fabricators/grpc/workflow_task_fabricator.rb +++ b/spec/fabricators/grpc/workflow_task_fabricator.rb @@ -10,6 +10,7 @@ scheduled_time { Google::Protobuf::Timestamp.new.tap { |t| t.from_time(Time.now) } } started_time { Google::Protobuf::Timestamp.new.tap { |t| t.from_time(Time.now) } } history { |attrs| Temporal::Api::History::V1::History.new(events: attrs[:events]) } + query { nil } end Fabricator(:api_paginated_workflow_task, from: :api_workflow_task) do diff --git a/spec/unit/lib/temporal/connection/serializer/query_answer_spec.rb b/spec/unit/lib/temporal/connection/serializer/query_answer_spec.rb new file mode 100644 index 00000000..8876bbd5 --- /dev/null +++ b/spec/unit/lib/temporal/connection/serializer/query_answer_spec.rb @@ -0,0 +1,23 @@ +require 'temporal/connection/serializer/query_failure' +require 'temporal/workflow/query_result' +require 'temporal/concerns/payloads' + +describe Temporal::Connection::Serializer::QueryAnswer do + class TestDeserializer + extend Temporal::Concerns::Payloads + end + + describe 'to_proto' do + let(:query_result) { Temporal::Workflow::QueryResult.answer(42) } + + it 'produces a protobuf' do + result = described_class.new(query_result).to_proto + + expect(result).to be_a(Temporal::Api::Query::V1::WorkflowQueryResult) + expect(result.result_type).to eq(Temporal::Api::Enums::V1::QueryResultType.lookup( + Temporal::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_ANSWERED) + ) + expect(result.answer).to eq(TestDeserializer.to_query_payloads(42)) + end + end +end diff --git a/spec/unit/lib/temporal/connection/serializer/query_failure_spec.rb b/spec/unit/lib/temporal/connection/serializer/query_failure_spec.rb new file mode 100644 index 00000000..7e948f4d --- /dev/null +++ b/spec/unit/lib/temporal/connection/serializer/query_failure_spec.rb @@ -0,0 +1,19 @@ +require 'temporal/connection/serializer/query_failure' +require 'temporal/workflow/query_result' + +describe Temporal::Connection::Serializer::QueryFailure do + describe 'to_proto' do + let(:exception) { StandardError.new('Test query failure') } + let(:query_result) { Temporal::Workflow::QueryResult.failure(exception) } + + it 'produces a protobuf' do + result = described_class.new(query_result).to_proto + + expect(result).to be_a(Temporal::Api::Query::V1::WorkflowQueryResult) + expect(result.result_type).to eq(Temporal::Api::Enums::V1::QueryResultType.lookup( + Temporal::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_FAILED) + ) + expect(result.error_message).to eq('Test query failure') + end + end +end diff --git a/spec/unit/lib/temporal/grpc_client_spec.rb b/spec/unit/lib/temporal/grpc_spec.rb similarity index 75% rename from spec/unit/lib/temporal/grpc_client_spec.rb rename to spec/unit/lib/temporal/grpc_spec.rb index 70a7885d..8cbe5af7 100644 --- a/spec/unit/lib/temporal/grpc_client_spec.rb +++ b/spec/unit/lib/temporal/grpc_spec.rb @@ -1,3 +1,6 @@ +require 'temporal/connection/grpc' +require 'temporal/workflow/query_result' + describe Temporal::Connection::GRPC do subject { Temporal::Connection::GRPC.new(nil, nil, nil) } let(:grpc_stub) { double('grpc stub') } @@ -6,9 +9,13 @@ let(:run_id) { SecureRandom.uuid } let(:now) { Time.now} + class TestDeserializer + extend Temporal::Concerns::Payloads + end + before do allow(subject).to receive(:client).and_return(grpc_stub) - + allow(Time).to receive(:now).and_return(now) end @@ -35,7 +42,7 @@ end end end - + describe '#signal_with_start_workflow' do let(:temporal_response) do Temporal::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionResponse.new(run_id: 'xxx') @@ -148,7 +155,7 @@ end end - it 'demands a timeout to be specified' do + it 'demands a timeout to be specified' do expect do subject.get_workflow_execution_history( namespace: namespace, @@ -161,7 +168,7 @@ end end - it 'disallows a timeout larger than the server timeout' do + it 'disallows a timeout larger than the server timeout' do expect do subject.get_workflow_execution_history( namespace: namespace, @@ -345,4 +352,109 @@ end end end + + describe '#respond_query_task_completed' do + let(:task_token) { SecureRandom.uuid } + + before do + allow(grpc_stub) + .to receive(:respond_query_task_completed) + .and_return(Temporal::Api::WorkflowService::V1::RespondQueryTaskCompletedResponse.new) + end + + context 'when query result is an answer' do + let(:query_result) { Temporal::Workflow::QueryResult.answer(42) } + + it 'makes an API request' do + subject.respond_query_task_completed( + namespace: namespace, + task_token: task_token, + query_result: query_result + ) + + expect(grpc_stub).to have_received(:respond_query_task_completed) do |request| + expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::RespondQueryTaskCompletedRequest) + expect(request.task_token).to eq(task_token) + expect(request.namespace).to eq(namespace) + expect(request.completed_type).to eq(Temporal::Api::Enums::V1::QueryResultType.lookup( + Temporal::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_ANSWERED) + ) + expect(request.query_result).to eq(TestDeserializer.to_query_payloads(42)) + expect(request.error_message).to eq('') + end + end + end + + context 'when query result is a failure' do + let(:query_result) { Temporal::Workflow::QueryResult.failure(StandardError.new('Test query failure')) } + + it 'makes an API request' do + subject.respond_query_task_completed( + namespace: namespace, + task_token: task_token, + query_result: query_result + ) + + expect(grpc_stub).to have_received(:respond_query_task_completed) do |request| + expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::RespondQueryTaskCompletedRequest) + expect(request.task_token).to eq(task_token) + expect(request.namespace).to eq(namespace) + expect(request.completed_type).to eq(Temporal::Api::Enums::V1::QueryResultType.lookup( + Temporal::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_FAILED) + ) + expect(request.query_result).to eq(nil) + expect(request.error_message).to eq('Test query failure') + end + end + end + end + + describe '#respond_workflow_task_completed' do + let(:task_token) { SecureRandom.uuid } + + before do + allow(grpc_stub) + .to receive(:respond_workflow_task_completed) + .and_return(Temporal::Api::WorkflowService::V1::RespondWorkflowTaskCompletedResponse.new) + end + + context 'when responding with query results' do + let(:query_results) do + { + '1' => Temporal::Workflow::QueryResult.answer(42), + '2' => Temporal::Workflow::QueryResult.failure(StandardError.new('Test query failure')), + } + end + + it 'makes an API request' do + subject.respond_workflow_task_completed( + namespace: namespace, + task_token: task_token, + commands: [], + query_results: query_results + ) + + expect(grpc_stub).to have_received(:respond_workflow_task_completed) do |request| + expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::RespondWorkflowTaskCompletedRequest) + expect(request.task_token).to eq(task_token) + expect(request.namespace).to eq(namespace) + expect(request.commands).to be_empty + + expect(request.query_results.length).to eq(2) + + expect(request.query_results['1']).to be_a(Temporal::Api::Query::V1::WorkflowQueryResult) + expect(request.query_results['1'].result_type).to eq(Temporal::Api::Enums::V1::QueryResultType.lookup( + Temporal::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_ANSWERED) + ) + expect(request.query_results['1'].answer).to eq(TestDeserializer.to_query_payloads(42)) + + expect(request.query_results['2']).to be_a(Temporal::Api::Query::V1::WorkflowQueryResult) + expect(request.query_results['2'].result_type).to eq(Temporal::Api::Enums::V1::QueryResultType.lookup( + Temporal::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_FAILED) + ) + expect(request.query_results['2'].error_message).to eq('Test query failure') + end + end + end + end end diff --git a/spec/unit/lib/temporal/metadata/workflow_task_spec.rb b/spec/unit/lib/temporal/metadata/workflow_task_spec.rb index 7259c1f9..99f95a84 100644 --- a/spec/unit/lib/temporal/metadata/workflow_task_spec.rb +++ b/spec/unit/lib/temporal/metadata/workflow_task_spec.rb @@ -32,7 +32,7 @@ 'namespace' => subject.namespace, 'workflow_id' => subject.workflow_id, 'workflow_name' => subject.workflow_name, - 'workflow_run_id' => subject.workflow_run_id, + 'workflow_run_id' => subject.workflow_run_id }) end end diff --git a/spec/unit/lib/temporal/workflow/context_spec.rb b/spec/unit/lib/temporal/workflow/context_spec.rb index 24768d82..a6dd2921 100644 --- a/spec/unit/lib/temporal/workflow/context_spec.rb +++ b/spec/unit/lib/temporal/workflow/context_spec.rb @@ -7,10 +7,32 @@ class MyTestWorkflow < Temporal::Workflow; end describe Temporal::Workflow::Context do let(:state_manager) { instance_double('Temporal::Workflow::StateManager') } let(:dispatcher) { instance_double('Temporal::Workflow::Dispatcher') } + let(:query_registry) { instance_double('Temporal::Workflow::QueryRegistry') } let(:metadata) { instance_double('Temporal::Metadata::Workflow') } - let(:workflow_context) { - Temporal::Workflow::Context.new(state_manager, dispatcher, MyTestWorkflow, metadata, Temporal.configuration) - } + let(:workflow_context) do + Temporal::Workflow::Context.new( + state_manager, + dispatcher, + MyTestWorkflow, + metadata, + Temporal.configuration, + query_registry + ) + end + + describe '#on_query' do + let(:handler) { Proc.new {} } + + before { allow(query_registry).to receive(:register) } + + it 'registers a query with the query registry' do + workflow_context.on_query('test-query', &handler) + + expect(query_registry).to have_received(:register).with('test-query') do |&block| + expect(block).to eq(handler) + end + end + end describe '#upsert_search_attributes' do it 'does not accept nil' do diff --git a/spec/unit/lib/temporal/workflow/executor_spec.rb b/spec/unit/lib/temporal/workflow/executor_spec.rb index bc9828da..b93105ac 100644 --- a/spec/unit/lib/temporal/workflow/executor_spec.rb +++ b/spec/unit/lib/temporal/workflow/executor_spec.rb @@ -1,6 +1,8 @@ require 'temporal/workflow/executor' require 'temporal/workflow/history' require 'temporal/workflow' +require 'temporal/workflow/task_processor' +require 'temporal/workflow/query_registry' describe Temporal::Workflow::Executor do subject { described_class.new(workflow, history, workflow_metadata, config) } @@ -80,4 +82,36 @@ def execute ) end end -end \ No newline at end of file + + describe '#process_queries' do + let(:query_registry) { Temporal::Workflow::QueryRegistry.new } + let(:query_1_result) { 42 } + let(:query_2_error) { StandardError.new('Test query failure') } + let(:queries) do + { + '1' => Temporal::Workflow::TaskProcessor::Query.new(Fabricate(:api_workflow_query, query_type: 'success')), + '2' => Temporal::Workflow::TaskProcessor::Query.new(Fabricate(:api_workflow_query, query_type: 'failure')), + '3' => Temporal::Workflow::TaskProcessor::Query.new(Fabricate(:api_workflow_query, query_type: 'unknown')), + } + end + + before do + allow(Temporal::Workflow::QueryRegistry).to receive(:new).and_return(query_registry) + query_registry.register('success') { query_1_result } + query_registry.register('failure') { raise query_2_error } + end + + it 'returns query results' do + results = subject.process_queries(queries) + + expect(results.length).to eq(3) + expect(results['1']).to be_a(Temporal::Workflow::QueryResult::Answer) + expect(results['1'].result).to eq(query_1_result) + expect(results['2']).to be_a(Temporal::Workflow::QueryResult::Failure) + expect(results['2'].error).to eq(query_2_error) + expect(results['3']).to be_a(Temporal::Workflow::QueryResult::Failure) + expect(results['3'].error).to be_a(Temporal::QueryFailed) + expect(results['3'].error.message).to eq('Workflow did not register a handler for unknown') + end + end +end diff --git a/spec/unit/lib/temporal/workflow/query_registry_spec.rb b/spec/unit/lib/temporal/workflow/query_registry_spec.rb new file mode 100644 index 00000000..3c5ced14 --- /dev/null +++ b/spec/unit/lib/temporal/workflow/query_registry_spec.rb @@ -0,0 +1,67 @@ +require 'temporal/workflow/query_registry' + +describe Temporal::Workflow::QueryRegistry do + subject { described_class.new } + + describe '#register' do + let(:handler) { Proc.new {} } + + it 'registers a query handler' do + subject.register('test-query', &handler) + + expect(subject.send(:handlers)['test-query']).to eq(handler) + end + + context 'when query handler is already registered' do + let(:handler_2) { Proc.new {} } + + before { subject.register('test-query', &handler) } + + it 'warns' do + allow(subject).to receive(:warn) + + subject.register('test-query', &handler_2) + + expect(subject) + .to have_received(:warn) + .with('[NOTICE] Overwriting a query handler for test-query') + end + + it 're-registers a query handler' do + subject.register('test-query', &handler_2) + + expect(subject.send(:handlers)['test-query']).to eq(handler_2) + end + end + end + + describe '#handle' do + context 'when a query handler has been registered' do + let(:handler) { Proc.new { 42 } } + + before { subject.register('test-query', &handler) } + + it 'runs the handler and returns the result' do + expect(subject.handle('test-query')).to eq(42) + end + end + + context 'when a query handler has been registered with args' do + let(:handler) { Proc.new { |arg_1, arg_2| arg_1 + arg_2 } } + + before { subject.register('test-query', &handler) } + + it 'runs the handler and returns the result' do + expect(subject.handle('test-query', [3, 5])).to eq(8) + end + end + + context 'when a query handler has not been registered' do + it 'raises' do + expect do + subject.handle('test-query') + end.to raise_error(Temporal::QueryFailed, 'Workflow did not register a handler for test-query') + end + end + end +end diff --git a/spec/unit/lib/temporal/workflow/query_result_spec.rb b/spec/unit/lib/temporal/workflow/query_result_spec.rb new file mode 100644 index 00000000..222446a8 --- /dev/null +++ b/spec/unit/lib/temporal/workflow/query_result_spec.rb @@ -0,0 +1,25 @@ +require 'temporal/workflow/query_result' + +describe Temporal::Workflow::QueryResult do + describe '.answer' do + it 'returns an anwer query result' do + result = described_class.answer(42) + + expect(result).to be_a(Temporal::Workflow::QueryResult::Answer) + expect(result).to be_frozen + expect(result.result).to eq(42) + end + end + + describe '.failure' do + let(:error) { StandardError.new('Test query failure') } + + it 'returns a failure query result' do + result = described_class.failure(error) + + expect(result).to be_a(Temporal::Workflow::QueryResult::Failure) + expect(result).to be_frozen + expect(result.error).to eq(error) + end + end +end diff --git a/spec/unit/lib/temporal/workflow/task_processor_spec.rb b/spec/unit/lib/temporal/workflow/task_processor_spec.rb index 188b3a90..5a02569e 100644 --- a/spec/unit/lib/temporal/workflow/task_processor_spec.rb +++ b/spec/unit/lib/temporal/workflow/task_processor_spec.rb @@ -7,7 +7,9 @@ let(:namespace) { 'test-namespace' } let(:lookup) { instance_double('Temporal::ExecutableLookup', find: nil) } - let(:task) { Fabricate(:api_workflow_task, workflow_type: api_workflow_type) } + let(:query) { nil } + let(:queries) { nil } + let(:task) { Fabricate(:api_workflow_task, { workflow_type: api_workflow_type, query: query, queries: queries }.compact) } let(:api_workflow_type) { Fabricate(:api_workflow_type, name: workflow_name) } let(:workflow_name) { 'TestWorkflow' } let(:connection) { instance_double('Temporal::Connection::GRPC') } @@ -24,6 +26,7 @@ .with(config.for_connection) .and_return(connection) allow(connection).to receive(:respond_workflow_task_completed) + allow(connection).to receive(:respond_query_task_completed) allow(connection).to receive(:respond_workflow_task_failed) allow(middleware_chain).to receive(:invoke).and_call_original @@ -65,6 +68,7 @@ allow(lookup).to receive(:find).with(workflow_name).and_return(workflow_class) allow(Temporal::Workflow::Executor).to receive(:new).and_return(executor) allow(executor).to receive(:run) { workflow_class.execute_in_context(context, input); commands } + allow(executor).to receive(:process_queries) end context 'when workflow task completes' do @@ -84,20 +88,78 @@ ) end - it 'completes the workflow task' do - subject.process + context 'when workflow task queries are included' do + let(:query_id) { SecureRandom.uuid } + let(:query_result) { Temporal::Workflow::QueryResult.answer(42) } + let(:queries) do + Google::Protobuf::Map.new(:string, :message, Temporal::Api::Query::V1::WorkflowQuery).tap do |map| + map[query_id] = Fabricate(:api_workflow_query) + end + end - expect(connection) - .to have_received(:respond_workflow_task_completed) - .with(namespace: namespace, task_token: task.task_token, commands: commands) + before do + allow(executor).to receive(:process_queries).and_return(query_id => query_result) + end + + it 'completes the workflow task with query results' do + subject.process + + expect(executor) + .to have_received(:process_queries) + .with(query_id => an_instance_of(Temporal::Workflow::TaskProcessor::Query)) + expect(connection) + .to have_received(:respond_workflow_task_completed) + .with( + namespace: namespace, + task_token: task.task_token, + commands: commands, + query_results: { query_id => query_result } + ) + end end - it 'ignores connection exception' do - allow(connection) - .to receive(:respond_workflow_task_completed) - .and_raise(StandardError) + context 'when deprecated task query is present' do + let(:query) { Fabricate(:api_workflow_query) } + let(:result) { Temporal::Workflow::QueryResult.answer(42) } - subject.process + before do + allow(executor).to receive(:process_queries).and_return(legacy_query: result) + end + + it 'completes the workflow query task with the result' do + subject.process + + expect(executor).to have_received(:process_queries).with( + legacy_query: an_instance_of(Temporal::Workflow::TaskProcessor::Query) + ) + expect(connection).to_not have_received(:respond_workflow_task_completed) + expect(connection) + .to have_received(:respond_query_task_completed) + .with( + task_token: task.task_token, + namespace: namespace, + query_result: result + ) + end + end + + context 'when deprecated task query is not present' do + it 'completes the workflow task' do + subject.process + + expect(connection).to_not have_received(:respond_query_task_completed) + expect(connection) + .to have_received(:respond_workflow_task_completed) + .with(namespace: namespace, task_token: task.task_token, commands: commands, query_results: nil) + end + + it 'ignores connection exception' do + allow(connection) + .to receive(:respond_workflow_task_completed) + .and_raise(StandardError) + + subject.process + end end it 'sends queue_time metric' do @@ -122,48 +184,67 @@ before { allow(workflow_class).to receive(:execute_in_context).and_raise(exception) } - it 'fails the workflow task' do - subject.process + context 'when deprecated task query is present' do + let(:query) { Fabricate(:api_workflow_query) } - expect(connection) - .to have_received(:respond_workflow_task_failed) - .with( - namespace: namespace, - task_token: task.task_token, - cause: Temporal::Api::Enums::V1::WorkflowTaskFailedCause::WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE, - exception: exception - ) + it 'fails the workflow task' do + subject.process + + expect(connection) + .to have_received(:respond_workflow_task_failed) + .with( + namespace: namespace, + task_token: task.task_token, + cause: Temporal::Api::Enums::V1::WorkflowTaskFailedCause::WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE, + exception: exception + ) + end end - it 'does not fail the task beyond the first attempt' do - task.attempt = 2 - subject.process + context 'when deprecated task query is not present' do + it 'fails the workflow task' do + subject.process - expect(connection) - .not_to have_received(:respond_workflow_task_failed) - end + expect(connection) + .to have_received(:respond_workflow_task_failed) + .with( + namespace: namespace, + task_token: task.task_token, + cause: Temporal::Api::Enums::V1::WorkflowTaskFailedCause::WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE, + exception: exception + ) + end - it 'ignores connection exception' do - allow(connection) - .to receive(:respond_workflow_task_failed) - .and_raise(StandardError) + it 'does not fail the task beyond the first attempt' do + task.attempt = 2 + subject.process - subject.process - end + expect(connection) + .not_to have_received(:respond_workflow_task_failed) + end - it 'calls error_handlers' do - reported_error = nil - reported_metadata = nil + it 'ignores connection exception' do + allow(connection) + .to receive(:respond_workflow_task_failed) + .and_raise(StandardError) - config.on_error do |error, metadata: nil| - reported_error = error - reported_metadata = metadata + subject.process end - subject.process + it 'calls error_handlers' do + reported_error = nil + reported_metadata = nil + + config.on_error do |error, metadata: nil| + reported_error = error + reported_metadata = metadata + end + + subject.process - expect(reported_error).to be_an_instance_of(StandardError) - expect(reported_metadata).to be_an_instance_of(Temporal::Metadata::WorkflowTask) + expect(reported_error).to be_an_instance_of(StandardError) + expect(reported_metadata).to be_an_instance_of(Temporal::Metadata::WorkflowTask) + end end it 'sends queue_time metric' do @@ -183,6 +264,30 @@ end end + context 'when legacy query fails' do + let(:query) { Fabricate(:api_workflow_query) } + let(:exception) { StandardError.new('workflow task failed') } + let(:query_failure) { Temporal::Workflow::QueryResult.failure(exception) } + + before do + allow(executor) + .to receive(:process_queries) + .and_return(legacy_query: query_failure) + end + + it 'fails the workflow task' do + subject.process + + expect(connection) + .to have_received(:respond_query_task_completed) + .with( + namespace: namespace, + task_token: task.task_token, + query_result: query_failure + ) + end + end + context 'when history is paginated' do let(:task) { Fabricate(:api_paginated_workflow_task, workflow_type: api_workflow_type) } let(:event) { Fabricate(:api_workflow_execution_started_event) } From dd52ab9ee157f1e64cd99c9184f4a2d8c4f8f731 Mon Sep 17 00:00:00 2001 From: nagl-stripe <86737162+nagl-stripe@users.noreply.github.com> Date: Tue, 19 Apr 2022 10:27:57 -0700 Subject: [PATCH 050/125] Expose workflow name in activity metadata in temporal-ruby's unit tester (#130) * Expose workflow name in activity metadata in temporal-ruby's unit tester * pr feedback --- .../testing/local_workflow_context.rb | 18 ++++++++++++--- .../testing/local_workflow_context_spec.rb | 23 ++++++++++++++++++- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/lib/temporal/testing/local_workflow_context.rb b/lib/temporal/testing/local_workflow_context.rb index 1f200e84..efeb47a8 100644 --- a/lib/temporal/testing/local_workflow_context.rb +++ b/lib/temporal/testing/local_workflow_context.rb @@ -58,7 +58,7 @@ def execute_activity(activity_class, *input, **args) attempt: 1, workflow_run_id: run_id, workflow_id: workflow_id, - workflow_name: nil, # not yet used, but will be in the future + workflow_name: self.metadata.name, headers: execution_options.headers, heartbeat_details: nil, scheduled_at: Time.now, @@ -108,7 +108,7 @@ def execute_local_activity(activity_class, *input, **args) attempt: 1, workflow_run_id: run_id, workflow_id: workflow_id, - workflow_name: nil, # not yet used, but will be in the future + workflow_name: self.metadata.name, headers: execution_options.headers, heartbeat_details: nil, scheduled_at: Time.now, @@ -131,8 +131,20 @@ def execute_workflow!(workflow_class, *input, **args) workflow_id = SecureRandom.uuid run_id = SecureRandom.uuid execution_options = ExecutionOptions.new(workflow_class, options, config.default_execution_options) + + child_metadata = Temporal::Metadata::Workflow.new( + namespace: execution_options.namespace, + id: workflow_id, + name: execution_options.name, # Workflow class name + run_id: run_id, + attempt: 1, + task_queue: execution_options.task_queue, + headers: execution_options.headers, + run_started_at: Time.now, + memo: {}, + ) context = Temporal::Testing::LocalWorkflowContext.new( - execution, workflow_id, run_id, workflow_class.disabled_releases, execution_options.headers + execution, workflow_id, run_id, workflow_class.disabled_releases, child_metadata ) workflow_class.execute_in_context(context, input) diff --git a/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb b/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb index ae8eca8c..6242be7d 100644 --- a/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb +++ b/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb @@ -5,6 +5,7 @@ describe Temporal::Testing::LocalWorkflowContext do let(:workflow_id) { 'workflow_id_1' } + let(:workflow_name) { 'HelloWorldWorkflow' } let(:run_id) { 'run_id_1' } let(:execution) { Temporal::Testing::WorkflowExecution.new } let(:task_queue) { 'my_test_queue' } @@ -17,7 +18,7 @@ Temporal::Metadata::Workflow.new( namespace: 'ruby-samples', id: workflow_id, - name: 'HelloWorldWorkflow', + name: workflow_name, run_id: run_id, parent_id: nil, parent_run_id: nil, @@ -63,6 +64,14 @@ def execute end end + class MetadataCapturingActivity < Temporal::Activity + def execute + # activity.metadata is private, which we work around in order to write unit tests that + # can observe activity metadata + activity.send :metadata + end + end + describe '#execute_activity' do describe 'outcome is captured in the future' do it 'delay failure' do @@ -139,6 +148,18 @@ def execute # Heartbeat doesn't do anything in local mode, but at least it can be called. workflow_context.execute_activity!(TestHeartbeatingActivity) end + + it 'has accurate metadata' do + result = workflow_context.execute_activity!(MetadataCapturingActivity) + expect(result.attempt).to eq(1) + expect(result.headers).to eq({}) + expect(result.id).to eq(1) + expect(result.name).to eq('MetadataCapturingActivity') + expect(result.namespace).to eq('default-namespace') + expect(result.workflow_id).to eq(workflow_id) + expect(result.workflow_name).to eq(workflow_name) + expect(result.workflow_run_id).to eq(run_id) + end end describe '#wait_for' do From 6b8973f68546ec2672bba27bbcf1d27d2c424a5d Mon Sep 17 00:00:00 2001 From: nagl-stripe <86737162+nagl-stripe@users.noreply.github.com> Date: Tue, 19 Apr 2022 10:28:23 -0700 Subject: [PATCH 051/125] Fix bugs around child workflow ids and workflow_reuse_policy (#172) * Factor out workflow_id_reuse_policy serialization * Respect workflow_id_reuse_policy for child workflows * Add integration tests * Use a nicer exception type * PR feedback * Fix integ test, add unit test --- examples/bin/worker | 1 + .../spec/integration/parent_id_reuse_spec.rb | 109 ++++++++++++++++++ .../spec/integration/start_workflow_spec.rb | 35 ++++++ .../workflows/parent_id_reuse_workflow.rb | 25 ++++ lib/temporal/connection/grpc.rb | 23 +--- .../serializer/start_child_workflow.rb | 2 + .../serializer/workflow_id_reuse_policy.rb | 25 ++++ lib/temporal/errors.rb | 3 +- lib/temporal/workflow/command.rb | 2 +- lib/temporal/workflow/context.rb | 5 +- lib/temporal/workflow/errors.rb | 15 +++ lib/temporal/workflow/state_manager.rb | 7 +- spec/unit/lib/temporal/client_spec.rb | 5 +- .../workflow_id_reuse_policy_spec.rb | 30 +++++ spec/unit/lib/temporal/grpc_spec.rb | 73 +++++++++++- 15 files changed, 332 insertions(+), 28 deletions(-) create mode 100644 examples/spec/integration/parent_id_reuse_spec.rb create mode 100644 examples/workflows/parent_id_reuse_workflow.rb create mode 100644 lib/temporal/connection/serializer/workflow_id_reuse_policy.rb create mode 100644 spec/unit/lib/temporal/connection/serializer/workflow_id_reuse_policy_spec.rb diff --git a/examples/bin/worker b/examples/bin/worker index c9c78145..291d16e0 100755 --- a/examples/bin/worker +++ b/examples/bin/worker @@ -34,6 +34,7 @@ worker.register_workflow(LongWorkflow) worker.register_workflow(LoopWorkflow) worker.register_workflow(MetadataWorkflow) worker.register_workflow(ParentCloseWorkflow) +worker.register_workflow(ParentIdReuseWorkflow) worker.register_workflow(ParentWorkflow) worker.register_workflow(ProcessFileWorkflow) worker.register_workflow(QueryWorkflow) diff --git a/examples/spec/integration/parent_id_reuse_spec.rb b/examples/spec/integration/parent_id_reuse_spec.rb new file mode 100644 index 00000000..e09c8790 --- /dev/null +++ b/examples/spec/integration/parent_id_reuse_spec.rb @@ -0,0 +1,109 @@ +require 'workflows/parent_id_reuse_workflow' + +describe ParentIdReuseWorkflow, :integration do + subject { described_class } + + it 'with :allow, allows duplicates' do + workflow_id = 'parent_id_reuse_wf-' + SecureRandom.uuid + child_workflow_id = 'child_id_reuse_wf-' + SecureRandom.uuid + + Temporal.start_workflow( + ParentIdReuseWorkflow, + child_workflow_id, + child_workflow_id, + false, + :allow, + options: { workflow_id: workflow_id } + ) + + Temporal.await_workflow_result( + ParentIdReuseWorkflow, + workflow_id: workflow_id, + ) + end + + it 'with :reject, rejects duplicates' do + workflow_id = 'parent_id_reuse_wf-' + SecureRandom.uuid + child_workflow_id = 'child_id_reuse_wf-' + SecureRandom.uuid + + Temporal.start_workflow( + ParentIdReuseWorkflow, + child_workflow_id, + child_workflow_id, + false, + :reject, + options: { workflow_id: workflow_id } + ) + + expect do + Temporal.await_workflow_result( + ParentIdReuseWorkflow, + workflow_id: workflow_id, + ) + end.to raise_error(Temporal::WorkflowExecutionAlreadyStartedFailure, + "The child workflow could not be started - per its workflow_id_reuse_policy, it conflicts with another workflow with the same id: #{child_workflow_id}" + ) + end + + it 'with :reject, does not reject non-duplicates' do + workflow_id = 'parent_id_reuse_wf-' + SecureRandom.uuid + child_workflow_id_1 = 'child_id_reuse_wf-' + SecureRandom.uuid + child_workflow_id_2 = 'child_id_reuse_wf-' + SecureRandom.uuid + + Temporal.start_workflow( + ParentIdReuseWorkflow, + child_workflow_id_1, + child_workflow_id_2, + false, + :reject, + options: { workflow_id: workflow_id } + ) + + Temporal.await_workflow_result( + ParentIdReuseWorkflow, + workflow_id: workflow_id, + ) + end + + it 'with :allow_failed, allows duplicates after failure' do + workflow_id = 'parent_id_reuse_wf-' + SecureRandom.uuid + child_workflow_id = 'child_id_reuse_wf-' + SecureRandom.uuid + + Temporal.start_workflow( + ParentIdReuseWorkflow, + child_workflow_id, + child_workflow_id, + true, + :allow_failed, + options: { workflow_id: workflow_id } + ) + + Temporal.await_workflow_result( + ParentIdReuseWorkflow, + workflow_id: workflow_id, + ) + end + + it 'with :allow_failed, rejects duplicates after success' do + workflow_id = 'parent_id_reuse_wf-' + SecureRandom.uuid + child_workflow_id = 'child_id_reuse_wf-' + SecureRandom.uuid + + Temporal.start_workflow( + ParentIdReuseWorkflow, + child_workflow_id, + child_workflow_id, + false, + :allow_failed, + options: { workflow_id: workflow_id } + ) + + expect do + Temporal.await_workflow_result( + ParentIdReuseWorkflow, + workflow_id: workflow_id, + ) + end.to raise_error(Temporal::WorkflowExecutionAlreadyStartedFailure, + "The child workflow could not be started - per its workflow_id_reuse_policy, it conflicts with another workflow with the same id: #{child_workflow_id}" + ) + end +end diff --git a/examples/spec/integration/start_workflow_spec.rb b/examples/spec/integration/start_workflow_spec.rb index 2380ade0..46fb34a5 100644 --- a/examples/spec/integration/start_workflow_spec.rb +++ b/examples/spec/integration/start_workflow_spec.rb @@ -33,4 +33,39 @@ expect(result).to eq('Hello World, Test') end + + it 'rejects duplicate workflow ids based on workflow_id_reuse_policy' do + # Run it once... + run_id = Temporal.start_workflow(HelloWorldWorkflow, 'Test', options: { + workflow_id: workflow_id, + }) + + result = Temporal.await_workflow_result( + HelloWorldWorkflow, + workflow_id: workflow_id, + run_id: run_id + ) + + expect(result).to eq('Hello World, Test') + + # And again, allowing duplicates... + run_id = Temporal.start_workflow(HelloWorldWorkflow, 'Test', options: { + workflow_id: workflow_id, + workflow_id_reuse_policy: :allow + }) + + Temporal.await_workflow_result( + HelloWorldWorkflow, + workflow_id: workflow_id, + run_id: run_id + ) + + # And again, rejecting duplicates... + expect do + Temporal.start_workflow(HelloWorldWorkflow, 'Test', options: { + workflow_id: workflow_id, + workflow_id_reuse_policy: :reject + }) + end.to raise_error(Temporal::WorkflowExecutionAlreadyStartedFailure) + end end diff --git a/examples/workflows/parent_id_reuse_workflow.rb b/examples/workflows/parent_id_reuse_workflow.rb new file mode 100644 index 00000000..f5aa9fa3 --- /dev/null +++ b/examples/workflows/parent_id_reuse_workflow.rb @@ -0,0 +1,25 @@ +require 'workflows/hello_world_workflow' +require 'workflows/failing_workflow' + +class ParentIdReuseWorkflow < Temporal::Workflow + def execute(workflow_id_1, workflow_id_2, fail_first, reuse_policy) + execute_child(workflow_id_1, fail_first, reuse_policy) + execute_child(workflow_id_2, false, reuse_policy) + end + + private + + def execute_child(workflow_id, fail, reuse_policy) + options = { + workflow_id: workflow_id, + workflow_id_reuse_policy: reuse_policy + } + + if fail + # wait for it, but don't raise when it fails + FailingWorkflow.execute(options: options).wait + else + HelloWorldWorkflow.execute!(options: options) + end + end +end diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index 747c13a8..2ae5f1f6 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -8,6 +8,7 @@ require 'temporal/connection/errors' require 'temporal/connection/serializer' require 'temporal/connection/serializer/failure' +require 'temporal/connection/serializer/workflow_id_reuse_policy' require 'temporal/concerns/payloads' module Temporal @@ -15,12 +16,6 @@ module Connection class GRPC include Concerns::Payloads - WORKFLOW_ID_REUSE_POLICY = { - allow_failed: Temporal::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY, - allow: Temporal::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, - reject: Temporal::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE - }.freeze - HISTORY_EVENT_FILTER = { all: Temporal::Api::Enums::V1::HistoryEventFilterType::HISTORY_EVENT_FILTER_TYPE_ALL_EVENT, close: Temporal::Api::Enums::V1::HistoryEventFilterType::HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT, @@ -106,6 +101,7 @@ def start_workflow_execution( name: workflow_name ), workflow_id: workflow_id, + workflow_id_reuse_policy: Temporal::Connection::Serializer::WorkflowIdReusePolicy.new(workflow_id_reuse_policy).to_proto, task_queue: Temporal::Api::TaskQueue::V1::TaskQueue.new( name: task_queue ), @@ -123,13 +119,6 @@ def start_workflow_execution( ) ) - if workflow_id_reuse_policy - policy = WORKFLOW_ID_REUSE_POLICY[workflow_id_reuse_policy] - raise Client::ArgumentError, 'Unknown workflow_id_reuse_policy specified' unless policy - - request.workflow_id_reuse_policy = policy - end - client.start_workflow_execution(request) rescue ::GRPC::AlreadyExists => e # Feel like there should be cleaner way to do this... @@ -365,6 +354,7 @@ def signal_with_start_workflow_execution( name: workflow_name ), workflow_id: workflow_id, + workflow_id_reuse_policy: Temporal::Connection::Serializer::WorkflowIdReusePolicy.new(workflow_id_reuse_policy).to_proto, task_queue: Temporal::Api::TaskQueue::V1::TaskQueue.new( name: task_queue ), @@ -384,13 +374,6 @@ def signal_with_start_workflow_execution( ), ) - if workflow_id_reuse_policy - policy = WORKFLOW_ID_REUSE_POLICY[workflow_id_reuse_policy] - raise Client::ArgumentError, 'Unknown workflow_id_reuse_policy specified' unless policy - - request.workflow_id_reuse_policy = policy - end - client.signal_with_start_workflow_execution(request) end diff --git a/lib/temporal/connection/serializer/start_child_workflow.rb b/lib/temporal/connection/serializer/start_child_workflow.rb index 3952ce97..46ae5400 100644 --- a/lib/temporal/connection/serializer/start_child_workflow.rb +++ b/lib/temporal/connection/serializer/start_child_workflow.rb @@ -1,5 +1,6 @@ require 'temporal/connection/serializer/base' require 'temporal/connection/serializer/retry_policy' +require 'temporal/connection/serializer/workflow_id_reuse_policy' require 'temporal/concerns/payloads' module Temporal @@ -31,6 +32,7 @@ def to_proto parent_close_policy: serialize_parent_close_policy(object.parent_close_policy), header: serialize_headers(object.headers), memo: serialize_memo(object.memo), + workflow_id_reuse_policy: Temporal::Connection::Serializer::WorkflowIdReusePolicy.new(object.workflow_id_reuse_policy).to_proto ) ) end diff --git a/lib/temporal/connection/serializer/workflow_id_reuse_policy.rb b/lib/temporal/connection/serializer/workflow_id_reuse_policy.rb new file mode 100644 index 00000000..b3040197 --- /dev/null +++ b/lib/temporal/connection/serializer/workflow_id_reuse_policy.rb @@ -0,0 +1,25 @@ +require 'temporal/connection' + +module Temporal + module Connection + module Serializer + class WorkflowIdReusePolicy < Base + + WORKFLOW_ID_REUSE_POLICY = { + allow_failed: Temporal::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY, + allow: Temporal::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, + reject: Temporal::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE + }.freeze + + def to_proto + return unless object + + policy = WORKFLOW_ID_REUSE_POLICY[object] + raise ArgumentError, "Unknown workflow_id_reuse_policy specified: #{object}" unless policy + + policy + end + end + end + end +end diff --git a/lib/temporal/errors.rb b/lib/temporal/errors.rb index 6d49bef2..dde124c4 100644 --- a/lib/temporal/errors.rb +++ b/lib/temporal/errors.rb @@ -57,10 +57,11 @@ class WorkflowAlreadyCompletingError < InternalError; end class WorkflowExecutionAlreadyStartedFailure < ApiError attr_reader :run_id - def initialize(message, run_id) + def initialize(message, run_id = nil) super(message) @run_id = run_id end + end class NamespaceNotActiveFailure < ApiError; end class ClientVersionNotSupportedFailure < ApiError; end diff --git a/lib/temporal/workflow/command.rb b/lib/temporal/workflow/command.rb index 52208307..b7ab9717 100644 --- a/lib/temporal/workflow/command.rb +++ b/lib/temporal/workflow/command.rb @@ -3,7 +3,7 @@ class Workflow module Command # TODO: Move these classes into their own directories under workflow/command/* ScheduleActivity = Struct.new(:activity_type, :activity_id, :input, :namespace, :task_queue, :retry_policy, :timeouts, :headers, keyword_init: true) - StartChildWorkflow = Struct.new(:workflow_type, :workflow_id, :input, :namespace, :task_queue, :retry_policy, :parent_close_policy, :timeouts, :headers, :memo, keyword_init: true) + StartChildWorkflow = Struct.new(:workflow_type, :workflow_id, :input, :namespace, :task_queue, :retry_policy, :parent_close_policy, :timeouts, :headers, :memo, :workflow_id_reuse_policy, keyword_init: true) ContinueAsNew = Struct.new(:workflow_type, :task_queue, :input, :timeouts, :retry_policy, :headers, :memo, keyword_init: true) RequestActivityCancellation = Struct.new(:activity_id, keyword_init: true) RecordMarker = Struct.new(:name, :details, keyword_init: true) diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index 74722f00..7aaa932a 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -105,6 +105,7 @@ def execute_workflow(workflow_class, *input, **args) input << args unless args.empty? parent_close_policy = options.delete(:parent_close_policy) + workflow_id_reuse_policy = options.delete(:workflow_id_reuse_policy) execution_options = ExecutionOptions.new(workflow_class, options, config.default_execution_options) command = Command::StartChildWorkflow.new( @@ -118,6 +119,7 @@ def execute_workflow(workflow_class, *input, **args) timeouts: execution_options.timeouts, headers: execution_options.headers, memo: execution_options.memo, + workflow_id_reuse_policy: workflow_id_reuse_policy ) target, cancelation_id = schedule_command(command) @@ -138,7 +140,8 @@ def execute_workflow(workflow_class, *input, **args) dispatcher.register_handler(target, 'started') do child_workflow_started = true end - wait_for { child_workflow_started } + + wait_for { child_workflow_started || future.failed? } future end diff --git a/lib/temporal/workflow/errors.rb b/lib/temporal/workflow/errors.rb index cea7f471..9b676f02 100644 --- a/lib/temporal/workflow/errors.rb +++ b/lib/temporal/workflow/errors.rb @@ -52,6 +52,21 @@ def self.generate_error(failure, default_exception_class = StandardError) end end + WORKFLOW_ALREADY_EXISTS_SYM = Temporal::Api::Enums::V1::StartChildWorkflowExecutionFailedCause.lookup( + Temporal::Api::Enums::V1::StartChildWorkflowExecutionFailedCause::START_CHILD_WORKFLOW_EXECUTION_FAILED_CAUSE_WORKFLOW_ALREADY_EXISTS + ) + + def self.generate_error_for_child_workflow_start(cause, workflow_id) + if cause == WORKFLOW_ALREADY_EXISTS_SYM + Temporal::WorkflowExecutionAlreadyStartedFailure.new( + "The child workflow could not be started - per its workflow_id_reuse_policy, it conflicts with another workflow with the same id: #{workflow_id}", + ) + else + # Right now, there's only one cause, but temporal may add more in the future + StandardError.new("The child workflow could not be started. Reason: #{cause}") + end + end + private_class_method def self.safe_constantize(const) Object.const_get(const) if Object.const_defined?(const) rescue NameError diff --git a/lib/temporal/workflow/state_manager.rb b/lib/temporal/workflow/state_manager.rb index 92e1ddd9..5fd2923b 100644 --- a/lib/temporal/workflow/state_manager.rb +++ b/lib/temporal/workflow/state_manager.rb @@ -241,8 +241,11 @@ def apply_event(event) when 'START_CHILD_WORKFLOW_EXECUTION_FAILED' state_machine.fail - dispatch(history_target, 'failed', 'StandardError', from_payloads(event.attributes.cause)) - + error = Temporal::Workflow::Errors.generate_error_for_child_workflow_start( + event.attributes.cause, + event.attributes.workflow_id + ) + dispatch(history_target, 'failed', error) when 'CHILD_WORKFLOW_EXECUTION_STARTED' dispatch(history_target, 'started') state_machine.start diff --git a/spec/unit/lib/temporal/client_spec.rb b/spec/unit/lib/temporal/client_spec.rb index 3872c8d2..8470116b 100644 --- a/spec/unit/lib/temporal/client_spec.rb +++ b/spec/unit/lib/temporal/client_spec.rb @@ -62,7 +62,7 @@ class TestStartWorkflow < Temporal::Workflow execution_timeout: Temporal.configuration.timeouts[:execution], workflow_id_reuse_policy: nil, headers: {}, - memo: {} + memo: {}, ) end @@ -75,6 +75,7 @@ class TestStartWorkflow < Temporal::Workflow namespace: 'test-namespace', task_queue: 'test-task-queue', headers: { 'Foo' => 'Bar' }, + workflow_id_reuse_policy: :reject, memo: { 'MemoKey1' => 'MemoValue1' } } ) @@ -90,7 +91,7 @@ class TestStartWorkflow < Temporal::Workflow task_timeout: Temporal.configuration.timeouts[:task], run_timeout: Temporal.configuration.timeouts[:run], execution_timeout: Temporal.configuration.timeouts[:execution], - workflow_id_reuse_policy: nil, + workflow_id_reuse_policy: :reject, headers: { 'Foo' => 'Bar' }, memo: { 'MemoKey1' => 'MemoValue1' } ) diff --git a/spec/unit/lib/temporal/connection/serializer/workflow_id_reuse_policy_spec.rb b/spec/unit/lib/temporal/connection/serializer/workflow_id_reuse_policy_spec.rb new file mode 100644 index 00000000..21c05302 --- /dev/null +++ b/spec/unit/lib/temporal/connection/serializer/workflow_id_reuse_policy_spec.rb @@ -0,0 +1,30 @@ +require 'temporal/retry_policy' +require 'temporal/connection/serializer/retry_policy' + +describe Temporal::Connection::Serializer::WorkflowIdReusePolicy do + describe 'to_proto' do + SYM_TO_PROTO = { + allow_failed: Temporal::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY, + allow: Temporal::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, + reject: Temporal::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE + }.freeze + + def self.test_valid_policy(policy_sym) + it "serializes #{policy_sym}" do + proto_enum = described_class.new(policy_sym).to_proto + expected = SYM_TO_PROTO[policy_sym] + expect(proto_enum).to eq(expected) + end + end + + test_valid_policy(:allow) + test_valid_policy(:allow_failed) + test_valid_policy(:reject) + + it "rejects invalid policies" do + expect do + described_class.new(:not_a_valid_policy).to_proto + end.to raise_error(Temporal::Connection::ArgumentError, 'Unknown workflow_id_reuse_policy specified: not_a_valid_policy') + end + end +end diff --git a/spec/unit/lib/temporal/grpc_spec.rb b/spec/unit/lib/temporal/grpc_spec.rb index 8cbe5af7..520f5c11 100644 --- a/spec/unit/lib/temporal/grpc_spec.rb +++ b/spec/unit/lib/temporal/grpc_spec.rb @@ -35,12 +35,61 @@ class TestDeserializer execution_timeout: 0, run_timeout: 0, task_timeout: 0, - memo: {} + memo: {}, + workflow_id_reuse_policy: :allow, ) end.to raise_error(Temporal::WorkflowExecutionAlreadyStartedFailure) do |e| expect(e.run_id).to eql('baaf1d86-4459-4ecd-a288-47aeae55245d') end end + + it 'starts a workflow with scalar arguments' do + allow(grpc_stub).to receive(:start_workflow_execution).and_return(Temporal::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionResponse.new(run_id: 'xxx')) + + subject.start_workflow_execution( + namespace: namespace, + workflow_id: workflow_id, + workflow_name: 'workflow_name', + task_queue: 'task_queue', + input: ['foo'], + execution_timeout: 1, + run_timeout: 2, + task_timeout: 3, + memo: {}, + workflow_id_reuse_policy: :reject, + ) + + expect(grpc_stub).to have_received(:start_workflow_execution) do |request| + expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::StartWorkflowExecutionRequest) + expect(request.namespace).to eq(namespace) + expect(request.workflow_id).to eq(workflow_id) + expect(request.workflow_type.name).to eq('workflow_name') + expect(request.task_queue.name).to eq('task_queue') + expect(request.input.payloads[0].data).to eq('"foo"') + expect(request.workflow_execution_timeout.seconds).to eq(1) + expect(request.workflow_run_timeout.seconds).to eq(2) + expect(request.workflow_task_timeout.seconds).to eq(3) + expect(request.workflow_id_reuse_policy).to eq(:WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE) + end + end + + it 'raises when an invalid workflow_id_reuse_policy is given' do + expect do + subject.start_workflow_execution( + namespace: namespace, + workflow_id: workflow_id, + workflow_name: 'Test', + task_queue: 'test', + execution_timeout: 0, + run_timeout: 0, + task_timeout: 0, + memo: {}, + workflow_id_reuse_policy: :not_a_valid_policy + ) + end.to raise_error(Temporal::Connection::ArgumentError) do |e| + expect(e.message).to eq('Unknown workflow_id_reuse_policy specified: not_a_valid_policy') + end + end end describe '#signal_with_start_workflow' do @@ -60,6 +109,7 @@ class TestDeserializer execution_timeout: 1, run_timeout: 2, task_timeout: 3, + workflow_id_reuse_policy: :allow, signal_name: 'the question', signal_input: 'what do you get if you multiply six by nine?' ) @@ -76,6 +126,27 @@ class TestDeserializer expect(request.workflow_task_timeout.seconds).to eq(3) expect(request.signal_name).to eq('the question') expect(request.signal_input.payloads[0].data).to eq('"what do you get if you multiply six by nine?"') + expect(request.workflow_id_reuse_policy).to eq(:WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE) + end + end + + it 'raises when an invalid workflow_id_reuse_policy is given' do + expect do + subject.signal_with_start_workflow_execution( + namespace: namespace, + workflow_id: workflow_id, + workflow_name: 'Test', + task_queue: 'test', + execution_timeout: 0, + run_timeout: 0, + task_timeout: 0, + memo: {}, + workflow_id_reuse_policy: :not_a_valid_policy, + signal_name: 'the question', + signal_input: 'what do you get if you multiply six by nine?' + ) + end.to raise_error(Temporal::Connection::ArgumentError) do |e| + expect(e.message).to eq('Unknown workflow_id_reuse_policy specified: not_a_valid_policy') end end end From 20bc15f35f7d0fccbbc6639abfb5140a506197f2 Mon Sep 17 00:00:00 2001 From: nagl-stripe <86737162+nagl-stripe@users.noreply.github.com> Date: Wed, 20 Apr 2022 12:04:53 -0700 Subject: [PATCH 052/125] Fix a merge error between child workflows and metadata enrichment (#180) --- lib/temporal/testing/local_workflow_context.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/temporal/testing/local_workflow_context.rb b/lib/temporal/testing/local_workflow_context.rb index efeb47a8..31aa4ef0 100644 --- a/lib/temporal/testing/local_workflow_context.rb +++ b/lib/temporal/testing/local_workflow_context.rb @@ -137,6 +137,8 @@ def execute_workflow!(workflow_class, *input, **args) id: workflow_id, name: execution_options.name, # Workflow class name run_id: run_id, + parent_id: @workflow_id, + parent_run_id: @run_id, attempt: 1, task_queue: execution_options.task_queue, headers: execution_options.headers, From fd6f98d94736365dec14206ce336e913c16c7eb5 Mon Sep 17 00:00:00 2001 From: calum-stripe <98350978+calum-stripe@users.noreply.github.com> Date: Thu, 5 May 2022 06:15:08 -0700 Subject: [PATCH 053/125] Added list workflows API and Pagination Support (#177) * added workflow executions class with enumerable * added extra unit tests * fixed nits and changed naming * fixed workflow executions name * removed () --- lib/temporal.rb | 3 +- lib/temporal/client.rb | 40 +++-------- lib/temporal/connection/grpc.rb | 18 +++-- lib/temporal/workflow/executions.rb | 66 ++++++++++++++++++ spec/unit/lib/temporal/client_spec.rb | 98 ++++++++++++++++++++++----- spec/unit/lib/temporal/grpc_spec.rb | 39 +++++++++++ 6 files changed, 209 insertions(+), 55 deletions(-) create mode 100644 lib/temporal/workflow/executions.rb diff --git a/lib/temporal.rb b/lib/temporal.rb index 7f62be78..99556ec0 100644 --- a/lib/temporal.rb +++ b/lib/temporal.rb @@ -28,7 +28,8 @@ module Temporal :complete_activity, :fail_activity, :list_open_workflow_executions, - :list_closed_workflow_executions + :list_closed_workflow_executions, + :query_workflow_executions class << self def configure(&block) diff --git a/lib/temporal/client.rb b/lib/temporal/client.rb index 6aede5a5..3f05d7aa 100644 --- a/lib/temporal/client.rb +++ b/lib/temporal/client.rb @@ -5,6 +5,7 @@ require 'temporal/workflow' require 'temporal/workflow/history' require 'temporal/workflow/execution_info' +require 'temporal/workflow/executions' require 'temporal/workflow/status' require 'temporal/reset_strategy' @@ -386,16 +387,20 @@ def get_workflow_history(namespace:, workflow_id:, run_id:) Workflow::History.new(history_response.history.events) end - def list_open_workflow_executions(namespace, from, to = Time.now, filter: {}) + def list_open_workflow_executions(namespace, from, to = Time.now, filter: {}, next_page_token: nil, max_page_size: nil) validate_filter(filter, :workflow, :workflow_id) - fetch_executions(:open, { namespace: namespace, from: from, to: to }.merge(filter)) + Temporal::Workflow::Executions.new(connection: connection, status: :open, request_options: { namespace: namespace, from: from, to: to, next_page_token: next_page_token, max_page_size: max_page_size}.merge(filter)) end - def list_closed_workflow_executions(namespace, from, to = Time.now, filter: {}) + def list_closed_workflow_executions(namespace, from, to = Time.now, filter: {}, next_page_token: nil, max_page_size: nil) validate_filter(filter, :status, :workflow, :workflow_id) - fetch_executions(:closed, { namespace: namespace, from: from, to: to }.merge(filter)) + Temporal::Workflow::Executions.new(connection: connection, status: :closed, request_options: { namespace: namespace, from: from, to: to, next_page_token: next_page_token, max_page_size: max_page_size}.merge(filter)) + end + + def query_workflow_executions(namespace, query, next_page_token: nil, max_page_size: nil) + Temporal::Workflow::Executions.new(connection: connection, status: :all, request_options: { namespace: namespace, query: query, next_page_token: next_page_token, max_page_size: max_page_size }.merge(filter)) end class ResultConverter @@ -449,32 +454,5 @@ def validate_filter(filter, *allowed_filters) raise ArgumentError, 'Only one filter is allowed' if filter.size > 1 end - def fetch_executions(status, request_options) - api_method = - if status == :open - :list_open_workflow_executions - else - :list_closed_workflow_executions - end - - executions = [] - next_page_token = nil - - loop do - response = connection.public_send( - api_method, - **request_options.merge(next_page_token: next_page_token) - ) - - executions += Array(response.executions) - next_page_token = response.next_page_token - - break if next_page_token.to_s.empty? - end - - executions.map do |raw_execution| - Temporal::Workflow::ExecutionInfo.generate_from(raw_execution) - end - end end end diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index 2ae5f1f6..b1c65bed 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -411,10 +411,10 @@ def terminate_workflow_execution( client.terminate_workflow_execution(request) end - def list_open_workflow_executions(namespace:, from:, to:, next_page_token: nil, workflow_id: nil, workflow: nil) + def list_open_workflow_executions(namespace:, from:, to:, next_page_token: nil, workflow_id: nil, workflow: nil, max_page_size: nil) request = Temporal::Api::WorkflowService::V1::ListOpenWorkflowExecutionsRequest.new( namespace: namespace, - maximum_page_size: options[:max_page_size], + maximum_page_size: max_page_size.nil? ? options[:max_page_size] : max_page_size, next_page_token: next_page_token, start_time_filter: serialize_time_filter(from, to), execution_filter: serialize_execution_filter(workflow_id), @@ -423,10 +423,10 @@ def list_open_workflow_executions(namespace:, from:, to:, next_page_token: nil, client.list_open_workflow_executions(request) end - def list_closed_workflow_executions(namespace:, from:, to:, next_page_token: nil, workflow_id: nil, workflow: nil, status: nil) + def list_closed_workflow_executions(namespace:, from:, to:, next_page_token: nil, workflow_id: nil, workflow: nil, status: nil, max_page_size: nil) request = Temporal::Api::WorkflowService::V1::ListClosedWorkflowExecutionsRequest.new( namespace: namespace, - maximum_page_size: options[:max_page_size], + maximum_page_size: max_page_size.nil? ? options[:max_page_size] : max_page_size, next_page_token: next_page_token, start_time_filter: serialize_time_filter(from, to), execution_filter: serialize_execution_filter(workflow_id), @@ -436,8 +436,14 @@ def list_closed_workflow_executions(namespace:, from:, to:, next_page_token: nil client.list_closed_workflow_executions(request) end - def list_workflow_executions - raise NotImplementedError + def list_workflow_executions(namespace:, query:, next_page_token: nil, max_page_size: nil) + request = Temporal::Api::WorkflowService::V1::ListWorkflowExecutionsRequest.new( + namespace: namespace, + page_size: max_page_size.nil? ? options[:max_page_size] : max_page_size, + next_page_token: next_page_token, + query: query + ) + client.list_workflow_executions(request) end def list_archived_workflow_executions diff --git a/lib/temporal/workflow/executions.rb b/lib/temporal/workflow/executions.rb new file mode 100644 index 00000000..83079b1a --- /dev/null +++ b/lib/temporal/workflow/executions.rb @@ -0,0 +1,66 @@ +require 'temporal/workflow/execution_info' + +module Temporal + class Workflow + class Executions + include Enumerable + + DEFAULT_REQUEST_OPTIONS = { + next_page_token: nil + }.freeze + + def initialize(connection:, status:, request_options:) + @connection = connection + @status = status + @request_options = DEFAULT_REQUEST_OPTIONS.merge(request_options) + end + + def next_page_token + @request_options[:next_page_token] + end + + def next_page + self.class.new(connection: @connection, status: @status, request_options: @request_options.merge(next_page_token: next_page_token)) + end + + def each + api_method = + if @status == :open + :list_open_workflow_executions + elsif @status == :closed + :list_closed_workflow_executions + else + :list_workflow_executions + end + + executions = [] + + loop do + response = @connection.public_send( + api_method, + **@request_options.merge(next_page_token: @request_options[:next_page_token]) + ) + + paginated_executions = response.executions.map do |raw_execution| + execution = Temporal::Workflow::ExecutionInfo.generate_from(raw_execution) + if block_given? + yield execution + end + + execution + end + + @request_options[:next_page_token] = response.next_page_token + + return paginated_executions unless @request_options[:max_page_size].nil? # return after the first loop if set pagination size + + executions += paginated_executions + + break if @request_options[:next_page_token].to_s.empty? + end + + executions + end + end + end +end diff --git a/spec/unit/lib/temporal/client_spec.rb b/spec/unit/lib/temporal/client_spec.rb index 8470116b..8cbde6cd 100644 --- a/spec/unit/lib/temporal/client_spec.rb +++ b/spec/unit/lib/temporal/client_spec.rb @@ -800,8 +800,7 @@ class NamespacedWorkflow < Temporal::Workflow it 'returns a list of executions' do executions = subject.list_open_workflow_executions(namespace, from) - - expect(executions.length).to eq(1) + expect(executions.count).to eq(1) expect(executions.first).to be_an_instance_of(Temporal::Workflow::ExecutionInfo) end @@ -832,34 +831,99 @@ class NamespacedWorkflow < Temporal::Workflow end it 'calls the API 3 times' do - subject.list_open_workflow_executions(namespace, from) + subject.list_open_workflow_executions(namespace, from).count expect(connection).to have_received(:list_open_workflow_executions).exactly(3).times expect(connection) .to have_received(:list_open_workflow_executions) - .with(namespace: namespace, from: from, to: now, next_page_token: nil) + .with(namespace: namespace, from: from, to: now, next_page_token: nil, max_page_size: nil) .once expect(connection) .to have_received(:list_open_workflow_executions) - .with(namespace: namespace, from: from, to: now, next_page_token: 'a') + .with(namespace: namespace, from: from, to: now, next_page_token: 'a', max_page_size: nil) .once expect(connection) .to have_received(:list_open_workflow_executions) - .with(namespace: namespace, from: from, to: now, next_page_token: 'b') + .with(namespace: namespace, from: from, to: now, next_page_token: 'b', max_page_size: nil) .once end it 'returns a list of executions' do executions = subject.list_open_workflow_executions(namespace, from) - expect(executions.length).to eq(3) + expect(executions.count).to eq(3) executions.each do |execution| expect(execution).to be_an_instance_of(Temporal::Workflow::ExecutionInfo) end end + + it 'returns the next page token and paginates correctly' do + executions1 = subject.list_open_workflow_executions(namespace, from, max_page_size: 10) + executions1.map do |execution| + expect(execution).to be_an_instance_of(Temporal::Workflow::ExecutionInfo) + end + expect(executions1.next_page_token).to eq('a') + expect(connection) + .to have_received(:list_open_workflow_executions) + .with(namespace: namespace, from: from, to: now, next_page_token: nil, max_page_size: 10) + .once + + executions2 = subject.list_open_workflow_executions(namespace, from, next_page_token: executions1.next_page_token, max_page_size: 10) + executions2.map do |execution| + expect(execution).to be_an_instance_of(Temporal::Workflow::ExecutionInfo) + end + expect(executions2.next_page_token).to eq('b') + expect(connection) + .to have_received(:list_open_workflow_executions) + .with(namespace: namespace, from: from, to: now, next_page_token: 'a', max_page_size: 10) + .once + + executions3 = subject.list_open_workflow_executions(namespace, from, next_page_token: executions2.next_page_token, max_page_size: 10) + executions3.map do |execution| + expect(execution).to be_an_instance_of(Temporal::Workflow::ExecutionInfo) + end + expect(executions3.next_page_token).to eq('') + expect(connection) + .to have_received(:list_open_workflow_executions) + .with(namespace: namespace, from: from, to: now, next_page_token: 'a', max_page_size: 10) + .once + end + + it 'returns the next page and paginates correctly' do + executions1 = subject.list_open_workflow_executions(namespace, from, max_page_size: 10) + executions1.map do |execution| + expect(execution).to be_an_instance_of(Temporal::Workflow::ExecutionInfo) + end + expect(executions1.next_page_token).to eq('a') + expect(connection) + .to have_received(:list_open_workflow_executions) + .with(namespace: namespace, from: from, to: now, next_page_token: nil, max_page_size: 10) + .once + + executions2 = executions1.next_page + executions2.map do |execution| + expect(execution).to be_an_instance_of(Temporal::Workflow::ExecutionInfo) + end + expect(executions2.next_page_token).to eq('b') + expect(connection) + .to have_received(:list_open_workflow_executions) + .with(namespace: namespace, from: from, to: now, next_page_token: 'a', max_page_size: 10) + .once + + executions3 = executions2.next_page + executions3.map do |execution| + expect(execution).to be_an_instance_of(Temporal::Workflow::ExecutionInfo) + end + expect(executions3.next_page_token).to eq('') + expect(connection) + .to have_received(:list_open_workflow_executions) + .with(namespace: namespace, from: from, to: now, next_page_token: 'a', max_page_size: 10) + .once + + end end context 'when given unsupported filter' do @@ -867,7 +931,7 @@ class NamespacedWorkflow < Temporal::Workflow it 'raises ArgumentError' do expect do - subject.list_open_workflow_executions(namespace, from, filter: filter) + subject.list_open_workflow_executions(namespace, from, filter: filter).to_a end.to raise_error(ArgumentError, 'Allowed filters are: [:workflow, :workflow_id]') end end @@ -877,48 +941,48 @@ class NamespacedWorkflow < Temporal::Workflow it 'raises ArgumentError' do expect do - subject.list_open_workflow_executions(namespace, from, filter: filter) + subject.list_open_workflow_executions(namespace, from, filter: filter).count end.to raise_error(ArgumentError, 'Only one filter is allowed') end end context 'when called without filters' do it 'makes a request' do - subject.list_open_workflow_executions(namespace, from) + subject.list_open_workflow_executions(namespace, from).to_a expect(connection) .to have_received(:list_open_workflow_executions) - .with(namespace: namespace, from: from, to: now, next_page_token: nil) + .with(namespace: namespace, from: from, to: now, next_page_token: nil, max_page_size: nil) end end context 'when called with :to' do it 'makes a request' do - subject.list_open_workflow_executions(namespace, from, now - 10) + subject.list_open_workflow_executions(namespace, from, now - 10).to_a expect(connection) .to have_received(:list_open_workflow_executions) - .with(namespace: namespace, from: from, to: now - 10, next_page_token: nil) + .with(namespace: namespace, from: from, to: now - 10, next_page_token: nil, max_page_size: nil) end end context 'when called with a :workflow filter' do it 'makes a request' do - subject.list_open_workflow_executions(namespace, from, filter: { workflow: 'TestWorkflow' }) + subject.list_open_workflow_executions(namespace, from, filter: { workflow: 'TestWorkflow' }).to_a expect(connection) .to have_received(:list_open_workflow_executions) - .with(namespace: namespace, from: from, to: now, next_page_token: nil, workflow: 'TestWorkflow') + .with(namespace: namespace, from: from, to: now, next_page_token: nil, workflow: 'TestWorkflow', max_page_size: nil) end end context 'when called with a :workflow_id filter' do it 'makes a request' do - subject.list_open_workflow_executions(namespace, from, filter: { workflow_id: 'xxx' }) + subject.list_open_workflow_executions(namespace, from, filter: { workflow_id: 'xxx' }).to_a expect(connection) .to have_received(:list_open_workflow_executions) - .with(namespace: namespace, from: from, to: now, next_page_token: nil, workflow_id: 'xxx') + .with(namespace: namespace, from: from, to: now, next_page_token: nil, workflow_id: 'xxx', max_page_size: nil) end end end diff --git a/spec/unit/lib/temporal/grpc_spec.rb b/spec/unit/lib/temporal/grpc_spec.rb index 520f5c11..7042c0de 100644 --- a/spec/unit/lib/temporal/grpc_spec.rb +++ b/spec/unit/lib/temporal/grpc_spec.rb @@ -340,6 +340,45 @@ class TestDeserializer end end + describe '#list_workflow_executions' do + let(:namespace) { 'test-namespace' } + let(:query) { 'StartDate < 2022-04-07T20:48:20Z order by StartTime desc' } + let(:args) { { namespace: namespace, query: query } } + let(:temporal_response) do + Temporal::Api::WorkflowService::V1::ListWorkflowExecutionsResponse.new(executions: [], next_page_token: '') + end + let(:temporal_paginated_response) do + Temporal::Api::WorkflowService::V1::ListWorkflowExecutionsResponse.new(executions: [], next_page_token: 'more-results') + end + + before do + allow(grpc_stub).to receive(:list_workflow_executions).and_return(temporal_response) + end + + it 'makes an API request' do + subject.list_workflow_executions(**args) + + expect(grpc_stub).to have_received(:list_workflow_executions) do |request| + expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListWorkflowExecutionsRequest) + expect(request.page_size).to eq(described_class::DEFAULT_OPTIONS[:max_page_size]) + expect(request.next_page_token).to eq('') + expect(request.namespace).to eq(namespace) + expect(request.query).to eq(query) + end + end + + context 'when next_page_token is supplied' do + it 'makes an API request' do + subject.list_workflow_executions(**args.merge(next_page_token: 'x')) + + expect(grpc_stub).to have_received(:list_workflow_executions) do |request| + expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListWorkflowExecutionsRequest) + expect(request.next_page_token).to eq('x') + end + end + end + end + describe '#list_closed_workflow_executions' do let(:namespace) { 'test-namespace' } let(:from) { Time.now - 600 } From aa30be1b69ee48c30623d25e2f4c5f81d1af6a74 Mon Sep 17 00:00:00 2001 From: jeffschoner-stripe <63118764+jeffschoner-stripe@users.noreply.github.com> Date: Thu, 5 May 2022 08:30:46 -0700 Subject: [PATCH 054/125] [API Breaking] Separate wait_for into two methods: wait_for_any and wait_until (#174) * Refactor wait_for into distinct wait_for_any and wait_for_condition methods * Check finished? on wait_for_any, add more unit specs * Remove dead code, improve error messages in local workflow context * Update new examples to use wait_for_any, wait_until --- examples/workflows/query_workflow.rb | 2 +- .../wait_for_external_signal_workflow.rb | 2 +- .../wait_for_named_signal_workflow.rb | 2 +- examples/workflows/wait_for_workflow.rb | 24 +-- .../testing/local_workflow_context.rb | 18 ++- lib/temporal/workflow/context.rb | 70 ++++----- lib/temporal/workflow/future.rb | 2 +- .../testing/local_workflow_context_spec.rb | 6 +- .../lib/temporal/workflow/context_spec.rb | 147 +++++++++++++++++- .../unit/lib/temporal/workflow/future_spec.rb | 8 +- 10 files changed, 206 insertions(+), 75 deletions(-) diff --git a/examples/workflows/query_workflow.rb b/examples/workflows/query_workflow.rb index 47650ca4..4ecc0f9f 100644 --- a/examples/workflows/query_workflow.rb +++ b/examples/workflows/query_workflow.rb @@ -14,7 +14,7 @@ def execute @last_signal_received = signal end - workflow.wait_for { last_signal_received == "finish" } + workflow.wait_until { last_signal_received == "finish" } @state = "finished" { diff --git a/examples/workflows/wait_for_external_signal_workflow.rb b/examples/workflows/wait_for_external_signal_workflow.rb index 03986309..69bd8eea 100644 --- a/examples/workflows/wait_for_external_signal_workflow.rb +++ b/examples/workflows/wait_for_external_signal_workflow.rb @@ -12,7 +12,7 @@ def execute(expected_signal) signal_counts[signal] += 1 end - workflow.wait_for do + workflow.wait_until do workflow.logger.info("Awaiting #{expected_signal}, signals received so far: #{signals_received}") signals_received.key?(expected_signal) end diff --git a/examples/workflows/wait_for_named_signal_workflow.rb b/examples/workflows/wait_for_named_signal_workflow.rb index 9f715a2a..96f96ece 100644 --- a/examples/workflows/wait_for_named_signal_workflow.rb +++ b/examples/workflows/wait_for_named_signal_workflow.rb @@ -20,7 +20,7 @@ def execute(expected_signal) end timeout_timer = workflow.start_timer(1) - workflow.wait_for(timeout_timer) + workflow.wait_for_any(timeout_timer) { received: signals_received, counts: signal_counts } end diff --git a/examples/workflows/wait_for_workflow.rb b/examples/workflows/wait_for_workflow.rb index 226ac9c4..6b26c28d 100644 --- a/examples/workflows/wait_for_workflow.rb +++ b/examples/workflows/wait_for_workflow.rb @@ -11,7 +11,7 @@ def execute(total_echos, max_echos_at_once, expected_signal) signals_received[signal] = input end - workflow.wait_for do + workflow.wait_until do workflow.logger.info("Awaiting #{expected_signal}, signals received so far: #{signals_received}") signals_received.key?(expected_signal) end @@ -21,35 +21,21 @@ def execute(total_echos, max_echos_at_once, expected_signal) # workflow is completed. long_running_future = LongRunningActivity.execute(15, 0.1) timeout_timer = workflow.start_timer(1) - workflow.wait_for(timeout_timer, long_running_future) + workflow.wait_for_any(timeout_timer, long_running_future) timer_beat_activity = timeout_timer.finished? && !long_running_future.finished? # This should not wait further. The first future has already finished, and therefore # the second one should not be awaited upon. long_timeout_timer = workflow.start_timer(15) - workflow.wait_for(timeout_timer, long_timeout_timer) - raise 'The workflow should not have waited for this timer to complete' if long_timeout_timer.finished? - - block_called = false - workflow.wait_for(timeout_timer) do - # This should never be called because the timeout_timer future was already - # finished before the wait was even called. - block_called = true - end - raise 'Block should not have been called' if block_called - - workflow.wait_for(long_timeout_timer) do - # This condition will immediately be true and not result in any waiting or dispatching - true - end + workflow.wait_for_any(timeout_timer, long_timeout_timer) raise 'The workflow should not have waited for this timer to complete' if long_timeout_timer.finished? activity_futures = {} echos_completed = 0 total_echos.times do |i| - workflow.wait_for do + workflow.wait_until do workflow.logger.info("Activities in flight #{activity_futures.length}") # Pause workflow until the number of active activity futures is less than 2. This # will throttle new activities from being started, guaranteeing that only two of these @@ -66,7 +52,7 @@ def execute(total_echos, max_echos_at_once, expected_signal) end end - workflow.wait_for do + workflow.wait_until do workflow.logger.info("Waiting for queue to drain, size: #{activity_futures.length}") activity_futures.empty? end diff --git a/lib/temporal/testing/local_workflow_context.rb b/lib/temporal/testing/local_workflow_context.rb index 31aa4ef0..c6251e53 100644 --- a/lib/temporal/testing/local_workflow_context.rb +++ b/lib/temporal/testing/local_workflow_context.rb @@ -184,14 +184,18 @@ def wait_for_all(*futures) return end - def wait_for(*futures, &unblock_condition) - if futures.empty? && unblock_condition.nil? - raise 'You must pass either a future or an unblock condition block to wait_for' - end + def wait_for_any(*futures) + return if futures.empty? - while (futures.empty? || futures.none?(&:finished?)) && (!unblock_condition || !unblock_condition.call) - Fiber.yield - end + Fiber.yield while futures.none?(&:finished?) + + return + end + + def wait_until(&unblock_condition) + raise 'You must pass an unblock condition block to wait_for' if unblock_condition.nil? + + Fiber.yield until unblock_condition.call return end diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index 7aaa932a..b5d4ffdb 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -141,7 +141,7 @@ def execute_workflow(workflow_class, *input, **args) child_workflow_started = true end - wait_for { child_workflow_started || future.failed? } + wait_until { child_workflow_started || future.failed? } future end @@ -233,60 +233,56 @@ def continue_as_new(*input, **args) completed! end + # Block workflow progress until all futures finish def wait_for_all(*futures) futures.each(&:wait) return end - # Block workflow progress until any future is finished or any unblock_condition - # block evaluates to true. - def wait_for(*futures, &unblock_condition) - if futures.empty? && unblock_condition.nil? - raise 'You must pass either a future or an unblock condition block to wait_for' - end + # Block workflow progress until one of the futures completes. Passing + # in an empty array will immediately unblock. + def wait_for_any(*futures) + return if futures.empty? || futures.any?(&:finished?) fiber = Fiber.current - should_yield = false blocked = true - if futures.any? - if futures.any?(&:finished?) - blocked = false - else - should_yield = true - futures.each do |future| - dispatcher.register_handler(future.target, Dispatcher::WILDCARD) do - if blocked && future.finished? - # Because this block can run for any dispatch, ensure the fiber is only - # resumed one time by checking if it's already been unblocked. - blocked = false - fiber.resume - end - end + futures.each do |future| + dispatcher.register_handler(future.target, Dispatcher::WILDCARD) do + # Because any of the futures can resume the fiber, ignore any callbacks + # from other futures after unblocking has occurred + if blocked && future.finished? + blocked = false + fiber.resume end end end - if blocked && unblock_condition - if unblock_condition.call + Fiber.yield + + return + end + + # Block workflow progress until the specified block evaluates to true. + def wait_until(&unblock_condition) + raise 'You must pass a block to wait_until' if unblock_condition.nil? + + return if unblock_condition.call + + fiber = Fiber.current + blocked = true + + dispatcher.register_handler(Dispatcher::TARGET_WILDCARD, Dispatcher::WILDCARD) do + # Because this block can run for any dispatch, ensure the fiber is only + # resumed one time by checking if it's already been unblocked. + if blocked && unblock_condition.call blocked = false - should_yield = false - else - should_yield = true - - dispatcher.register_handler(Dispatcher::TARGET_WILDCARD, Dispatcher::WILDCARD) do - # Because this block can run for any dispatch, ensure the fiber is only - # resumed one time by checking if it's already been unblocked. - if blocked && unblock_condition.call - blocked = false - fiber.resume - end - end + fiber.resume end end - Fiber.yield if should_yield + Fiber.yield return end diff --git a/lib/temporal/workflow/future.rb b/lib/temporal/workflow/future.rb index 550a038b..26929e86 100644 --- a/lib/temporal/workflow/future.rb +++ b/lib/temporal/workflow/future.rb @@ -31,7 +31,7 @@ def failed? def wait return if finished? - context.wait_for(self) + context.wait_for_any(self) end def get diff --git a/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb b/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb index 6242be7d..66c68769 100644 --- a/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb +++ b/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb @@ -167,7 +167,7 @@ def execute can_continue = false exited = false fiber = Fiber.new do - workflow_context.wait_for do + workflow_context.wait_until do can_continue end @@ -188,7 +188,7 @@ def execute future = workflow_context.execute_activity(TestAsyncActivity) fiber = Fiber.new do - workflow_context.wait_for(future) do + workflow_context.wait_for_any(future) do false end @@ -212,7 +212,7 @@ def execute future.wait fiber = Fiber.new do - workflow_context.wait_for(future, async_future) + workflow_context.wait_for_any(future, async_future) exited = true end diff --git a/spec/unit/lib/temporal/workflow/context_spec.rb b/spec/unit/lib/temporal/workflow/context_spec.rb index a6dd2921..75731d4c 100644 --- a/spec/unit/lib/temporal/workflow/context_spec.rb +++ b/spec/unit/lib/temporal/workflow/context_spec.rb @@ -1,12 +1,14 @@ require 'temporal/workflow' require 'temporal/workflow/context' +require 'temporal/workflow/dispatcher' +require 'temporal/workflow/future' require 'time' class MyTestWorkflow < Temporal::Workflow; end describe Temporal::Workflow::Context do let(:state_manager) { instance_double('Temporal::Workflow::StateManager') } - let(:dispatcher) { instance_double('Temporal::Workflow::Dispatcher') } + let(:dispatcher) { Temporal::Workflow::Dispatcher.new } let(:query_registry) { instance_double('Temporal::Workflow::QueryRegistry') } let(:metadata) { instance_double('Temporal::Metadata::Workflow') } let(:workflow_context) do @@ -69,4 +71,147 @@ class MyTestWorkflow < Temporal::Workflow; end ).to eq({ 'CustomDatetimeField' => time.utc.iso8601 }) end end + + describe '#wait_for_all' do + let(:target_1) { 'target1' } + let(:future_1) { Temporal::Workflow::Future.new(target_1, workflow_context) } + let(:target_2) { 'target2' } + let(:future_2) { Temporal::Workflow::Future.new(target_2, workflow_context) } + + def wait_for_all + unblocked = false + + Fiber.new do + workflow_context.wait_for_all(future_1, future_2) + unblocked = true + end.resume + + proc { unblocked } + end + + it 'no futures returns immediately' do + workflow_context.wait_for_all + end + + it 'futures already finished' do + future_1.set('done') + future_2.set('also done') + check_unblocked = wait_for_all + + expect(check_unblocked.call).to be(true) + end + + it 'futures finished' do + check_unblocked = wait_for_all + + future_1.set('done') + dispatcher.dispatch(target_1, 'foo') + expect(check_unblocked.call).to be(false) + + future_2.set('also done') + dispatcher.dispatch(target_2, 'foo') + expect(check_unblocked.call).to be(true) + end + end + + describe '#wait_for_any' do + let(:target_1) { 'target1' } + let(:future_1) { Temporal::Workflow::Future.new(target_1, workflow_context) } + let(:target_2) { 'target2' } + let(:future_2) { Temporal::Workflow::Future.new(target_2, workflow_context) } + + def wait_for_any + unblocked = false + + Fiber.new do + workflow_context.wait_for_any(future_1, future_2) + unblocked = true + end.resume + + proc { unblocked } + end + + it 'no futures returns immediately' do + workflow_context.wait_for_any + end + + it 'one future already finished' do + future_1.set("it's done") + check_unblocked = wait_for_any + + expect(check_unblocked.call).to be(true) + end + + it 'one future becomes finished' do + check_unblocked = wait_for_any + future_1.set("it's done") + dispatcher.dispatch(target_1, 'foo') + + expect(check_unblocked.call).to be(true) + + # Dispatch a second time. This should not attempt to + # resume the fiber which by now should already be dead. + dispatcher.dispatch(target_1, 'foo') + end + + it 'both futures becomes finished' do + check_unblocked = wait_for_any + future_1.set("it's done") + future_2.set("it's done") + dispatcher.dispatch(target_1, 'foo') + dispatcher.dispatch(target_2, 'foo') + + expect(check_unblocked.call).to be(true) + end + + it 'one future dispatched but not finished' do + check_unblocked = wait_for_any + dispatcher.dispatch(target_1, 'foo') + + expect(check_unblocked.call).to be(false) + end + end + + describe '#wait_until' do + def wait_until(&blk) + unblocked = false + + Fiber.new do + workflow_context.wait_until(&blk) + unblocked = true + end.resume + + proc { unblocked } + end + + it 'block already true' do + check_unblocked = wait_until { true } + + expect(check_unblocked.call).to be(true) + end + + it 'block is always false' do + check_unblocked = wait_until { false } + + dispatcher.dispatch('target', 'foo') + expect(check_unblocked.call).to be(false) + end + + it 'block becomes true' do + value = false + check_unblocked = wait_until { value } + + expect(check_unblocked.call).to be(false) + + dispatcher.dispatch('target', 'foo') + expect(check_unblocked.call).to be(false) + + value = true + dispatcher.dispatch('target', 'foo') + expect(check_unblocked.call).to be(true) + + # Can dispatch again safely without resuming dead fiber + dispatcher.dispatch('target', 'foo') + end + end end diff --git a/spec/unit/lib/temporal/workflow/future_spec.rb b/spec/unit/lib/temporal/workflow/future_spec.rb index 4fbc5b37..293a7d84 100644 --- a/spec/unit/lib/temporal/workflow/future_spec.rb +++ b/spec/unit/lib/temporal/workflow/future_spec.rb @@ -46,8 +46,8 @@ expect(subject.get).to be exception end - it 'calls context.wait_for if not finished' do - allow(workflow_context).to receive(:wait_for).with(subject) + it 'calls context.wait_for_any if not finished' do + allow(workflow_context).to receive(:wait_for_any).with(subject) subject.get end end @@ -58,8 +58,8 @@ subject.wait end - it 'calls context.wait_for if not already done' do - allow(workflow_context).to receive(:wait_for).with(subject) + it 'calls context.wait_for_any if not already done' do + allow(workflow_context).to receive(:wait_for_any).with(subject) subject.wait end end From d686c32596903a96c8e0eb5375f2e2664b0818f7 Mon Sep 17 00:00:00 2001 From: jeffschoner-stripe <63118764+jeffschoner-stripe@users.noreply.github.com> Date: Thu, 5 May 2022 10:54:45 -0700 Subject: [PATCH 055/125] Add binary checksum (#179) --- examples/bin/worker | 2 +- .../upsert_search_attributes_spec.rb | 13 +++++-- lib/temporal/connection/grpc.rb | 15 ++++---- lib/temporal/worker.rb | 16 ++++++++- lib/temporal/workflow/poller.rb | 11 ++++-- lib/temporal/workflow/task_processor.rb | 9 +++-- .../grpc/history_event_fabricator.rb | 3 +- spec/unit/lib/temporal/grpc_spec.rb | 36 +++++++++++++++++-- spec/unit/lib/temporal/worker_spec.rb | 34 ++++++++++++++++-- .../unit/lib/temporal/workflow/poller_spec.rb | 22 +++++++++--- .../temporal/workflow/task_processor_spec.rb | 15 +++++--- 11 files changed, 143 insertions(+), 33 deletions(-) diff --git a/examples/bin/worker b/examples/bin/worker index 291d16e0..6cf9b257 100755 --- a/examples/bin/worker +++ b/examples/bin/worker @@ -17,7 +17,7 @@ if !ENV['USE_ENCRYPTION'].nil? end end -worker = Temporal::Worker.new +worker = Temporal::Worker.new(binary_checksum: `git show HEAD -s --format=%H`.strip) worker.register_workflow(AsyncActivityWorkflow) worker.register_workflow(AsyncHelloWorldWorkflow) diff --git a/examples/spec/integration/upsert_search_attributes_spec.rb b/examples/spec/integration/upsert_search_attributes_spec.rb index 05d6f71a..6c8c0835 100644 --- a/examples/spec/integration/upsert_search_attributes_spec.rb +++ b/examples/spec/integration/upsert_search_attributes_spec.rb @@ -4,8 +4,9 @@ describe 'Temporal::Workflow::Context.upsert_search_attributes', :integration do it 'can upsert a search attribute and then retrieve it' do workflow_id = 'upsert_search_attributes_test_wf-' + SecureRandom.uuid + expected_binary_checksum = `git show HEAD -s --format=%H`.strip - expected_attributes = { + expected_added_attributes = { 'CustomStringField' => 'moo', 'CustomBoolField' => true, 'CustomDoubleField' => 3.14, @@ -15,7 +16,7 @@ run_id = Temporal.start_workflow( UpsertSearchAttributesWorkflow, - *expected_attributes.values, + *expected_added_attributes.values, options: { workflow_id: workflow_id, }, @@ -26,7 +27,13 @@ workflow_id: workflow_id, run_id: run_id, ) - expect(added_attributes).to eq(expected_attributes) + expect(added_attributes).to eq(expected_added_attributes) + + # These attributes are set for the worker in bin/worker + expected_attributes = { + # Contains a list of all binary checksums seen for this workflow execution + 'BinaryChecksums' => [expected_binary_checksum] + }.merge(expected_added_attributes) execution_info = Temporal.fetch_workflow_execution_info( integration_spec_namespace, diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index b1c65bed..dd306b97 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -161,13 +161,14 @@ def get_workflow_execution_history( client.get_workflow_execution_history(request, deadline: deadline) end - def poll_workflow_task_queue(namespace:, task_queue:) + def poll_workflow_task_queue(namespace:, task_queue:, binary_checksum:) request = Temporal::Api::WorkflowService::V1::PollWorkflowTaskQueueRequest.new( identity: identity, namespace: namespace, task_queue: Temporal::Api::TaskQueue::V1::TaskQueue.new( name: task_queue - ) + ), + binary_checksum: binary_checksum ) poll_mutex.synchronize do @@ -191,25 +192,27 @@ def respond_query_task_completed(namespace:, task_token:, query_result:) client.respond_query_task_completed(request) end - def respond_workflow_task_completed(namespace:, task_token:, commands:, query_results: {}) + def respond_workflow_task_completed(namespace:, task_token:, commands:, binary_checksum:, query_results: {}) request = Temporal::Api::WorkflowService::V1::RespondWorkflowTaskCompletedRequest.new( namespace: namespace, identity: identity, task_token: task_token, commands: Array(commands).map { |(_, command)| Serializer.serialize(command) }, - query_results: query_results.transform_values { |value| Serializer.serialize(value) } + query_results: query_results.transform_values { |value| Serializer.serialize(value) }, + binary_checksum: binary_checksum ) client.respond_workflow_task_completed(request) end - def respond_workflow_task_failed(namespace:, task_token:, cause:, exception: nil) + def respond_workflow_task_failed(namespace:, task_token:, cause:, exception:, binary_checksum:) request = Temporal::Api::WorkflowService::V1::RespondWorkflowTaskFailedRequest.new( namespace: namespace, identity: identity, task_token: task_token, cause: cause, - failure: Serializer::Failure.new(exception).to_proto + failure: Serializer::Failure.new(exception).to_proto, + binary_checksum: binary_checksum ) client.respond_workflow_task_failed(request) end diff --git a/lib/temporal/worker.rb b/lib/temporal/worker.rb index 81881b67..20071a27 100644 --- a/lib/temporal/worker.rb +++ b/lib/temporal/worker.rb @@ -8,10 +8,23 @@ module Temporal class Worker # activity_thread_pool_size: number of threads that the poller can use to run activities. # can be set to 1 if you want no paralellism in your activities, at the cost of throughput. + + # binary_checksum: The binary checksum identifies the version of workflow worker code. It is set on each completed or failed workflow + # task. It is present in API responses that return workflow execution info, and is shown in temporal-web and tctl. + # It is traditionally a checksum of the application binary. However, Temporal server treats this as an opaque + # identifier and it does not have to be a "checksum". Typical values for a Ruby application might include the hash + # of the latest git commit or a semantic version number. + # + # It can be used to reset workflow history to before a "bad binary" was deployed. Bad checksum values can also + # be marked at the namespace level. This will cause Temporal server to reject any polling for workflow tasks + # from workers with these bad versions. + # + # See https://docs.temporal.io/docs/tctl/how-to-use-tctl/#recovery-from-bad-deployment----auto-reset-workflow def initialize( config = Temporal.configuration, activity_thread_pool_size: Temporal::Activity::Poller::DEFAULT_OPTIONS[:thread_pool_size], - workflow_thread_pool_size: Temporal::Workflow::Poller::DEFAULT_OPTIONS[:thread_pool_size] + workflow_thread_pool_size: Temporal::Workflow::Poller::DEFAULT_OPTIONS[:thread_pool_size], + binary_checksum: Temporal::Workflow::Poller::DEFAULT_OPTIONS[:binary_checksum] ) @config = config @workflows = Hash.new { |hash, key| hash[key] = ExecutableLookup.new } @@ -25,6 +38,7 @@ def initialize( } @workflow_poller_options = { thread_pool_size: workflow_thread_pool_size, + binary_checksum: binary_checksum } end diff --git a/lib/temporal/workflow/poller.rb b/lib/temporal/workflow/poller.rb index cf557b79..095f28b0 100644 --- a/lib/temporal/workflow/poller.rb +++ b/lib/temporal/workflow/poller.rb @@ -9,7 +9,8 @@ module Temporal class Workflow class Poller DEFAULT_OPTIONS = { - thread_pool_size: 10 + thread_pool_size: 10, + binary_checksum: nil }.freeze def initialize(namespace, task_queue, workflow_lookup, config, middleware = [], options = {}) @@ -75,7 +76,7 @@ def poll_loop end def poll_for_task - connection.poll_workflow_task_queue(namespace: namespace, task_queue: task_queue) + connection.poll_workflow_task_queue(namespace: namespace, task_queue: task_queue, binary_checksum: binary_checksum) rescue ::GRPC::Cancelled # We're shutting down and we've already reported that in the logs nil @@ -89,12 +90,16 @@ def poll_for_task def process(task) middleware_chain = Middleware::Chain.new(middleware) - TaskProcessor.new(task, namespace, workflow_lookup, middleware_chain, config).process + TaskProcessor.new(task, namespace, workflow_lookup, middleware_chain, config, binary_checksum).process end def thread_pool @thread_pool ||= ThreadPool.new(options[:thread_pool_size]) end + + def binary_checksum + @options[:binary_checksum] + end end end end diff --git a/lib/temporal/workflow/task_processor.rb b/lib/temporal/workflow/task_processor.rb index 4b80918a..2bd7b7d6 100644 --- a/lib/temporal/workflow/task_processor.rb +++ b/lib/temporal/workflow/task_processor.rb @@ -22,7 +22,7 @@ def query_args MAX_FAILED_ATTEMPTS = 1 LEGACY_QUERY_KEY = :legacy_query - def initialize(task, namespace, workflow_lookup, middleware_chain, config) + def initialize(task, namespace, workflow_lookup, middleware_chain, config, binary_checksum) @task = task @namespace = namespace @metadata = Metadata.generate_workflow_task_metadata(task, namespace) @@ -31,6 +31,7 @@ def initialize(task, namespace, workflow_lookup, middleware_chain, config) @workflow_class = workflow_lookup.find(workflow_name) @middleware_chain = middleware_chain @config = config + @binary_checksum = binary_checksum end def process @@ -71,7 +72,7 @@ def process private attr_reader :task, :namespace, :task_token, :workflow_name, :workflow_class, - :middleware_chain, :metadata, :config + :middleware_chain, :metadata, :config, :binary_checksum def connection @connection ||= Temporal::Connection.generate(config.for_connection) @@ -128,6 +129,7 @@ def complete_task(commands, query_results) namespace: namespace, task_token: task_token, commands: commands, + binary_checksum: binary_checksum, query_results: query_results ) end @@ -159,7 +161,8 @@ def fail_task(error) namespace: namespace, task_token: task_token, cause: Temporal::Api::Enums::V1::WorkflowTaskFailedCause::WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE, - exception: error + exception: error, + binary_checksum: binary_checksum ) rescue StandardError => error Temporal.logger.error("Unable to fail Workflow task", metadata.to_h.merge(error: error.inspect)) diff --git a/spec/fabricators/grpc/history_event_fabricator.rb b/spec/fabricators/grpc/history_event_fabricator.rb index 9e8538eb..957f717d 100644 --- a/spec/fabricators/grpc/history_event_fabricator.rb +++ b/spec/fabricators/grpc/history_event_fabricator.rb @@ -73,7 +73,8 @@ class TestSerializer Temporal::Api::History::V1::WorkflowTaskCompletedEventAttributes.new( scheduled_event_id: attrs[:event_id] - 2, started_event_id: attrs[:event_id] - 1, - identity: 'test-worker@test-host' + identity: 'test-worker@test-host', + binary_checksum: 'v1.0.0', ) end end diff --git a/spec/unit/lib/temporal/grpc_spec.rb b/spec/unit/lib/temporal/grpc_spec.rb index 7042c0de..d828d537 100644 --- a/spec/unit/lib/temporal/grpc_spec.rb +++ b/spec/unit/lib/temporal/grpc_spec.rb @@ -2,13 +2,16 @@ require 'temporal/workflow/query_result' describe Temporal::Connection::GRPC do - subject { Temporal::Connection::GRPC.new(nil, nil, nil) } + let(:identity) { 'my-identity' } + let(:binary_checksum) { 'v1.0.0' } let(:grpc_stub) { double('grpc stub') } let(:namespace) { 'test-namespace' } let(:workflow_id) { SecureRandom.uuid } let(:run_id) { SecureRandom.uuid } let(:now) { Time.now} + subject { Temporal::Connection::GRPC.new(nil, nil, identity) } + class TestDeserializer extend Temporal::Concerns::Payloads end @@ -541,7 +544,8 @@ class TestDeserializer namespace: namespace, task_token: task_token, commands: [], - query_results: query_results + query_results: query_results, + binary_checksum: binary_checksum ) expect(grpc_stub).to have_received(:respond_workflow_task_completed) do |request| @@ -549,6 +553,8 @@ class TestDeserializer expect(request.task_token).to eq(task_token) expect(request.namespace).to eq(namespace) expect(request.commands).to be_empty + expect(request.identity).to eq(identity) + expect(request.binary_checksum).to eq(binary_checksum) expect(request.query_results.length).to eq(2) @@ -567,4 +573,30 @@ class TestDeserializer end end end + + describe '#respond_workflow_task_failed' do + let(:task_token) { 'task-token' } + let(:cause) { Temporal::Api::Enums::V1::WorkflowTaskFailedCause::WORKFLOW_TASK_FAILED_CAUSE_UNHANDLED_COMMAND } + + before { allow(grpc_stub).to receive(:respond_workflow_task_failed) } + + it 'calls GRPC service with supplied arguments' do + subject.respond_workflow_task_failed( + namespace: namespace, + task_token: task_token, + cause: cause, + exception: Exception.new('something went wrong'), + binary_checksum: binary_checksum + ) + + expect(grpc_stub).to have_received(:respond_workflow_task_failed) do |request| + expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::RespondWorkflowTaskFailedRequest) + expect(request.namespace).to eq(namespace) + expect(request.task_token).to eq(task_token) + expect(request.cause).to be(Temporal::Api::Enums::V1::WorkflowTaskFailedCause.lookup(cause)) + expect(request.identity).to eq(identity) + expect(request.binary_checksum).to eq(binary_checksum) + end + end + end end diff --git a/spec/unit/lib/temporal/worker_spec.rb b/spec/unit/lib/temporal/worker_spec.rb index d986958a..c1c5cd16 100644 --- a/spec/unit/lib/temporal/worker_spec.rb +++ b/spec/unit/lib/temporal/worker_spec.rb @@ -144,7 +144,8 @@ class TestWorkerActivity < Temporal::Activity an_instance_of(Temporal::ExecutableLookup), config, [], - thread_pool_size: 10 + thread_pool_size: 10, + binary_checksum: nil ) .and_return(workflow_poller_1) @@ -156,7 +157,8 @@ class TestWorkerActivity < Temporal::Activity an_instance_of(Temporal::ExecutableLookup), config, [], - thread_pool_size: 10 + thread_pool_size: 10, + binary_checksum: nil ) .and_return(workflow_poller_2) @@ -219,7 +221,32 @@ class TestWorkerActivity < Temporal::Activity worker.start expect(activity_poller).to have_received(:start) + end + + it 'can have a worklow poller with a binary checksum' do + workflow_poller = instance_double(Temporal::Workflow::Poller, start: nil) + binary_checksum = 'abc123' + expect(Temporal::Workflow::Poller) + .to receive(:new) + .with( + 'default-namespace', + 'default-task-queue', + an_instance_of(Temporal::ExecutableLookup), + an_instance_of(Temporal::Configuration), + [], + thread_pool_size: 10, + binary_checksum: binary_checksum + ) + .and_return(workflow_poller) + + worker = Temporal::Worker.new(binary_checksum: binary_checksum) + allow(worker).to receive(:shutting_down?).and_return(true) + worker.register_workflow(TestWorkerWorkflow) + worker.register_activity(TestWorkerActivity) + + worker.start + expect(workflow_poller).to have_received(:start) end context 'when middleware is configured' do @@ -252,7 +279,8 @@ class TestWorkerActivity < Temporal::Activity an_instance_of(Temporal::ExecutableLookup), config, [entry_1], - thread_pool_size: 10 + thread_pool_size: 10, + binary_checksum: nil ) .and_return(workflow_poller_1) diff --git a/spec/unit/lib/temporal/workflow/poller_spec.rb b/spec/unit/lib/temporal/workflow/poller_spec.rb index 7f907f69..1fdb023d 100644 --- a/spec/unit/lib/temporal/workflow/poller_spec.rb +++ b/spec/unit/lib/temporal/workflow/poller_spec.rb @@ -10,8 +10,20 @@ let(:config) { Temporal::Configuration.new } let(:middleware_chain) { instance_double(Temporal::Middleware::Chain) } let(:middleware) { [] } - - subject { described_class.new(namespace, task_queue, lookup, config, middleware) } + let(:binary_checksum) { 'v1.0.0' } + + subject do + described_class.new( + namespace, + task_queue, + lookup, + config, + middleware, + { + binary_checksum: binary_checksum + } + ) + end before do allow(Temporal::Connection).to receive(:generate).and_return(connection) @@ -31,7 +43,7 @@ expect(connection) .to have_received(:poll_workflow_task_queue) - .with(namespace: namespace, task_queue: task_queue) + .with(namespace: namespace, task_queue: task_queue, binary_checksum: binary_checksum) .twice end @@ -75,7 +87,7 @@ expect(Temporal::Workflow::TaskProcessor) .to have_received(:new) - .with(task, namespace, lookup, middleware_chain, config) + .with(task, namespace, lookup, middleware_chain, config, binary_checksum) expect(task_processor).to have_received(:process) end @@ -98,7 +110,7 @@ def call(_); end expect(Temporal::Middleware::Chain).to have_received(:new).with(middleware) expect(Temporal::Workflow::TaskProcessor) .to have_received(:new) - .with(task, namespace, lookup, middleware_chain, config) + .with(task, namespace, lookup, middleware_chain, config, binary_checksum) end end end diff --git a/spec/unit/lib/temporal/workflow/task_processor_spec.rb b/spec/unit/lib/temporal/workflow/task_processor_spec.rb index 5a02569e..aa6c7029 100644 --- a/spec/unit/lib/temporal/workflow/task_processor_spec.rb +++ b/spec/unit/lib/temporal/workflow/task_processor_spec.rb @@ -3,7 +3,7 @@ require 'temporal/configuration' describe Temporal::Workflow::TaskProcessor do - subject { described_class.new(task, namespace, lookup, middleware_chain, config) } + subject { described_class.new(task, namespace, lookup, middleware_chain, config, binary_checksum) } let(:namespace) { 'test-namespace' } let(:lookup) { instance_double('Temporal::ExecutableLookup', find: nil) } @@ -16,6 +16,7 @@ let(:middleware_chain) { Temporal::Middleware::Chain.new } let(:input) { ['arg1', 'arg2'] } let(:config) { Temporal::Configuration.new } + let(:binary_checksum) { 'v1.0.0' } describe '#process' do let(:context) { instance_double('Temporal::Workflow::Context') } @@ -113,6 +114,7 @@ namespace: namespace, task_token: task.task_token, commands: commands, + binary_checksum: binary_checksum, query_results: { query_id => query_result } ) end @@ -150,7 +152,7 @@ expect(connection).to_not have_received(:respond_query_task_completed) expect(connection) .to have_received(:respond_workflow_task_completed) - .with(namespace: namespace, task_token: task.task_token, commands: commands, query_results: nil) + .with(namespace: namespace, task_token: task.task_token, commands: commands, query_results: nil, binary_checksum: binary_checksum) end it 'ignores connection exception' do @@ -196,7 +198,8 @@ namespace: namespace, task_token: task.task_token, cause: Temporal::Api::Enums::V1::WorkflowTaskFailedCause::WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE, - exception: exception + exception: exception, + binary_checksum: binary_checksum ) end end @@ -211,7 +214,8 @@ namespace: namespace, task_token: task.task_token, cause: Temporal::Api::Enums::V1::WorkflowTaskFailedCause::WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE, - exception: exception + exception: exception, + binary_checksum: binary_checksum ) end @@ -325,7 +329,8 @@ namespace: namespace, task_token: task.task_token, cause: Temporal::Api::Enums::V1::WorkflowTaskFailedCause::WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE, - exception: an_instance_of(Temporal::UnexpectedResponse) + exception: an_instance_of(Temporal::UnexpectedResponse), + binary_checksum: binary_checksum ) end end From dbf327384cac9e034300a8d19de2cad855009bd9 Mon Sep 17 00:00:00 2001 From: calum-stripe <98350978+calum-stripe@users.noreply.github.com> Date: Thu, 5 May 2022 10:56:06 -0700 Subject: [PATCH 056/125] Child workflow futures (#181) * added future to execute workflow future added should_wait_for_start fixed content added attr_accessor added context fixed formatting removed import * fixed unit tests * fixed * removed unused code * added child workflow future fixed integrations tests added more unit tests fixed integration tests updated comment removed old code * fixed nits * fixed tests --- examples/bin/worker | 1 + .../start_child_workflow_workflow_spec.rb | 24 +++++ examples/workflows/parent_close_workflow.rb | 5 +- .../start_child_workflow_workflow.rb | 17 ++++ .../workflow/child_workflow_future.rb | 18 ++++ lib/temporal/workflow/context.rb | 30 +++--- lib/temporal/workflow/state_manager.rb | 2 +- .../lib/temporal/workflow/context_spec.rb | 95 +++++++++++++++++++ 8 files changed, 178 insertions(+), 14 deletions(-) create mode 100644 examples/spec/integration/start_child_workflow_workflow_spec.rb create mode 100644 examples/workflows/start_child_workflow_workflow.rb create mode 100644 lib/temporal/workflow/child_workflow_future.rb diff --git a/examples/bin/worker b/examples/bin/worker index 6cf9b257..cccf739d 100755 --- a/examples/bin/worker +++ b/examples/bin/worker @@ -48,6 +48,7 @@ worker.register_workflow(SideEffectWorkflow) worker.register_workflow(SignalWithStartWorkflow) worker.register_workflow(SimpleTimerWorkflow) worker.register_workflow(SlowChildWorkflow) +worker.register_workflow(StartChildWorkflowWorkflow) worker.register_workflow(TimeoutWorkflow) worker.register_workflow(TripBookingWorkflow) worker.register_workflow(UpsertSearchAttributesWorkflow) diff --git a/examples/spec/integration/start_child_workflow_workflow_spec.rb b/examples/spec/integration/start_child_workflow_workflow_spec.rb new file mode 100644 index 00000000..e4e1be9e --- /dev/null +++ b/examples/spec/integration/start_child_workflow_workflow_spec.rb @@ -0,0 +1,24 @@ +require 'workflows/start_child_workflow_workflow' + +describe StartChildWorkflowWorkflow, :integration do + subject { described_class } + + it 'StartChildWorkflowWorkflow returns the child workflows information on the start future' do + workflow_id = 'parent_close_test_wf-' + SecureRandom.uuid + child_workflow_id = 'slow_child_test_wf-' + SecureRandom.uuid + + run_id = Temporal.start_workflow( + StartChildWorkflowWorkflow, + child_workflow_id, + options: { workflow_id: workflow_id } + ) + + result = Temporal.await_workflow_result( + StartChildWorkflowWorkflow, + workflow_id: workflow_id, + run_id: run_id, + ) + + expect(result.workflow_id).to start_with(child_workflow_id) + end +end diff --git a/examples/workflows/parent_close_workflow.rb b/examples/workflows/parent_close_workflow.rb index 355bc7ca..a711d882 100644 --- a/examples/workflows/parent_close_workflow.rb +++ b/examples/workflows/parent_close_workflow.rb @@ -6,7 +6,10 @@ def execute(child_workflow_id, parent_close_policy) workflow_id: child_workflow_id, parent_close_policy: parent_close_policy, } - SlowChildWorkflow.execute(1, options: options) + result = SlowChildWorkflow.execute(1, options: options) + + # waits for the child workflow to start before exiting + result.child_workflow_execution_future.get return end end diff --git a/examples/workflows/start_child_workflow_workflow.rb b/examples/workflows/start_child_workflow_workflow.rb new file mode 100644 index 00000000..de6bb9b0 --- /dev/null +++ b/examples/workflows/start_child_workflow_workflow.rb @@ -0,0 +1,17 @@ +require 'workflows/slow_child_workflow' + +class StartChildWorkflowWorkflow < Temporal::Workflow + def execute(child_workflow_id) + options = { + workflow_id: child_workflow_id, + parent_close_policy: :abandon, + } + result = SlowChildWorkflow.execute(1, options: options) + child_workflow_execution = result.child_workflow_execution_future.get + + # return back the workflow_id and run_id so we can nicely check if + # everything was passed correctly + response = Struct.new(:workflow_id, :run_id) + response.new(child_workflow_execution.workflow_id, child_workflow_execution.run_id) + end +end diff --git a/lib/temporal/workflow/child_workflow_future.rb b/lib/temporal/workflow/child_workflow_future.rb new file mode 100644 index 00000000..3e56a835 --- /dev/null +++ b/lib/temporal/workflow/child_workflow_future.rb @@ -0,0 +1,18 @@ +require 'fiber' +require 'temporal/workflow/future' + +module Temporal + class Workflow + # A future that represents a child workflow execution + class ChildWorkflowFuture < Future + attr_reader :child_workflow_execution_future + + def initialize(target, context, cancelation_id: nil) + super + + # create a future which will keep track of when the child workflow starts + @child_workflow_execution_future = Future.new(target, context, cancelation_id: cancelation_id) + end + end + end +end diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index b5d4ffdb..a5d50e89 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -7,6 +7,7 @@ require 'temporal/workflow/command' require 'temporal/workflow/context_helpers' require 'temporal/workflow/future' +require 'temporal/workflow/child_workflow_future' require 'temporal/workflow/replay_aware_logger' require 'temporal/workflow/state_manager' require 'temporal/workflow/signal' @@ -123,27 +124,32 @@ def execute_workflow(workflow_class, *input, **args) ) target, cancelation_id = schedule_command(command) - future = Future.new(target, self, cancelation_id: cancelation_id) + + child_workflow_future = ChildWorkflowFuture.new(target, self, cancelation_id: cancelation_id) dispatcher.register_handler(target, 'completed') do |result| - future.set(result) - future.success_callbacks.each { |callback| call_in_fiber(callback, result) } + child_workflow_future.set(result) + child_workflow_future.success_callbacks.each { |callback| call_in_fiber(callback, result) } end dispatcher.register_handler(target, 'failed') do |exception| - future.fail(exception) - future.failure_callbacks.each { |callback| call_in_fiber(callback, exception) } - end + # if the child workflow didn't start already then also fail that future + unless child_workflow_future.child_workflow_execution_future.ready? + child_workflow_future.child_workflow_execution_future.fail(exception) + child_workflow_future.child_workflow_execution_future.failure_callbacks.each { |callback| call_in_fiber(callback, exception) } + end - # Temporal docs say that we *must* wait for the child to get spawned: - child_workflow_started = false - dispatcher.register_handler(target, 'started') do - child_workflow_started = true + child_workflow_future.fail(exception) + child_workflow_future.failure_callbacks.each { |callback| call_in_fiber(callback, exception) } end - wait_until { child_workflow_started || future.failed? } + dispatcher.register_handler(target, 'started') do |event| + # once the workflow starts, complete the child workflow execution future + child_workflow_future.child_workflow_execution_future.set(event) + child_workflow_future.child_workflow_execution_future.success_callbacks.each { |callback| call_in_fiber(callback, result) } + end - future + child_workflow_future end def execute_workflow!(workflow_class, *input, **args) diff --git a/lib/temporal/workflow/state_manager.rb b/lib/temporal/workflow/state_manager.rb index 5fd2923b..663d8169 100644 --- a/lib/temporal/workflow/state_manager.rb +++ b/lib/temporal/workflow/state_manager.rb @@ -247,7 +247,7 @@ def apply_event(event) ) dispatch(history_target, 'failed', error) when 'CHILD_WORKFLOW_EXECUTION_STARTED' - dispatch(history_target, 'started') + dispatch(history_target, 'started', event.attributes.workflow_execution) state_machine.start when 'CHILD_WORKFLOW_EXECUTION_COMPLETED' diff --git a/spec/unit/lib/temporal/workflow/context_spec.rb b/spec/unit/lib/temporal/workflow/context_spec.rb index 75731d4c..e008eb9e 100644 --- a/spec/unit/lib/temporal/workflow/context_spec.rb +++ b/spec/unit/lib/temporal/workflow/context_spec.rb @@ -21,6 +21,7 @@ class MyTestWorkflow < Temporal::Workflow; end query_registry ) end + let(:child_workflow_execution) { Fabricate(:api_workflow_execution) } describe '#on_query' do let(:handler) { Proc.new {} } @@ -36,6 +37,100 @@ class MyTestWorkflow < Temporal::Workflow; end end end + describe '#execute_workflow' do + it 'returns the correct futures when starting a child workflow' do + allow(state_manager).to receive(:schedule) + allow(dispatcher).to receive(:register_handler) + + result = workflow_context.execute_workflow(MyTestWorkflow) + expect(result).to be_instance_of(Temporal::Workflow::ChildWorkflowFuture) + expect(result.child_workflow_execution_future).to be_instance_of(Temporal::Workflow::Future) + end + + it 'futures behave as expected when events are successful' do + started_proc = nil + completed_proc = nil + + allow(state_manager).to receive(:schedule) + allow(dispatcher).to receive(:register_handler) do |target, event_name, &handler| + case event_name + when 'started' + started_proc = handler + when 'completed' + completed_proc = handler + end + end + + child_workflow_future = workflow_context.execute_workflow(MyTestWorkflow) + + # expect all futures to be false as nothing has happened + expect(child_workflow_future.finished?).to be false + expect(child_workflow_future.child_workflow_execution_future.finished?).to be false + + # dispatch the start event and check if the child workflow execution changes to true + started_proc.call(child_workflow_execution) + expect(child_workflow_future.finished?).to be false + expect(child_workflow_future.child_workflow_execution_future.finished?).to be true + expect(child_workflow_future.child_workflow_execution_future.get).to be_instance_of(Temporal::Api::Common::V1::WorkflowExecution) + + # complete the workflow via dispatch and check if the child workflow future is finished + completed_proc.call('finished result') + expect(child_workflow_future.finished?).to be true + expect(child_workflow_future.child_workflow_execution_future.finished?).to be true + end + + it 'futures behave as expected when child workflow fails' do + started_proc = nil + failed_proc = nil + + allow(state_manager).to receive(:schedule) + allow(dispatcher).to receive(:register_handler) do |target, event_name, &handler| + case event_name + when 'started' + started_proc = handler + when 'failed' + failed_proc = handler + end + end + + child_workflow_future = workflow_context.execute_workflow(MyTestWorkflow) + + # expect all futures to be false as nothing has happened + expect(child_workflow_future.finished?).to be false + expect(child_workflow_future.child_workflow_execution_future.finished?).to be false + + started_proc.call(child_workflow_execution) + + # dispatch the failed event and check the child_workflow_future failed but the child_workflow_execution_future finished + failed_proc.call(Temporal::Workflow::Errors.generate_error_for_child_workflow_start("failed to start", "random-workflow-id")) + expect(child_workflow_future.failed?).to be true + expect(child_workflow_future.child_workflow_execution_future.failed?).to be false + end + + it 'futures behave as expected when child execution workflow fails to start' do + failed_proc = nil + + allow(state_manager).to receive(:schedule) + allow(dispatcher).to receive(:register_handler) do |target, event_name, &handler| + case event_name + when 'failed' + failed_proc = handler + end + end + + child_workflow_future = workflow_context.execute_workflow(MyTestWorkflow) + + # expect all futures to be false as nothing has happened + expect(child_workflow_future.finished?).to be false + expect(child_workflow_future.child_workflow_execution_future.finished?).to be false + + # dispatch the failed event and check what happens + failed_proc.call(Temporal::Workflow::Errors.generate_error_for_child_workflow_start("failed to start", "random-workflow-id")) + expect(child_workflow_future.failed?).to be true + expect(child_workflow_future.child_workflow_execution_future.failed?).to be true + end + end + describe '#upsert_search_attributes' do it 'does not accept nil' do expect do From dd0a489952d903bc51573b0cd7a952203860c086 Mon Sep 17 00:00:00 2001 From: jeffschoner-stripe <63118764+jeffschoner-stripe@users.noreply.github.com> Date: Mon, 6 Jun 2022 11:52:48 -0700 Subject: [PATCH 057/125] Remove finished dispatcher handlers, order dispatch handlers (#183) --- lib/temporal/workflow/context.rb | 26 ++++------- lib/temporal/workflow/dispatcher.rb | 41 ++++++++++++++--- .../lib/temporal/workflow/dispatcher_spec.rb | 45 ++++++++++++++++--- 3 files changed, 82 insertions(+), 30 deletions(-) diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index a5d50e89..23e86ad8 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -252,20 +252,15 @@ def wait_for_any(*futures) return if futures.empty? || futures.any?(&:finished?) fiber = Fiber.current - blocked = true - futures.each do |future| + handlers = futures.map do |future| dispatcher.register_handler(future.target, Dispatcher::WILDCARD) do - # Because any of the futures can resume the fiber, ignore any callbacks - # from other futures after unblocking has occurred - if blocked && future.finished? - blocked = false - fiber.resume - end + fiber.resume if future.finished? end end Fiber.yield + handlers.each(&:unregister) return end @@ -277,18 +272,13 @@ def wait_until(&unblock_condition) return if unblock_condition.call fiber = Fiber.current - blocked = true - - dispatcher.register_handler(Dispatcher::TARGET_WILDCARD, Dispatcher::WILDCARD) do - # Because this block can run for any dispatch, ensure the fiber is only - # resumed one time by checking if it's already been unblocked. - if blocked && unblock_condition.call - blocked = false - fiber.resume - end + + handler = dispatcher.register_handler(Dispatcher::TARGET_WILDCARD, Dispatcher::WILDCARD) do + fiber.resume if unblock_condition.call end Fiber.yield + handler.unregister return end @@ -316,6 +306,8 @@ def on_signal(signal_name = nil, &block) call_in_fiber(block, signal, input) end end + + return end def on_query(query, &block) diff --git a/lib/temporal/workflow/dispatcher.rb b/lib/temporal/workflow/dispatcher.rb index 58b32f46..d327cbe5 100644 --- a/lib/temporal/workflow/dispatcher.rb +++ b/lib/temporal/workflow/dispatcher.rb @@ -1,3 +1,5 @@ +require 'temporal/errors' + module Temporal class Workflow # This provides a generic event dispatcher mechanism. There are two main entry @@ -13,18 +15,44 @@ class Workflow # the event_name. The order of this dispatch is not guaranteed. # class Dispatcher + # Raised if a duplicate ID is encountered during dispatch handling. + # This likely indicates a bug in temporal-ruby or that unsupported multithreaded + # workflow code is being used. + class DuplicateIDError < InternalError; end + + # Tracks a registered handle so that it can be unregistered later + # The handlers are passed by reference here to be mutated (removed) by the + # unregister call below. + class RegistrationHandle + def initialize(handlers_for_target, id) + @handlers_for_target = handlers_for_target + @id = id + end + + # Unregister the handler from the dispatcher + def unregister + handlers_for_target.delete(id) + end + + private + + attr_reader :handlers_for_target, :id + end + WILDCARD = '*'.freeze TARGET_WILDCARD = '*'.freeze EventStruct = Struct.new(:event_name, :handler) def initialize - @handlers = Hash.new { |hash, key| hash[key] = [] } + @handlers = Hash.new { |hash, key| hash[key] = {} } + @next_id = 0 end def register_handler(target, event_name, &handler) - handlers[target] << EventStruct.new(event_name, handler) - self + @next_id += 1 + handlers[target][@next_id] = EventStruct.new(event_name, handler) + RegistrationHandle.new(handlers[target], @next_id) end def dispatch(target, event_name, args = nil) @@ -39,9 +67,10 @@ def dispatch(target, event_name, args = nil) def handlers_for(target, event_name) handlers[target] - .concat(handlers[TARGET_WILDCARD]) - .select { |event_struct| match?(event_struct, event_name) } - .map(&:handler) + .merge(handlers[TARGET_WILDCARD]) { raise DuplicateIDError.new('Cannot resolve duplicate dispatcher handler IDs') } + .select { |_, event_struct| match?(event_struct, event_name) } + .sort + .map { |_, event_struct| event_struct.handler } end def match?(event_struct, event_name) diff --git a/spec/unit/lib/temporal/workflow/dispatcher_spec.rb b/spec/unit/lib/temporal/workflow/dispatcher_spec.rb index 6e1ae8d6..18aa1a9e 100644 --- a/spec/unit/lib/temporal/workflow/dispatcher_spec.rb +++ b/spec/unit/lib/temporal/workflow/dispatcher_spec.rb @@ -8,7 +8,10 @@ describe '#register_handler' do let(:block) { -> { 'handler body' } } let(:event_name) { 'signaled' } - let(:dispatcher) { subject.register_handler(target, event_name, &block) } + let(:dispatcher) do + subject.register_handler(target, event_name, &block) + subject + end let(:handlers) { dispatcher.send(:handlers) } context 'with default handler_name' do @@ -19,17 +22,17 @@ end it 'stores the target and handler once' do - expect(handlers[target]).to be_kind_of(Array) + expect(handlers[target]).to be_kind_of(Hash) expect(handlers[target].count).to eq 1 end it 'associates the event name with the target' do - event = handlers[target].first + event = handlers[target][1] expect(event.event_name).to eq(event_name) end it 'associates the handler with the target' do - event = handlers[target].first + event = handlers[target][1] expect(event.handler).to eq(block) end end @@ -43,20 +46,44 @@ end it 'stores the target and handler once' do - expect(handlers[target]).to be_kind_of(Array) + expect(handlers[target]).to be_kind_of(Hash) expect(handlers[target].count).to eq 1 end it 'associates the event name and handler name with the target' do - event = handlers[target].first + event = handlers[target][1] expect(event.event_name).to eq(event_name) end it 'associates the handler with the target' do - event = handlers[target].first + event = handlers[target][1] expect(event.handler).to eq(block) end end + + it 'removes a given handler against the target' do + block1 = -> { 'handler body' } + block2 = -> { 'other handler body' } + block3 = -> { 'yet another handler body' } + + handle1 = subject.register_handler(target, 'signaled', &block1) + subject.register_handler(target, 'signaled', &block2) + subject.register_handler(other_target, 'signaled', &block3) + + expect(subject.send(:handlers)[target][1].event_name).to eq('signaled') + expect(subject.send(:handlers)[target][1].handler).to be(block1) + + expect(subject.send(:handlers)[target][2].event_name).to eq('signaled') + expect(subject.send(:handlers)[target][2].handler).to be(block2) + + expect(subject.send(:handlers)[other_target][3].event_name).to eq('signaled') + expect(subject.send(:handlers)[other_target][3].handler).to be(block3) + + handle1.unregister + expect(subject.send(:handlers)[target][1]).to be(nil) + expect(subject.send(:handlers)[target][2]).to_not be(nil) + expect(subject.send(:handlers)[other_target][3]).to_not be(nil) + end end describe '#dispatch' do @@ -114,10 +141,13 @@ context 'with TARGET_WILDCARD target handler' do let(:handler_6) { -> { 'sixth block' } } + let(:handler_7) { -> { 'seventh block' } } before do allow(handler_6).to receive(:call) + allow(handler_7).to receive(:call) subject.register_handler(described_class::TARGET_WILDCARD, described_class::WILDCARD, &handler_6) + subject.register_handler(target, 'completed', &handler_7) end it 'calls the handler' do @@ -127,6 +157,7 @@ expect(handler_1).to have_received(:call).ordered expect(handler_4).to have_received(:call).ordered expect(handler_6).to have_received(:call).ordered + expect(handler_7).to have_received(:call).ordered end it 'TARGET_WILDCARD can be compared to an EventTarget object' do From 78e4fb88f5acd91c0fb31b1c6e45f0774b86a054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelizaveta=20Leme=C5=A1eva?= Date: Fri, 24 Jun 2022 19:53:13 +0200 Subject: [PATCH 058/125] Add gRPC credentials (#186) * Add gRPC credentials to cofiguration * Add tests * Update README.md * Use default DYNAMIC_CONFIG_FILE_PATH env var for temporalio/auto-setup container in CI * Update README.md --- .circleci/config.yml | 1 - README.md | 52 ++++++++++++++++++++ lib/temporal/configuration.rb | 8 ++-- lib/temporal/connection.rb | 3 +- lib/temporal/connection/grpc.rb | 7 +-- spec/unit/lib/temporal/connection_spec.rb | 58 +++++++++++++++++++++++ spec/unit/lib/temporal/grpc_spec.rb | 2 +- 7 files changed, 122 insertions(+), 9 deletions(-) create mode 100644 spec/unit/lib/temporal/connection_spec.rb diff --git a/.circleci/config.yml b/.circleci/config.yml index b23efce7..ed0d5eb0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -29,7 +29,6 @@ jobs: - POSTGRES_USER=postgres - POSTGRES_PWD=temporal - POSTGRES_SEEDS=postgres - - DYNAMIC_CONFIG_FILE_PATH=config/dynamicconfig/development.yaml environment: - TEMPORAL_HOST=temporal diff --git a/README.md b/README.md index 838fda7a..7f5ff27d 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ Temporal.configure do |config| config.port = 7233 config.namespace = 'ruby-samples' config.task_queue = 'hello-world' + config.credentials = :this_channel_is_insecure end begin @@ -114,6 +115,57 @@ curl -O https://raw.githubusercontent.com/temporalio/docker-compose/main/docker- docker-compose up ``` +## Using Credentials + +### SSL + +In many production deployments you will end up connecting to your Temporal Services via SSL. In this +case you must read the public certificate of the CA that issued your Temporal server's SSL certificate and create +an instance of [gRPC Channel Credentials](https://grpc.io/docs/guides/auth/#with-server-authentication-ssltls-1). + +Configure your Temporal connection: + +```ruby +Temporal.configure do |config| + config.host = 'localhost' + config.port = 7233 + config.namespace = 'ruby-samples' + config.task_queue = 'hello-world' + config.credentials = GRPC::Core::ChannelCredentials.new(root_cert, client_key, client_chain) +end +``` + +### OAuth2 Token + +Use gRPC Call Credentials to add OAuth2 token to gRPC calls: + +```ruby +Temporal.configure do |config| + config.host = 'localhost' + config.port = 7233 + config.namespace = 'ruby-samples' + config.task_queue = 'hello-world' + config.credentials = GRPC::Core::CallCredentials.new(updater_proc) +end +``` +`updater_proc` should be a method that returns `proc`. See an example of `updater_proc` in [googleauth](https://www.rubydoc.info/gems/googleauth/0.1.0/Signet/OAuth2/Client) library. + +### Combining Credentials + +To configure both SSL and OAuth2 token cedentials use `compose` method: + +```ruby +Temporal.configure do |config| + config.host = 'localhost' + config.port = 7233 + config.namespace = 'ruby-samples' + config.task_queue = 'hello-world' + config.credentials = GRPC::Core::ChannelCredentials.new(root_cert, client_key, client_chain).compose( + GRPC::Core::CallCredentials.new(token.updater_proc) + ) +end +``` + ## Workflows A workflow is defined using pure Ruby code, however it should contain only a high-level diff --git a/lib/temporal/configuration.rb b/lib/temporal/configuration.rb index 8d36615c..d2edaead 100644 --- a/lib/temporal/configuration.rb +++ b/lib/temporal/configuration.rb @@ -7,12 +7,12 @@ module Temporal class Configuration - Connection = Struct.new(:type, :host, :port, keyword_init: true) + Connection = Struct.new(:type, :host, :port, :credentials, keyword_init: true) Execution = Struct.new(:namespace, :task_queue, :timeouts, :headers, keyword_init: true) attr_reader :timeouts, :error_handlers attr_writer :converter - attr_accessor :connection_type, :host, :port, :logger, :metrics_adapter, :namespace, :task_queue, :headers + attr_accessor :connection_type, :host, :port, :credentials, :logger, :metrics_adapter, :namespace, :task_queue, :headers # See https://docs.temporal.io/blog/activity-timeouts/ for general docs. # We want an infinite execution timeout for cron schedules and other perpetual workflows. @@ -53,6 +53,7 @@ def initialize @headers = DEFAULT_HEADERS @converter = DEFAULT_CONVERTER @error_handlers = [] + @credentials = :this_channel_is_insecure end def on_error(&block) @@ -79,7 +80,8 @@ def for_connection Connection.new( type: connection_type, host: host, - port: port + port: port, + credentials: credentials ).freeze end diff --git a/lib/temporal/connection.rb b/lib/temporal/connection.rb index b499ca73..791534bf 100644 --- a/lib/temporal/connection.rb +++ b/lib/temporal/connection.rb @@ -10,12 +10,13 @@ def self.generate(configuration) connection_class = CLIENT_TYPES_MAP[configuration.type] host = configuration.host port = configuration.port + credentials = configuration.credentials hostname = `hostname` thread_id = Thread.current.object_id identity = "#{thread_id}@#{hostname}" - connection_class.new(host, port, identity) + connection_class.new(host, port, identity, credentials) end end end diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index dd306b97..9b0d9699 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -31,9 +31,10 @@ class GRPC max_page_size: 100 }.freeze - def initialize(host, port, identity, options = {}) + def initialize(host, port, identity, credentials, options = {}) @url = "#{host}:#{port}" @identity = identity + @credentials = credentials @poll = true @poll_mutex = Mutex.new @poll_request = nil @@ -536,12 +537,12 @@ def cancel_polling_request private - attr_reader :url, :identity, :options, :poll_mutex, :poll_request + attr_reader :url, :identity, :credentials, :options, :poll_mutex, :poll_request def client @client ||= Temporal::Api::WorkflowService::V1::WorkflowService::Stub.new( url, - :this_channel_is_insecure, + credentials, timeout: 60 ) end diff --git a/spec/unit/lib/temporal/connection_spec.rb b/spec/unit/lib/temporal/connection_spec.rb new file mode 100644 index 00000000..dff88b0a --- /dev/null +++ b/spec/unit/lib/temporal/connection_spec.rb @@ -0,0 +1,58 @@ +describe Temporal::Connection do + subject { described_class.generate(config.for_connection) } + + let(:connection_type) { :grpc } + let(:credentials) { nil } + let(:config) do + config = Temporal::Configuration.new + config.connection_type = connection_type + config.credentials = credentials if credentials + config + end + + context 'insecure' do + let(:credentials) { :this_channel_is_insecure } + + it 'generates a grpc connection' do + expect(subject).to be_kind_of(Temporal::Connection::GRPC) + expect(subject.send(:identity)).not_to be_nil + expect(subject.send(:credentials)).to eq(:this_channel_is_insecure) + end + end + + context 'ssl' do + let(:credentials) { GRPC::Core::ChannelCredentials.new } + + it 'generates a grpc connection' do + expect(subject).to be_kind_of(Temporal::Connection::GRPC) + expect(subject.send(:identity)).not_to be_nil + expect(subject.send(:credentials)).to be_kind_of(GRPC::Core::ChannelCredentials) + end + end + + context 'oauth2' do + let(:credentials) { GRPC::Core::CallCredentials.new(proc { { authorization: 'token' } }) } + + it 'generates a grpc connection' do + expect(subject).to be_kind_of(Temporal::Connection::GRPC) + expect(subject.send(:identity)).not_to be_nil + expect(subject.send(:credentials)).to be_kind_of(GRPC::Core::CallCredentials) + end + end + + context 'ssl + oauth2' do + let(:credentials) do + GRPC::Core::ChannelCredentials.new.compose( + GRPC::Core::CallCredentials.new( + proc { { authorization: 'token' } } + ) + ) + end + + it 'generates a grpc connection' do + expect(subject).to be_kind_of(Temporal::Connection::GRPC) + expect(subject.send(:identity)).not_to be_nil + expect(subject.send(:credentials)).to be_kind_of(GRPC::Core::ChannelCredentials) + end + end +end diff --git a/spec/unit/lib/temporal/grpc_spec.rb b/spec/unit/lib/temporal/grpc_spec.rb index d828d537..ef617341 100644 --- a/spec/unit/lib/temporal/grpc_spec.rb +++ b/spec/unit/lib/temporal/grpc_spec.rb @@ -10,7 +10,7 @@ let(:run_id) { SecureRandom.uuid } let(:now) { Time.now} - subject { Temporal::Connection::GRPC.new(nil, nil, identity) } + subject { Temporal::Connection::GRPC.new(nil, nil, identity, :this_channel_is_insecure) } class TestDeserializer extend Temporal::Concerns::Payloads From 0e3e7b14e11e12635407e53fe1a8a66b9a32db5c Mon Sep 17 00:00:00 2001 From: Jeff Schoner Date: Fri, 24 Jun 2022 10:53:53 -0700 Subject: [PATCH 059/125] Query functionality for temporal-web: stack trace and registered queries (#184) * Add built-in stack trace query * Match unknown query error message to Go SDK --- .../spec/integration/query_workflow_spec.rb | 9 +- lib/temporal/workflow/context.rb | 33 ++++++-- lib/temporal/workflow/executor.rb | 13 +-- lib/temporal/workflow/query_registry.rb | 5 +- lib/temporal/workflow/stack_trace_tracker.rb | 39 +++++++++ lib/temporal/workflow/task_processor.rb | 16 ++-- .../lib/temporal/workflow/context_spec.rb | 82 ++++++++++++++++++- .../lib/temporal/workflow/executor_spec.rb | 4 +- .../temporal/workflow/query_registry_spec.rb | 2 +- .../workflow/stack_trace_tracker_spec.rb | 56 +++++++++++++ 10 files changed, 233 insertions(+), 26 deletions(-) create mode 100644 lib/temporal/workflow/stack_trace_tracker.rb create mode 100644 spec/unit/lib/temporal/workflow/stack_trace_tracker_spec.rb diff --git a/examples/spec/integration/query_workflow_spec.rb b/examples/spec/integration/query_workflow_spec.rb index 93812635..fb54b0d9 100644 --- a/examples/spec/integration/query_workflow_spec.rb +++ b/examples/spec/integration/query_workflow_spec.rb @@ -22,7 +22,12 @@ # Query with unregistered handler expect { Temporal.query_workflow(described_class, 'unknown_query', workflow_id, run_id) } - .to raise_error(Temporal::QueryFailed, 'Workflow did not register a handler for unknown_query') + .to raise_error(Temporal::QueryFailed, "Workflow did not register a handler for 'unknown_query'. KnownQueryTypes=[__stack_trace, state, signal_count]") + + # Query built-in stack trace handler, looking for a couple of key parts of the contents + stack_trace = Temporal.query_workflow(described_class, '__stack_trace', workflow_id, run_id) + expect(stack_trace).to start_with "Fiber count: 1\n\n" + expect(stack_trace).to include "/examples/workflows/query_workflow.rb:" Temporal.signal_workflow(described_class, 'make_progress', workflow_id, run_id) @@ -45,7 +50,7 @@ .to eq 2 expect { Temporal.query_workflow(described_class, 'unknown_query', workflow_id, run_id) } - .to raise_error(Temporal::QueryFailed, 'Workflow did not register a handler for unknown_query') + .to raise_error(Temporal::QueryFailed, "Workflow did not register a handler for 'unknown_query'. KnownQueryTypes=[__stack_trace, state, signal_count]") # Now that the workflow is completed, test a query with a reject condition satisfied expect { Temporal.query_workflow(described_class, 'state', workflow_id, run_id, query_reject_condition: :not_open) } diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index 23e86ad8..462e154a 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -9,6 +9,7 @@ require 'temporal/workflow/future' require 'temporal/workflow/child_workflow_future' require 'temporal/workflow/replay_aware_logger' +require 'temporal/workflow/stack_trace_tracker' require 'temporal/workflow/state_manager' require 'temporal/workflow/signal' @@ -20,7 +21,7 @@ class Workflow class Context attr_reader :metadata, :config - def initialize(state_manager, dispatcher, workflow_class, metadata, config, query_registry) + def initialize(state_manager, dispatcher, workflow_class, metadata, config, query_registry, track_stack_trace) @state_manager = state_manager @dispatcher = dispatcher @query_registry = query_registry @@ -28,6 +29,16 @@ def initialize(state_manager, dispatcher, workflow_class, metadata, config, quer @metadata = metadata @completed = false @config = config + + if track_stack_trace + @stack_trace_tracker = StackTraceTracker.new + else + @stack_trace_tracker = nil + end + + query_registry.register(StackTraceTracker::STACK_TRACE_QUERY_NAME) do + stack_trace_tracker&.to_s + end end def completed? @@ -259,8 +270,13 @@ def wait_for_any(*futures) end end - Fiber.yield - handlers.each(&:unregister) + stack_trace_tracker&.record + begin + Fiber.yield + ensure + stack_trace_tracker&.clear + handlers.each(&:unregister) + end return end @@ -277,8 +293,13 @@ def wait_until(&unblock_condition) fiber.resume if unblock_condition.call end - Fiber.yield - handler.unregister + stack_trace_tracker&.record + begin + Fiber.yield + ensure + stack_trace_tracker&.clear + handler.unregister + end return end @@ -398,7 +419,7 @@ def upsert_search_attributes(search_attributes) private - attr_reader :state_manager, :dispatcher, :workflow_class, :query_registry + attr_reader :state_manager, :dispatcher, :workflow_class, :query_registry, :stack_trace_tracker def completed! @completed = true diff --git a/lib/temporal/workflow/executor.rb b/lib/temporal/workflow/executor.rb index 546fe7f6..90b090f0 100644 --- a/lib/temporal/workflow/executor.rb +++ b/lib/temporal/workflow/executor.rb @@ -1,9 +1,10 @@ require 'fiber' +require 'temporal/workflow/context' require 'temporal/workflow/dispatcher' require 'temporal/workflow/query_registry' +require 'temporal/workflow/stack_trace_tracker' require 'temporal/workflow/state_manager' -require 'temporal/workflow/context' require 'temporal/workflow/history/event_target' require 'temporal/metadata' @@ -14,7 +15,8 @@ class Executor # @param history [Workflow::History] # @param task_metadata [Metadata::WorkflowTask] # @param config [Configuration] - def initialize(workflow_class, history, task_metadata, config) + # @param track_stack_trace [Boolean] + def initialize(workflow_class, history, task_metadata, config, track_stack_trace) @workflow_class = workflow_class @dispatcher = Dispatcher.new @query_registry = QueryRegistry.new @@ -22,6 +24,7 @@ def initialize(workflow_class, history, task_metadata, config) @history = history @task_metadata = task_metadata @config = config + @track_stack_trace = track_stack_trace end def run @@ -46,13 +49,13 @@ def run # @param queries [Hash] # # @return [Hash] - def process_queries(queries = {}) + def process_queries(queries) queries.transform_values(&method(:process_query)) end private - attr_reader :workflow_class, :dispatcher, :query_registry, :state_manager, :task_metadata, :history, :config + attr_reader :workflow_class, :dispatcher, :query_registry, :state_manager, :task_metadata, :history, :config, :track_stack_trace def process_query(query) result = query_registry.handle(query.query_type, query.query_args) @@ -64,7 +67,7 @@ def process_query(query) def execute_workflow(input, workflow_started_event) metadata = Metadata.generate_workflow_metadata(workflow_started_event, task_metadata) - context = Workflow::Context.new(state_manager, dispatcher, workflow_class, metadata, config, query_registry) + context = Workflow::Context.new(state_manager, dispatcher, workflow_class, metadata, config, query_registry, track_stack_trace) Fiber.new do workflow_class.execute_in_context(context, input) diff --git a/lib/temporal/workflow/query_registry.rb b/lib/temporal/workflow/query_registry.rb index babdda66..162b7a7f 100644 --- a/lib/temporal/workflow/query_registry.rb +++ b/lib/temporal/workflow/query_registry.rb @@ -19,7 +19,10 @@ def handle(type, args = nil) handler = handlers[type] unless handler - raise Temporal::QueryFailed, "Workflow did not register a handler for #{type}" + # The end of the formatted error message (e.g., "KnownQueryTypes=[query-1, query-2, query-3]") + # is used by temporal-web to show a list of queries that can be run on the 'Query' tab of a + # workflow. If that part of the error message is changed, that functionality will break. + raise Temporal::QueryFailed, "Workflow did not register a handler for '#{type}'. KnownQueryTypes=[#{handlers.keys.join(", ")}]" end handler.call(*args) diff --git a/lib/temporal/workflow/stack_trace_tracker.rb b/lib/temporal/workflow/stack_trace_tracker.rb new file mode 100644 index 00000000..dfa35c94 --- /dev/null +++ b/lib/temporal/workflow/stack_trace_tracker.rb @@ -0,0 +1,39 @@ +require 'fiber' + +module Temporal + class Workflow + # Temporal-web issues a query that returns the stack trace for all workflow fibers + # that are currently scheduled. This is helpful for understanding what exactly a + # workflow is waiting on. + class StackTraceTracker + STACK_TRACE_QUERY_NAME = '__stack_trace' + + def initialize + @stack_traces = {} + end + + # Record the stack trace for the current fiber + def record + stack_traces[Fiber.current] = Kernel.caller + end + + # Clear the stack traces for the current fiber + def clear + stack_traces.delete(Fiber.current) + end + + # Format all recorded backtraces in a human readable format + def to_s + formatted_stack_traces = ["Fiber count: #{stack_traces.count}"] + stack_traces.map do |_, stack_trace| + stack_trace.join("\n") + end + + formatted_stack_traces.join("\n\n") + "\n" + end + + private + + attr_reader :stack_traces + end + end +end diff --git a/lib/temporal/workflow/task_processor.rb b/lib/temporal/workflow/task_processor.rb index 2bd7b7d6..d161e1c2 100644 --- a/lib/temporal/workflow/task_processor.rb +++ b/lib/temporal/workflow/task_processor.rb @@ -1,8 +1,9 @@ -require 'temporal/workflow/executor' -require 'temporal/workflow/history' -require 'temporal/metadata' require 'temporal/error_handler' require 'temporal/errors' +require 'temporal/metadata' +require 'temporal/workflow/executor' +require 'temporal/workflow/history' +require 'temporal/workflow/stack_trace_tracker' module Temporal class Workflow @@ -45,14 +46,19 @@ def process end history = fetch_full_history + queries = parse_queries + + # We only need to track the stack trace if this is a stack trace query + track_stack_trace = queries.values.map(&:query_type).include?(StackTraceTracker::STACK_TRACE_QUERY_NAME) + # TODO: For sticky workflows we need to cache the Executor instance - executor = Workflow::Executor.new(workflow_class, history, metadata, config) + executor = Workflow::Executor.new(workflow_class, history, metadata, config, track_stack_trace) commands = middleware_chain.invoke(metadata) do executor.run end - query_results = executor.process_queries(parse_queries) + query_results = executor.process_queries(queries) if legacy_query_task? complete_query(query_results[LEGACY_QUERY_KEY]) diff --git a/spec/unit/lib/temporal/workflow/context_spec.rb b/spec/unit/lib/temporal/workflow/context_spec.rb index e008eb9e..5d7f40ba 100644 --- a/spec/unit/lib/temporal/workflow/context_spec.rb +++ b/spec/unit/lib/temporal/workflow/context_spec.rb @@ -2,6 +2,8 @@ require 'temporal/workflow/context' require 'temporal/workflow/dispatcher' require 'temporal/workflow/future' +require 'temporal/workflow/query_registry' +require 'temporal/workflow/stack_trace_tracker' require 'time' class MyTestWorkflow < Temporal::Workflow; end @@ -9,7 +11,11 @@ class MyTestWorkflow < Temporal::Workflow; end describe Temporal::Workflow::Context do let(:state_manager) { instance_double('Temporal::Workflow::StateManager') } let(:dispatcher) { Temporal::Workflow::Dispatcher.new } - let(:query_registry) { instance_double('Temporal::Workflow::QueryRegistry') } + let(:query_registry) do + double = instance_double('Temporal::Workflow::QueryRegistry') + allow(double).to receive(:register) + double + end let(:metadata) { instance_double('Temporal::Metadata::Workflow') } let(:workflow_context) do Temporal::Workflow::Context.new( @@ -18,16 +24,16 @@ class MyTestWorkflow < Temporal::Workflow; end MyTestWorkflow, metadata, Temporal.configuration, - query_registry + query_registry, + track_stack_trace ) end let(:child_workflow_execution) { Fabricate(:api_workflow_execution) } + let(:track_stack_trace) { false } describe '#on_query' do let(:handler) { Proc.new {} } - before { allow(query_registry).to receive(:register) } - it 'registers a query with the query registry' do workflow_context.on_query('test-query', &handler) @@ -35,6 +41,23 @@ class MyTestWorkflow < Temporal::Workflow; end expect(block).to eq(handler) end end + + it 'automatically registers stack trace query' do + expect(workflow_context).to_not be(nil) # ensure constructor is called + expect(query_registry).to have_received(:register) + .with(Temporal::Workflow::StackTraceTracker::STACK_TRACE_QUERY_NAME) + end + + context 'stack trace' do + let(:track_stack_trace) { true } + let(:query_registry) { Temporal::Workflow::QueryRegistry.new } + + it 'cleared to start' do + expect(workflow_context).to_not be(nil) # ensure constructor is called + stack_trace = query_registry.handle(Temporal::Workflow::StackTraceTracker::STACK_TRACE_QUERY_NAME) + expect(stack_trace).to eq("Fiber count: 0\n") + end + end end describe '#execute_workflow' do @@ -265,6 +288,32 @@ def wait_for_any expect(check_unblocked.call).to be(false) end + + context 'stack trace' do + let(:track_stack_trace) { true } + let(:query_registry) { Temporal::Workflow::QueryRegistry.new } + + it 'is recorded' do + wait_for_any + stack_trace = query_registry.handle(Temporal::Workflow::StackTraceTracker::STACK_TRACE_QUERY_NAME) + + expect(stack_trace).to start_with('Fiber count: 1') + expect(stack_trace).to include('block in wait_for_any') + end + + it 'cleared after unblocked' do + wait_for_any + + future_1.set("it's done") + future_2.set("it's done") + dispatcher.dispatch(target_1, 'foo') + dispatcher.dispatch(target_2, 'foo') + + stack_trace = query_registry.handle(Temporal::Workflow::StackTraceTracker::STACK_TRACE_QUERY_NAME) + + expect(stack_trace).to eq("Fiber count: 0\n") + end + end end describe '#wait_until' do @@ -308,5 +357,30 @@ def wait_until(&blk) # Can dispatch again safely without resuming dead fiber dispatcher.dispatch('target', 'foo') end + + context 'stack trace' do + let(:track_stack_trace) { true } + let(:query_registry) { Temporal::Workflow::QueryRegistry.new } + + it 'is recorded' do + wait_until { false } + stack_trace = query_registry.handle(Temporal::Workflow::StackTraceTracker::STACK_TRACE_QUERY_NAME) + + expect(stack_trace).to start_with('Fiber count: 1') + expect(stack_trace).to include('block in wait_until') + end + + it 'cleared after unblocked' do + value = false + wait_until { value } + + value = true + dispatcher.dispatch('target', 'foo') + + stack_trace = query_registry.handle(Temporal::Workflow::StackTraceTracker::STACK_TRACE_QUERY_NAME) + + expect(stack_trace).to eq("Fiber count: 0\n") + end + end end end diff --git a/spec/unit/lib/temporal/workflow/executor_spec.rb b/spec/unit/lib/temporal/workflow/executor_spec.rb index b93105ac..0808d3f6 100644 --- a/spec/unit/lib/temporal/workflow/executor_spec.rb +++ b/spec/unit/lib/temporal/workflow/executor_spec.rb @@ -5,7 +5,7 @@ require 'temporal/workflow/query_registry' describe Temporal::Workflow::Executor do - subject { described_class.new(workflow, history, workflow_metadata, config) } + subject { described_class.new(workflow, history, workflow_metadata, config, false) } let(:workflow_started_event) { Fabricate(:api_workflow_execution_started_event, event_id: 1) } let(:history) do @@ -111,7 +111,7 @@ def execute expect(results['2'].error).to eq(query_2_error) expect(results['3']).to be_a(Temporal::Workflow::QueryResult::Failure) expect(results['3'].error).to be_a(Temporal::QueryFailed) - expect(results['3'].error.message).to eq('Workflow did not register a handler for unknown') + expect(results['3'].error.message).to eq("Workflow did not register a handler for 'unknown'. KnownQueryTypes=[success, failure]") end end end diff --git a/spec/unit/lib/temporal/workflow/query_registry_spec.rb b/spec/unit/lib/temporal/workflow/query_registry_spec.rb index 3c5ced14..d65405fc 100644 --- a/spec/unit/lib/temporal/workflow/query_registry_spec.rb +++ b/spec/unit/lib/temporal/workflow/query_registry_spec.rb @@ -60,7 +60,7 @@ it 'raises' do expect do subject.handle('test-query') - end.to raise_error(Temporal::QueryFailed, 'Workflow did not register a handler for test-query') + end.to raise_error(Temporal::QueryFailed, "Workflow did not register a handler for 'test-query'. KnownQueryTypes=[]") end end end diff --git a/spec/unit/lib/temporal/workflow/stack_trace_tracker_spec.rb b/spec/unit/lib/temporal/workflow/stack_trace_tracker_spec.rb new file mode 100644 index 00000000..5db23fb7 --- /dev/null +++ b/spec/unit/lib/temporal/workflow/stack_trace_tracker_spec.rb @@ -0,0 +1,56 @@ +require 'temporal/workflow/stack_trace_tracker' + +describe Temporal::Workflow::StackTraceTracker do + subject { described_class.new } + describe '#to_s' do + def record_function + subject.record + end + + def record_and_clear_function + subject.record + subject.clear + end + + def record_two_function + subject.record + + Fiber.new do + subject.record + end.resume + end + + it 'starts empty' do + expect(subject.to_s).to eq("Fiber count: 0\n") + end + + it 'one fiber' do + record_function + stack_trace = subject.to_s + expect(stack_trace).to start_with("Fiber count: 1\n\n") + + first_stack_line = stack_trace.split("\n")[2] + expect(first_stack_line).to include("record_function") + end + + it 'one fiber cleared' do + record_and_clear_function + stack_trace = subject.to_s + expect(stack_trace).to start_with("Fiber count: 0\n") + end + + it 'two fibers' do + record_two_function + output = subject.to_s + expect(output).to start_with("Fiber count: 2\n\n") + + stack_traces = output.split("\n\n") + + first_stack = stack_traces[1] + expect(first_stack).to include("record_two_function") + + second_stack = stack_traces[2] + expect(second_stack).to include("block in record_two_function") + end + end +end \ No newline at end of file From ac8ae20c89b302228bcdc26fb553f067f57e724b Mon Sep 17 00:00:00 2001 From: jazevedo-stripe <109185961+jazevedo-stripe@users.noreply.github.com> Date: Fri, 5 Aug 2022 05:19:55 -0700 Subject: [PATCH 060/125] Allow initial search attributes to be specified when starting workflows (#188) * add option to specify search attributes when starting workflows * move empty check out of Temporal::Workflow::Context::Helpers.process_search_attributes * move process_search_attributes out of ExecutionOptions.initialize * allow default search attributes to be configured globally * fix tests, unit test global default search attributes * add unit test for gRPC serialization * clean up the integration test * fix requires * improve integration test label * fix integration test failure * undo unintentional changes --- .../initial_search_attributes_spec.rb | 63 +++++++++++++++++++ .../upsert_search_attributes_spec.rb | 6 +- .../upsert_search_attributes_workflow.rb | 14 +++-- lib/temporal/client.rb | 8 ++- lib/temporal/configuration.rb | 8 ++- lib/temporal/connection/grpc.rb | 14 ++++- .../connection/serializer/continue_as_new.rb | 9 ++- .../serializer/start_child_workflow.rb | 9 ++- lib/temporal/execution_options.rb | 4 +- .../testing/local_workflow_context.rb | 3 + lib/temporal/testing/temporal_override.rb | 4 +- lib/temporal/testing/workflow_execution.rb | 4 +- lib/temporal/workflow/command.rb | 4 +- lib/temporal/workflow/context.rb | 7 ++- lib/temporal/workflow/context_helpers.rb | 5 +- spec/unit/lib/temporal/client_spec.rb | 21 +++++-- .../serializer/continue_as_new_spec.rb | 2 + .../serializer/start_child_workflow_spec.rb | 1 + .../lib/temporal/execution_options_spec.rb | 10 ++- spec/unit/lib/temporal/grpc_spec.rb | 21 +++++++ .../testing/temporal_override_spec.rb | 8 ++- .../temporal/workflow/execution_info_spec.rb | 1 + 22 files changed, 189 insertions(+), 37 deletions(-) create mode 100644 examples/spec/integration/initial_search_attributes_spec.rb diff --git a/examples/spec/integration/initial_search_attributes_spec.rb b/examples/spec/integration/initial_search_attributes_spec.rb new file mode 100644 index 00000000..29febed0 --- /dev/null +++ b/examples/spec/integration/initial_search_attributes_spec.rb @@ -0,0 +1,63 @@ +require 'workflows/upsert_search_attributes_workflow' +require 'time' + +describe 'starting workflow with initial search attributes', :integration do + it 'has attributes appear in final execution info, but can get overriden by upserting' do + workflow_id = 'initial_search_attributes_test_wf-' + SecureRandom.uuid + expected_binary_checksum = `git show HEAD -s --format=%H`.strip + + initial_search_attributes = { + 'CustomBoolField' => false, + 'CustomIntField' => -1, + 'CustomDatetimeField' => Time.now, + + # These should get overriden when the workflow upserts them + 'CustomStringField' => 'meow', + 'CustomDoubleField' => 6.28, + } + # Override some of the initial search attributes by upserting them during the workflow execution. + upserted_search_attributes = { + 'CustomStringField' => 'moo', + 'CustomDoubleField' => 3.14, + } + expected_custom_attributes = initial_search_attributes.merge(upserted_search_attributes) + # Datetime fields get converted to the Time#iso8601 format, in UTC + expected_custom_attributes['CustomDatetimeField'] = expected_custom_attributes['CustomDatetimeField'].utc.iso8601 + + run_id = Temporal.start_workflow( + UpsertSearchAttributesWorkflow, + string_value: upserted_search_attributes['CustomStringField'], + float_value: upserted_search_attributes['CustomDoubleField'], + # Don't upsert anything for the bool, int, or time search attributes; + # their values should be the initial ones set when first starting the workflow. + bool_value: nil, + int_value: nil, + time_value: nil, + options: { + workflow_id: workflow_id, + search_attributes: initial_search_attributes, + }, + ) + + # UpsertSearchAttributesWorkflow returns the search attributes it upserted during its execution + added_attributes = Temporal.await_workflow_result( + UpsertSearchAttributesWorkflow, + workflow_id: workflow_id, + run_id: run_id, + ) + expect(added_attributes).to eq(upserted_search_attributes) + + # These attributes are set for the worker in bin/worker + expected_attributes = { + # Contains a list of all binary checksums seen for this workflow execution + 'BinaryChecksums' => [expected_binary_checksum] + }.merge(expected_custom_attributes) + + execution_info = Temporal.fetch_workflow_execution_info( + integration_spec_namespace, + workflow_id, + nil + ) + expect(execution_info.search_attributes).to eq(expected_attributes) + end +end diff --git a/examples/spec/integration/upsert_search_attributes_spec.rb b/examples/spec/integration/upsert_search_attributes_spec.rb index 6c8c0835..0757da3d 100644 --- a/examples/spec/integration/upsert_search_attributes_spec.rb +++ b/examples/spec/integration/upsert_search_attributes_spec.rb @@ -16,7 +16,11 @@ run_id = Temporal.start_workflow( UpsertSearchAttributesWorkflow, - *expected_added_attributes.values, + string_value: expected_added_attributes['CustomStringField'], + bool_value: expected_added_attributes['CustomBoolField'], + float_value: expected_added_attributes['CustomDoubleField'], + int_value: expected_added_attributes['CustomIntField'], + time_value: expected_added_attributes['CustomDatetimeField'], options: { workflow_id: workflow_id, }, diff --git a/examples/workflows/upsert_search_attributes_workflow.rb b/examples/workflows/upsert_search_attributes_workflow.rb index 434d2c87..fd92e108 100644 --- a/examples/workflows/upsert_search_attributes_workflow.rb +++ b/examples/workflows/upsert_search_attributes_workflow.rb @@ -1,18 +1,20 @@ require 'activities/hello_world_activity' class UpsertSearchAttributesWorkflow < Temporal::Workflow # time_value example: use this format: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ") - def execute(string_value, bool_value, float_value, int_value, time_value) + # values comes from keyword args passed to start_workflow + def execute(values) # These are included in the default temporal docker setup. # Run tctl admin cluster get-search-attributes to list the options and # See https://docs.temporal.io/docs/tctl/how-to-add-a-custom-search-attribute-to-a-cluster-using-tctl # for instructions on adding them. attributes = { - 'CustomStringField' => string_value, - 'CustomBoolField' => bool_value, - 'CustomDoubleField' => float_value, - 'CustomIntField' => int_value, - 'CustomDatetimeField' => time_value, + 'CustomStringField' => values[:string_value], + 'CustomBoolField' => values[:bool_value], + 'CustomDoubleField' => values[:float_value], + 'CustomIntField' => values[:int_value], + 'CustomDatetimeField' => values[:time_value], } + attributes.compact! workflow.upsert_search_attributes(attributes) # The following lines are extra complexity to test if upsert_search_attributes is tracked properly in the internal # state machine. diff --git a/lib/temporal/client.rb b/lib/temporal/client.rb index 3f05d7aa..163d4e08 100644 --- a/lib/temporal/client.rb +++ b/lib/temporal/client.rb @@ -3,6 +3,7 @@ require 'temporal/activity' require 'temporal/activity/async_token' require 'temporal/workflow' +require 'temporal/workflow/context_helpers' require 'temporal/workflow/history' require 'temporal/workflow/execution_info' require 'temporal/workflow/executions' @@ -36,6 +37,7 @@ def initialize(config) # @option options [Hash] :retry_policy check Temporal::RetryPolicy for available options # @option options [Hash] :timeouts check Temporal::Configuration::DEFAULT_TIMEOUTS # @option options [Hash] :headers + # @option options [Hash] :search_attributes # # @return [String] workflow's run ID def start_workflow(workflow, *input, options: {}, **args) @@ -61,6 +63,7 @@ def start_workflow(workflow, *input, options: {}, **args) workflow_id_reuse_policy: options[:workflow_id_reuse_policy], headers: execution_options.headers, memo: execution_options.memo, + search_attributes: Workflow::Context::Helpers.process_search_attributes(execution_options.search_attributes), ) else raise ArgumentError, 'If signal_input is provided, you must also provide signal_name' if signal_name.nil? @@ -77,6 +80,7 @@ def start_workflow(workflow, *input, options: {}, **args) workflow_id_reuse_policy: options[:workflow_id_reuse_policy], headers: execution_options.headers, memo: execution_options.memo, + search_attributes: Workflow::Context::Helpers.process_search_attributes(execution_options.search_attributes), signal_name: signal_name, signal_input: signal_input ) @@ -101,6 +105,7 @@ def start_workflow(workflow, *input, options: {}, **args) # @option options [Hash] :retry_policy check Temporal::RetryPolicy for available options # @option options [Hash] :timeouts check Temporal::Configuration::DEFAULT_TIMEOUTS # @option options [Hash] :headers + # @option options [Hash] :search_attributes # # @return [String] workflow's run ID def schedule_workflow(workflow, cron_schedule, *input, options: {}, **args) @@ -124,7 +129,8 @@ def schedule_workflow(workflow, cron_schedule, *input, options: {}, **args) workflow_id_reuse_policy: options[:workflow_id_reuse_policy], headers: execution_options.headers, cron_schedule: cron_schedule, - memo: execution_options.memo + memo: execution_options.memo, + search_attributes: Workflow::Context::Helpers.process_search_attributes(execution_options.search_attributes), ) response.run_id diff --git a/lib/temporal/configuration.rb b/lib/temporal/configuration.rb index d2edaead..99e582a2 100644 --- a/lib/temporal/configuration.rb +++ b/lib/temporal/configuration.rb @@ -8,11 +8,11 @@ module Temporal class Configuration Connection = Struct.new(:type, :host, :port, :credentials, keyword_init: true) - Execution = Struct.new(:namespace, :task_queue, :timeouts, :headers, keyword_init: true) + Execution = Struct.new(:namespace, :task_queue, :timeouts, :headers, :search_attributes, keyword_init: true) attr_reader :timeouts, :error_handlers attr_writer :converter - attr_accessor :connection_type, :host, :port, :credentials, :logger, :metrics_adapter, :namespace, :task_queue, :headers + attr_accessor :connection_type, :host, :port, :credentials, :logger, :metrics_adapter, :namespace, :task_queue, :headers, :search_attributes # See https://docs.temporal.io/blog/activity-timeouts/ for general docs. # We want an infinite execution timeout for cron schedules and other perpetual workflows. @@ -54,6 +54,7 @@ def initialize @converter = DEFAULT_CONVERTER @error_handlers = [] @credentials = :this_channel_is_insecure + @search_attributes = {} end def on_error(&block) @@ -90,7 +91,8 @@ def default_execution_options namespace: namespace, task_queue: task_list, timeouts: timeouts, - headers: headers + headers: headers, + search_attributes: search_attributes, ).freeze end end diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index 9b0d9699..91bb3774 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -93,7 +93,8 @@ def start_workflow_execution( workflow_id_reuse_policy: nil, headers: nil, cron_schedule: nil, - memo: nil + memo: nil, + search_attributes: nil ) request = Temporal::Api::WorkflowService::V1::StartWorkflowExecutionRequest.new( identity: identity, @@ -117,7 +118,10 @@ def start_workflow_execution( cron_schedule: cron_schedule, memo: Temporal::Api::Common::V1::Memo.new( fields: to_payload_map(memo || {}) - ) + ), + search_attributes: Temporal::Api::Common::V1::SearchAttributes.new( + indexed_fields: to_payload_map(search_attributes || {}) + ), ) client.start_workflow_execution(request) @@ -339,7 +343,8 @@ def signal_with_start_workflow_execution( cron_schedule: nil, signal_name:, signal_input:, - memo: nil + memo: nil, + search_attributes: nil ) proto_header_fields = if headers.nil? to_payload_map({}) @@ -376,6 +381,9 @@ def signal_with_start_workflow_execution( memo: Temporal::Api::Common::V1::Memo.new( fields: to_payload_map(memo || {}) ), + search_attributes: Temporal::Api::Common::V1::SearchAttributes.new( + indexed_fields: to_payload_map(search_attributes || {}) + ), ) client.signal_with_start_workflow_execution(request) diff --git a/lib/temporal/connection/serializer/continue_as_new.rb b/lib/temporal/connection/serializer/continue_as_new.rb index 357f0008..2dc5d174 100644 --- a/lib/temporal/connection/serializer/continue_as_new.rb +++ b/lib/temporal/connection/serializer/continue_as_new.rb @@ -20,7 +20,8 @@ def to_proto workflow_task_timeout: object.timeouts[:task], retry_policy: Temporal::Connection::Serializer::RetryPolicy.new(object.retry_policy).to_proto, header: serialize_headers(object.headers), - memo: serialize_memo(object.memo) + memo: serialize_memo(object.memo), + search_attributes: serialize_search_attributes(object.search_attributes), ) ) end @@ -38,6 +39,12 @@ def serialize_memo(memo) Temporal::Api::Common::V1::Memo.new(fields: to_payload_map(memo)) end + + def serialize_search_attributes(search_attributes) + return unless search_attributes + + Temporal::Api::Common::V1::SearchAttributes.new(indexed_fields: to_payload_map(search_attributes)) + end end end end diff --git a/lib/temporal/connection/serializer/start_child_workflow.rb b/lib/temporal/connection/serializer/start_child_workflow.rb index 46ae5400..878e4a0f 100644 --- a/lib/temporal/connection/serializer/start_child_workflow.rb +++ b/lib/temporal/connection/serializer/start_child_workflow.rb @@ -32,7 +32,8 @@ def to_proto parent_close_policy: serialize_parent_close_policy(object.parent_close_policy), header: serialize_headers(object.headers), memo: serialize_memo(object.memo), - workflow_id_reuse_policy: Temporal::Connection::Serializer::WorkflowIdReusePolicy.new(object.workflow_id_reuse_policy).to_proto + workflow_id_reuse_policy: Temporal::Connection::Serializer::WorkflowIdReusePolicy.new(object.workflow_id_reuse_policy).to_proto, + search_attributes: serialize_search_attributes(object.search_attributes), ) ) end @@ -60,6 +61,12 @@ def serialize_parent_close_policy(parent_close_policy) PARENT_CLOSE_POLICY[parent_close_policy] end + + def serialize_search_attributes(search_attributes) + return unless search_attributes + + Temporal::Api::Common::V1::SearchAttributes.new(indexed_fields: to_payload_map(search_attributes)) + end end end end diff --git a/lib/temporal/execution_options.rb b/lib/temporal/execution_options.rb index b1cfc94d..681cac83 100644 --- a/lib/temporal/execution_options.rb +++ b/lib/temporal/execution_options.rb @@ -3,7 +3,7 @@ module Temporal class ExecutionOptions - attr_reader :name, :namespace, :task_queue, :retry_policy, :timeouts, :headers, :memo + attr_reader :name, :namespace, :task_queue, :retry_policy, :timeouts, :headers, :memo, :search_attributes def initialize(object, options, defaults = nil) # Options are treated as overrides and take precedence @@ -14,6 +14,7 @@ def initialize(object, options, defaults = nil) @timeouts = options[:timeouts] || {} @headers = options[:headers] || {} @memo = options[:memo] || {} + @search_attributes = options[:search_attributes] || {} # For Temporal::Workflow and Temporal::Activity use defined values as the next option if has_executable_concern?(object) @@ -30,6 +31,7 @@ def initialize(object, options, defaults = nil) @task_queue ||= defaults.task_queue @timeouts = defaults.timeouts.merge(@timeouts) @headers = defaults.headers.merge(@headers) + @search_attributes = defaults.search_attributes.merge(@search_attributes) end if @retry_policy.empty? diff --git a/lib/temporal/testing/local_workflow_context.rb b/lib/temporal/testing/local_workflow_context.rb index c6251e53..9f03d9bf 100644 --- a/lib/temporal/testing/local_workflow_context.rb +++ b/lib/temporal/testing/local_workflow_context.rb @@ -226,6 +226,9 @@ def signal_external_workflow(workflow, signal, workflow_id, run_id = nil, input def upsert_search_attributes(search_attributes) search_attributes = Temporal::Workflow::Context::Helpers.process_search_attributes(search_attributes) + if search_attributes.empty? + raise ArgumentError, "Cannot upsert an empty hash for search_attributes, as this would do nothing." + end execution.upsert_search_attributes(search_attributes) end diff --git a/lib/temporal/testing/temporal_override.rb b/lib/temporal/testing/temporal_override.rb index 1fae6c36..c67515e4 100644 --- a/lib/temporal/testing/temporal_override.rb +++ b/lib/temporal/testing/temporal_override.rb @@ -1,5 +1,6 @@ require 'securerandom' require 'temporal/activity/async_token' +require 'temporal/workflow/context_helpers' require 'temporal/workflow/execution_info' require 'temporal/workflow/status' require 'temporal/testing/workflow_execution' @@ -83,6 +84,7 @@ def start_locally(workflow, schedule, *input, **args) workflow_id = options[:workflow_id] || SecureRandom.uuid run_id = SecureRandom.uuid memo = options[:memo] || {} + initial_search_attributes = Workflow::Context::Helpers.process_search_attributes(options[:search_attributes] || {}) if !allowed?(workflow_id, reuse_policy) raise Temporal::WorkflowExecutionAlreadyStartedFailure.new( @@ -91,7 +93,7 @@ def start_locally(workflow, schedule, *input, **args) ) end - execution = WorkflowExecution.new + execution = WorkflowExecution.new(initial_search_attributes: initial_search_attributes) executions[[workflow_id, run_id]] = execution execution_options = ExecutionOptions.new(workflow, options) diff --git a/lib/temporal/testing/workflow_execution.rb b/lib/temporal/testing/workflow_execution.rb index 8e42518b..6ddbb18e 100644 --- a/lib/temporal/testing/workflow_execution.rb +++ b/lib/temporal/testing/workflow_execution.rb @@ -6,10 +6,10 @@ module Testing class WorkflowExecution attr_reader :status, :search_attributes - def initialize + def initialize(initial_search_attributes: {}) @status = Workflow::Status::RUNNING @futures = FutureRegistry.new - @search_attributes = {} + @search_attributes = initial_search_attributes end def run(&block) diff --git a/lib/temporal/workflow/command.rb b/lib/temporal/workflow/command.rb index b7ab9717..599ac040 100644 --- a/lib/temporal/workflow/command.rb +++ b/lib/temporal/workflow/command.rb @@ -3,8 +3,8 @@ class Workflow module Command # TODO: Move these classes into their own directories under workflow/command/* ScheduleActivity = Struct.new(:activity_type, :activity_id, :input, :namespace, :task_queue, :retry_policy, :timeouts, :headers, keyword_init: true) - StartChildWorkflow = Struct.new(:workflow_type, :workflow_id, :input, :namespace, :task_queue, :retry_policy, :parent_close_policy, :timeouts, :headers, :memo, :workflow_id_reuse_policy, keyword_init: true) - ContinueAsNew = Struct.new(:workflow_type, :task_queue, :input, :timeouts, :retry_policy, :headers, :memo, keyword_init: true) + StartChildWorkflow = Struct.new(:workflow_type, :workflow_id, :input, :namespace, :task_queue, :retry_policy, :parent_close_policy, :timeouts, :headers, :memo, :workflow_id_reuse_policy, :search_attributes, keyword_init: true) + ContinueAsNew = Struct.new(:workflow_type, :task_queue, :input, :timeouts, :retry_policy, :headers, :memo, :search_attributes, keyword_init: true) RequestActivityCancellation = Struct.new(:activity_id, keyword_init: true) RecordMarker = Struct.new(:name, :details, keyword_init: true) StartTimer = Struct.new(:timeout, :timer_id, keyword_init: true) diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index 462e154a..15af2bd1 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -131,7 +131,8 @@ def execute_workflow(workflow_class, *input, **args) timeouts: execution_options.timeouts, headers: execution_options.headers, memo: execution_options.memo, - workflow_id_reuse_policy: workflow_id_reuse_policy + workflow_id_reuse_policy: workflow_id_reuse_policy, + search_attributes: Helpers.process_search_attributes(execution_options.search_attributes), ) target, cancelation_id = schedule_command(command) @@ -245,6 +246,7 @@ def continue_as_new(*input, **args) retry_policy: execution_options.retry_policy, headers: execution_options.headers, memo: execution_options.memo, + search_attributes: Helpers.process_search_attributes(execution_options.search_attributes), ) schedule_command(command) completed! @@ -410,6 +412,9 @@ def signal_external_workflow(workflow, signal, workflow_id, run_id = nil, input # def upsert_search_attributes(search_attributes) search_attributes = Helpers.process_search_attributes(search_attributes) + if search_attributes.empty? + raise ArgumentError, "Cannot upsert an empty hash for search_attributes, as this would do nothing." + end command = Command::UpsertSearchAttributes.new( search_attributes: search_attributes ) diff --git a/lib/temporal/workflow/context_helpers.rb b/lib/temporal/workflow/context_helpers.rb index 0ecc345f..0006606d 100644 --- a/lib/temporal/workflow/context_helpers.rb +++ b/lib/temporal/workflow/context_helpers.rb @@ -2,7 +2,7 @@ module Temporal class Workflow class Context - # Shared between Context and LocalWorkflowContext so we can do the same validations in test and production. + # Shared between Context, and LocalWorkflowContext, and Client so we can do the same validations in test and production. module Helpers def self.process_search_attributes(search_attributes) @@ -12,9 +12,6 @@ def self.process_search_attributes(search_attributes) if !search_attributes.is_a?(Hash) raise ArgumentError, "for search_attributes, expecting a Hash, not #{search_attributes.class}" end - if search_attributes.empty? - raise ArgumentError, "Cannot upsert an empty hash for search_attributes, as this would do nothing." - end search_attributes.transform_values do |attribute| if attribute.is_a?(Time) # The server expects UTC times in the standard format. diff --git a/spec/unit/lib/temporal/client_spec.rb b/spec/unit/lib/temporal/client_spec.rb index 8cbde6cd..73b9b3fb 100644 --- a/spec/unit/lib/temporal/client_spec.rb +++ b/spec/unit/lib/temporal/client_spec.rb @@ -63,6 +63,7 @@ class TestStartWorkflow < Temporal::Workflow workflow_id_reuse_policy: nil, headers: {}, memo: {}, + search_attributes: {}, ) end @@ -76,7 +77,8 @@ class TestStartWorkflow < Temporal::Workflow task_queue: 'test-task-queue', headers: { 'Foo' => 'Bar' }, workflow_id_reuse_policy: :reject, - memo: { 'MemoKey1' => 'MemoValue1' } + memo: { 'MemoKey1' => 'MemoValue1' }, + search_attributes: { 'SearchAttribute1' => 256 }, } ) @@ -93,7 +95,8 @@ class TestStartWorkflow < Temporal::Workflow execution_timeout: Temporal.configuration.timeouts[:execution], workflow_id_reuse_policy: :reject, headers: { 'Foo' => 'Bar' }, - memo: { 'MemoKey1' => 'MemoValue1' } + memo: { 'MemoKey1' => 'MemoValue1' }, + search_attributes: { 'SearchAttribute1' => 256 }, ) end @@ -119,7 +122,8 @@ class TestStartWorkflow < Temporal::Workflow execution_timeout: Temporal.configuration.timeouts[:execution], workflow_id_reuse_policy: nil, headers: {}, - memo: {} + memo: {}, + search_attributes: {}, ) end @@ -139,7 +143,8 @@ class TestStartWorkflow < Temporal::Workflow execution_timeout: Temporal.configuration.timeouts[:execution], workflow_id_reuse_policy: nil, headers: {}, - memo: {} + memo: {}, + search_attributes: {}, ) end @@ -161,7 +166,8 @@ class TestStartWorkflow < Temporal::Workflow execution_timeout: Temporal.configuration.timeouts[:execution], workflow_id_reuse_policy: :allow, headers: {}, - memo: {} + memo: {}, + search_attributes: {}, ) end end @@ -187,7 +193,8 @@ class TestStartWorkflow < Temporal::Workflow execution_timeout: Temporal.configuration.timeouts[:execution], workflow_id_reuse_policy: nil, headers: {}, - memo: {} + memo: {}, + search_attributes: {}, ) end end @@ -215,6 +222,7 @@ def expect_signal_with_start(expected_arguments, expected_signal_argument) workflow_id_reuse_policy: nil, headers: {}, memo: {}, + search_attributes: {}, signal_name: 'the question', signal_input: expected_signal_argument, ) @@ -295,6 +303,7 @@ def expect_signal_with_start(expected_arguments, expected_signal_argument) execution_timeout: Temporal.configuration.timeouts[:execution], workflow_id_reuse_policy: nil, memo: {}, + search_attributes: {}, headers: {}, ) end diff --git a/spec/unit/lib/temporal/connection/serializer/continue_as_new_spec.rb b/spec/unit/lib/temporal/connection/serializer/continue_as_new_spec.rb index 18de4355..38166828 100644 --- a/spec/unit/lib/temporal/connection/serializer/continue_as_new_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/continue_as_new_spec.rb @@ -11,6 +11,7 @@ timeouts: Temporal.configuration.timeouts, headers: {'foo-header': 'bar'}, memo: {'foo-memo': 'baz'}, + search_attributes: {'foo-search-attribute': 'qux'}, ) result = described_class.new(command).to_proto @@ -31,6 +32,7 @@ expect(attribs.header.fields['foo-header'].data).to eq('"bar"') expect(attribs.memo.fields['foo-memo'].data).to eq('"baz"') + expect(attribs.search_attributes.indexed_fields['foo-search-attribute'].data).to eq('"qux"') end end end diff --git a/spec/unit/lib/temporal/connection/serializer/start_child_workflow_spec.rb b/spec/unit/lib/temporal/connection/serializer/start_child_workflow_spec.rb index 5e3278b4..2e72951c 100644 --- a/spec/unit/lib/temporal/connection/serializer/start_child_workflow_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/start_child_workflow_spec.rb @@ -14,6 +14,7 @@ timeouts: { execution: 1, run: 1, task: 1 }, headers: nil, memo: {}, + search_attributes: {}, ) end diff --git a/spec/unit/lib/temporal/execution_options_spec.rb b/spec/unit/lib/temporal/execution_options_spec.rb index ba4c84d1..98fbe380 100644 --- a/spec/unit/lib/temporal/execution_options_spec.rb +++ b/spec/unit/lib/temporal/execution_options_spec.rb @@ -66,7 +66,8 @@ class TestExecutionOptionsWorkflow < Temporal::Workflow { namespace: 'test-namespace', timeouts: { start_to_close: 10 }, - headers: { 'TestHeader' => 'Test' } + headers: { 'TestHeader' => 'Test' }, + search_attributes: { 'DoubleSearchAttribute' => 3.14 }, } end let(:defaults) do @@ -74,7 +75,8 @@ class TestExecutionOptionsWorkflow < Temporal::Workflow namespace: 'default-namespace', task_queue: 'default-task-queue', timeouts: { schedule_to_close: 42 }, - headers: { 'DefaultHeader' => 'Default' } + headers: { 'DefaultHeader' => 'Default' }, + search_attributes: { 'DefaultIntSearchAttribute' => 256 }, ) end @@ -85,6 +87,7 @@ class TestExecutionOptionsWorkflow < Temporal::Workflow expect(subject.retry_policy).to be_nil expect(subject.timeouts).to eq(schedule_to_close: 42, start_to_close: 10) expect(subject.headers).to eq('DefaultHeader' => 'Default', 'TestHeader' => 'Test') + expect(subject.search_attributes).to eq('DefaultIntSearchAttribute' => 256, 'DoubleSearchAttribute' => 3.14) end end @@ -191,7 +194,8 @@ class TestWorkflow < Temporal::Workflow namespace: 'default-namespace', task_queue: 'default-task-queue', timeouts: { schedule_to_close: 42 }, - headers: { 'DefaultHeader' => 'Default', 'HeaderA' => 'DefaultA' } + headers: { 'DefaultHeader' => 'Default', 'HeaderA' => 'DefaultA' }, + search_attributes: {}, ) end diff --git a/spec/unit/lib/temporal/grpc_spec.rb b/spec/unit/lib/temporal/grpc_spec.rb index ef617341..fecb25e4 100644 --- a/spec/unit/lib/temporal/grpc_spec.rb +++ b/spec/unit/lib/temporal/grpc_spec.rb @@ -39,6 +39,7 @@ class TestDeserializer run_timeout: 0, task_timeout: 0, memo: {}, + search_attributes: {}, workflow_id_reuse_policy: :allow, ) end.to raise_error(Temporal::WorkflowExecutionAlreadyStartedFailure) do |e| @@ -49,6 +50,7 @@ class TestDeserializer it 'starts a workflow with scalar arguments' do allow(grpc_stub).to receive(:start_workflow_execution).and_return(Temporal::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionResponse.new(run_id: 'xxx')) + datetime_attribute_value = Time.now subject.start_workflow_execution( namespace: namespace, workflow_id: workflow_id, @@ -59,6 +61,16 @@ class TestDeserializer run_timeout: 2, task_timeout: 3, memo: {}, + search_attributes: { + 'foo-int-attribute' => 256, + 'foo-string-attribute' => "bar", + 'foo-double-attribute' => 6.28, + 'foo-bool-attribute' => false, + # Temporal::Workflow::Context::Helpers.process_search_attributes will have converted + # any `Time` instances to strings by the time `start_workflow_execution` is called, + # so do the same here. + 'foo-datetime-attribute' => datetime_attribute_value.utc.iso8601, + }, workflow_id_reuse_policy: :reject, ) @@ -73,6 +85,13 @@ class TestDeserializer expect(request.workflow_run_timeout.seconds).to eq(2) expect(request.workflow_task_timeout.seconds).to eq(3) expect(request.workflow_id_reuse_policy).to eq(:WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE) + expect(request.search_attributes.indexed_fields).to eq({ + 'foo-int-attribute' => Temporal::Api::Common::V1::Payload.new(data: '256', metadata: { 'encoding' => 'json/plain' }), + 'foo-string-attribute' => Temporal::Api::Common::V1::Payload.new(data: '"bar"', metadata: { 'encoding' => 'json/plain' }), + 'foo-double-attribute' => Temporal::Api::Common::V1::Payload.new(data: '6.28', metadata: { 'encoding' => 'json/plain' }), + 'foo-bool-attribute' => Temporal::Api::Common::V1::Payload.new(data: 'false', metadata: { 'encoding' => 'json/plain' }), + 'foo-datetime-attribute' => Temporal::Api::Common::V1::Payload.new(data: "\"#{datetime_attribute_value.utc.iso8601}\"", metadata: { 'encoding' => 'json/plain' }), + }) end end @@ -87,6 +106,7 @@ class TestDeserializer run_timeout: 0, task_timeout: 0, memo: {}, + search_attributes: {}, workflow_id_reuse_policy: :not_a_valid_policy ) end.to raise_error(Temporal::Connection::ArgumentError) do |e| @@ -144,6 +164,7 @@ class TestDeserializer run_timeout: 0, task_timeout: 0, memo: {}, + search_attributes: {}, workflow_id_reuse_policy: :not_a_valid_policy, signal_name: 'the question', signal_input: 'what do you get if you multiply six by nine?' diff --git a/spec/unit/lib/temporal/testing/temporal_override_spec.rb b/spec/unit/lib/temporal/testing/temporal_override_spec.rb index 9280efb5..81a4345b 100644 --- a/spec/unit/lib/temporal/testing/temporal_override_spec.rb +++ b/spec/unit/lib/temporal/testing/temporal_override_spec.rb @@ -278,11 +278,17 @@ def execute UpsertSearchAttributesWorkflow, options: { workflow_id: workflow_id, + search_attributes: { + 'AdditionalSearchAttribute' => 189, + }, }, ) info = client.fetch_workflow_execution_info('default-namespace', workflow_id, run_id) - expect(info.search_attributes).to eq({'CustomIntField' => 5}) + expect(info.search_attributes).to eq({ + 'CustomIntField' => 5, + 'AdditionalSearchAttribute' => 189, + }) end end diff --git a/spec/unit/lib/temporal/workflow/execution_info_spec.rb b/spec/unit/lib/temporal/workflow/execution_info_spec.rb index f5cf1f1b..c40dfd98 100644 --- a/spec/unit/lib/temporal/workflow/execution_info_spec.rb +++ b/spec/unit/lib/temporal/workflow/execution_info_spec.rb @@ -15,6 +15,7 @@ expect(subject.status).to eq(:COMPLETED) expect(subject.history_length).to eq(api_info.history_length) expect(subject.memo).to eq({ 'foo' => 'bar' }) + expect(subject.search_attributes).to eq({ 'foo' => 'bar' }) end it 'freezes the info' do From bb3f330bebf8d380925d24074114fa9eba3ade00 Mon Sep 17 00:00:00 2001 From: Jeff Schoner Date: Tue, 6 Sep 2022 08:39:43 -0700 Subject: [PATCH 061/125] Fix dispatch ordering of wait_until handlers (#189) * Remove dead code from previous messy merge * Separate wait_until handlers, execute at end * Modify signal_with_start_workflow to cover dwillett's repro * Decouple register_handler from wait_until --- .../integration/signal_with_start_spec.rb | 8 +- .../workflows/signal_with_start_workflow.rb | 10 +- lib/temporal/workflow/context.rb | 11 +- lib/temporal/workflow/dispatcher.rb | 33 +++--- .../lib/temporal/workflow/dispatcher_spec.rb | 109 +++++++----------- 5 files changed, 79 insertions(+), 92 deletions(-) diff --git a/examples/spec/integration/signal_with_start_spec.rb b/examples/spec/integration/signal_with_start_spec.rb index 4af4f6f1..2971404c 100644 --- a/examples/spec/integration/signal_with_start_spec.rb +++ b/examples/spec/integration/signal_with_start_spec.rb @@ -7,11 +7,11 @@ run_id = Temporal.start_workflow( SignalWithStartWorkflow, 'signal_name', - 0.1, options: { workflow_id: workflow_id, signal_name: 'signal_name', signal_input: 'expected value', + timeouts: { execution: 10 }, } ) @@ -29,10 +29,10 @@ run_id = Temporal.start_workflow( SignalWithStartWorkflow, 'signal_name', - 0.1, options: { workflow_id: workflow_id, signal_name: 'signal_name', + timeouts: { execution: 10 }, } ) @@ -50,22 +50,22 @@ run_id = Temporal.start_workflow( SignalWithStartWorkflow, 'signal_name', - 10, options: { workflow_id: workflow_id, signal_name: 'signal_name', signal_input: 'expected value', + timeouts: { execution: 10 }, } ) second_run_id = Temporal.start_workflow( SignalWithStartWorkflow, 'signal_name', - 0.1, options: { workflow_id: workflow_id, signal_name: 'signal_name', signal_input: 'expected value', + timeouts: { execution: 10 }, } ) diff --git a/examples/workflows/signal_with_start_workflow.rb b/examples/workflows/signal_with_start_workflow.rb index dbcb186a..f8693ce1 100644 --- a/examples/workflows/signal_with_start_workflow.rb +++ b/examples/workflows/signal_with_start_workflow.rb @@ -1,16 +1,20 @@ +require 'activities/hello_world_activity' + class SignalWithStartWorkflow < Temporal::Workflow - def execute(expected_signal, sleep_for) - received = 'no signal received' + def execute(expected_signal) + initial_value = 'no signal received' + received = initial_value workflow.on_signal do |signal, input| if signal == expected_signal + HelloWorldActivity.execute!('expected signal') received = input end end # Do something to get descheduled so the signal handler has a chance to run - workflow.sleep(sleep_for) + workflow.wait_until { received != initial_value } received end end diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index 15af2bd1..902d60d3 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -291,7 +291,14 @@ def wait_until(&unblock_condition) fiber = Fiber.current - handler = dispatcher.register_handler(Dispatcher::TARGET_WILDCARD, Dispatcher::WILDCARD) do + # wait_until condition blocks often read state modified by target-specfic handlers like + # signal handlers or callbacks for timer or activity completion. Running the wait_until + # handlers after the other handlers ensures that state is correctly updated before being + # read. + handler = dispatcher.register_handler( + Dispatcher::WILDCARD, # any target + Dispatcher::WILDCARD, # any event type + Dispatcher::Order::AT_END) do fiber.resume if unblock_condition.call end @@ -325,7 +332,7 @@ def on_signal(signal_name = nil, &block) call_in_fiber(block, input) end else - dispatcher.register_handler(Dispatcher::TARGET_WILDCARD, 'signaled') do |signal, input| + dispatcher.register_handler(Dispatcher::WILDCARD, 'signaled') do |signal, input| call_in_fiber(block, signal, input) end end diff --git a/lib/temporal/workflow/dispatcher.rb b/lib/temporal/workflow/dispatcher.rb index d327cbe5..eb72b4e3 100644 --- a/lib/temporal/workflow/dispatcher.rb +++ b/lib/temporal/workflow/dispatcher.rb @@ -9,11 +9,6 @@ class Workflow # elsewhere in the system we may dispatch the event and execute the handler. # We *always* execute the handler associated with the event_name. # - # Optionally, we may register a named handler that is triggered when an event _and - # an optional handler_name key_ are provided. In this situation, we dispatch to both - # the handler associated to event_name+handler_name and to the handler associated with - # the event_name. The order of this dispatch is not guaranteed. - # class Dispatcher # Raised if a duplicate ID is encountered during dispatch handling. # This likely indicates a bug in temporal-ruby or that unsupported multithreaded @@ -40,19 +35,23 @@ def unregister end WILDCARD = '*'.freeze - TARGET_WILDCARD = '*'.freeze - EventStruct = Struct.new(:event_name, :handler) + module Order + AT_BEGINNING = 1 + AT_END = 2 + end + + EventStruct = Struct.new(:event_name, :handler, :order) def initialize - @handlers = Hash.new { |hash, key| hash[key] = {} } + @event_handlers = Hash.new { |hash, key| hash[key] = {} } @next_id = 0 end - def register_handler(target, event_name, &handler) + def register_handler(target, event_name, order=Order::AT_BEGINNING, &handler) @next_id += 1 - handlers[target][@next_id] = EventStruct.new(event_name, handler) - RegistrationHandle.new(handlers[target], @next_id) + event_handlers[target][@next_id] = EventStruct.new(event_name, handler, order) + RegistrationHandle.new(event_handlers[target], @next_id) end def dispatch(target, event_name, args = nil) @@ -63,14 +62,14 @@ def dispatch(target, event_name, args = nil) private - attr_reader :handlers + attr_reader :event_handlers def handlers_for(target, event_name) - handlers[target] - .merge(handlers[TARGET_WILDCARD]) { raise DuplicateIDError.new('Cannot resolve duplicate dispatcher handler IDs') } - .select { |_, event_struct| match?(event_struct, event_name) } - .sort - .map { |_, event_struct| event_struct.handler } + event_handlers[target] + .merge(event_handlers[WILDCARD]) { raise DuplicateIDError.new('Cannot resolve duplicate dispatcher handler IDs') } + .select { |_, event| match?(event, event_name) } + .sort_by{ |id, event_struct| [event_struct.order, id]} + .map { |_, event| event.handler } end def match?(event_struct, event_name) diff --git a/spec/unit/lib/temporal/workflow/dispatcher_spec.rb b/spec/unit/lib/temporal/workflow/dispatcher_spec.rb index 18aa1a9e..bf1887b9 100644 --- a/spec/unit/lib/temporal/workflow/dispatcher_spec.rb +++ b/spec/unit/lib/temporal/workflow/dispatcher_spec.rb @@ -12,53 +12,25 @@ subject.register_handler(target, event_name, &block) subject end - let(:handlers) { dispatcher.send(:handlers) } + let(:handlers) { dispatcher.send(:event_handlers) } - context 'with default handler_name' do - let(:handler_name) { nil } - - it 'stores the target' do - expect(handlers.key?(target)).to be true - end - - it 'stores the target and handler once' do - expect(handlers[target]).to be_kind_of(Hash) - expect(handlers[target].count).to eq 1 - end - - it 'associates the event name with the target' do - event = handlers[target][1] - expect(event.event_name).to eq(event_name) - end - - it 'associates the handler with the target' do - event = handlers[target][1] - expect(event.handler).to eq(block) - end + it 'stores the target' do + expect(handlers.key?(target)).to be true end - context 'with a specific handler_name' do - let(:handler_name) { 'specific name' } - let(:event_name) { "signaled:#{handler_name}" } - - it 'stores the target' do - expect(handlers.key?(target)).to be true - end - - it 'stores the target and handler once' do - expect(handlers[target]).to be_kind_of(Hash) - expect(handlers[target].count).to eq 1 - end + it 'stores the target and handler once' do + expect(handlers[target]).to be_kind_of(Hash) + expect(handlers[target].count).to eq 1 + end - it 'associates the event name and handler name with the target' do - event = handlers[target][1] - expect(event.event_name).to eq(event_name) - end + it 'associates the event name with the target' do + event = handlers[target][1] + expect(event.event_name).to eq(event_name) + end - it 'associates the handler with the target' do - event = handlers[target][1] - expect(event.handler).to eq(block) - end + it 'associates the handler with the target' do + event = handlers[target][1] + expect(event.handler).to eq(block) end it 'removes a given handler against the target' do @@ -70,19 +42,19 @@ subject.register_handler(target, 'signaled', &block2) subject.register_handler(other_target, 'signaled', &block3) - expect(subject.send(:handlers)[target][1].event_name).to eq('signaled') - expect(subject.send(:handlers)[target][1].handler).to be(block1) + expect(handlers[target][1].event_name).to eq('signaled') + expect(handlers[target][1].handler).to be(block1) - expect(subject.send(:handlers)[target][2].event_name).to eq('signaled') - expect(subject.send(:handlers)[target][2].handler).to be(block2) + expect(handlers[target][2].event_name).to eq('signaled') + expect(handlers[target][2].handler).to be(block2) - expect(subject.send(:handlers)[other_target][3].event_name).to eq('signaled') - expect(subject.send(:handlers)[other_target][3].handler).to be(block3) + expect(handlers[other_target][3].event_name).to eq('signaled') + expect(handlers[other_target][3].handler).to be(block3) handle1.unregister - expect(subject.send(:handlers)[target][1]).to be(nil) - expect(subject.send(:handlers)[target][2]).to_not be(nil) - expect(subject.send(:handlers)[other_target][3]).to_not be(nil) + expect(handlers[target][1]).to be(nil) + expect(handlers[target][2]).to_not be(nil) + expect(handlers[other_target][3]).to_not be(nil) end end @@ -139,14 +111,14 @@ end end - context 'with TARGET_WILDCARD target handler' do + context 'with WILDCARD target handler' do let(:handler_6) { -> { 'sixth block' } } let(:handler_7) { -> { 'seventh block' } } before do allow(handler_6).to receive(:call) allow(handler_7).to receive(:call) - subject.register_handler(described_class::TARGET_WILDCARD, described_class::WILDCARD, &handler_6) + subject.register_handler(described_class::WILDCARD, described_class::WILDCARD, &handler_6) subject.register_handler(target, 'completed', &handler_7) end @@ -160,31 +132,36 @@ expect(handler_7).to have_received(:call).ordered end - it 'TARGET_WILDCARD can be compared to an EventTarget object' do - expect(target.eql?(described_class::TARGET_WILDCARD)).to be(false) + it 'WILDCARD can be compared to an EventTarget object' do + expect(target.eql?(described_class::WILDCARD)).to be(false) end end - context 'with a named handler' do + context 'with AT_END order' do + let(:handler_5) { -> { 'fifth block' } } + let(:handler_6) { -> { 'sixth block' } } let(:handler_7) { -> { 'seventh block' } } - let(:handler_name) { 'specific name' } before do + allow(handler_5).to receive(:call) + allow(handler_6).to receive(:call) allow(handler_7).to receive(:call) + subject.register_handler(described_class::WILDCARD, described_class::WILDCARD, described_class::Order::AT_END, &handler_5) + subject.register_handler(described_class::WILDCARD, described_class::WILDCARD, described_class::Order::AT_END, &handler_6) subject.register_handler(target, 'completed', &handler_7) end - it 'calls the named handler and the default' do - subject.dispatch(target, 'completed', handler_name: handler_name) + it 'calls the handler' do + subject.dispatch(target, 'completed') - # the parent context "before" block registers the handlers with the target - # so look there for why handlers 1 and 4 are also called - expect(handler_7).to have_received(:call) - expect(handler_1).to have_received(:call) - expect(handler_4).to have_received(:call) + # Target handlers still invoked + expect(handler_1).to have_received(:call).ordered + expect(handler_4).to have_received(:call).ordered + expect(handler_7).to have_received(:call).ordered - expect(handler_2).not_to have_received(:call) - expect(handler_3).not_to have_received(:call) + # AT_END handlers are invoked at the end, in order + expect(handler_5).to have_received(:call).ordered + expect(handler_6).to have_received(:call).ordered end end end From a7259e19a83ac14c48380315dc76a1cf7b23aadd Mon Sep 17 00:00:00 2001 From: Dave Willett Date: Fri, 9 Sep 2022 08:42:23 -0700 Subject: [PATCH 062/125] Add an interface and support for cron scheduling child workflows (#190) * Add an interface and support for cron scheduling child workflows * Address PR feedback and add unit test coverage * Simplify syntax in spec for keyword args * More consistent setup for the integration test * Feels a little more consistent to use an exception result --- examples/bin/worker | 1 + .../schedule_child_workflow_spec.rb | 37 +++++++++++ examples/workflows/schedule_child_workflow.rb | 6 ++ .../serializer/start_child_workflow.rb | 1 + lib/temporal/workflow/command.rb | 2 +- lib/temporal/workflow/context.rb | 7 +++ lib/temporal/workflow/convenience_methods.rb | 7 +++ .../lib/temporal/workflow/context_spec.rb | 63 ++++++++++++++++++- .../workflow/convenience_methods_spec.rb | 33 +++++++++- 9 files changed, 151 insertions(+), 6 deletions(-) create mode 100644 examples/spec/integration/schedule_child_workflow_spec.rb create mode 100644 examples/workflows/schedule_child_workflow.rb diff --git a/examples/bin/worker b/examples/bin/worker index cccf739d..92f990b8 100755 --- a/examples/bin/worker +++ b/examples/bin/worker @@ -42,6 +42,7 @@ worker.register_workflow(QuickTimeoutWorkflow) worker.register_workflow(RandomlyFailingWorkflow) worker.register_workflow(ReleaseWorkflow) worker.register_workflow(ResultWorkflow) +worker.register_workflow(ScheduleChildWorkflow) worker.register_workflow(SendSignalToExternalWorkflow) worker.register_workflow(SerialHelloWorldWorkflow) worker.register_workflow(SideEffectWorkflow) diff --git a/examples/spec/integration/schedule_child_workflow_spec.rb b/examples/spec/integration/schedule_child_workflow_spec.rb new file mode 100644 index 00000000..a49f918a --- /dev/null +++ b/examples/spec/integration/schedule_child_workflow_spec.rb @@ -0,0 +1,37 @@ +require 'workflows/schedule_child_workflow' +require 'workflows/hello_world_workflow' + +describe ScheduleChildWorkflow, :integration do + let(:cron_schedule) { "*/6 * * * *" } + + it 'schedules a child workflow with a given cron schedule' do + child_workflow_id = 'schedule_child_test_wf-' + SecureRandom.uuid + workflow_id, run_id = run_workflow( + described_class, + child_workflow_id, + cron_schedule, + options: { + timeouts: { execution: 10 } + } + ) + + wait_for_workflow_completion(workflow_id, run_id) + parent_history = fetch_history(workflow_id, run_id) + + child_workflow_event = parent_history.history.events.detect do |event| + event.event_type == :EVENT_TYPE_START_CHILD_WORKFLOW_EXECUTION_INITIATED + end + expect( + child_workflow_event.start_child_workflow_execution_initiated_event_attributes.cron_schedule + ).to eq(cron_schedule) + + # Expecting the child workflow to terminate as a result of the parent close policy + expect do + Temporal.await_workflow_result( + HelloWorldWorkflow, + workflow_id: child_workflow_id + ) + end.to raise_error(Temporal::WorkflowTerminated) + + end +end diff --git a/examples/workflows/schedule_child_workflow.rb b/examples/workflows/schedule_child_workflow.rb new file mode 100644 index 00000000..da4fa3c6 --- /dev/null +++ b/examples/workflows/schedule_child_workflow.rb @@ -0,0 +1,6 @@ +class ScheduleChildWorkflow < Temporal::Workflow + def execute(child_workflow_id, cron_schedule) + HelloWorldWorkflow.schedule(cron_schedule, options: { workflow_id: child_workflow_id }) + workflow.sleep(1) + end +end diff --git a/lib/temporal/connection/serializer/start_child_workflow.rb b/lib/temporal/connection/serializer/start_child_workflow.rb index 878e4a0f..7d1c03e4 100644 --- a/lib/temporal/connection/serializer/start_child_workflow.rb +++ b/lib/temporal/connection/serializer/start_child_workflow.rb @@ -31,6 +31,7 @@ def to_proto retry_policy: Temporal::Connection::Serializer::RetryPolicy.new(object.retry_policy).to_proto, parent_close_policy: serialize_parent_close_policy(object.parent_close_policy), header: serialize_headers(object.headers), + cron_schedule: object.cron_schedule, memo: serialize_memo(object.memo), workflow_id_reuse_policy: Temporal::Connection::Serializer::WorkflowIdReusePolicy.new(object.workflow_id_reuse_policy).to_proto, search_attributes: serialize_search_attributes(object.search_attributes), diff --git a/lib/temporal/workflow/command.rb b/lib/temporal/workflow/command.rb index 599ac040..b81f2deb 100644 --- a/lib/temporal/workflow/command.rb +++ b/lib/temporal/workflow/command.rb @@ -3,7 +3,7 @@ class Workflow module Command # TODO: Move these classes into their own directories under workflow/command/* ScheduleActivity = Struct.new(:activity_type, :activity_id, :input, :namespace, :task_queue, :retry_policy, :timeouts, :headers, keyword_init: true) - StartChildWorkflow = Struct.new(:workflow_type, :workflow_id, :input, :namespace, :task_queue, :retry_policy, :parent_close_policy, :timeouts, :headers, :memo, :workflow_id_reuse_policy, :search_attributes, keyword_init: true) + StartChildWorkflow = Struct.new(:workflow_type, :workflow_id, :input, :namespace, :task_queue, :retry_policy, :parent_close_policy, :timeouts, :headers, :cron_schedule, :memo, :workflow_id_reuse_policy, :search_attributes, keyword_init: true) ContinueAsNew = Struct.new(:workflow_type, :task_queue, :input, :timeouts, :retry_policy, :headers, :memo, :search_attributes, keyword_init: true) RequestActivityCancellation = Struct.new(:activity_id, keyword_init: true) RecordMarker = Struct.new(:name, :details, keyword_init: true) diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index 902d60d3..bbd5cb0b 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -117,6 +117,7 @@ def execute_workflow(workflow_class, *input, **args) input << args unless args.empty? parent_close_policy = options.delete(:parent_close_policy) + cron_schedule = options.delete(:cron_schedule) workflow_id_reuse_policy = options.delete(:workflow_id_reuse_policy) execution_options = ExecutionOptions.new(workflow_class, options, config.default_execution_options) @@ -130,6 +131,7 @@ def execute_workflow(workflow_class, *input, **args) parent_close_policy: parent_close_policy, timeouts: execution_options.timeouts, headers: execution_options.headers, + cron_schedule: cron_schedule, memo: execution_options.memo, workflow_id_reuse_policy: workflow_id_reuse_policy, search_attributes: Helpers.process_search_attributes(execution_options.search_attributes), @@ -173,6 +175,11 @@ def execute_workflow!(workflow_class, *input, **args) result end + def schedule_workflow(workflow_class, cron_schedule, *input, **args) + args[:options] = (args[:options] || {}).merge(cron_schedule: cron_schedule) + execute_workflow(workflow_class, *input, **args) + end + def side_effect(&block) marker = state_manager.next_side_effect return marker.last if marker diff --git a/lib/temporal/workflow/convenience_methods.rb b/lib/temporal/workflow/convenience_methods.rb index 93f97812..90741f34 100644 --- a/lib/temporal/workflow/convenience_methods.rb +++ b/lib/temporal/workflow/convenience_methods.rb @@ -29,6 +29,13 @@ def execute!(*input, **args) context.execute_workflow!(self, *input, **args) end + + def schedule(cron_schedule, *input, **args) + context = Temporal::ThreadLocalContext.get + raise 'Called Workflow#schedule outside of a Workflow context' unless context + + context.schedule_workflow(self, cron_schedule, *input, **args) + end end end end diff --git a/spec/unit/lib/temporal/workflow/context_spec.rb b/spec/unit/lib/temporal/workflow/context_spec.rb index 5d7f40ba..76e99dc1 100644 --- a/spec/unit/lib/temporal/workflow/context_spec.rb +++ b/spec/unit/lib/temporal/workflow/context_spec.rb @@ -85,7 +85,7 @@ class MyTestWorkflow < Temporal::Workflow; end end child_workflow_future = workflow_context.execute_workflow(MyTestWorkflow) - + # expect all futures to be false as nothing has happened expect(child_workflow_future.finished?).to be false expect(child_workflow_future.child_workflow_execution_future.finished?).to be false @@ -117,7 +117,7 @@ class MyTestWorkflow < Temporal::Workflow; end end child_workflow_future = workflow_context.execute_workflow(MyTestWorkflow) - + # expect all futures to be false as nothing has happened expect(child_workflow_future.finished?).to be false expect(child_workflow_future.child_workflow_execution_future.finished?).to be false @@ -142,7 +142,7 @@ class MyTestWorkflow < Temporal::Workflow; end end child_workflow_future = workflow_context.execute_workflow(MyTestWorkflow) - + # expect all futures to be false as nothing has happened expect(child_workflow_future.finished?).to be false expect(child_workflow_future.child_workflow_execution_future.finished?).to be false @@ -154,6 +154,63 @@ class MyTestWorkflow < Temporal::Workflow; end end end + describe '#execute_workflow!' do + let(:child_workflow_future) do + double = instance_double('Temporal::Workflow::ChildWorkflowFuture') + allow(double).to receive(:get).and_return(result) + double + end + + before do + expect(workflow_context).to receive(:execute_workflow).and_return(child_workflow_future) + end + + context 'when future fails' do + let(:result) { Temporal::WorkflowRunError } + + it 'raises the future result exception' do + expect(child_workflow_future).to receive(:failed?).and_return(true) + expect { workflow_context.execute_workflow!(MyTestWorkflow) }.to raise_error(result) + end + end + + context 'when future succeeds' do + let(:result) { 'result' } + + it 'returns the future result' do + expect(child_workflow_future).to receive(:failed?).and_return(false) + expect(workflow_context.execute_workflow!(MyTestWorkflow)).to eq(result) + end + end + end + + describe '#schedule_workflow' do + let(:cron_schedule) { '* * * * *' } + + context 'when given workflow options' do + it 'executes workflow with merged cron_schedule option' do + expect(workflow_context).to receive(:execute_workflow).with(MyTestWorkflow, + options: { + parent_close_policy: :abandon, + cron_schedule: cron_schedule + } + ) + workflow_context.schedule_workflow(MyTestWorkflow, cron_schedule, options: { parent_close_policy: :abandon }) + end + end + + context 'when not given workflow options' do + it 'executes workflow with cron_schedule option' do + expect(workflow_context).to receive(:execute_workflow).with(MyTestWorkflow, + options: { + cron_schedule: cron_schedule + } + ) + workflow_context.schedule_workflow(MyTestWorkflow, cron_schedule) + end + end + end + describe '#upsert_search_attributes' do it 'does not accept nil' do expect do diff --git a/spec/unit/lib/temporal/workflow/convenience_methods_spec.rb b/spec/unit/lib/temporal/workflow/convenience_methods_spec.rb index 46183fac..f865f16e 100644 --- a/spec/unit/lib/temporal/workflow/convenience_methods_spec.rb +++ b/spec/unit/lib/temporal/workflow/convenience_methods_spec.rb @@ -19,7 +19,7 @@ class TestWorkflow < Temporal::Workflow; end allow(context).to receive(:execute_workflow) end - it 'executes activity' do + it 'executes workflow' do subject.execute(input, **options) expect(context) @@ -46,7 +46,7 @@ class TestWorkflow < Temporal::Workflow; end allow(context).to receive(:execute_workflow!) end - it 'executes activity' do + it 'executes workflow' do subject.execute!(input, **options) expect(context) @@ -65,4 +65,33 @@ class TestWorkflow < Temporal::Workflow; end end end end + + describe '.schedule' do + let(:cron_schedule) { '* * * * *' } + + context 'with local context' do + before do + Temporal::ThreadLocalContext.set(context) + allow(context).to receive(:schedule_workflow) + end + + it 'schedules workflow' do + subject.schedule(cron_schedule, input, **options) + + expect(context) + .to have_received(:schedule_workflow) + .with(subject, cron_schedule, input, options) + end + end + + context 'without local context' do + before { Temporal::ThreadLocalContext.set(nil) } + + it 'raises an error' do + expect do + subject.schedule(cron_schedule, input, **options) + end.to raise_error('Called Workflow#schedule outside of a Workflow context') + end + end + end end From 17181e43f83badd5d8ac0131d6ddbfcba7b9c0ba Mon Sep 17 00:00:00 2001 From: stuppy Date: Fri, 16 Sep 2022 10:07:14 -0500 Subject: [PATCH 063/125] Adds ProtoJSON support for protobuf inputs (#192) * Adds ProtoJSON support for protobuf inputs * review feedback update --- lib/temporal/configuration.rb | 2 ++ .../converter/payload/proto_json.rb | 35 +++++++++++++++++++ .../converter/payload/proto_json_spec.rb | 25 +++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 lib/temporal/connection/converter/payload/proto_json.rb create mode 100644 spec/unit/lib/temporal/connection/converter/payload/proto_json_spec.rb diff --git a/lib/temporal/configuration.rb b/lib/temporal/configuration.rb index 99e582a2..b93c9dbb 100644 --- a/lib/temporal/configuration.rb +++ b/lib/temporal/configuration.rb @@ -3,6 +3,7 @@ require 'temporal/connection/converter/payload/nil' require 'temporal/connection/converter/payload/bytes' require 'temporal/connection/converter/payload/json' +require 'temporal/connection/converter/payload/proto_json' require 'temporal/connection/converter/composite' module Temporal @@ -39,6 +40,7 @@ class Configuration payload_converters: [ Temporal::Connection::Converter::Payload::Nil.new, Temporal::Connection::Converter::Payload::Bytes.new, + Temporal::Connection::Converter::Payload::ProtoJSON.new, Temporal::Connection::Converter::Payload::JSON.new, ] ).freeze diff --git a/lib/temporal/connection/converter/payload/proto_json.rb b/lib/temporal/connection/converter/payload/proto_json.rb new file mode 100644 index 00000000..994c1e24 --- /dev/null +++ b/lib/temporal/connection/converter/payload/proto_json.rb @@ -0,0 +1,35 @@ +require 'temporal/json' + +module Temporal + module Connection + module Converter + module Payload + class ProtoJSON + ENCODING = 'json/protobuf'.freeze + + def encoding + ENCODING + end + + def from_payload(payload) + # TODO: Add error handling. + message_type = payload.metadata['messageType'] + descriptor = Google::Protobuf::DescriptorPool.generated_pool.lookup(message_type) + descriptor.msgclass.decode_json(payload.data) + end + + def to_payload(data) + return unless data.is_a?(Google::Protobuf::MessageExts) + Temporal::Api::Common::V1::Payload.new( + metadata: { + 'encoding' => ENCODING, + 'messageType' => data.class.descriptor.name, + }, + data: data.to_json, + ) + end + end + end + end + end +end diff --git a/spec/unit/lib/temporal/connection/converter/payload/proto_json_spec.rb b/spec/unit/lib/temporal/connection/converter/payload/proto_json_spec.rb new file mode 100644 index 00000000..5570d9df --- /dev/null +++ b/spec/unit/lib/temporal/connection/converter/payload/proto_json_spec.rb @@ -0,0 +1,25 @@ +require 'temporal/connection/converter/payload/proto_json' + +describe Temporal::Connection::Converter::Payload::ProtoJSON do + subject { described_class.new } + + describe 'round trip' do + it 'converts' do + # Temporal::Api::Common::V1::Payload is a protobuf. + # Using it as the "input" here to show the roundtrip. + # #to_payload will return a wrapped Payload around this one. + input = Temporal::Api::Common::V1::Payload.new( + metadata: { 'hello' => 'world' }, + data: 'hello world', + ) + + expect(subject.from_payload(subject.to_payload(input))).to eq(input) + end + end + + it 'skips if not proto message' do + input = { hello: 'world' } + + expect(subject.to_payload(input)).to be nil + end +end From bd5b7053d175c7bbea79b95255a46b68fa096776 Mon Sep 17 00:00:00 2001 From: Anthony D Date: Tue, 25 Oct 2022 20:43:45 +0100 Subject: [PATCH 064/125] Pin RSpec to 3.10.0 (#200) --- temporal.gemspec | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/temporal.gemspec b/temporal.gemspec index c8589c9e..8c51adef 100644 --- a/temporal.gemspec +++ b/temporal.gemspec @@ -18,7 +18,8 @@ Gem::Specification.new do |spec| spec.add_dependency 'oj' spec.add_development_dependency 'pry' - spec.add_development_dependency 'rspec' + # TODO: Investigate spec failure surfacing in RSpec 3.11 + spec.add_development_dependency 'rspec', '~> 3.10.0' spec.add_development_dependency 'fabrication' spec.add_development_dependency 'grpc-tools' spec.add_development_dependency 'yard' From 155a04608122fc28b0e7f9604c32e2dbd54a7576 Mon Sep 17 00:00:00 2001 From: calum-stripe <98350978+calum-stripe@users.noreply.github.com> Date: Wed, 26 Oct 2022 08:18:11 -0700 Subject: [PATCH 065/125] Fix for incorrect handling of Child Workflows that time out and are terminated (#195) * added fix for children workflows timing out fixed fixed erors fixed fixed * reverted to client master * fixed * state manager update * emppty * Revert "state manager update" This reverts commit ca37f73c435ef28ae5a2159d43dcc3280c52091b. * fixed --- .../activities/terminate_workflow_activity.rb | 5 +++++ examples/bin/worker | 3 +++ ...child_workflow_terminated_workflow_spec.rb | 22 +++++++++++++++++++ .../child_workflow_timeout_workflow_spec.rb | 22 +++++++++++++++++++ .../child_workflow_terminated_workflow.rb | 21 ++++++++++++++++++ .../child_workflow_timeout_workflow.rb | 15 +++++++++++++ lib/temporal/errors.rb | 8 +++++-- .../workflow/command_state_machine.rb | 5 +++++ lib/temporal/workflow/errors.rb | 1 - lib/temporal/workflow/state_manager.rb | 6 ++--- 10 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 examples/activities/terminate_workflow_activity.rb create mode 100644 examples/spec/integration/child_workflow_terminated_workflow_spec.rb create mode 100644 examples/spec/integration/child_workflow_timeout_workflow_spec.rb create mode 100644 examples/workflows/child_workflow_terminated_workflow.rb create mode 100644 examples/workflows/child_workflow_timeout_workflow.rb diff --git a/examples/activities/terminate_workflow_activity.rb b/examples/activities/terminate_workflow_activity.rb new file mode 100644 index 00000000..2f1486b8 --- /dev/null +++ b/examples/activities/terminate_workflow_activity.rb @@ -0,0 +1,5 @@ +class TerminateWorkflowActivity < Temporal::Activity + def execute(namespace, workflow_id, run_id) + Temporal.terminate_workflow(workflow_id, namespace: namespace, run_id: run_id) + end +end diff --git a/examples/bin/worker b/examples/bin/worker index 92f990b8..b21935ba 100755 --- a/examples/bin/worker +++ b/examples/bin/worker @@ -25,6 +25,8 @@ worker.register_workflow(BranchingWorkflow) worker.register_workflow(CallFailingActivityWorkflow) worker.register_workflow(CancellingTimerWorkflow) worker.register_workflow(CheckWorkflow) +worker.register_workflow(ChildWorkflowTimeoutWorkflow) +worker.register_workflow(ChildWorkflowTerminatedWorkflow) worker.register_workflow(FailingActivitiesWorkflow) worker.register_workflow(FailingWorkflow) worker.register_workflow(HelloWorldWorkflow) @@ -67,6 +69,7 @@ worker.register_activity(LongRunningActivity) worker.register_activity(ProcessFileActivity) worker.register_activity(RandomlyFailingActivity) worker.register_activity(RandomNumberActivity) +worker.register_activity(TerminateWorkflowActivity) worker.register_activity(SleepActivity) worker.register_activity(UploadFileActivity) worker.register_activity(Trip::BookFlightActivity) diff --git a/examples/spec/integration/child_workflow_terminated_workflow_spec.rb b/examples/spec/integration/child_workflow_terminated_workflow_spec.rb new file mode 100644 index 00000000..f8ad62a2 --- /dev/null +++ b/examples/spec/integration/child_workflow_terminated_workflow_spec.rb @@ -0,0 +1,22 @@ +require 'workflows/child_workflow_terminated_workflow.rb' + +describe ChildWorkflowTerminatedWorkflow do + subject { described_class } + + it 'successfully can catch if a child workflow times out' do + workflow_id = SecureRandom.uuid + + Temporal.start_workflow( + subject, + options: { workflow_id: workflow_id } + ) + + result = Temporal.await_workflow_result( + subject, + workflow_id: workflow_id + ) + + expect(result[:child_workflow_terminated]).to eq(true) + expect(result[:error]).to be_a(Temporal::ChildWorkflowTerminatedError) + end +end diff --git a/examples/spec/integration/child_workflow_timeout_workflow_spec.rb b/examples/spec/integration/child_workflow_timeout_workflow_spec.rb new file mode 100644 index 00000000..43736b7b --- /dev/null +++ b/examples/spec/integration/child_workflow_timeout_workflow_spec.rb @@ -0,0 +1,22 @@ +require 'workflows/child_workflow_timeout_workflow.rb' + +describe ChildWorkflowTimeoutWorkflow do + subject { described_class } + + it 'successfully can catch if a child workflow times out' do + workflow_id = SecureRandom.uuid + + Temporal.start_workflow( + subject, + options: { workflow_id: workflow_id } + ) + + result = Temporal.await_workflow_result( + subject, + workflow_id: workflow_id + ) + puts result + expect(result[:child_workflow_failed]).to eq(true) + expect(result[:error]).to be_a(Temporal::ChildWorkflowTimeoutError) + end +end diff --git a/examples/workflows/child_workflow_terminated_workflow.rb b/examples/workflows/child_workflow_terminated_workflow.rb new file mode 100644 index 00000000..56e2d1ba --- /dev/null +++ b/examples/workflows/child_workflow_terminated_workflow.rb @@ -0,0 +1,21 @@ +require 'workflows/simple_timer_workflow' +require 'activities/terminate_workflow_activity' + +class ChildWorkflowTerminatedWorkflow < Temporal::Workflow + def execute + # start a child workflow that executes for 60 seconds, then attempts to try and terminate that workflow + result = SimpleTimerWorkflow.execute(60) + child_workflow_execution = result.child_workflow_execution_future.get + TerminateWorkflowActivity.execute!( + 'ruby-samples', + child_workflow_execution.workflow_id, + child_workflow_execution.run_id + ) + + # check that the result is now 'failed' + { + child_workflow_terminated: result.failed?, # terminated is represented as failed? with the Terminated Error + error: result.get + } + end +end diff --git a/examples/workflows/child_workflow_timeout_workflow.rb b/examples/workflows/child_workflow_timeout_workflow.rb new file mode 100644 index 00000000..41a2bde4 --- /dev/null +++ b/examples/workflows/child_workflow_timeout_workflow.rb @@ -0,0 +1,15 @@ +require 'workflows/quick_timeout_workflow' + +class ChildWorkflowTimeoutWorkflow < Temporal::Workflow + def execute + # workflow timesout before it can finish running, we should be able to detect that with .failed? + result = QuickTimeoutWorkflow.execute + + result.get # wait for the workflow to finish so we can detect if it failed or not + + { + child_workflow_failed: result.failed?, + error: result.get + } + end +end diff --git a/lib/temporal/errors.rb b/lib/temporal/errors.rb index dde124c4..41789960 100644 --- a/lib/temporal/errors.rb +++ b/lib/temporal/errors.rb @@ -15,6 +15,12 @@ class ClientError < Error; end # Represents any timeout class TimeoutError < ClientError; end + # Represents when a child workflow times out + class ChildWorkflowTimeoutError < Error; end + + # Represents when a child workflow is terminated + class ChildWorkflowTerminatedError < Error; end + # A superclass for activity exceptions raised explicitly # with the intent to propagate to a workflow class ActivityException < ClientError; end @@ -61,7 +67,6 @@ def initialize(message, run_id = nil) super(message) @run_id = run_id end - end class NamespaceNotActiveFailure < ApiError; end class ClientVersionNotSupportedFailure < ApiError; end @@ -70,5 +75,4 @@ class NamespaceAlreadyExistsFailure < ApiError; end class CancellationAlreadyRequestedFailure < ApiError; end class QueryFailed < ApiError; end class UnexpectedResponse < ApiError; end - end diff --git a/lib/temporal/workflow/command_state_machine.rb b/lib/temporal/workflow/command_state_machine.rb index 09366cad..74adcf16 100644 --- a/lib/temporal/workflow/command_state_machine.rb +++ b/lib/temporal/workflow/command_state_machine.rb @@ -9,6 +9,7 @@ class CommandStateMachine CANCELED_STATE = :canceled FAILED_STATE = :failed TIMED_OUT_STATE = :timed_out + TERMINATED_STATE = :terminated attr_reader :state @@ -36,6 +37,10 @@ def cancel @state = CANCELED_STATE end + def terminated + @state = TERMINATED_STATE + end + def fail @state = FAILED_STATE end diff --git a/lib/temporal/workflow/errors.rb b/lib/temporal/workflow/errors.rb index 9b676f02..25396702 100644 --- a/lib/temporal/workflow/errors.rb +++ b/lib/temporal/workflow/errors.rb @@ -72,7 +72,6 @@ def self.generate_error_for_child_workflow_start(cause, workflow_id) rescue NameError nil end - end end end diff --git a/lib/temporal/workflow/state_manager.rb b/lib/temporal/workflow/state_manager.rb index 663d8169..9c391e90 100644 --- a/lib/temporal/workflow/state_manager.rb +++ b/lib/temporal/workflow/state_manager.rb @@ -264,11 +264,11 @@ def apply_event(event) when 'CHILD_WORKFLOW_EXECUTION_TIMED_OUT' state_machine.time_out - dispatch(history_target, 'failed', Temporal::Workflow::Errors.generate_error(event.attributes.failure)) + dispatch(history_target, 'failed', ChildWorkflowTimeoutError.new('The child workflow timed out before succeeding')) when 'CHILD_WORKFLOW_EXECUTION_TERMINATED' - # todo - + state_machine.terminated + dispatch(history_target, 'failed', ChildWorkflowTerminatedError.new('The child workflow was terminated')) when 'SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_INITIATED' # Temporal Server will try to Signal the targeted Workflow # Contains the Signal name, as well as a Signal payload From b50f0d09537e42d502ad4252bd337efa39886c91 Mon Sep 17 00:00:00 2001 From: jeffschoner-stripe <63118764+jeffschoner-stripe@users.noreply.github.com> Date: Wed, 26 Oct 2022 08:21:54 -0700 Subject: [PATCH 066/125] Derive client identity from pid, make configurable (#198) * Allow client identity to be configurable * Use PID instead of thread ID in default identity --- lib/temporal/configuration.rb | 23 +++++++++++++------- lib/temporal/connection.rb | 5 +---- spec/unit/lib/temporal/configuration_spec.rb | 13 +++++++++++ spec/unit/lib/temporal/connection_spec.rb | 8 +++++++ 4 files changed, 37 insertions(+), 12 deletions(-) diff --git a/lib/temporal/configuration.rb b/lib/temporal/configuration.rb index b93c9dbb..c9d8f95f 100644 --- a/lib/temporal/configuration.rb +++ b/lib/temporal/configuration.rb @@ -8,12 +8,12 @@ module Temporal class Configuration - Connection = Struct.new(:type, :host, :port, :credentials, keyword_init: true) + Connection = Struct.new(:type, :host, :port, :credentials, :identity, keyword_init: true) Execution = Struct.new(:namespace, :task_queue, :timeouts, :headers, :search_attributes, keyword_init: true) attr_reader :timeouts, :error_handlers attr_writer :converter - attr_accessor :connection_type, :host, :port, :credentials, :logger, :metrics_adapter, :namespace, :task_queue, :headers, :search_attributes + attr_accessor :connection_type, :host, :port, :credentials, :identity, :logger, :metrics_adapter, :namespace, :task_queue, :headers, :search_attributes # See https://docs.temporal.io/blog/activity-timeouts/ for general docs. # We want an infinite execution timeout for cron schedules and other perpetual workflows. @@ -41,7 +41,7 @@ class Configuration Temporal::Connection::Converter::Payload::Nil.new, Temporal::Connection::Converter::Payload::Bytes.new, Temporal::Connection::Converter::Payload::ProtoJSON.new, - Temporal::Connection::Converter::Payload::JSON.new, + Temporal::Connection::Converter::Payload::JSON.new ] ).freeze @@ -56,6 +56,7 @@ def initialize @converter = DEFAULT_CONVERTER @error_handlers = [] @credentials = :this_channel_is_insecure + @identity = nil @search_attributes = {} end @@ -75,26 +76,32 @@ def timeouts=(new_timeouts) @timeouts = DEFAULT_TIMEOUTS.merge(new_timeouts) end - def converter - @converter - end + attr_reader :converter def for_connection Connection.new( type: connection_type, host: host, port: port, - credentials: credentials + credentials: credentials, + identity: identity || default_identity ).freeze end + def default_identity + hostname = `hostname` + pid = Process.pid + + "#{pid}@#{hostname}" + end + def default_execution_options Execution.new( namespace: namespace, task_queue: task_list, timeouts: timeouts, headers: headers, - search_attributes: search_attributes, + search_attributes: search_attributes ).freeze end end diff --git a/lib/temporal/connection.rb b/lib/temporal/connection.rb index 791534bf..b70bcbed 100644 --- a/lib/temporal/connection.rb +++ b/lib/temporal/connection.rb @@ -11,10 +11,7 @@ def self.generate(configuration) host = configuration.host port = configuration.port credentials = configuration.credentials - - hostname = `hostname` - thread_id = Thread.current.object_id - identity = "#{thread_id}@#{hostname}" + identity = configuration.identity connection_class.new(host, port, identity, credentials) end diff --git a/spec/unit/lib/temporal/configuration_spec.rb b/spec/unit/lib/temporal/configuration_spec.rb index 7e083429..c9ee44c0 100644 --- a/spec/unit/lib/temporal/configuration_spec.rb +++ b/spec/unit/lib/temporal/configuration_spec.rb @@ -26,4 +26,17 @@ expect(timeouts[:heartbeat]).to be(nil) end end + + describe '#for_connection' do + let (:new_identity) { 'new_identity' } + + it 'default identity' do + expect(subject.for_connection).to have_attributes(identity: "#{Process.pid}@#{`hostname`}") + end + + it 'override identity' do + subject.identity = new_identity + expect(subject.for_connection).to have_attributes(identity: new_identity) + end + end end \ No newline at end of file diff --git a/spec/unit/lib/temporal/connection_spec.rb b/spec/unit/lib/temporal/connection_spec.rb index dff88b0a..f334d6d8 100644 --- a/spec/unit/lib/temporal/connection_spec.rb +++ b/spec/unit/lib/temporal/connection_spec.rb @@ -10,6 +10,14 @@ config end + context 'identity' do + let(:identity) { 'my_identity' } + it 'overrides' do + config.identity = identity + expect(subject.send(:identity)).to eq(identity) + end + end + context 'insecure' do let(:credentials) { :this_channel_is_insecure } From 13ac2e2eb785ceb591fcb74a2a9e987b7fa38961 Mon Sep 17 00:00:00 2001 From: jeffschoner-stripe <63118764+jeffschoner-stripe@users.noreply.github.com> Date: Fri, 28 Oct 2022 15:21:41 -0700 Subject: [PATCH 067/125] Task metrics (#199) * Consolidate metrics constants * Workflow task failure counter * Workflow and activity poller completed counters * Thread pool counters * Remove thread pool queue size metric * Use metric name constants in existing tests * Workflow task failure counter tests * Unit tests for workflow and activity polling completed metrics * Use module instead of class for MetricKeys * Basic thread pool tests * Add missing metric_keys requires, clean up requires in tests --- lib/temporal/activity/poller.rb | 18 +++++- lib/temporal/activity/task_processor.rb | 5 +- lib/temporal/metric_keys.rb | 16 +++++ lib/temporal/thread_pool.rb | 19 ++++-- lib/temporal/workflow/poller.rb | 18 +++++- lib/temporal/workflow/task_processor.rb | 8 ++- .../unit/lib/temporal/activity/poller_spec.rb | 48 +++++++++++++-- .../temporal/activity/task_processor_spec.rb | 37 ++++++++++-- spec/unit/lib/temporal/thread_pool_spec.rb | 43 +++++++++++++ .../unit/lib/temporal/workflow/poller_spec.rb | 50 ++++++++++++++-- .../temporal/workflow/task_processor_spec.rb | 60 ++++++++++++++++--- 11 files changed, 286 insertions(+), 36 deletions(-) create mode 100644 lib/temporal/metric_keys.rb create mode 100644 spec/unit/lib/temporal/thread_pool_spec.rb diff --git a/lib/temporal/activity/poller.rb b/lib/temporal/activity/poller.rb index bc01290f..767d11e2 100644 --- a/lib/temporal/activity/poller.rb +++ b/lib/temporal/activity/poller.rb @@ -3,6 +3,7 @@ require 'temporal/middleware/chain' require 'temporal/activity/task_processor' require 'temporal/error_handler' +require 'temporal/metric_keys' module Temporal class Activity @@ -62,11 +63,17 @@ def poll_loop return if shutting_down? time_diff_ms = ((Time.now - last_poll_time) * 1000).round - Temporal.metrics.timing('activity_poller.time_since_last_poll', time_diff_ms, metrics_tags) + Temporal.metrics.timing(Temporal::MetricKeys::ACTIVITY_POLLER_TIME_SINCE_LAST_POLL, time_diff_ms, metrics_tags) Temporal.logger.debug("Polling activity task queue", { namespace: namespace, task_queue: task_queue }) task = poll_for_task last_poll_time = Time.now + + Temporal.metrics.increment( + Temporal::MetricKeys::ACTIVITY_POLLER_POLL_COMPLETED, + metrics_tags.merge(received_task: (!task.nil?).to_s) + ) + next unless task&.activity_type thread_pool.schedule { process(task) } @@ -93,7 +100,14 @@ def process(task) end def thread_pool - @thread_pool ||= ThreadPool.new(options[:thread_pool_size]) + @thread_pool ||= ThreadPool.new( + options[:thread_pool_size], + { + pool_name: 'activity_task_poller', + namespace: namespace, + task_queue: task_queue + } + ) end end end diff --git a/lib/temporal/activity/task_processor.rb b/lib/temporal/activity/task_processor.rb index 27ee7ba4..c79b742f 100644 --- a/lib/temporal/activity/task_processor.rb +++ b/lib/temporal/activity/task_processor.rb @@ -5,6 +5,7 @@ require 'temporal/concerns/payloads' require 'temporal/connection/retryer' require 'temporal/connection' +require 'temporal/metric_keys' module Temporal class Activity @@ -26,7 +27,7 @@ def process start_time = Time.now Temporal.logger.debug("Processing Activity task", metadata.to_h) - Temporal.metrics.timing('activity_task.queue_time', queue_time_ms, activity: activity_name, namespace: namespace, workflow: metadata.workflow_name) + Temporal.metrics.timing(Temporal::MetricKeys::ACTIVITY_TASK_QUEUE_TIME, queue_time_ms, activity: activity_name, namespace: namespace, workflow: metadata.workflow_name) context = Activity::Context.new(connection, metadata) @@ -46,7 +47,7 @@ def process respond_failed(error) ensure time_diff_ms = ((Time.now - start_time) * 1000).round - Temporal.metrics.timing('activity_task.latency', time_diff_ms, activity: activity_name, namespace: namespace, workflow: metadata.workflow_name) + Temporal.metrics.timing(Temporal::MetricKeys::ACTIVITY_TASK_LATENCY, time_diff_ms, activity: activity_name, namespace: namespace, workflow: metadata.workflow_name) Temporal.logger.debug("Activity task processed", metadata.to_h.merge(execution_time: time_diff_ms)) end diff --git a/lib/temporal/metric_keys.rb b/lib/temporal/metric_keys.rb new file mode 100644 index 00000000..e945f0b6 --- /dev/null +++ b/lib/temporal/metric_keys.rb @@ -0,0 +1,16 @@ +module Temporal + module MetricKeys + ACTIVITY_POLLER_TIME_SINCE_LAST_POLL = 'activity_poller.time_since_last_poll'.freeze + ACTIVITY_POLLER_POLL_COMPLETED = 'activity_poller.poll_completed'.freeze + ACTIVITY_TASK_QUEUE_TIME = 'activity_task.queue_time'.freeze + ACTIVITY_TASK_LATENCY = 'activity_task.latency'.freeze + + WORKFLOW_POLLER_TIME_SINCE_LAST_POLL = 'workflow_poller.time_since_last_poll'.freeze + WORKFLOW_POLLER_POLL_COMPLETED = 'workflow_poller.poll_completed'.freeze + WORKFLOW_TASK_QUEUE_TIME = 'workflow_task.queue_time'.freeze + WORKFLOW_TASK_LATENCY = 'workflow_task.latency'.freeze + WORKFLOW_TASK_EXECUTION_FAILED = 'workflow_task.execution_failed'.freeze + + THREAD_POOL_AVAILABLE_THREADS = 'thread_pool.available_threads'.freeze + end +end diff --git a/lib/temporal/thread_pool.rb b/lib/temporal/thread_pool.rb index 487686fb..7ff2a2fe 100644 --- a/lib/temporal/thread_pool.rb +++ b/lib/temporal/thread_pool.rb @@ -1,3 +1,5 @@ +require 'temporal/metric_keys' + # This class implements a very simple ThreadPool with the ability to # block until at least one thread becomes available. This allows Pollers # to only poll when there's an available thread in the pool. @@ -9,22 +11,25 @@ module Temporal class ThreadPool attr_reader :size - def initialize(size) + def initialize(size, metrics_tags) @size = size + @metrics_tags = metrics_tags @queue = Queue.new @mutex = Mutex.new @availability = ConditionVariable.new @available_threads = size - @pool = Array.new(size) do |i| + @pool = Array.new(size) do |_i| Thread.new { poll } end end + def report_metrics + Temporal.metrics.gauge(Temporal::MetricKeys::THREAD_POOL_AVAILABLE_THREADS, @available_threads, @metrics_tags) + end + def wait_for_available_threads @mutex.synchronize do - while @available_threads <= 0 - @availability.wait(@mutex) - end + @availability.wait(@mutex) while @available_threads <= 0 end end @@ -33,6 +38,8 @@ def schedule(&block) @available_threads -= 1 @queue << block end + + report_metrics end def shutdown @@ -56,6 +63,8 @@ def poll @available_threads += 1 @availability.signal end + + report_metrics end end end diff --git a/lib/temporal/workflow/poller.rb b/lib/temporal/workflow/poller.rb index 095f28b0..5f1fa042 100644 --- a/lib/temporal/workflow/poller.rb +++ b/lib/temporal/workflow/poller.rb @@ -4,6 +4,7 @@ require 'temporal/middleware/chain' require 'temporal/workflow/task_processor' require 'temporal/error_handler' +require 'temporal/metric_keys' module Temporal class Workflow @@ -64,11 +65,17 @@ def poll_loop return if shutting_down? time_diff_ms = ((Time.now - last_poll_time) * 1000).round - Temporal.metrics.timing('workflow_poller.time_since_last_poll', time_diff_ms, metrics_tags) + Temporal.metrics.timing(Temporal::MetricKeys::WORKFLOW_POLLER_TIME_SINCE_LAST_POLL, time_diff_ms, metrics_tags) Temporal.logger.debug("Polling workflow task queue", { namespace: namespace, task_queue: task_queue }) task = poll_for_task last_poll_time = Time.now + + Temporal.metrics.increment( + Temporal::MetricKeys::WORKFLOW_POLLER_POLL_COMPLETED, + metrics_tags.merge(received_task: (!task.nil?).to_s) + ) + next unless task&.workflow_type thread_pool.schedule { process(task) } @@ -94,7 +101,14 @@ def process(task) end def thread_pool - @thread_pool ||= ThreadPool.new(options[:thread_pool_size]) + @thread_pool ||= ThreadPool.new( + options[:thread_pool_size], + { + pool_name: 'workflow_task_poller', + namespace: namespace, + task_queue: task_queue + } + ) end def binary_checksum diff --git a/lib/temporal/workflow/task_processor.rb b/lib/temporal/workflow/task_processor.rb index d161e1c2..56057f5d 100644 --- a/lib/temporal/workflow/task_processor.rb +++ b/lib/temporal/workflow/task_processor.rb @@ -4,6 +4,7 @@ require 'temporal/workflow/executor' require 'temporal/workflow/history' require 'temporal/workflow/stack_trace_tracker' +require 'temporal/metric_keys' module Temporal class Workflow @@ -39,7 +40,7 @@ def process start_time = Time.now Temporal.logger.debug("Processing Workflow task", metadata.to_h) - Temporal.metrics.timing('workflow_task.queue_time', queue_time_ms, workflow: workflow_name, namespace: namespace) + Temporal.metrics.timing(Temporal::MetricKeys::WORKFLOW_TASK_QUEUE_TIME, queue_time_ms, workflow: workflow_name, namespace: namespace) if !workflow_class raise Temporal::WorkflowNotRegistered, 'Workflow is not registered with this worker' @@ -71,7 +72,7 @@ def process fail_task(error) ensure time_diff_ms = ((Time.now - start_time) * 1000).round - Temporal.metrics.timing('workflow_task.latency', time_diff_ms, workflow: workflow_name, namespace: namespace) + Temporal.metrics.timing(Temporal::MetricKeys::WORKFLOW_TASK_LATENCY, time_diff_ms, workflow: workflow_name, namespace: namespace) Temporal.logger.debug("Workflow task processed", metadata.to_h.merge(execution_time: time_diff_ms)) end @@ -155,7 +156,8 @@ def complete_query(result) end def fail_task(error) - Temporal.logger.error("Workflow task failed", metadata.to_h.merge(error: error.inspect)) + Temporal.metrics.increment(Temporal::MetricKeys::WORKFLOW_TASK_EXECUTION_FAILED, workflow: workflow_name, namespace: namespace) + Temporal.logger.error('Workflow task failed', metadata.to_h.merge(error: error.inspect)) Temporal.logger.debug(error.backtrace.join("\n")) # Only fail the workflow task on the first attempt. Subsequent failures of the same workflow task diff --git a/spec/unit/lib/temporal/activity/poller_spec.rb b/spec/unit/lib/temporal/activity/poller_spec.rb index 04de13ed..d066ae24 100644 --- a/spec/unit/lib/temporal/activity/poller_spec.rb +++ b/spec/unit/lib/temporal/activity/poller_spec.rb @@ -1,6 +1,7 @@ require 'temporal/activity/poller' -require 'temporal/middleware/entry' require 'temporal/configuration' +require 'temporal/metric_keys' +require 'temporal/middleware/entry' describe Temporal::Activity::Poller do let(:connection) { instance_double('Temporal::Connection::GRPC', cancel_polling_request: nil) } @@ -21,6 +22,7 @@ allow(Temporal::ThreadPool).to receive(:new).and_return(thread_pool) allow(Temporal::Middleware::Chain).to receive(:new).and_return(middleware_chain) allow(Temporal.metrics).to receive(:timing) + allow(Temporal.metrics).to receive(:increment) end describe '#start' do @@ -51,8 +53,28 @@ expect(Temporal.metrics) .to have_received(:timing) .with( - 'activity_poller.time_since_last_poll', - an_instance_of(Fixnum), + Temporal::MetricKeys::ACTIVITY_POLLER_TIME_SINCE_LAST_POLL, + an_instance_of(Integer), + namespace: namespace, + task_queue: task_queue + ) + .twice + end + + it 'reports polling completed with received_task false' do + allow(subject).to receive(:shutting_down?).and_return(false, false, true) + allow(connection).to receive(:poll_activity_task_queue).and_return(nil) + + subject.start + + # stop poller before inspecting + subject.stop_polling; subject.wait + + expect(Temporal.metrics) + .to have_received(:increment) + .with( + Temporal::MetricKeys::ACTIVITY_POLLER_POLL_COMPLETED, + received_task: 'false', namespace: namespace, task_queue: task_queue ) @@ -91,9 +113,27 @@ expect(task_processor).to have_received(:process) end + it 'reports polling completed with received_task true' do + subject.start + + # stop poller before inspecting + subject.stop_polling; subject.wait + + expect(Temporal.metrics) + .to have_received(:increment) + .with( + Temporal::MetricKeys::ACTIVITY_POLLER_POLL_COMPLETED, + received_task: 'true', + namespace: namespace, + task_queue: task_queue + ) + .once + end + context 'with middleware configured' do class TestPollerMiddleware def initialize(_); end + def call(_); end end @@ -131,7 +171,7 @@ def call(_); end expect(Temporal.logger) .to have_received(:error) - .with('Unable to poll activity task queue', { namespace: 'test-namespace', task_queue: 'test-task-queue', error: '#'}) + .with('Unable to poll activity task queue', { namespace: 'test-namespace', task_queue: 'test-task-queue', error: '#' }) end end end diff --git a/spec/unit/lib/temporal/activity/task_processor_spec.rb b/spec/unit/lib/temporal/activity/task_processor_spec.rb index 87688a20..65560d63 100644 --- a/spec/unit/lib/temporal/activity/task_processor_spec.rb +++ b/spec/unit/lib/temporal/activity/task_processor_spec.rb @@ -1,6 +1,7 @@ require 'temporal/activity/task_processor' -require 'temporal/middleware/chain' require 'temporal/configuration' +require 'temporal/metric_keys' +require 'temporal/middleware/chain' describe Temporal::Activity::TaskProcessor do subject { described_class.new(task, namespace, lookup, middleware_chain, config) } @@ -20,7 +21,7 @@ let(:connection) { instance_double('Temporal::Connection::GRPC') } let(:middleware_chain) { Temporal::Middleware::Chain.new } let(:config) { Temporal::Configuration.new } - let(:input) { ['arg1', 'arg2'] } + let(:input) { %w[arg1 arg2] } describe '#process' do let(:context) { instance_double('Temporal::Activity::Context', async?: false) } @@ -127,7 +128,13 @@ expect(Temporal.metrics) .to have_received(:timing) - .with('activity_task.queue_time', an_instance_of(Integer), activity: activity_name, namespace: namespace, workflow: workflow_name) + .with( + Temporal::MetricKeys::ACTIVITY_TASK_QUEUE_TIME, + an_instance_of(Integer), + activity: activity_name, + namespace: namespace, + workflow: workflow_name + ) end it 'sends latency metric' do @@ -135,7 +142,13 @@ expect(Temporal.metrics) .to have_received(:timing) - .with('activity_task.latency', an_instance_of(Integer), activity: activity_name, namespace: namespace, workflow: workflow_name) + .with( + Temporal::MetricKeys::ACTIVITY_TASK_LATENCY, + an_instance_of(Integer), + activity: activity_name, + namespace: namespace, + workflow: workflow_name + ) end context 'with async activity' do @@ -206,7 +219,13 @@ expect(Temporal.metrics) .to have_received(:timing) - .with('activity_task.queue_time', an_instance_of(Integer), activity: activity_name, namespace: namespace, workflow: workflow_name) + .with( + Temporal::MetricKeys::ACTIVITY_TASK_QUEUE_TIME, + an_instance_of(Integer), + activity: activity_name, + namespace: namespace, + workflow: workflow_name + ) end it 'sends latency metric' do @@ -214,7 +233,13 @@ expect(Temporal.metrics) .to have_received(:timing) - .with('activity_task.latency', an_instance_of(Integer), activity: activity_name, namespace: namespace, workflow: workflow_name) + .with( + Temporal::MetricKeys::ACTIVITY_TASK_LATENCY, + an_instance_of(Integer), + activity: activity_name, + namespace: namespace, + workflow: workflow_name + ) end context 'with ScriptError exception' do diff --git a/spec/unit/lib/temporal/thread_pool_spec.rb b/spec/unit/lib/temporal/thread_pool_spec.rb new file mode 100644 index 00000000..20659367 --- /dev/null +++ b/spec/unit/lib/temporal/thread_pool_spec.rb @@ -0,0 +1,43 @@ +require 'temporal/thread_pool' + +describe Temporal::ThreadPool do + before do + allow(Temporal.metrics).to receive(:gauge) + end + + let(:size) { 2 } + let(:tags) { { foo: 'bar', bat: 'baz' } } + let(:thread_pool) { described_class.new(size, tags) } + + describe '#new' do + it 'executes one task on a thread and exits' do + times = 0 + + thread_pool.schedule do + times += 1 + end + + thread_pool.shutdown + + expect(times).to eq(1) + end + + it 'reports thread available metrics' do + thread_pool.schedule do + end + + thread_pool.shutdown + + # Thread behavior is not deterministic. Ensure the calls match without + # verifying exact gauge values. + expect(Temporal.metrics) + .to have_received(:gauge) + .with( + Temporal::MetricKeys::THREAD_POOL_AVAILABLE_THREADS, + instance_of(Integer), + tags + ) + .at_least(:once) + end + end +end diff --git a/spec/unit/lib/temporal/workflow/poller_spec.rb b/spec/unit/lib/temporal/workflow/poller_spec.rb index 1fdb023d..083ec376 100644 --- a/spec/unit/lib/temporal/workflow/poller_spec.rb +++ b/spec/unit/lib/temporal/workflow/poller_spec.rb @@ -1,6 +1,7 @@ -require 'temporal/workflow/poller' -require 'temporal/middleware/entry' require 'temporal/configuration' +require 'temporal/metric_keys' +require 'temporal/middleware/entry' +require 'temporal/workflow/poller' describe Temporal::Workflow::Poller do let(:connection) { instance_double('Temporal::Connection::GRPC') } @@ -29,6 +30,7 @@ allow(Temporal::Connection).to receive(:generate).and_return(connection) allow(Temporal::Middleware::Chain).to receive(:new).and_return(middleware_chain) allow(Temporal.metrics).to receive(:timing) + allow(Temporal.metrics).to receive(:increment) end describe '#start' do @@ -59,15 +61,35 @@ expect(Temporal.metrics) .to have_received(:timing) .with( - 'workflow_poller.time_since_last_poll', - an_instance_of(Fixnum), + Temporal::MetricKeys::WORKFLOW_POLLER_TIME_SINCE_LAST_POLL, + an_instance_of(Integer), namespace: namespace, task_queue: task_queue ) .twice end - context 'when an decision task is received' do + it 'reports polling completed with received_task false' do + allow(subject).to receive(:shutting_down?).and_return(false, false, true) + allow(connection).to receive(:poll_workflow_task_queue).and_return(nil) + + subject.start + + # stop poller before inspecting + subject.stop_polling; subject.wait + + expect(Temporal.metrics) + .to have_received(:increment) + .with( + Temporal::MetricKeys::WORKFLOW_POLLER_POLL_COMPLETED, + received_task: 'false', + namespace: namespace, + task_queue: task_queue + ) + .twice + end + + context 'when a workflow task is received' do let(:task_processor) do instance_double(Temporal::Workflow::TaskProcessor, process: nil) end @@ -91,9 +113,27 @@ expect(task_processor).to have_received(:process) end + it 'reports polling completed with received_task true' do + subject.start + + # stop poller before inspecting + subject.stop_polling; subject.wait + + expect(Temporal.metrics) + .to have_received(:increment) + .with( + Temporal::MetricKeys::WORKFLOW_POLLER_POLL_COMPLETED, + received_task: 'true', + namespace: namespace, + task_queue: task_queue + ) + .once + end + context 'with middleware configured' do class TestPollerMiddleware def initialize(_); end + def call(_); end end diff --git a/spec/unit/lib/temporal/workflow/task_processor_spec.rb b/spec/unit/lib/temporal/workflow/task_processor_spec.rb index aa6c7029..00d1d745 100644 --- a/spec/unit/lib/temporal/workflow/task_processor_spec.rb +++ b/spec/unit/lib/temporal/workflow/task_processor_spec.rb @@ -1,6 +1,7 @@ -require 'temporal/workflow/task_processor' -require 'temporal/middleware/chain' require 'temporal/configuration' +require 'temporal/metric_keys' +require 'temporal/middleware/chain' +require 'temporal/workflow/task_processor' describe Temporal::Workflow::TaskProcessor do subject { described_class.new(task, namespace, lookup, middleware_chain, config, binary_checksum) } @@ -14,7 +15,7 @@ let(:workflow_name) { 'TestWorkflow' } let(:connection) { instance_double('Temporal::Connection::GRPC') } let(:middleware_chain) { Temporal::Middleware::Chain.new } - let(:input) { ['arg1', 'arg2'] } + let(:input) { %w[arg1 arg2] } let(:config) { Temporal::Configuration.new } let(:binary_checksum) { 'v1.0.0' } @@ -33,6 +34,7 @@ allow(middleware_chain).to receive(:invoke).and_call_original allow(Temporal.metrics).to receive(:timing) + allow(Temporal.metrics).to receive(:increment) end context 'when workflow is not registered' do @@ -58,6 +60,18 @@ expect(reported_error).to be_an_instance_of(Temporal::WorkflowNotRegistered) expect(reported_metadata).to be_an_instance_of(Temporal::Metadata::WorkflowTask) end + + it 'emits workflow task failure metric' do + subject.process + + expect(Temporal.metrics) + .to have_received(:increment) + .with( + Temporal::MetricKeys::WORKFLOW_TASK_EXECUTION_FAILED, + workflow: workflow_name, + namespace: namespace + ) + end end context 'when workflow is registered' do @@ -169,7 +183,12 @@ expect(Temporal.metrics) .to have_received(:timing) - .with('workflow_task.queue_time', an_instance_of(Integer), workflow: workflow_name, namespace: namespace) + .with( + Temporal::MetricKeys::WORKFLOW_TASK_QUEUE_TIME, + an_instance_of(Integer), + workflow: workflow_name, + namespace: namespace + ) end it 'sends latency metric' do @@ -177,7 +196,12 @@ expect(Temporal.metrics) .to have_received(:timing) - .with('workflow_task.latency', an_instance_of(Integer), workflow: workflow_name, namespace: namespace) + .with( + Temporal::MetricKeys::WORKFLOW_TASK_LATENCY, + an_instance_of(Integer), + workflow: workflow_name, + namespace: namespace + ) end end @@ -202,6 +226,18 @@ binary_checksum: binary_checksum ) end + + it 'emits workflow task failure metric' do + subject.process + + expect(Temporal.metrics) + .to have_received(:increment) + .with( + Temporal::MetricKeys::WORKFLOW_TASK_EXECUTION_FAILED, + workflow: workflow_name, + namespace: namespace + ) + end end context 'when deprecated task query is not present' do @@ -256,7 +292,12 @@ expect(Temporal.metrics) .to have_received(:timing) - .with('workflow_task.queue_time', an_instance_of(Integer), workflow: workflow_name, namespace: namespace) + .with( + Temporal::MetricKeys::WORKFLOW_TASK_QUEUE_TIME, + an_instance_of(Integer), + workflow: workflow_name, + namespace: namespace + ) end it 'sends latency metric' do @@ -264,7 +305,12 @@ expect(Temporal.metrics) .to have_received(:timing) - .with('workflow_task.latency', an_instance_of(Integer), workflow: workflow_name, namespace: namespace) + .with( + Temporal::MetricKeys::WORKFLOW_TASK_LATENCY, + an_instance_of(Integer), + workflow: workflow_name, + namespace: namespace + ) end end From e84f6696ff2138f85c0d45099ea6494673ac6dcf Mon Sep 17 00:00:00 2001 From: jeffschoner-stripe <63118764+jeffschoner-stripe@users.noreply.github.com> Date: Fri, 28 Oct 2022 15:22:15 -0700 Subject: [PATCH 068/125] Unify converter accessor, make default_identity private (#202) --- lib/temporal/configuration.rb | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/lib/temporal/configuration.rb b/lib/temporal/configuration.rb index c9d8f95f..b397d03c 100644 --- a/lib/temporal/configuration.rb +++ b/lib/temporal/configuration.rb @@ -12,8 +12,7 @@ class Configuration Execution = Struct.new(:namespace, :task_queue, :timeouts, :headers, :search_attributes, keyword_init: true) attr_reader :timeouts, :error_handlers - attr_writer :converter - attr_accessor :connection_type, :host, :port, :credentials, :identity, :logger, :metrics_adapter, :namespace, :task_queue, :headers, :search_attributes + attr_accessor :connection_type, :converter, :host, :port, :credentials, :identity, :logger, :metrics_adapter, :namespace, :task_queue, :headers, :search_attributes # See https://docs.temporal.io/blog/activity-timeouts/ for general docs. # We want an infinite execution timeout for cron schedules and other perpetual workflows. @@ -76,8 +75,6 @@ def timeouts=(new_timeouts) @timeouts = DEFAULT_TIMEOUTS.merge(new_timeouts) end - attr_reader :converter - def for_connection Connection.new( type: connection_type, @@ -88,13 +85,6 @@ def for_connection ).freeze end - def default_identity - hostname = `hostname` - pid = Process.pid - - "#{pid}@#{hostname}" - end - def default_execution_options Execution.new( namespace: namespace, @@ -104,5 +94,14 @@ def default_execution_options search_attributes: search_attributes ).freeze end + + private + + def default_identity + hostname = `hostname` + pid = Process.pid + + "#{pid}@#{hostname}".freeze + end end end From dcec7165182cb256a34d9d675eedc29035bfaf44 Mon Sep 17 00:00:00 2001 From: calum-stripe <98350978+calum-stripe@users.noreply.github.com> Date: Fri, 18 Nov 2022 12:39:21 -0800 Subject: [PATCH 069/125] x (#205) --- lib/temporal.rb | 3 ++- lib/temporal/client.rb | 8 ++++---- lib/temporal/connection/grpc.rb | 8 ++++++-- spec/unit/lib/temporal/grpc_spec.rb | 23 +++++++++++++++++++++++ 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/lib/temporal.rb b/lib/temporal.rb index 99556ec0..90baf264 100644 --- a/lib/temporal.rb +++ b/lib/temporal.rb @@ -29,7 +29,8 @@ module Temporal :fail_activity, :list_open_workflow_executions, :list_closed_workflow_executions, - :query_workflow_executions + :query_workflow_executions, + :connection class << self def configure(&block) diff --git a/lib/temporal/client.rb b/lib/temporal/client.rb index 163d4e08..912a63d0 100644 --- a/lib/temporal/client.rb +++ b/lib/temporal/client.rb @@ -409,6 +409,10 @@ def query_workflow_executions(namespace, query, next_page_token: nil, max_page_s Temporal::Workflow::Executions.new(connection: connection, status: :all, request_options: { namespace: namespace, query: query, next_page_token: next_page_token, max_page_size: max_page_size }.merge(filter)) end + def connection + @connection ||= Temporal::Connection.generate(config.for_connection) + end + class ResultConverter extend Concerns::Payloads end @@ -418,10 +422,6 @@ class ResultConverter attr_reader :config - def connection - @connection ||= Temporal::Connection.generate(config.for_connection) - end - def compute_run_timeout(execution_options) execution_options.timeouts[:run] || execution_options.timeouts[:execution] end diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index 91bb3774..a5f7b287 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -466,8 +466,12 @@ def scan_workflow_executions raise NotImplementedError end - def count_workflow_executions - raise NotImplementedError + def count_workflow_executions(namespace:, query:) + request = Temporal::Api::WorkflowService::V1::CountWorkflowExecutionsRequest.new( + namespace: namespace, + query: query + ) + client.count_workflow_executions(request) end def get_search_attributes diff --git a/spec/unit/lib/temporal/grpc_spec.rb b/spec/unit/lib/temporal/grpc_spec.rb index fecb25e4..92e37e28 100644 --- a/spec/unit/lib/temporal/grpc_spec.rb +++ b/spec/unit/lib/temporal/grpc_spec.rb @@ -363,6 +363,29 @@ class TestDeserializer end end end + + describe "#count_workflow_executions" do + let(:namespace) { 'test-namespace' } + let(:query) { 'StartDate < 2022-04-07T20:48:20Z order by StartTime desc' } + let(:args) { { namespace: namespace, query: query } } + let(:temporal_response) do + Temporal::Api::WorkflowService::V1::CountWorkflowExecutionsResponse.new(count: 0) + end + + before do + allow(grpc_stub).to receive(:count_workflow_executions).and_return(temporal_response) + end + + it 'makes an API request' do + subject.count_workflow_executions(**args) + + expect(grpc_stub).to have_received(:count_workflow_executions) do |request| + expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::CountWorkflowExecutionsRequest) + expect(request.namespace).to eq(namespace) + expect(request.query).to eq(query) + end + end + end describe '#list_workflow_executions' do let(:namespace) { 'test-namespace' } From 58379cd819376901d01f41bf3fe73c362cbaa2ec Mon Sep 17 00:00:00 2001 From: Drew Hoskins <37816070+drewhoskins-stripe@users.noreply.github.com> Date: Tue, 3 Jan 2023 07:34:22 -0800 Subject: [PATCH 070/125] dynamic Activity (#208) * DynamicActivity * Use const_get * Cleanup error message * Jeff feedback * register_dynamic_activity * use workflow.execute_activity * No need to pass the activity name in. * Remove dynamic attribute from executables * More contextual error message on double-registration --- examples/activities/delegator_activity.rb | 33 ++++++++++++++ examples/bin/worker | 2 + .../spec/integration/dynamic_activity_spec.rb | 19 ++++++++ .../workflows/calls_delegator_workflow.rb | 21 +++++++++ lib/temporal/activity/context.rb | 8 +++- lib/temporal/errors.rb | 1 + lib/temporal/executable_lookup.rb | 25 ++++++++++- lib/temporal/worker.rb | 27 +++++++++-- .../lib/temporal/activity/context_spec.rb | 6 +++ .../lib/temporal/executable_lookup_spec.rb | 30 ++++++++++++- spec/unit/lib/temporal/worker_spec.rb | 45 ++++++++++++++++--- 11 files changed, 205 insertions(+), 12 deletions(-) create mode 100644 examples/activities/delegator_activity.rb create mode 100644 examples/spec/integration/dynamic_activity_spec.rb create mode 100644 examples/workflows/calls_delegator_workflow.rb diff --git a/examples/activities/delegator_activity.rb b/examples/activities/delegator_activity.rb new file mode 100644 index 00000000..1e095d19 --- /dev/null +++ b/examples/activities/delegator_activity.rb @@ -0,0 +1,33 @@ +# This sample illustrates using a dynamic Activity to delegate to another set of non-activity +# classes. This is an advanced use case, used, for example, for integrating with an existing framework +# that doesn't know about temporal. +# See Temporal::Worker#register_dynamic_activity for more info. + +# An example of another non-Activity class hierarchy. +class MyExecutor + def do_it(_args) + raise NotImplementedError + end +end + +class Plus < MyExecutor + def do_it(args) + args[:a] + args[:b] + end +end + +class Times < MyExecutor + def do_it(args) + args[:a] * args[:b] + end +end + +# Calls into our other class hierarchy. +class DelegatorActivity < Temporal::Activity + def execute(input) + executor = Object.const_get(activity.name).new + raise ArgumentError, "Unknown activity: #{executor.class}" unless executor.is_a?(MyExecutor) + + executor.do_it(input) + end +end diff --git a/examples/bin/worker b/examples/bin/worker index b21935ba..b4586649 100755 --- a/examples/bin/worker +++ b/examples/bin/worker @@ -22,6 +22,7 @@ worker = Temporal::Worker.new(binary_checksum: `git show HEAD -s --format=%H`.st worker.register_workflow(AsyncActivityWorkflow) worker.register_workflow(AsyncHelloWorldWorkflow) worker.register_workflow(BranchingWorkflow) +worker.register_workflow(CallsDelegatorWorkflow) worker.register_workflow(CallFailingActivityWorkflow) worker.register_workflow(CancellingTimerWorkflow) worker.register_workflow(CheckWorkflow) @@ -79,6 +80,7 @@ worker.register_activity(Trip::CancelFlightActivity) worker.register_activity(Trip::CancelHotelActivity) worker.register_activity(Trip::MakePaymentActivity) worker.register_activity(Trip::RentCarActivity) +worker.register_dynamic_activity(DelegatorActivity) worker.add_workflow_task_middleware(LoggingMiddleware, 'EXAMPLE') worker.add_activity_middleware(LoggingMiddleware, 'EXAMPLE') diff --git a/examples/spec/integration/dynamic_activity_spec.rb b/examples/spec/integration/dynamic_activity_spec.rb new file mode 100644 index 00000000..1abec847 --- /dev/null +++ b/examples/spec/integration/dynamic_activity_spec.rb @@ -0,0 +1,19 @@ +require 'workflows/calls_delegator_workflow' + +describe 'Dynamic activities' do + let(:workflow_id) { SecureRandom.uuid } + + it 'can delegate to other classes' do + run_id = Temporal.start_workflow(CallsDelegatorWorkflow, options: { + workflow_id: workflow_id + }) + + result = Temporal.await_workflow_result( + CallsDelegatorWorkflow, + workflow_id: workflow_id, + run_id: run_id + ) + expect(result[:sum]).to eq(8) + expect(result[:product]).to eq(15) + end +end diff --git a/examples/workflows/calls_delegator_workflow.rb b/examples/workflows/calls_delegator_workflow.rb new file mode 100644 index 00000000..253e6a5f --- /dev/null +++ b/examples/workflows/calls_delegator_workflow.rb @@ -0,0 +1,21 @@ +require 'activities/delegator_activity' + +class CallsDelegatorWorkflow < Temporal::Workflow + + # In-workflow client to remotely invoke activity. + def call_executor(executor_class, args) + # We want temporal to record the MyExecutor class--e.g. 'Plus','Times'--as the name of the activites, + # rather than DelegatorActivity, for better debuggability + workflow.execute_activity!( + executor_class, + args + ) + end + + def execute + operands = { a: 5, b: 3 } + result_1 = call_executor(Plus, operands) + result_2 = call_executor(Times, operands) + { sum: result_1, product: result_2 } + end +end diff --git a/lib/temporal/activity/context.rb b/lib/temporal/activity/context.rb index a5515142..289024a8 100644 --- a/lib/temporal/activity/context.rb +++ b/lib/temporal/activity/context.rb @@ -31,7 +31,7 @@ def async_token end def heartbeat(details = nil) - logger.debug("Activity heartbeat", metadata.to_h) + logger.debug('Activity heartbeat', metadata.to_h) connection.record_activity_task_heartbeat(namespace: metadata.namespace, task_token: task_token, details: details) end @@ -56,6 +56,12 @@ def headers metadata.headers end + # The name of the activity's class. In a dynamic Activity, it may be the name + # of a class or a key to an executor you want to delegate to. + def name + metadata.name + end + private attr_reader :connection, :metadata diff --git a/lib/temporal/errors.rb b/lib/temporal/errors.rb index 41789960..d0239abe 100644 --- a/lib/temporal/errors.rb +++ b/lib/temporal/errors.rb @@ -30,6 +30,7 @@ class ActivityCanceled < ActivityException; end class ActivityNotRegistered < ClientError; end class WorkflowNotRegistered < ClientError; end + class SecondDynamicActivityError < ClientError; end class ApiError < Error; end diff --git a/lib/temporal/executable_lookup.rb b/lib/temporal/executable_lookup.rb index 651951b3..88d85bf4 100644 --- a/lib/temporal/executable_lookup.rb +++ b/lib/temporal/executable_lookup.rb @@ -1,3 +1,5 @@ +require 'temporal/errors' + # This class is responsible for matching an executable (activity or workflow) name # to a class implementing it. # @@ -6,20 +8,39 @@ # module Temporal class ExecutableLookup + + class SecondDynamicExecutableError < StandardError + attr_reader :previous_executable_name + + def initialize(previous_executable_name) + @previous_executable_name = previous_executable_name + end + end + def initialize @executables = {} end + # Register an executable to call as a fallback when one of that name isn't registered. + def add_dynamic(name, executable) + if @fallback_executable_name + raise SecondDynamicExecutableError, @fallback_executable_name + end + + @fallback_executable = executable + @fallback_executable_name = name + end + def add(name, executable) executables[name] = executable end def find(name) - executables[name] + executables[name] || @fallback_executable end private - attr_reader :executables + attr_reader :executables, :fallback_executable, :fallback_executable_name end end diff --git a/lib/temporal/worker.rb b/lib/temporal/worker.rb index 20071a27..a051c2b2 100644 --- a/lib/temporal/worker.rb +++ b/lib/temporal/worker.rb @@ -50,12 +50,26 @@ def register_workflow(workflow_class, options = {}) end def register_activity(activity_class, options = {}) - execution_options = ExecutionOptions.new(activity_class, options, config.default_execution_options) - key = [execution_options.namespace, execution_options.task_queue] - + key, execution_options = activity_registration(activity_class, options) @activities[key].add(execution_options.name, activity_class) end + # Register one special activity that you want to intercept any unknown Activities, + # perhaps so you can delegate work to other classes, somewhat analogous to ruby's method_missing. + # Only one dynamic Activity may be registered per task queue. + # Within Activity#execute, you may retrieve the name of the unknown class via activity.name. + def register_dynamic_activity(activity_class, options = {}) + key, execution_options = activity_registration(activity_class, options) + begin + @activities[key].add_dynamic(execution_options.name, activity_class) + rescue Temporal::ExecutableLookup::SecondDynamicExecutableError => e + raise Temporal::SecondDynamicActivityError, + "Temporal::Worker#register_dynamic_activity: cannot register #{execution_options.name} "\ + "dynamically; #{e.previous_executable_name} was already registered dynamically for task queue "\ + "'#{execution_options.task_queue}', and there can be only one." + end + end + def add_workflow_task_middleware(middleware_class, *args) @workflow_task_middleware << Middleware::Entry.new(middleware_class, args) end @@ -112,10 +126,17 @@ def activity_poller_for(namespace, task_queue, lookup) Activity::Poller.new(namespace, task_queue, lookup.freeze, config, activity_middleware, activity_poller_options) end + def activity_registration(activity_class, options) + execution_options = ExecutionOptions.new(activity_class, options, config.default_execution_options) + key = [execution_options.namespace, execution_options.task_queue] + [key, execution_options] + end + def trap_signals %w[TERM INT].each do |signal| Signal.trap(signal) { stop } end end + end end diff --git a/spec/unit/lib/temporal/activity/context_spec.rb b/spec/unit/lib/temporal/activity/context_spec.rb index df0ff740..e9bee2a5 100644 --- a/spec/unit/lib/temporal/activity/context_spec.rb +++ b/spec/unit/lib/temporal/activity/context_spec.rb @@ -107,4 +107,10 @@ expect(subject.headers).to eq('Foo' => 'Bar') end end + + describe '#name' do + it 'returns the class name of the activity' do + expect(subject.name).to eq('TestActivity') + end + end end diff --git a/spec/unit/lib/temporal/executable_lookup_spec.rb b/spec/unit/lib/temporal/executable_lookup_spec.rb index ec9d59a5..197dc334 100644 --- a/spec/unit/lib/temporal/executable_lookup_spec.rb +++ b/spec/unit/lib/temporal/executable_lookup_spec.rb @@ -1,7 +1,18 @@ require 'temporal/executable_lookup' +require 'temporal/concerns/executable' describe Temporal::ExecutableLookup do - class TestClass; end + class TestClass + extend Temporal::Concerns::Executable + end + + class MyDynamicActivity + extend Temporal::Concerns::Executable + end + + class IllegalSecondDynamicActivity + extend Temporal::Concerns::Executable + end describe '#add' do it 'adds a class to the lookup map' do @@ -11,6 +22,15 @@ class TestClass; end end end + describe '#add_dynamic' do + it 'fails on the second dynamic activity' do + subject.add_dynamic('MyDynamicActivity', MyDynamicActivity) + expect do + subject.add_dynamic('IllegalSecondDynamicActivity', IllegalSecondDynamicActivity) + end.to raise_error(Temporal::ExecutableLookup::SecondDynamicExecutableError) + end + end + describe '#find' do before { subject.add('foo', TestClass) } @@ -21,5 +41,13 @@ class TestClass; end it 'returns nil if there were no matches' do expect(subject.find('bar')).to eq(nil) end + + it 'falls back to the dynamic executable' do + subject.add('TestClass', TestClass) + subject.add_dynamic('MyDynamicActivity', MyDynamicActivity) + + expect(subject.find('TestClass')).to eq(TestClass) + expect(subject.find('SomethingElse')).to eq(MyDynamicActivity) + end end end diff --git a/spec/unit/lib/temporal/worker_spec.rb b/spec/unit/lib/temporal/worker_spec.rb index c1c5cd16..6c153a88 100644 --- a/spec/unit/lib/temporal/worker_spec.rb +++ b/spec/unit/lib/temporal/worker_spec.rb @@ -25,9 +25,10 @@ class TestWorkerActivityMiddleware def call(_); end end - class TestWorkerActivity < Temporal::Activity + class OtherTestWorkerActivity < Temporal::Activity namespace 'default-namespace' task_queue 'default-task-queue' + end THREAD_SYNC_DELAY = 0.01 @@ -67,16 +68,17 @@ class TestWorkerActivity < Temporal::Activity let(:lookup) { instance_double(Temporal::ExecutableLookup, add: nil) } let(:activity_keys) { subject.send(:activities).keys } - before { expect(Temporal::ExecutableLookup).to receive(:new).and_return(lookup) } - it 'registers an activity based on the default config options' do + expect(Temporal::ExecutableLookup).to receive(:new).and_return(lookup) subject.register_activity(TestWorkerActivity) expect(lookup).to have_received(:add).with('TestWorkerActivity', TestWorkerActivity) - expect(activity_keys).to include(['default-namespace', 'default-task-queue']) + expect(activity_keys).to include(%w[default-namespace default-task-queue]) end it 'registers an activity with provided config options' do + expect(Temporal::ExecutableLookup).to receive(:new).and_return(lookup) + subject.register_activity( TestWorkerActivity, name: 'test-activity', @@ -85,10 +87,43 @@ class TestWorkerActivity < Temporal::Activity ) expect(lookup).to have_received(:add).with('test-activity', TestWorkerActivity) - expect(activity_keys).to include(['test-namespace', 'test-task-queue']) + expect(activity_keys).to include(%w[test-namespace test-task-queue]) end end + describe '#register_dynamic_activity' do + let(:activity_keys) { subject.send(:activities).keys } + + it 'registers a dynamic activity with the provided config options' do + lookup = instance_double(Temporal::ExecutableLookup, add: nil) + expect(Temporal::ExecutableLookup).to receive(:new).and_return(lookup) + expect(lookup).to receive(:add_dynamic).with('test-dynamic-activity', TestWorkerActivity) + + subject.register_dynamic_activity( + TestWorkerActivity, + name: 'test-dynamic-activity', + namespace: 'test-namespace', + task_queue: 'test-task-queue' + ) + + expect(activity_keys).to include(%w[test-namespace test-task-queue]) + end + + it 'cannot double-register an activity' do + subject.register_dynamic_activity(TestWorkerActivity) + expect do + subject.register_dynamic_activity(OtherTestWorkerActivity) + end.to raise_error( + Temporal::SecondDynamicActivityError, + 'Temporal::Worker#register_dynamic_activity: cannot register OtherTestWorkerActivity dynamically; ' \ + 'TestWorkerActivity was already registered dynamically for task queue \'default-task-queue\', ' \ + 'and there can be only one.' + ) + end + + + end + describe '#add_workflow_task_middleware' do let(:middleware) { subject.send(:workflow_task_middleware) } From 214b334d6e51ae28b9fc823e393428e6260a1f40 Mon Sep 17 00:00:00 2001 From: Drew Hoskins <37816070+drewhoskins-stripe@users.noreply.github.com> Date: Thu, 9 Feb 2023 11:40:46 -0800 Subject: [PATCH 071/125] Allow exceptions thrown from activities to accept more args (#214) * Allow ActivityException to accept args * Improve deserialization code flow * Use the default converter to serialize errors when configured to do so * Get tests working * Cleanup * fix nit * Show bad data on Activity error serialization failure --- .circleci/config.yml | 7 ++++ .../failing_with_structured_error_activity.rb | 21 ++++++++++ examples/bin/worker | 9 +++++ ...handling_structured_error_workflow_spec.rb | 33 +++++++++++++++ .../handling_structured_error_workflow.rb | 17 ++++++++ lib/temporal/configuration.rb | 3 +- lib/temporal/connection/grpc.rb | 3 +- lib/temporal/connection/serializer/failure.rb | 12 +++++- lib/temporal/errors.rb | 2 + lib/temporal/workflow/errors.rb | 37 +++++++++++------ .../connection/serializer/failure_spec.rb | 33 +++++++++++++++ .../unit/lib/temporal/workflow/errors_spec.rb | 40 ++++++++++++++++--- 12 files changed, 196 insertions(+), 21 deletions(-) create mode 100644 examples/activities/failing_with_structured_error_activity.rb create mode 100644 examples/spec/integration/handling_structured_error_workflow_spec.rb create mode 100644 examples/workflows/handling_structured_error_workflow.rb diff --git a/.circleci/config.yml b/.circleci/config.yml index ed0d5eb0..72570bbd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -60,6 +60,13 @@ jobs: environment: USE_ENCRYPTION: 1 + - run: + name: Boot up worker for v2 error serialization tests + command: cd examples && bin/worker + background: true + environment: + USE_ERROR_SERIALIZATION_V2: 1 + - run: name: Run RSpec command: cd examples && bundle exec rspec diff --git a/examples/activities/failing_with_structured_error_activity.rb b/examples/activities/failing_with_structured_error_activity.rb new file mode 100644 index 00000000..0d7543ba --- /dev/null +++ b/examples/activities/failing_with_structured_error_activity.rb @@ -0,0 +1,21 @@ +require 'temporal/json' + +# Illustrates raising an error with a non-standard initializer that +# is handleable by the Workflow. +class FailingWithStructuredErrorActivity < Temporal::Activity + retry_policy(max_attempts: 1) + + class MyError < Temporal::ActivityException + attr_reader :foo, :bar + + def initialize(foo, bar) + @foo = foo + @bar = bar + end + end + + def execute(foo, bar) + # Pass activity args into the error for better testing + raise MyError.new(foo, bar) + end +end diff --git a/examples/bin/worker b/examples/bin/worker index b4586649..29e55b69 100755 --- a/examples/bin/worker +++ b/examples/bin/worker @@ -17,6 +17,13 @@ if !ENV['USE_ENCRYPTION'].nil? end end +if !ENV['USE_ERROR_SERIALIZATION_V2'].nil? + Temporal.configure do |config| + config.task_queue = 'error_serialization_v2' + config.use_error_serialization_v2 = true + end +end + worker = Temporal::Worker.new(binary_checksum: `git show HEAD -s --format=%H`.strip) worker.register_workflow(AsyncActivityWorkflow) @@ -30,6 +37,7 @@ worker.register_workflow(ChildWorkflowTimeoutWorkflow) worker.register_workflow(ChildWorkflowTerminatedWorkflow) worker.register_workflow(FailingActivitiesWorkflow) worker.register_workflow(FailingWorkflow) +worker.register_workflow(HandlingStructuredErrorWorkflow) worker.register_workflow(HelloWorldWorkflow) worker.register_workflow(InvalidContinueAsNewWorkflow) worker.register_workflow(LocalHelloWorldWorkflow) @@ -63,6 +71,7 @@ worker.register_workflow(WaitForNamedSignalWorkflow) worker.register_activity(AsyncActivity) worker.register_activity(EchoActivity) worker.register_activity(FailingActivity) +worker.register_activity(FailingWithStructuredErrorActivity) worker.register_activity(GenerateFileActivity) worker.register_activity(GuessActivity) worker.register_activity(HelloWorldActivity) diff --git a/examples/spec/integration/handling_structured_error_workflow_spec.rb b/examples/spec/integration/handling_structured_error_workflow_spec.rb new file mode 100644 index 00000000..094fb139 --- /dev/null +++ b/examples/spec/integration/handling_structured_error_workflow_spec.rb @@ -0,0 +1,33 @@ +require 'workflows/handling_structured_error_workflow' + +describe HandlingStructuredErrorWorkflow, :integration do + # This test should be run when a worker with USE_ERROR_SERIALIZATION_V2 is running. + # That worker runs a task queue, error_serialization_v2. This setup code will + # route workflow requests to that task queue. + around(:each) do |example| + task_queue = Temporal.configuration.task_queue + + Temporal.configure do |config| + config.task_queue = 'error_serialization_v2' + end + + example.run + ensure + Temporal.configure do |config| + config.task_queue = task_queue + end + end + + it 'correctly re-raises an activity-thrown exception in the workflow' do + workflow_id = SecureRandom.uuid + + Temporal.start_workflow(described_class, 'foo', 5.0, options: { workflow_id: workflow_id }) + begin + result = Temporal.await_workflow_result(described_class, workflow_id: workflow_id) + expect(result).to eq('successfully handled error') + rescue Temporal::ActivityException + raise "Error deserialization failed. You probably need to run USE_ERROR_SERIALIZATION_V2=1 ./bin/worker and try again." + end + end + +end diff --git a/examples/workflows/handling_structured_error_workflow.rb b/examples/workflows/handling_structured_error_workflow.rb new file mode 100644 index 00000000..6d50fbc2 --- /dev/null +++ b/examples/workflows/handling_structured_error_workflow.rb @@ -0,0 +1,17 @@ +require 'activities/failing_with_structured_error_activity' + +class HandlingStructuredErrorWorkflow < Temporal::Workflow + + def execute(foo, bar) + begin + FailingWithStructuredErrorActivity.execute!(foo, bar) + rescue FailingWithStructuredErrorActivity::MyError => e + if e.foo == foo && e.bar == bar + return 'successfully handled error' + else + raise "Failure: didn't receive expected error from the activity" + end + end + raise "Failure: didn't receive any error from the activity" + end +end diff --git a/lib/temporal/configuration.rb b/lib/temporal/configuration.rb index b397d03c..820e5173 100644 --- a/lib/temporal/configuration.rb +++ b/lib/temporal/configuration.rb @@ -12,7 +12,7 @@ class Configuration Execution = Struct.new(:namespace, :task_queue, :timeouts, :headers, :search_attributes, keyword_init: true) attr_reader :timeouts, :error_handlers - attr_accessor :connection_type, :converter, :host, :port, :credentials, :identity, :logger, :metrics_adapter, :namespace, :task_queue, :headers, :search_attributes + attr_accessor :connection_type, :converter, :use_error_serialization_v2, :host, :port, :credentials, :identity, :logger, :metrics_adapter, :namespace, :task_queue, :headers, :search_attributes # See https://docs.temporal.io/blog/activity-timeouts/ for general docs. # We want an infinite execution timeout for cron schedules and other perpetual workflows. @@ -53,6 +53,7 @@ def initialize @task_queue = DEFAULT_TASK_QUEUE @headers = DEFAULT_HEADERS @converter = DEFAULT_CONVERTER + @use_error_serialization_v2 = false @error_handlers = [] @credentials = :this_channel_is_insecure @identity = nil diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index a5f7b287..9335e51b 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -276,11 +276,12 @@ def respond_activity_task_completed_by_id(namespace:, activity_id:, workflow_id: end def respond_activity_task_failed(namespace:, task_token:, exception:) + serialize_whole_error = Temporal.configuration.use_error_serialization_v2 request = Temporal::Api::WorkflowService::V1::RespondActivityTaskFailedRequest.new( namespace: namespace, identity: identity, task_token: task_token, - failure: Serializer::Failure.new(exception).to_proto + failure: Serializer::Failure.new(exception, serialize_whole_error: serialize_whole_error).to_proto ) client.respond_activity_task_failed(request) end diff --git a/lib/temporal/connection/serializer/failure.rb b/lib/temporal/connection/serializer/failure.rb index 15dfc555..dbba5fb5 100644 --- a/lib/temporal/connection/serializer/failure.rb +++ b/lib/temporal/connection/serializer/failure.rb @@ -7,13 +7,23 @@ module Serializer class Failure < Base include Concerns::Payloads + def initialize(error, serialize_whole_error: false) + @serialize_whole_error = serialize_whole_error + super(error) + end + def to_proto + details = if @serialize_whole_error + to_details_payloads(object) + else + to_details_payloads(object.message) + end Temporal::Api::Failure::V1::Failure.new( message: object.message, stack_trace: stack_trace_from(object.backtrace), application_failure_info: Temporal::Api::Failure::V1::ApplicationFailureInfo.new( type: object.class.name, - details: to_details_payloads(object.message) + details: details ) ) end diff --git a/lib/temporal/errors.rb b/lib/temporal/errors.rb index d0239abe..84da25f2 100644 --- a/lib/temporal/errors.rb +++ b/lib/temporal/errors.rb @@ -23,6 +23,8 @@ class ChildWorkflowTerminatedError < Error; end # A superclass for activity exceptions raised explicitly # with the intent to propagate to a workflow + # With v2 serialization (set with Temporal.configuration set with use_error_serialization_v2=true) you can + # throw any exception from an activity and expect that it can be handled by the workflow. class ActivityException < ClientError; end # Represents cancellation of a non-started activity diff --git a/lib/temporal/workflow/errors.rb b/lib/temporal/workflow/errors.rb index 25396702..d8194359 100644 --- a/lib/temporal/workflow/errors.rb +++ b/lib/temporal/workflow/errors.rb @@ -10,33 +10,44 @@ class Errors def self.generate_error(failure, default_exception_class = StandardError) case failure.failure_info when :application_failure_info - message = from_details_payloads(failure.application_failure_info.details) - exception_class = safe_constantize(failure.application_failure_info.type) + error_type = failure.application_failure_info.type + exception_class = safe_constantize(error_type) + message = failure.message + if exception_class.nil? Temporal.logger.error( - "Could not find original error class. Defaulting to StandardError.", - {original_error: failure.application_failure_info.type}, + 'Could not find original error class. Defaulting to StandardError.', + { original_error: error_type } ) - message = "#{failure.application_failure_info.type}: #{message}" + message = "#{error_type}: #{failure.message}" exception_class = default_exception_class end - - begin - exception = exception_class.new(message) - rescue => deserialization_error - # We don't currently support serializing/deserializing exceptions with more than one argument. + details = failure.application_failure_info.details + exception_or_message = from_details_payloads(details) + # v1 serialization only supports StandardErrors with a single "message" argument. + # v2 serialization supports complex errors using our converters to serialize them. + # enable v2 serialization in activities with Temporal.configuration.use_error_serialization_v2 + if exception_or_message.is_a?(Exception) + exception = exception_or_message + else + exception = exception_class.new(message) + end + rescue StandardError => deserialization_error message = "#{exception_class}: #{message}" exception = default_exception_class.new(message) Temporal.logger.error( - "Could not instantiate original error. Defaulting to StandardError.", + "Could not instantiate original error. Defaulting to StandardError. It's likely that your error's " \ + "initializer takes something more than just one positional argument. If so, make sure the worker running "\ + "your activities is setting Temporal.configuration.use_error_serialization_v2 to support this.", { - original_error: failure.application_failure_info.type, + original_error: error_type, + serialized_error: details.payloads.first.data, instantiation_error_class: deserialization_error.class.to_s, instantiation_error_message: deserialization_error.message, }, - ) + ) end exception.tap do |exception| backtrace = failure.stack_trace.split("\n") diff --git a/spec/unit/lib/temporal/connection/serializer/failure_spec.rb b/spec/unit/lib/temporal/connection/serializer/failure_spec.rb index cff68c52..2dd32370 100644 --- a/spec/unit/lib/temporal/connection/serializer/failure_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/failure_spec.rb @@ -1,6 +1,10 @@ require 'temporal/connection/serializer/failure' require 'temporal/workflow/command' +class TestDeserializer + include Temporal::Concerns::Payloads +end + describe Temporal::Connection::Serializer::Failure do describe 'to_proto' do it 'produces a protobuf' do @@ -8,5 +12,34 @@ expect(result).to be_an_instance_of(Temporal::Api::Failure::V1::Failure) end + + class NaughtyClass; end + + class MyError < StandardError + attr_reader :foo, :bad_class + + def initialize(foo, bar, bad_class:) + @foo = foo + @bad_class = bad_class + + # Ensure that we serialize derived properties. + my_message = "Hello, #{bar}!" + super(my_message) + end + end + + it 'Serializes round-trippable full errors when asked to' do + # Make sure serializing various bits round-trips + e = MyError.new(['seven', 'three'], "Bar", bad_class: NaughtyClass) + failure_proto = described_class.new(e, serialize_whole_error: true).to_proto + expect(failure_proto.application_failure_info.type).to eq("MyError") + + deserialized_error = TestDeserializer.new.from_details_payloads(failure_proto.application_failure_info.details) + expect(deserialized_error).to be_an_instance_of(MyError) + expect(deserialized_error.message).to eq("Hello, Bar!") + expect(deserialized_error.foo).to eq(['seven', 'three']) + expect(deserialized_error.bad_class).to eq(NaughtyClass) + end + end end diff --git a/spec/unit/lib/temporal/workflow/errors_spec.rb b/spec/unit/lib/temporal/workflow/errors_spec.rb index 05d5f82a..5947e245 100644 --- a/spec/unit/lib/temporal/workflow/errors_spec.rb +++ b/spec/unit/lib/temporal/workflow/errors_spec.rb @@ -15,6 +15,17 @@ def initialize(message) class SomeError < StandardError; end +class MyFancyError < Exception + + attr_reader :foo, :bar + + # Initializer doesn't just take one argument as StandardError does. + def initialize(foo, bar) + @foo = foo + @bar = bar + end +end + describe Temporal::Workflow::Errors do describe '.generate_error' do it "instantiates properly when the client has the error" do @@ -34,7 +45,18 @@ class SomeError < StandardError; end end - it "falls back to StandardError when the client doesn't have the error class" do + it 'correctly deserializes a complex error' do + error = MyFancyError.new('foo', 'bar') + failure = Temporal::Connection::Serializer::Failure.new(error, serialize_whole_error: true).to_proto + + e = Temporal::Workflow::Errors.generate_error(failure) + expect(e).to be_a(MyFancyError) + expect(e.foo).to eq('foo') + expect(e.bar).to eq('bar') + end + + + it "falls back to StandardError when the client doesn't have the error class" do allow(Temporal.logger).to receive(:error) message = "An error message" @@ -60,7 +82,7 @@ class SomeError < StandardError; end end - it "falls back to StandardError when the client can't initialize the error class due to arity" do + it "falls back to StandardError when the client can't initialize the error class due to arity" do allow(Temporal.logger).to receive(:error) message = "An error message" @@ -79,16 +101,20 @@ class SomeError < StandardError; end expect(Temporal.logger) .to have_received(:error) .with( - 'Could not instantiate original error. Defaulting to StandardError.', + 'Could not instantiate original error. Defaulting to StandardError. ' \ + 'It\'s likely that your error\'s initializer takes something more than just one positional argument. '\ + 'If so, make sure the worker running your activities is setting '\ + 'Temporal.configuration.use_error_serialization_v2 to support this.', { original_error: "ErrorWithTwoArgs", + serialized_error: '"An error message"', instantiation_error_class: "ArgumentError", instantiation_error_message: "wrong number of arguments (given 1, expected 2)", }, ) end - it "falls back to StandardError when the client can't initialize the error class when initialize doesn't take a string" do + it "falls back to StandardError when the client can't initialize the error class when initialize doesn't take a string" do allow(Temporal.logger).to receive(:error) message = "An error message" @@ -107,9 +133,13 @@ class SomeError < StandardError; end expect(Temporal.logger) .to have_received(:error) .with( - 'Could not instantiate original error. Defaulting to StandardError.', + 'Could not instantiate original error. Defaulting to StandardError. ' \ + 'It\'s likely that your error\'s initializer takes something more than just one positional argument. '\ + 'If so, make sure the worker running your activities is setting '\ + 'Temporal.configuration.use_error_serialization_v2 to support this.', { original_error: "ErrorThatRaisesInInitialize", + serialized_error: '"An error message"', instantiation_error_class: "TypeError", instantiation_error_message: "String can't be coerced into Integer", }, From f16228e04c727c2c7bf218e3613a0005ec44e32a Mon Sep 17 00:00:00 2001 From: Jeff Schoner Date: Mon, 13 Feb 2023 06:11:03 -0800 Subject: [PATCH 072/125] Update Temporal API to 1.16 (#217) * Upgrade Temporal proto API to version 1.16 * Rename Temporal::Api -> Temporalio::Api * Remove deprecated namespace field for activity task scheduling * Increase version to 0.0.2 because of Temporalio package change --- Makefile | 4 +- examples/lib/cryptconverter.rb | 8 +- .../integration/describe_namespace_spec.rb | 2 +- .../spec/integration/list_namespaces_spec.rb | 2 +- lib/gen/dependencies/gogoproto/gogo_pb.rb | 14 + lib/gen/temporal/api/batch/v1/message_pb.rb | 50 +++ lib/gen/temporal/api/command/v1/message_pb.rb | 18 +- lib/gen/temporal/api/common/v1/message_pb.rb | 4 +- .../api/enums/v1/batch_operation_pb.rb | 33 ++ .../temporal/api/enums/v1/command_type_pb.rb | 4 +- lib/gen/temporal/api/enums/v1/common_pb.rb | 5 +- .../temporal/api/enums/v1/event_type_pb.rb | 8 +- .../temporal/api/enums/v1/failed_cause_pb.rb | 21 +- lib/gen/temporal/api/enums/v1/namespace_pb.rb | 8 +- lib/gen/temporal/api/enums/v1/query_pb.rb | 2 +- lib/gen/temporal/api/enums/v1/reset_pb.rb | 24 ++ lib/gen/temporal/api/enums/v1/schedule_pb.rb | 28 ++ .../temporal/api/enums/v1/task_queue_pb.rb | 2 +- lib/gen/temporal/api/enums/v1/update_pb.rb | 25 ++ lib/gen/temporal/api/enums/v1/workflow_pb.rb | 9 +- .../api/errordetails/v1/message_pb.rb | 38 +- lib/gen/temporal/api/failure/v1/message_pb.rb | 4 +- lib/gen/temporal/api/filter/v1/message_pb.rb | 4 +- lib/gen/temporal/api/history/v1/message_pb.rb | 78 +++- .../temporal/api/namespace/v1/message_pb.rb | 11 +- .../operatorservice/v1/request_response_pb.rb | 88 ++++ .../api/operatorservice/v1/service_pb.rb | 20 + .../operatorservice/v1/service_services_pb.rb | 78 ++++ .../temporal/api/protocol/v1/message_pb.rb | 30 ++ lib/gen/temporal/api/query/v1/message_pb.rb | 4 +- .../temporal/api/replication/v1/message_pb.rb | 12 +- .../temporal/api/schedule/v1/message_pb.rb | 149 +++++++ .../temporal/api/taskqueue/v1/message_pb.rb | 15 +- lib/gen/temporal/api/update/v1/message_pb.rb | 72 ++++ lib/gen/temporal/api/version/v1/message_pb.rb | 4 +- .../temporal/api/workflow/v1/message_pb.rb | 30 +- .../workflowservice/v1/request_response_pb.rb | 245 ++++++++++- .../api/workflowservice/v1/service_pb.rb | 3 +- .../workflowservice/v1/service_services_pb.rb | 386 +++++++++++------- lib/temporal/client.rb | 2 +- lib/temporal/connection/converter/base.rb | 2 +- .../connection/converter/payload/bytes.rb | 2 +- .../connection/converter/payload/json.rb | 2 +- .../connection/converter/payload/nil.rb | 2 +- .../converter/payload/proto_json.rb | 2 +- lib/temporal/connection/grpc.rb | 124 +++--- .../connection/serializer/cancel_timer.rb | 6 +- .../serializer/complete_workflow.rb | 6 +- .../connection/serializer/continue_as_new.rb | 16 +- .../connection/serializer/fail_workflow.rb | 6 +- lib/temporal/connection/serializer/failure.rb | 4 +- .../connection/serializer/query_answer.rb | 4 +- .../connection/serializer/query_failure.rb | 4 +- .../connection/serializer/record_marker.rb | 6 +- .../request_activity_cancellation.rb | 6 +- .../connection/serializer/retry_policy.rb | 2 +- .../serializer/schedule_activity.rb | 13 +- .../serializer/signal_external_workflow.rb | 8 +- .../serializer/start_child_workflow.rb | 22 +- .../connection/serializer/start_timer.rb | 6 +- .../serializer/upsert_search_attributes.rb | 8 +- .../serializer/workflow_id_reuse_policy.rb | 6 +- lib/temporal/metadata.rb | 2 +- lib/temporal/version.rb | 2 +- lib/temporal/workflow/command.rb | 2 +- lib/temporal/workflow/context.rb | 1 - lib/temporal/workflow/errors.rb | 6 +- lib/temporal/workflow/task_processor.rb | 2 +- proto | 2 +- .../grpc/activity_task_fabricator.rb | 4 +- .../grpc/activity_type_fabricator.rb | 2 +- .../grpc/application_failure_fabricator.rb | 4 +- spec/fabricators/grpc/header_fabricator.rb | 4 +- .../grpc/history_event_fabricator.rb | 65 ++- spec/fabricators/grpc/memo_fabricator.rb | 4 +- spec/fabricators/grpc/payload_fabricator.rb | 2 +- .../grpc/search_attributes_fabricator.rb | 4 +- .../fabricators/grpc/task_queue_fabricator.rb | 2 +- .../grpc/workflow_execution_fabricator.rb | 2 +- .../workflow_execution_info_fabricator.rb | 4 +- ...ion_started_event_attributes_fabricator.rb | 4 +- .../grpc/workflow_query_fabricator.rb | 2 +- .../grpc/workflow_task_fabricator.rb | 4 +- .../grpc/workflow_type_fabricator.rb | 2 +- .../workflow_canceled_event_fabricator.rb | 4 +- .../workflow_completed_event_fabricator.rb | 4 +- .../workflow_execution_history_fabricator.rb | 4 +- spec/unit/lib/temporal/client_spec.rb | 24 +- .../connection/converter/composite_spec.rb | 10 +- .../converter/payload/bytes_spec.rb | 4 +- .../connection/converter/payload/nil_spec.rb | 4 +- .../converter/payload/proto_json_spec.rb | 4 +- .../serializer/continue_as_new_spec.rb | 2 +- .../connection/serializer/failure_spec.rb | 2 +- .../serializer/query_answer_spec.rb | 6 +- .../serializer/query_failure_spec.rb | 6 +- .../upsert_search_attributes_spec.rb | 2 +- .../workflow_id_reuse_policy_spec.rb | 6 +- spec/unit/lib/temporal/grpc_spec.rb | 124 +++--- .../testing/temporal_override_spec.rb | 2 +- .../lib/temporal/workflow/context_spec.rb | 2 +- .../temporal/workflow/execution_info_spec.rb | 6 +- .../lib/temporal/workflow/executor_spec.rb | 4 +- .../temporal/workflow/task_processor_spec.rb | 8 +- 104 files changed, 1667 insertions(+), 488 deletions(-) create mode 100644 lib/gen/dependencies/gogoproto/gogo_pb.rb create mode 100644 lib/gen/temporal/api/batch/v1/message_pb.rb create mode 100644 lib/gen/temporal/api/enums/v1/batch_operation_pb.rb create mode 100644 lib/gen/temporal/api/enums/v1/reset_pb.rb create mode 100644 lib/gen/temporal/api/enums/v1/schedule_pb.rb create mode 100644 lib/gen/temporal/api/enums/v1/update_pb.rb create mode 100644 lib/gen/temporal/api/operatorservice/v1/request_response_pb.rb create mode 100644 lib/gen/temporal/api/operatorservice/v1/service_pb.rb create mode 100644 lib/gen/temporal/api/operatorservice/v1/service_services_pb.rb create mode 100644 lib/gen/temporal/api/protocol/v1/message_pb.rb create mode 100644 lib/gen/temporal/api/schedule/v1/message_pb.rb create mode 100644 lib/gen/temporal/api/update/v1/message_pb.rb diff --git a/Makefile b/Makefile index 078e4e0d..6967cb5b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PROTO_ROOT := proto/temporal +PROTO_ROOT := proto PROTO_FILES = $(shell find $(PROTO_ROOT) -name "*.proto") PROTO_DIRS = $(sort $(dir $(PROTO_FILES))) PROTO_OUT := lib/gen @@ -6,4 +6,4 @@ PROTO_OUT := lib/gen proto: $(foreach PROTO_DIR,$(PROTO_DIRS),bundle exec grpc_tools_ruby_protoc -Iproto --ruby_out=$(PROTO_OUT) --grpc_out=$(PROTO_OUT) $(PROTO_DIR)*.proto;) -.PHONY: proto \ No newline at end of file +.PHONY: proto diff --git a/examples/lib/cryptconverter.rb b/examples/lib/cryptconverter.rb index b3c7b77a..c968bfc9 100644 --- a/examples/lib/cryptconverter.rb +++ b/examples/lib/cryptconverter.rb @@ -16,7 +16,7 @@ def to_payloads(data) payloads = super(data) - Temporal::Api::Common::V1::Payloads.new( + Temporalio::Api::Common::V1::Payloads.new( payloads: payloads.payloads.map { |payload| encrypt_payload(payload, key_id, key) } ) end @@ -55,12 +55,12 @@ def encrypt(data, key) end def encrypt_payload(payload, key_id, key) - Temporal::Api::Common::V1::Payload.new( + Temporalio::Api::Common::V1::Payload.new( metadata: { METADATA_ENCODING_KEY => METADATA_ENCODING, METADATA_KEY_ID_KEY => key_id, }, - data: encrypt(Temporal::Api::Common::V1::Payload.encode(payload), key) + data: encrypt(Temporalio::Api::Common::V1::Payload.encode(payload), key) ) end @@ -85,7 +85,7 @@ def decrypt_payload(payload) key = get_key(key_id) serialized_payload = decrypt(payload.data, key) - Temporal::Api::Common::V1::Payload.decode(serialized_payload) + Temporalio::Api::Common::V1::Payload.decode(serialized_payload) end end end diff --git a/examples/spec/integration/describe_namespace_spec.rb b/examples/spec/integration/describe_namespace_spec.rb index d042190e..fe2416db 100644 --- a/examples/spec/integration/describe_namespace_spec.rb +++ b/examples/spec/integration/describe_namespace_spec.rb @@ -11,7 +11,7 @@ end expect(rescued).to eq(true) result = Temporal.describe_namespace(namespace) - expect(result).to be_an_instance_of(Temporal::Api::WorkflowService::V1::DescribeNamespaceResponse) + expect(result).to be_an_instance_of(Temporalio::Api::WorkflowService::V1::DescribeNamespaceResponse) expect(result.namespace_info.name).to eq(namespace) expect(result.namespace_info.state).to eq(:NAMESPACE_STATE_REGISTERED) expect(result.namespace_info.description).to_not eq(nil) diff --git a/examples/spec/integration/list_namespaces_spec.rb b/examples/spec/integration/list_namespaces_spec.rb index d22ed82c..974e882d 100644 --- a/examples/spec/integration/list_namespaces_spec.rb +++ b/examples/spec/integration/list_namespaces_spec.rb @@ -1,6 +1,6 @@ describe 'Temporal.list_namespaces', :integration do it 'returns the correct values' do result = Temporal.list_namespaces(page_size: 100) - expect(result).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListNamespacesResponse) + expect(result).to be_an_instance_of(Temporalio::Api::WorkflowService::V1::ListNamespacesResponse) end end diff --git a/lib/gen/dependencies/gogoproto/gogo_pb.rb b/lib/gen/dependencies/gogoproto/gogo_pb.rb new file mode 100644 index 00000000..d63bf773 --- /dev/null +++ b/lib/gen/dependencies/gogoproto/gogo_pb.rb @@ -0,0 +1,14 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: dependencies/gogoproto/gogo.proto + +require 'google/protobuf' + +require 'google/protobuf/descriptor_pb' + +Google::Protobuf::DescriptorPool.generated_pool.build do + add_file("dependencies/gogoproto/gogo.proto", :syntax => :proto2) do + end +end + +module Gogoproto +end diff --git a/lib/gen/temporal/api/batch/v1/message_pb.rb b/lib/gen/temporal/api/batch/v1/message_pb.rb new file mode 100644 index 00000000..5f1d88e4 --- /dev/null +++ b/lib/gen/temporal/api/batch/v1/message_pb.rb @@ -0,0 +1,50 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporal/api/batch/v1/message.proto + +require 'google/protobuf' + +require 'dependencies/gogoproto/gogo_pb' +require 'google/protobuf/timestamp_pb' +require 'temporal/api/common/v1/message_pb' +require 'temporal/api/enums/v1/batch_operation_pb' + +Google::Protobuf::DescriptorPool.generated_pool.build do + add_file("temporal/api/batch/v1/message.proto", :syntax => :proto3) do + add_message "temporal.api.batch.v1.BatchOperationInfo" do + optional :job_id, :string, 1 + optional :state, :enum, 2, "temporal.api.enums.v1.BatchOperationState" + optional :start_time, :message, 3, "google.protobuf.Timestamp" + optional :close_time, :message, 4, "google.protobuf.Timestamp" + end + add_message "temporal.api.batch.v1.BatchOperationTermination" do + optional :details, :message, 1, "temporal.api.common.v1.Payloads" + optional :identity, :string, 2 + end + add_message "temporal.api.batch.v1.BatchOperationSignal" do + optional :signal, :string, 1 + optional :input, :message, 2, "temporal.api.common.v1.Payloads" + optional :header, :message, 3, "temporal.api.common.v1.Header" + optional :identity, :string, 4 + end + add_message "temporal.api.batch.v1.BatchOperationCancellation" do + optional :identity, :string, 1 + end + add_message "temporal.api.batch.v1.BatchOperationDeletion" do + optional :identity, :string, 1 + end + end +end + +module Temporalio + module Api + module Batch + module V1 + BatchOperationInfo = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.batch.v1.BatchOperationInfo").msgclass + BatchOperationTermination = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.batch.v1.BatchOperationTermination").msgclass + BatchOperationSignal = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.batch.v1.BatchOperationSignal").msgclass + BatchOperationCancellation = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.batch.v1.BatchOperationCancellation").msgclass + BatchOperationDeletion = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.batch.v1.BatchOperationDeletion").msgclass + end + end + end +end diff --git a/lib/gen/temporal/api/command/v1/message_pb.rb b/lib/gen/temporal/api/command/v1/message_pb.rb index fa13cc7a..0dd08254 100644 --- a/lib/gen/temporal/api/command/v1/message_pb.rb +++ b/lib/gen/temporal/api/command/v1/message_pb.rb @@ -4,17 +4,18 @@ require 'google/protobuf' require 'google/protobuf/duration_pb' +require 'dependencies/gogoproto/gogo_pb' require 'temporal/api/enums/v1/workflow_pb' require 'temporal/api/enums/v1/command_type_pb' require 'temporal/api/common/v1/message_pb' require 'temporal/api/failure/v1/message_pb' require 'temporal/api/taskqueue/v1/message_pb' + Google::Protobuf::DescriptorPool.generated_pool.build do add_file("temporal/api/command/v1/message.proto", :syntax => :proto3) do add_message "temporal.api.command.v1.ScheduleActivityTaskCommandAttributes" do optional :activity_id, :string, 1 optional :activity_type, :message, 2, "temporal.api.common.v1.ActivityType" - optional :namespace, :string, 3 optional :task_queue, :message, 4, "temporal.api.taskqueue.v1.TaskQueue" optional :header, :message, 5, "temporal.api.common.v1.Header" optional :input, :message, 6, "temporal.api.common.v1.Payloads" @@ -23,6 +24,7 @@ optional :start_to_close_timeout, :message, 9, "google.protobuf.Duration" optional :heartbeat_timeout, :message, 10, "google.protobuf.Duration" optional :retry_policy, :message, 11, "temporal.api.common.v1.RetryPolicy" + optional :request_eager_execution, :bool, 12 end add_message "temporal.api.command.v1.RequestCancelActivityTaskCommandAttributes" do optional :scheduled_event_id, :int64, 1 @@ -49,6 +51,7 @@ optional :run_id, :string, 3 optional :control, :string, 4 optional :child_workflow_only, :bool, 5 + optional :reason, :string, 6 end add_message "temporal.api.command.v1.SignalExternalWorkflowExecutionCommandAttributes" do optional :namespace, :string, 1 @@ -57,10 +60,14 @@ optional :input, :message, 4, "temporal.api.common.v1.Payloads" optional :control, :string, 5 optional :child_workflow_only, :bool, 6 + optional :header, :message, 7, "temporal.api.common.v1.Header" end add_message "temporal.api.command.v1.UpsertWorkflowSearchAttributesCommandAttributes" do optional :search_attributes, :message, 1, "temporal.api.common.v1.SearchAttributes" end + add_message "temporal.api.command.v1.ModifyWorkflowPropertiesCommandAttributes" do + optional :upserted_memo, :message, 1, "temporal.api.common.v1.Memo" + end add_message "temporal.api.command.v1.RecordMarkerCommandAttributes" do optional :marker_name, :string, 1 map :details, :string, :message, 2, "temporal.api.common.v1.Payloads" @@ -101,6 +108,9 @@ optional :memo, :message, 15, "temporal.api.common.v1.Memo" optional :search_attributes, :message, 16, "temporal.api.common.v1.SearchAttributes" end + add_message "temporal.api.command.v1.ProtocolMessageCommandAttributes" do + optional :message_id, :string, 1 + end add_message "temporal.api.command.v1.Command" do optional :command_type, :enum, 1, "temporal.api.enums.v1.CommandType" oneof :attributes do @@ -117,12 +127,14 @@ optional :start_child_workflow_execution_command_attributes, :message, 12, "temporal.api.command.v1.StartChildWorkflowExecutionCommandAttributes" optional :signal_external_workflow_execution_command_attributes, :message, 13, "temporal.api.command.v1.SignalExternalWorkflowExecutionCommandAttributes" optional :upsert_workflow_search_attributes_command_attributes, :message, 14, "temporal.api.command.v1.UpsertWorkflowSearchAttributesCommandAttributes" + optional :protocol_message_command_attributes, :message, 15, "temporal.api.command.v1.ProtocolMessageCommandAttributes" + optional :modify_workflow_properties_command_attributes, :message, 17, "temporal.api.command.v1.ModifyWorkflowPropertiesCommandAttributes" end end end end -module Temporal +module Temporalio module Api module Command module V1 @@ -136,9 +148,11 @@ module V1 RequestCancelExternalWorkflowExecutionCommandAttributes = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.command.v1.RequestCancelExternalWorkflowExecutionCommandAttributes").msgclass SignalExternalWorkflowExecutionCommandAttributes = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.command.v1.SignalExternalWorkflowExecutionCommandAttributes").msgclass UpsertWorkflowSearchAttributesCommandAttributes = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.command.v1.UpsertWorkflowSearchAttributesCommandAttributes").msgclass + ModifyWorkflowPropertiesCommandAttributes = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.command.v1.ModifyWorkflowPropertiesCommandAttributes").msgclass RecordMarkerCommandAttributes = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.command.v1.RecordMarkerCommandAttributes").msgclass ContinueAsNewWorkflowExecutionCommandAttributes = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.command.v1.ContinueAsNewWorkflowExecutionCommandAttributes").msgclass StartChildWorkflowExecutionCommandAttributes = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.command.v1.StartChildWorkflowExecutionCommandAttributes").msgclass + ProtocolMessageCommandAttributes = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.command.v1.ProtocolMessageCommandAttributes").msgclass Command = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.command.v1.Command").msgclass end end diff --git a/lib/gen/temporal/api/common/v1/message_pb.rb b/lib/gen/temporal/api/common/v1/message_pb.rb index 281878d8..ef18cc6e 100644 --- a/lib/gen/temporal/api/common/v1/message_pb.rb +++ b/lib/gen/temporal/api/common/v1/message_pb.rb @@ -4,7 +4,9 @@ require 'google/protobuf' require 'google/protobuf/duration_pb' +require 'dependencies/gogoproto/gogo_pb' require 'temporal/api/enums/v1/common_pb' + Google::Protobuf::DescriptorPool.generated_pool.build do add_file("temporal/api/common/v1/message.proto", :syntax => :proto3) do add_message "temporal.api.common.v1.DataBlob" do @@ -47,7 +49,7 @@ end end -module Temporal +module Temporalio module Api module Common module V1 diff --git a/lib/gen/temporal/api/enums/v1/batch_operation_pb.rb b/lib/gen/temporal/api/enums/v1/batch_operation_pb.rb new file mode 100644 index 00000000..aff7e54f --- /dev/null +++ b/lib/gen/temporal/api/enums/v1/batch_operation_pb.rb @@ -0,0 +1,33 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporal/api/enums/v1/batch_operation.proto + +require 'google/protobuf' + +Google::Protobuf::DescriptorPool.generated_pool.build do + add_file("temporal/api/enums/v1/batch_operation.proto", :syntax => :proto3) do + add_enum "temporal.api.enums.v1.BatchOperationType" do + value :BATCH_OPERATION_TYPE_UNSPECIFIED, 0 + value :BATCH_OPERATION_TYPE_TERMINATE, 1 + value :BATCH_OPERATION_TYPE_CANCEL, 2 + value :BATCH_OPERATION_TYPE_SIGNAL, 3 + value :BATCH_OPERATION_TYPE_DELETE, 4 + end + add_enum "temporal.api.enums.v1.BatchOperationState" do + value :BATCH_OPERATION_STATE_UNSPECIFIED, 0 + value :BATCH_OPERATION_STATE_RUNNING, 1 + value :BATCH_OPERATION_STATE_COMPLETED, 2 + value :BATCH_OPERATION_STATE_FAILED, 3 + end + end +end + +module Temporalio + module Api + module Enums + module V1 + BatchOperationType = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.enums.v1.BatchOperationType").enummodule + BatchOperationState = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.enums.v1.BatchOperationState").enummodule + end + end + end +end diff --git a/lib/gen/temporal/api/enums/v1/command_type_pb.rb b/lib/gen/temporal/api/enums/v1/command_type_pb.rb index d77269e0..fc4270ea 100644 --- a/lib/gen/temporal/api/enums/v1/command_type_pb.rb +++ b/lib/gen/temporal/api/enums/v1/command_type_pb.rb @@ -20,11 +20,13 @@ value :COMMAND_TYPE_START_CHILD_WORKFLOW_EXECUTION, 11 value :COMMAND_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION, 12 value :COMMAND_TYPE_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES, 13 + value :COMMAND_TYPE_PROTOCOL_MESSAGE, 14 + value :COMMAND_TYPE_MODIFY_WORKFLOW_PROPERTIES, 16 end end end -module Temporal +module Temporalio module Api module Enums module V1 diff --git a/lib/gen/temporal/api/enums/v1/common_pb.rb b/lib/gen/temporal/api/enums/v1/common_pb.rb index 3b1ca3b6..56c53671 100644 --- a/lib/gen/temporal/api/enums/v1/common_pb.rb +++ b/lib/gen/temporal/api/enums/v1/common_pb.rb @@ -12,12 +12,13 @@ end add_enum "temporal.api.enums.v1.IndexedValueType" do value :INDEXED_VALUE_TYPE_UNSPECIFIED, 0 - value :INDEXED_VALUE_TYPE_STRING, 1 + value :INDEXED_VALUE_TYPE_TEXT, 1 value :INDEXED_VALUE_TYPE_KEYWORD, 2 value :INDEXED_VALUE_TYPE_INT, 3 value :INDEXED_VALUE_TYPE_DOUBLE, 4 value :INDEXED_VALUE_TYPE_BOOL, 5 value :INDEXED_VALUE_TYPE_DATETIME, 6 + value :INDEXED_VALUE_TYPE_KEYWORD_LIST, 7 end add_enum "temporal.api.enums.v1.Severity" do value :SEVERITY_UNSPECIFIED, 0 @@ -28,7 +29,7 @@ end end -module Temporal +module Temporalio module Api module Enums module V1 diff --git a/lib/gen/temporal/api/enums/v1/event_type_pb.rb b/lib/gen/temporal/api/enums/v1/event_type_pb.rb index d1fa17b7..b18c13c6 100644 --- a/lib/gen/temporal/api/enums/v1/event_type_pb.rb +++ b/lib/gen/temporal/api/enums/v1/event_type_pb.rb @@ -47,11 +47,17 @@ value :EVENT_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_FAILED, 38 value :EVENT_TYPE_EXTERNAL_WORKFLOW_EXECUTION_SIGNALED, 39 value :EVENT_TYPE_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES, 40 + value :EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_ACCEPTED, 41 + value :EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_REJECTED, 42 + value :EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_COMPLETED, 43 + value :EVENT_TYPE_WORKFLOW_PROPERTIES_MODIFIED_EXTERNALLY, 44 + value :EVENT_TYPE_ACTIVITY_PROPERTIES_MODIFIED_EXTERNALLY, 45 + value :EVENT_TYPE_WORKFLOW_PROPERTIES_MODIFIED, 46 end end end -module Temporal +module Temporalio module Api module Enums module V1 diff --git a/lib/gen/temporal/api/enums/v1/failed_cause_pb.rb b/lib/gen/temporal/api/enums/v1/failed_cause_pb.rb index f61ea661..e0e21568 100644 --- a/lib/gen/temporal/api/enums/v1/failed_cause_pb.rb +++ b/lib/gen/temporal/api/enums/v1/failed_cause_pb.rb @@ -30,23 +30,41 @@ value :WORKFLOW_TASK_FAILED_CAUSE_BAD_BINARY, 21 value :WORKFLOW_TASK_FAILED_CAUSE_SCHEDULE_ACTIVITY_DUPLICATE_ID, 22 value :WORKFLOW_TASK_FAILED_CAUSE_BAD_SEARCH_ATTRIBUTES, 23 + value :WORKFLOW_TASK_FAILED_CAUSE_NON_DETERMINISTIC_ERROR, 24 + value :WORKFLOW_TASK_FAILED_CAUSE_BAD_MODIFY_WORKFLOW_PROPERTIES_ATTRIBUTES, 25 + value :WORKFLOW_TASK_FAILED_CAUSE_PENDING_CHILD_WORKFLOWS_LIMIT_EXCEEDED, 26 + value :WORKFLOW_TASK_FAILED_CAUSE_PENDING_ACTIVITIES_LIMIT_EXCEEDED, 27 + value :WORKFLOW_TASK_FAILED_CAUSE_PENDING_SIGNALS_LIMIT_EXCEEDED, 28 + value :WORKFLOW_TASK_FAILED_CAUSE_PENDING_REQUEST_CANCEL_LIMIT_EXCEEDED, 29 + value :WORKFLOW_TASK_FAILED_CAUSE_BAD_UPDATE_WORKFLOW_EXECUTION_MESSAGE, 30 + value :WORKFLOW_TASK_FAILED_CAUSE_UNHANDLED_UPDATE, 31 end add_enum "temporal.api.enums.v1.StartChildWorkflowExecutionFailedCause" do value :START_CHILD_WORKFLOW_EXECUTION_FAILED_CAUSE_UNSPECIFIED, 0 value :START_CHILD_WORKFLOW_EXECUTION_FAILED_CAUSE_WORKFLOW_ALREADY_EXISTS, 1 + value :START_CHILD_WORKFLOW_EXECUTION_FAILED_CAUSE_NAMESPACE_NOT_FOUND, 2 end add_enum "temporal.api.enums.v1.CancelExternalWorkflowExecutionFailedCause" do value :CANCEL_EXTERNAL_WORKFLOW_EXECUTION_FAILED_CAUSE_UNSPECIFIED, 0 value :CANCEL_EXTERNAL_WORKFLOW_EXECUTION_FAILED_CAUSE_EXTERNAL_WORKFLOW_EXECUTION_NOT_FOUND, 1 + value :CANCEL_EXTERNAL_WORKFLOW_EXECUTION_FAILED_CAUSE_NAMESPACE_NOT_FOUND, 2 end add_enum "temporal.api.enums.v1.SignalExternalWorkflowExecutionFailedCause" do value :SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_FAILED_CAUSE_UNSPECIFIED, 0 value :SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_FAILED_CAUSE_EXTERNAL_WORKFLOW_EXECUTION_NOT_FOUND, 1 + value :SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_FAILED_CAUSE_NAMESPACE_NOT_FOUND, 2 + end + add_enum "temporal.api.enums.v1.ResourceExhaustedCause" do + value :RESOURCE_EXHAUSTED_CAUSE_UNSPECIFIED, 0 + value :RESOURCE_EXHAUSTED_CAUSE_RPS_LIMIT, 1 + value :RESOURCE_EXHAUSTED_CAUSE_CONCURRENT_LIMIT, 2 + value :RESOURCE_EXHAUSTED_CAUSE_SYSTEM_OVERLOADED, 3 + value :RESOURCE_EXHAUSTED_CAUSE_PERSISTENCE_LIMIT, 4 end end end -module Temporal +module Temporalio module Api module Enums module V1 @@ -54,6 +72,7 @@ module V1 StartChildWorkflowExecutionFailedCause = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.enums.v1.StartChildWorkflowExecutionFailedCause").enummodule CancelExternalWorkflowExecutionFailedCause = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.enums.v1.CancelExternalWorkflowExecutionFailedCause").enummodule SignalExternalWorkflowExecutionFailedCause = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.enums.v1.SignalExternalWorkflowExecutionFailedCause").enummodule + ResourceExhaustedCause = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.enums.v1.ResourceExhaustedCause").enummodule end end end diff --git a/lib/gen/temporal/api/enums/v1/namespace_pb.rb b/lib/gen/temporal/api/enums/v1/namespace_pb.rb index 65980db0..d8478407 100644 --- a/lib/gen/temporal/api/enums/v1/namespace_pb.rb +++ b/lib/gen/temporal/api/enums/v1/namespace_pb.rb @@ -16,15 +16,21 @@ value :ARCHIVAL_STATE_DISABLED, 1 value :ARCHIVAL_STATE_ENABLED, 2 end + add_enum "temporal.api.enums.v1.ReplicationState" do + value :REPLICATION_STATE_UNSPECIFIED, 0 + value :REPLICATION_STATE_NORMAL, 1 + value :REPLICATION_STATE_HANDOVER, 2 + end end end -module Temporal +module Temporalio module Api module Enums module V1 NamespaceState = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.enums.v1.NamespaceState").enummodule ArchivalState = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.enums.v1.ArchivalState").enummodule + ReplicationState = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.enums.v1.ReplicationState").enummodule end end end diff --git a/lib/gen/temporal/api/enums/v1/query_pb.rb b/lib/gen/temporal/api/enums/v1/query_pb.rb index d6eaabb2..34e4f770 100644 --- a/lib/gen/temporal/api/enums/v1/query_pb.rb +++ b/lib/gen/temporal/api/enums/v1/query_pb.rb @@ -19,7 +19,7 @@ end end -module Temporal +module Temporalio module Api module Enums module V1 diff --git a/lib/gen/temporal/api/enums/v1/reset_pb.rb b/lib/gen/temporal/api/enums/v1/reset_pb.rb new file mode 100644 index 00000000..6eb81b8d --- /dev/null +++ b/lib/gen/temporal/api/enums/v1/reset_pb.rb @@ -0,0 +1,24 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporal/api/enums/v1/reset.proto + +require 'google/protobuf' + +Google::Protobuf::DescriptorPool.generated_pool.build do + add_file("temporal/api/enums/v1/reset.proto", :syntax => :proto3) do + add_enum "temporal.api.enums.v1.ResetReapplyType" do + value :RESET_REAPPLY_TYPE_UNSPECIFIED, 0 + value :RESET_REAPPLY_TYPE_SIGNAL, 1 + value :RESET_REAPPLY_TYPE_NONE, 2 + end + end +end + +module Temporalio + module Api + module Enums + module V1 + ResetReapplyType = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.enums.v1.ResetReapplyType").enummodule + end + end + end +end diff --git a/lib/gen/temporal/api/enums/v1/schedule_pb.rb b/lib/gen/temporal/api/enums/v1/schedule_pb.rb new file mode 100644 index 00000000..14d7b311 --- /dev/null +++ b/lib/gen/temporal/api/enums/v1/schedule_pb.rb @@ -0,0 +1,28 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporal/api/enums/v1/schedule.proto + +require 'google/protobuf' + +Google::Protobuf::DescriptorPool.generated_pool.build do + add_file("temporal/api/enums/v1/schedule.proto", :syntax => :proto3) do + add_enum "temporal.api.enums.v1.ScheduleOverlapPolicy" do + value :SCHEDULE_OVERLAP_POLICY_UNSPECIFIED, 0 + value :SCHEDULE_OVERLAP_POLICY_SKIP, 1 + value :SCHEDULE_OVERLAP_POLICY_BUFFER_ONE, 2 + value :SCHEDULE_OVERLAP_POLICY_BUFFER_ALL, 3 + value :SCHEDULE_OVERLAP_POLICY_CANCEL_OTHER, 4 + value :SCHEDULE_OVERLAP_POLICY_TERMINATE_OTHER, 5 + value :SCHEDULE_OVERLAP_POLICY_ALLOW_ALL, 6 + end + end +end + +module Temporalio + module Api + module Enums + module V1 + ScheduleOverlapPolicy = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.enums.v1.ScheduleOverlapPolicy").enummodule + end + end + end +end diff --git a/lib/gen/temporal/api/enums/v1/task_queue_pb.rb b/lib/gen/temporal/api/enums/v1/task_queue_pb.rb index bc0a59f6..53d99653 100644 --- a/lib/gen/temporal/api/enums/v1/task_queue_pb.rb +++ b/lib/gen/temporal/api/enums/v1/task_queue_pb.rb @@ -18,7 +18,7 @@ end end -module Temporal +module Temporalio module Api module Enums module V1 diff --git a/lib/gen/temporal/api/enums/v1/update_pb.rb b/lib/gen/temporal/api/enums/v1/update_pb.rb new file mode 100644 index 00000000..205d140a --- /dev/null +++ b/lib/gen/temporal/api/enums/v1/update_pb.rb @@ -0,0 +1,25 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporal/api/enums/v1/update.proto + +require 'google/protobuf' + +Google::Protobuf::DescriptorPool.generated_pool.build do + add_file("temporal/api/enums/v1/update.proto", :syntax => :proto3) do + add_enum "temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage" do + value :UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_UNSPECIFIED, 0 + value :UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED, 1 + value :UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, 2 + value :UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 3 + end + end +end + +module Temporalio + module Api + module Enums + module V1 + UpdateWorkflowExecutionLifecycleStage = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage").enummodule + end + end + end +end diff --git a/lib/gen/temporal/api/enums/v1/workflow_pb.rb b/lib/gen/temporal/api/enums/v1/workflow_pb.rb index e65f3cd9..e640dc9c 100644 --- a/lib/gen/temporal/api/enums/v1/workflow_pb.rb +++ b/lib/gen/temporal/api/enums/v1/workflow_pb.rb @@ -10,6 +10,7 @@ value :WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, 1 value :WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY, 2 value :WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE, 3 + value :WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING, 4 end add_enum "temporal.api.enums.v1.ParentClosePolicy" do value :PARENT_CLOSE_POLICY_UNSPECIFIED, 0 @@ -39,6 +40,11 @@ value :PENDING_ACTIVITY_STATE_STARTED, 2 value :PENDING_ACTIVITY_STATE_CANCEL_REQUESTED, 3 end + add_enum "temporal.api.enums.v1.PendingWorkflowTaskState" do + value :PENDING_WORKFLOW_TASK_STATE_UNSPECIFIED, 0 + value :PENDING_WORKFLOW_TASK_STATE_SCHEDULED, 1 + value :PENDING_WORKFLOW_TASK_STATE_STARTED, 2 + end add_enum "temporal.api.enums.v1.HistoryEventFilterType" do value :HISTORY_EVENT_FILTER_TYPE_UNSPECIFIED, 0 value :HISTORY_EVENT_FILTER_TYPE_ALL_EVENT, 1 @@ -64,7 +70,7 @@ end end -module Temporal +module Temporalio module Api module Enums module V1 @@ -73,6 +79,7 @@ module V1 ContinueAsNewInitiator = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.enums.v1.ContinueAsNewInitiator").enummodule WorkflowExecutionStatus = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.enums.v1.WorkflowExecutionStatus").enummodule PendingActivityState = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.enums.v1.PendingActivityState").enummodule + PendingWorkflowTaskState = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.enums.v1.PendingWorkflowTaskState").enummodule HistoryEventFilterType = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.enums.v1.HistoryEventFilterType").enummodule RetryState = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.enums.v1.RetryState").enummodule TimeoutType = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.enums.v1.TimeoutType").enummodule diff --git a/lib/gen/temporal/api/errordetails/v1/message_pb.rb b/lib/gen/temporal/api/errordetails/v1/message_pb.rb index 64cf7725..87c68595 100644 --- a/lib/gen/temporal/api/errordetails/v1/message_pb.rb +++ b/lib/gen/temporal/api/errordetails/v1/message_pb.rb @@ -3,6 +3,10 @@ require 'google/protobuf' +require 'temporal/api/common/v1/message_pb' +require 'temporal/api/enums/v1/failed_cause_pb' +require 'temporal/api/enums/v1/namespace_pb' + Google::Protobuf::DescriptorPool.generated_pool.build do add_file("temporal/api/errordetails/v1/message.proto", :syntax => :proto3) do add_message "temporal.api.errordetails.v1.NotFoundFailure" do @@ -18,6 +22,16 @@ optional :current_cluster, :string, 2 optional :active_cluster, :string, 3 end + add_message "temporal.api.errordetails.v1.NamespaceInvalidStateFailure" do + optional :namespace, :string, 1 + optional :state, :enum, 2, "temporal.api.enums.v1.NamespaceState" + repeated :allowed_states, :enum, 3, "temporal.api.enums.v1.NamespaceState" + end + add_message "temporal.api.errordetails.v1.NamespaceNotFoundFailure" do + optional :namespace, :string, 1 + end + add_message "temporal.api.errordetails.v1.NamespaceAlreadyExistsFailure" do + end add_message "temporal.api.errordetails.v1.ClientVersionNotSupportedFailure" do optional :client_version, :string, 1 optional :client_name, :string, 2 @@ -27,27 +41,43 @@ optional :server_version, :string, 1 optional :client_supported_server_versions, :string, 2 end - add_message "temporal.api.errordetails.v1.NamespaceAlreadyExistsFailure" do - end add_message "temporal.api.errordetails.v1.CancellationAlreadyRequestedFailure" do end add_message "temporal.api.errordetails.v1.QueryFailedFailure" do end + add_message "temporal.api.errordetails.v1.PermissionDeniedFailure" do + optional :reason, :string, 1 + end + add_message "temporal.api.errordetails.v1.ResourceExhaustedFailure" do + optional :cause, :enum, 1, "temporal.api.enums.v1.ResourceExhaustedCause" + end + add_message "temporal.api.errordetails.v1.SystemWorkflowFailure" do + optional :workflow_execution, :message, 1, "temporal.api.common.v1.WorkflowExecution" + optional :workflow_error, :string, 2 + end + add_message "temporal.api.errordetails.v1.WorkflowNotReadyFailure" do + end end end -module Temporal +module Temporalio module Api module ErrorDetails module V1 NotFoundFailure = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.errordetails.v1.NotFoundFailure").msgclass WorkflowExecutionAlreadyStartedFailure = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.errordetails.v1.WorkflowExecutionAlreadyStartedFailure").msgclass NamespaceNotActiveFailure = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.errordetails.v1.NamespaceNotActiveFailure").msgclass + NamespaceInvalidStateFailure = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.errordetails.v1.NamespaceInvalidStateFailure").msgclass + NamespaceNotFoundFailure = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.errordetails.v1.NamespaceNotFoundFailure").msgclass + NamespaceAlreadyExistsFailure = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.errordetails.v1.NamespaceAlreadyExistsFailure").msgclass ClientVersionNotSupportedFailure = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.errordetails.v1.ClientVersionNotSupportedFailure").msgclass ServerVersionNotSupportedFailure = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.errordetails.v1.ServerVersionNotSupportedFailure").msgclass - NamespaceAlreadyExistsFailure = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.errordetails.v1.NamespaceAlreadyExistsFailure").msgclass CancellationAlreadyRequestedFailure = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.errordetails.v1.CancellationAlreadyRequestedFailure").msgclass QueryFailedFailure = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.errordetails.v1.QueryFailedFailure").msgclass + PermissionDeniedFailure = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.errordetails.v1.PermissionDeniedFailure").msgclass + ResourceExhaustedFailure = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.errordetails.v1.ResourceExhaustedFailure").msgclass + SystemWorkflowFailure = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.errordetails.v1.SystemWorkflowFailure").msgclass + WorkflowNotReadyFailure = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.errordetails.v1.WorkflowNotReadyFailure").msgclass end end end diff --git a/lib/gen/temporal/api/failure/v1/message_pb.rb b/lib/gen/temporal/api/failure/v1/message_pb.rb index 757b4693..041653ff 100644 --- a/lib/gen/temporal/api/failure/v1/message_pb.rb +++ b/lib/gen/temporal/api/failure/v1/message_pb.rb @@ -5,6 +5,7 @@ require 'temporal/api/common/v1/message_pb' require 'temporal/api/enums/v1/workflow_pb' + Google::Protobuf::DescriptorPool.generated_pool.build do add_file("temporal/api/failure/v1/message.proto", :syntax => :proto3) do add_message "temporal.api.failure.v1.ApplicationFailureInfo" do @@ -47,6 +48,7 @@ optional :message, :string, 1 optional :source, :string, 2 optional :stack_trace, :string, 3 + optional :encoded_attributes, :message, 20, "temporal.api.common.v1.Payload" optional :cause, :message, 4, "temporal.api.failure.v1.Failure" oneof :failure_info do optional :application_failure_info, :message, 5, "temporal.api.failure.v1.ApplicationFailureInfo" @@ -62,7 +64,7 @@ end end -module Temporal +module Temporalio module Api module Failure module V1 diff --git a/lib/gen/temporal/api/filter/v1/message_pb.rb b/lib/gen/temporal/api/filter/v1/message_pb.rb index 5a440018..b38072ce 100644 --- a/lib/gen/temporal/api/filter/v1/message_pb.rb +++ b/lib/gen/temporal/api/filter/v1/message_pb.rb @@ -4,7 +4,9 @@ require 'google/protobuf' require 'google/protobuf/timestamp_pb' +require 'dependencies/gogoproto/gogo_pb' require 'temporal/api/enums/v1/workflow_pb' + Google::Protobuf::DescriptorPool.generated_pool.build do add_file("temporal/api/filter/v1/message.proto", :syntax => :proto3) do add_message "temporal.api.filter.v1.WorkflowExecutionFilter" do @@ -24,7 +26,7 @@ end end -module Temporal +module Temporalio module Api module Filter module V1 diff --git a/lib/gen/temporal/api/history/v1/message_pb.rb b/lib/gen/temporal/api/history/v1/message_pb.rb index bc3586ae..e2d6e2f4 100644 --- a/lib/gen/temporal/api/history/v1/message_pb.rb +++ b/lib/gen/temporal/api/history/v1/message_pb.rb @@ -5,18 +5,22 @@ require 'google/protobuf/duration_pb' require 'google/protobuf/timestamp_pb' +require 'dependencies/gogoproto/gogo_pb' require 'temporal/api/enums/v1/event_type_pb' require 'temporal/api/enums/v1/failed_cause_pb' require 'temporal/api/enums/v1/workflow_pb' require 'temporal/api/common/v1/message_pb' require 'temporal/api/failure/v1/message_pb' -require 'temporal/api/workflow/v1/message_pb' require 'temporal/api/taskqueue/v1/message_pb' +require 'temporal/api/update/v1/message_pb' +require 'temporal/api/workflow/v1/message_pb' + Google::Protobuf::DescriptorPool.generated_pool.build do add_file("temporal/api/history/v1/message.proto", :syntax => :proto3) do add_message "temporal.api.history.v1.WorkflowExecutionStartedEventAttributes" do optional :workflow_type, :message, 1, "temporal.api.common.v1.WorkflowType" optional :parent_workflow_namespace, :string, 2 + optional :parent_workflow_namespace_id, :string, 27 optional :parent_workflow_execution, :message, 3, "temporal.api.common.v1.WorkflowExecution" optional :parent_initiated_event_id, :int64, 4 optional :task_queue, :message, 5, "temporal.api.taskqueue.v1.TaskQueue" @@ -40,18 +44,22 @@ optional :search_attributes, :message, 23, "temporal.api.common.v1.SearchAttributes" optional :prev_auto_reset_points, :message, 24, "temporal.api.workflow.v1.ResetPoints" optional :header, :message, 25, "temporal.api.common.v1.Header" + optional :parent_initiated_event_version, :int64, 26 end add_message "temporal.api.history.v1.WorkflowExecutionCompletedEventAttributes" do optional :result, :message, 1, "temporal.api.common.v1.Payloads" optional :workflow_task_completed_event_id, :int64, 2 + optional :new_execution_run_id, :string, 3 end add_message "temporal.api.history.v1.WorkflowExecutionFailedEventAttributes" do optional :failure, :message, 1, "temporal.api.failure.v1.Failure" optional :retry_state, :enum, 2, "temporal.api.enums.v1.RetryState" optional :workflow_task_completed_event_id, :int64, 3 + optional :new_execution_run_id, :string, 4 end add_message "temporal.api.history.v1.WorkflowExecutionTimedOutEventAttributes" do optional :retry_state, :enum, 1, "temporal.api.enums.v1.RetryState" + optional :new_execution_run_id, :string, 2 end add_message "temporal.api.history.v1.WorkflowExecutionContinuedAsNewEventAttributes" do optional :new_execution_run_id, :string, 1 @@ -78,12 +86,15 @@ optional :scheduled_event_id, :int64, 1 optional :identity, :string, 2 optional :request_id, :string, 3 + optional :suggest_continue_as_new, :bool, 4 + optional :history_size_bytes, :int64, 5 end add_message "temporal.api.history.v1.WorkflowTaskCompletedEventAttributes" do optional :scheduled_event_id, :int64, 1 optional :started_event_id, :int64, 2 optional :identity, :string, 3 optional :binary_checksum, :string, 4 + optional :worker_versioning_id, :message, 5, "temporal.api.taskqueue.v1.VersionId" end add_message "temporal.api.history.v1.WorkflowTaskTimedOutEventAttributes" do optional :scheduled_event_id, :int64, 1 @@ -104,7 +115,6 @@ add_message "temporal.api.history.v1.ActivityTaskScheduledEventAttributes" do optional :activity_id, :string, 1 optional :activity_type, :message, 2, "temporal.api.common.v1.ActivityType" - optional :namespace, :string, 3 optional :task_queue, :message, 4, "temporal.api.taskqueue.v1.TaskQueue" optional :header, :message, 5, "temporal.api.common.v1.Header" optional :input, :message, 6, "temporal.api.common.v1.Payloads" @@ -188,6 +198,7 @@ optional :signal_name, :string, 1 optional :input, :message, 2, "temporal.api.common.v1.Payloads" optional :identity, :string, 3 + optional :header, :message, 4, "temporal.api.common.v1.Header" end add_message "temporal.api.history.v1.WorkflowExecutionTerminatedEventAttributes" do optional :reason, :string, 1 @@ -197,14 +208,17 @@ add_message "temporal.api.history.v1.RequestCancelExternalWorkflowExecutionInitiatedEventAttributes" do optional :workflow_task_completed_event_id, :int64, 1 optional :namespace, :string, 2 + optional :namespace_id, :string, 7 optional :workflow_execution, :message, 3, "temporal.api.common.v1.WorkflowExecution" optional :control, :string, 4 optional :child_workflow_only, :bool, 5 + optional :reason, :string, 6 end add_message "temporal.api.history.v1.RequestCancelExternalWorkflowExecutionFailedEventAttributes" do optional :cause, :enum, 1, "temporal.api.enums.v1.CancelExternalWorkflowExecutionFailedCause" optional :workflow_task_completed_event_id, :int64, 2 optional :namespace, :string, 3 + optional :namespace_id, :string, 7 optional :workflow_execution, :message, 4, "temporal.api.common.v1.WorkflowExecution" optional :initiated_event_id, :int64, 5 optional :control, :string, 6 @@ -212,21 +226,25 @@ add_message "temporal.api.history.v1.ExternalWorkflowExecutionCancelRequestedEventAttributes" do optional :initiated_event_id, :int64, 1 optional :namespace, :string, 2 + optional :namespace_id, :string, 4 optional :workflow_execution, :message, 3, "temporal.api.common.v1.WorkflowExecution" end add_message "temporal.api.history.v1.SignalExternalWorkflowExecutionInitiatedEventAttributes" do optional :workflow_task_completed_event_id, :int64, 1 optional :namespace, :string, 2 + optional :namespace_id, :string, 9 optional :workflow_execution, :message, 3, "temporal.api.common.v1.WorkflowExecution" optional :signal_name, :string, 4 optional :input, :message, 5, "temporal.api.common.v1.Payloads" optional :control, :string, 6 optional :child_workflow_only, :bool, 7 + optional :header, :message, 8, "temporal.api.common.v1.Header" end add_message "temporal.api.history.v1.SignalExternalWorkflowExecutionFailedEventAttributes" do optional :cause, :enum, 1, "temporal.api.enums.v1.SignalExternalWorkflowExecutionFailedCause" optional :workflow_task_completed_event_id, :int64, 2 optional :namespace, :string, 3 + optional :namespace_id, :string, 7 optional :workflow_execution, :message, 4, "temporal.api.common.v1.WorkflowExecution" optional :initiated_event_id, :int64, 5 optional :control, :string, 6 @@ -234,6 +252,7 @@ add_message "temporal.api.history.v1.ExternalWorkflowExecutionSignaledEventAttributes" do optional :initiated_event_id, :int64, 1 optional :namespace, :string, 2 + optional :namespace_id, :string, 5 optional :workflow_execution, :message, 3, "temporal.api.common.v1.WorkflowExecution" optional :control, :string, 4 end @@ -241,8 +260,13 @@ optional :workflow_task_completed_event_id, :int64, 1 optional :search_attributes, :message, 2, "temporal.api.common.v1.SearchAttributes" end + add_message "temporal.api.history.v1.WorkflowPropertiesModifiedEventAttributes" do + optional :workflow_task_completed_event_id, :int64, 1 + optional :upserted_memo, :message, 2, "temporal.api.common.v1.Memo" + end add_message "temporal.api.history.v1.StartChildWorkflowExecutionInitiatedEventAttributes" do optional :namespace, :string, 1 + optional :namespace_id, :string, 18 optional :workflow_id, :string, 2 optional :workflow_type, :message, 3, "temporal.api.common.v1.WorkflowType" optional :task_queue, :message, 4, "temporal.api.taskqueue.v1.TaskQueue" @@ -262,6 +286,7 @@ end add_message "temporal.api.history.v1.StartChildWorkflowExecutionFailedEventAttributes" do optional :namespace, :string, 1 + optional :namespace_id, :string, 8 optional :workflow_id, :string, 2 optional :workflow_type, :message, 3, "temporal.api.common.v1.WorkflowType" optional :cause, :enum, 4, "temporal.api.enums.v1.StartChildWorkflowExecutionFailedCause" @@ -271,6 +296,7 @@ end add_message "temporal.api.history.v1.ChildWorkflowExecutionStartedEventAttributes" do optional :namespace, :string, 1 + optional :namespace_id, :string, 6 optional :initiated_event_id, :int64, 2 optional :workflow_execution, :message, 3, "temporal.api.common.v1.WorkflowExecution" optional :workflow_type, :message, 4, "temporal.api.common.v1.WorkflowType" @@ -279,6 +305,7 @@ add_message "temporal.api.history.v1.ChildWorkflowExecutionCompletedEventAttributes" do optional :result, :message, 1, "temporal.api.common.v1.Payloads" optional :namespace, :string, 2 + optional :namespace_id, :string, 7 optional :workflow_execution, :message, 3, "temporal.api.common.v1.WorkflowExecution" optional :workflow_type, :message, 4, "temporal.api.common.v1.WorkflowType" optional :initiated_event_id, :int64, 5 @@ -287,6 +314,7 @@ add_message "temporal.api.history.v1.ChildWorkflowExecutionFailedEventAttributes" do optional :failure, :message, 1, "temporal.api.failure.v1.Failure" optional :namespace, :string, 2 + optional :namespace_id, :string, 8 optional :workflow_execution, :message, 3, "temporal.api.common.v1.WorkflowExecution" optional :workflow_type, :message, 4, "temporal.api.common.v1.WorkflowType" optional :initiated_event_id, :int64, 5 @@ -296,6 +324,7 @@ add_message "temporal.api.history.v1.ChildWorkflowExecutionCanceledEventAttributes" do optional :details, :message, 1, "temporal.api.common.v1.Payloads" optional :namespace, :string, 2 + optional :namespace_id, :string, 7 optional :workflow_execution, :message, 3, "temporal.api.common.v1.WorkflowExecution" optional :workflow_type, :message, 4, "temporal.api.common.v1.WorkflowType" optional :initiated_event_id, :int64, 5 @@ -303,6 +332,7 @@ end add_message "temporal.api.history.v1.ChildWorkflowExecutionTimedOutEventAttributes" do optional :namespace, :string, 1 + optional :namespace_id, :string, 7 optional :workflow_execution, :message, 2, "temporal.api.common.v1.WorkflowExecution" optional :workflow_type, :message, 3, "temporal.api.common.v1.WorkflowType" optional :initiated_event_id, :int64, 4 @@ -311,17 +341,47 @@ end add_message "temporal.api.history.v1.ChildWorkflowExecutionTerminatedEventAttributes" do optional :namespace, :string, 1 + optional :namespace_id, :string, 6 optional :workflow_execution, :message, 2, "temporal.api.common.v1.WorkflowExecution" optional :workflow_type, :message, 3, "temporal.api.common.v1.WorkflowType" optional :initiated_event_id, :int64, 4 optional :started_event_id, :int64, 5 end + add_message "temporal.api.history.v1.WorkflowPropertiesModifiedExternallyEventAttributes" do + optional :new_task_queue, :string, 1 + optional :new_workflow_task_timeout, :message, 2, "google.protobuf.Duration" + optional :new_workflow_run_timeout, :message, 3, "google.protobuf.Duration" + optional :new_workflow_execution_timeout, :message, 4, "google.protobuf.Duration" + optional :upserted_memo, :message, 5, "temporal.api.common.v1.Memo" + end + add_message "temporal.api.history.v1.ActivityPropertiesModifiedExternallyEventAttributes" do + optional :scheduled_event_id, :int64, 1 + optional :new_retry_policy, :message, 2, "temporal.api.common.v1.RetryPolicy" + end + add_message "temporal.api.history.v1.WorkflowExecutionUpdateAcceptedEventAttributes" do + optional :protocol_instance_id, :string, 1 + optional :accepted_request_message_id, :string, 2 + optional :accepted_request_sequencing_event_id, :int64, 3 + optional :accepted_request, :message, 4, "temporal.api.update.v1.Request" + end + add_message "temporal.api.history.v1.WorkflowExecutionUpdateCompletedEventAttributes" do + optional :meta, :message, 1, "temporal.api.update.v1.Meta" + optional :outcome, :message, 2, "temporal.api.update.v1.Outcome" + end + add_message "temporal.api.history.v1.WorkflowExecutionUpdateRejectedEventAttributes" do + optional :protocol_instance_id, :string, 1 + optional :rejected_request_message_id, :string, 2 + optional :rejected_request_sequencing_event_id, :int64, 3 + optional :rejected_request, :message, 4, "temporal.api.update.v1.Request" + optional :failure, :message, 5, "temporal.api.failure.v1.Failure" + end add_message "temporal.api.history.v1.HistoryEvent" do optional :event_id, :int64, 1 optional :event_time, :message, 2, "google.protobuf.Timestamp" optional :event_type, :enum, 3, "temporal.api.enums.v1.EventType" optional :version, :int64, 4 optional :task_id, :int64, 5 + optional :worker_may_ignore, :bool, 300 oneof :attributes do optional :workflow_execution_started_event_attributes, :message, 6, "temporal.api.history.v1.WorkflowExecutionStartedEventAttributes" optional :workflow_execution_completed_event_attributes, :message, 7, "temporal.api.history.v1.WorkflowExecutionCompletedEventAttributes" @@ -363,6 +423,12 @@ optional :signal_external_workflow_execution_failed_event_attributes, :message, 43, "temporal.api.history.v1.SignalExternalWorkflowExecutionFailedEventAttributes" optional :external_workflow_execution_signaled_event_attributes, :message, 44, "temporal.api.history.v1.ExternalWorkflowExecutionSignaledEventAttributes" optional :upsert_workflow_search_attributes_event_attributes, :message, 45, "temporal.api.history.v1.UpsertWorkflowSearchAttributesEventAttributes" + optional :workflow_execution_update_accepted_event_attributes, :message, 46, "temporal.api.history.v1.WorkflowExecutionUpdateAcceptedEventAttributes" + optional :workflow_execution_update_rejected_event_attributes, :message, 47, "temporal.api.history.v1.WorkflowExecutionUpdateRejectedEventAttributes" + optional :workflow_execution_update_completed_event_attributes, :message, 48, "temporal.api.history.v1.WorkflowExecutionUpdateCompletedEventAttributes" + optional :workflow_properties_modified_externally_event_attributes, :message, 49, "temporal.api.history.v1.WorkflowPropertiesModifiedExternallyEventAttributes" + optional :activity_properties_modified_externally_event_attributes, :message, 50, "temporal.api.history.v1.ActivityPropertiesModifiedExternallyEventAttributes" + optional :workflow_properties_modified_event_attributes, :message, 51, "temporal.api.history.v1.WorkflowPropertiesModifiedEventAttributes" end end add_message "temporal.api.history.v1.History" do @@ -371,7 +437,7 @@ end end -module Temporal +module Temporalio module Api module History module V1 @@ -407,6 +473,7 @@ module V1 SignalExternalWorkflowExecutionFailedEventAttributes = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.history.v1.SignalExternalWorkflowExecutionFailedEventAttributes").msgclass ExternalWorkflowExecutionSignaledEventAttributes = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.history.v1.ExternalWorkflowExecutionSignaledEventAttributes").msgclass UpsertWorkflowSearchAttributesEventAttributes = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.history.v1.UpsertWorkflowSearchAttributesEventAttributes").msgclass + WorkflowPropertiesModifiedEventAttributes = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.history.v1.WorkflowPropertiesModifiedEventAttributes").msgclass StartChildWorkflowExecutionInitiatedEventAttributes = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.history.v1.StartChildWorkflowExecutionInitiatedEventAttributes").msgclass StartChildWorkflowExecutionFailedEventAttributes = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.history.v1.StartChildWorkflowExecutionFailedEventAttributes").msgclass ChildWorkflowExecutionStartedEventAttributes = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.history.v1.ChildWorkflowExecutionStartedEventAttributes").msgclass @@ -415,6 +482,11 @@ module V1 ChildWorkflowExecutionCanceledEventAttributes = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.history.v1.ChildWorkflowExecutionCanceledEventAttributes").msgclass ChildWorkflowExecutionTimedOutEventAttributes = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.history.v1.ChildWorkflowExecutionTimedOutEventAttributes").msgclass ChildWorkflowExecutionTerminatedEventAttributes = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.history.v1.ChildWorkflowExecutionTerminatedEventAttributes").msgclass + WorkflowPropertiesModifiedExternallyEventAttributes = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.history.v1.WorkflowPropertiesModifiedExternallyEventAttributes").msgclass + ActivityPropertiesModifiedExternallyEventAttributes = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.history.v1.ActivityPropertiesModifiedExternallyEventAttributes").msgclass + WorkflowExecutionUpdateAcceptedEventAttributes = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.history.v1.WorkflowExecutionUpdateAcceptedEventAttributes").msgclass + WorkflowExecutionUpdateCompletedEventAttributes = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.history.v1.WorkflowExecutionUpdateCompletedEventAttributes").msgclass + WorkflowExecutionUpdateRejectedEventAttributes = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.history.v1.WorkflowExecutionUpdateRejectedEventAttributes").msgclass HistoryEvent = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.history.v1.HistoryEvent").msgclass History = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.history.v1.History").msgclass end diff --git a/lib/gen/temporal/api/namespace/v1/message_pb.rb b/lib/gen/temporal/api/namespace/v1/message_pb.rb index cea6262d..3042febe 100644 --- a/lib/gen/temporal/api/namespace/v1/message_pb.rb +++ b/lib/gen/temporal/api/namespace/v1/message_pb.rb @@ -5,7 +5,9 @@ require 'google/protobuf/duration_pb' require 'google/protobuf/timestamp_pb' +require 'dependencies/gogoproto/gogo_pb' require 'temporal/api/enums/v1/namespace_pb' + Google::Protobuf::DescriptorPool.generated_pool.build do add_file("temporal/api/namespace/v1/message.proto", :syntax => :proto3) do add_message "temporal.api.namespace.v1.NamespaceInfo" do @@ -15,6 +17,7 @@ optional :owner_email, :string, 4 map :data, :string, :string, 5 optional :id, :string, 6 + optional :supports_schedules, :bool, 100 end add_message "temporal.api.namespace.v1.NamespaceConfig" do optional :workflow_execution_retention_ttl, :message, 1, "google.protobuf.Duration" @@ -23,6 +26,7 @@ optional :history_archival_uri, :string, 4 optional :visibility_archival_state, :enum, 5, "temporal.api.enums.v1.ArchivalState" optional :visibility_archival_uri, :string, 6 + map :custom_search_attribute_aliases, :string, :string, 7 end add_message "temporal.api.namespace.v1.BadBinaries" do map :binaries, :string, :message, 1, "temporal.api.namespace.v1.BadBinaryInfo" @@ -36,11 +40,15 @@ optional :description, :string, 1 optional :owner_email, :string, 2 map :data, :string, :string, 3 + optional :state, :enum, 4, "temporal.api.enums.v1.NamespaceState" + end + add_message "temporal.api.namespace.v1.NamespaceFilter" do + optional :include_deleted, :bool, 1 end end end -module Temporal +module Temporalio module Api module Namespace module V1 @@ -49,6 +57,7 @@ module V1 BadBinaries = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.namespace.v1.BadBinaries").msgclass BadBinaryInfo = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.namespace.v1.BadBinaryInfo").msgclass UpdateNamespaceInfo = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.namespace.v1.UpdateNamespaceInfo").msgclass + NamespaceFilter = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.namespace.v1.NamespaceFilter").msgclass end end end diff --git a/lib/gen/temporal/api/operatorservice/v1/request_response_pb.rb b/lib/gen/temporal/api/operatorservice/v1/request_response_pb.rb new file mode 100644 index 00000000..f85cb8ee --- /dev/null +++ b/lib/gen/temporal/api/operatorservice/v1/request_response_pb.rb @@ -0,0 +1,88 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporal/api/operatorservice/v1/request_response.proto + +require 'google/protobuf' + +require 'temporal/api/enums/v1/common_pb' + +Google::Protobuf::DescriptorPool.generated_pool.build do + add_file("temporal/api/operatorservice/v1/request_response.proto", :syntax => :proto3) do + add_message "temporal.api.operatorservice.v1.AddSearchAttributesRequest" do + map :search_attributes, :string, :enum, 1, "temporal.api.enums.v1.IndexedValueType" + optional :namespace, :string, 2 + end + add_message "temporal.api.operatorservice.v1.AddSearchAttributesResponse" do + end + add_message "temporal.api.operatorservice.v1.RemoveSearchAttributesRequest" do + repeated :search_attributes, :string, 1 + optional :namespace, :string, 2 + end + add_message "temporal.api.operatorservice.v1.RemoveSearchAttributesResponse" do + end + add_message "temporal.api.operatorservice.v1.ListSearchAttributesRequest" do + optional :namespace, :string, 1 + end + add_message "temporal.api.operatorservice.v1.ListSearchAttributesResponse" do + map :custom_attributes, :string, :enum, 1, "temporal.api.enums.v1.IndexedValueType" + map :system_attributes, :string, :enum, 2, "temporal.api.enums.v1.IndexedValueType" + map :storage_schema, :string, :string, 3 + end + add_message "temporal.api.operatorservice.v1.DeleteNamespaceRequest" do + optional :namespace, :string, 1 + end + add_message "temporal.api.operatorservice.v1.DeleteNamespaceResponse" do + optional :deleted_namespace, :string, 1 + end + add_message "temporal.api.operatorservice.v1.AddOrUpdateRemoteClusterRequest" do + optional :frontend_address, :string, 1 + optional :enable_remote_cluster_connection, :bool, 2 + end + add_message "temporal.api.operatorservice.v1.AddOrUpdateRemoteClusterResponse" do + end + add_message "temporal.api.operatorservice.v1.RemoveRemoteClusterRequest" do + optional :cluster_name, :string, 1 + end + add_message "temporal.api.operatorservice.v1.RemoveRemoteClusterResponse" do + end + add_message "temporal.api.operatorservice.v1.ListClustersRequest" do + optional :page_size, :int32, 1 + optional :next_page_token, :bytes, 2 + end + add_message "temporal.api.operatorservice.v1.ListClustersResponse" do + repeated :clusters, :message, 1, "temporal.api.operatorservice.v1.ClusterMetadata" + optional :next_page_token, :bytes, 4 + end + add_message "temporal.api.operatorservice.v1.ClusterMetadata" do + optional :cluster_name, :string, 1 + optional :cluster_id, :string, 2 + optional :address, :string, 3 + optional :initial_failover_version, :int64, 4 + optional :history_shard_count, :int32, 5 + optional :is_connection_enabled, :bool, 6 + end + end +end + +module Temporalio + module Api + module OperatorService + module V1 + AddSearchAttributesRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.operatorservice.v1.AddSearchAttributesRequest").msgclass + AddSearchAttributesResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.operatorservice.v1.AddSearchAttributesResponse").msgclass + RemoveSearchAttributesRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.operatorservice.v1.RemoveSearchAttributesRequest").msgclass + RemoveSearchAttributesResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.operatorservice.v1.RemoveSearchAttributesResponse").msgclass + ListSearchAttributesRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.operatorservice.v1.ListSearchAttributesRequest").msgclass + ListSearchAttributesResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.operatorservice.v1.ListSearchAttributesResponse").msgclass + DeleteNamespaceRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.operatorservice.v1.DeleteNamespaceRequest").msgclass + DeleteNamespaceResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.operatorservice.v1.DeleteNamespaceResponse").msgclass + AddOrUpdateRemoteClusterRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.operatorservice.v1.AddOrUpdateRemoteClusterRequest").msgclass + AddOrUpdateRemoteClusterResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.operatorservice.v1.AddOrUpdateRemoteClusterResponse").msgclass + RemoveRemoteClusterRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.operatorservice.v1.RemoveRemoteClusterRequest").msgclass + RemoveRemoteClusterResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.operatorservice.v1.RemoveRemoteClusterResponse").msgclass + ListClustersRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.operatorservice.v1.ListClustersRequest").msgclass + ListClustersResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.operatorservice.v1.ListClustersResponse").msgclass + ClusterMetadata = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.operatorservice.v1.ClusterMetadata").msgclass + end + end + end +end diff --git a/lib/gen/temporal/api/operatorservice/v1/service_pb.rb b/lib/gen/temporal/api/operatorservice/v1/service_pb.rb new file mode 100644 index 00000000..515e36b1 --- /dev/null +++ b/lib/gen/temporal/api/operatorservice/v1/service_pb.rb @@ -0,0 +1,20 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporal/api/operatorservice/v1/service.proto + +require 'google/protobuf' + +require 'temporal/api/operatorservice/v1/request_response_pb' + +Google::Protobuf::DescriptorPool.generated_pool.build do + add_file("temporal/api/operatorservice/v1/service.proto", :syntax => :proto3) do + end +end + +module Temporalio + module Api + module OperatorService + module V1 + end + end + end +end diff --git a/lib/gen/temporal/api/operatorservice/v1/service_services_pb.rb b/lib/gen/temporal/api/operatorservice/v1/service_services_pb.rb new file mode 100644 index 00000000..44a8c571 --- /dev/null +++ b/lib/gen/temporal/api/operatorservice/v1/service_services_pb.rb @@ -0,0 +1,78 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# Source: temporal/api/operatorservice/v1/service.proto for package 'Temporalio.Api.OperatorService.V1' +# Original file comments: +# The MIT License +# +# Copyright (c) 2020 Temporal Technologies Inc. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +require 'grpc' +require 'temporal/api/operatorservice/v1/service_pb' + +module Temporalio + module Api + module OperatorService + module V1 + module OperatorService + # OperatorService API defines how Temporal SDKs and other clients interact with the Temporal server + # to perform administrative functions like registering a search attribute or a namespace. + # APIs in this file could be not compatible with Temporal Cloud, hence it's usage in SDKs should be limited by + # designated APIs that clearly state that they shouldn't be used by the main Application (Workflows & Activities) framework. + class Service + + include ::GRPC::GenericService + + self.marshal_class_method = :encode + self.unmarshal_class_method = :decode + self.service_name = 'temporal.api.operatorservice.v1.OperatorService' + + # AddSearchAttributes add custom search attributes. + # + # Returns ALREADY_EXISTS status code if a Search Attribute with any of the specified names already exists + # Returns INTERNAL status code with temporal.api.errordetails.v1.SystemWorkflowFailure in Error Details if registration process fails, + rpc :AddSearchAttributes, ::Temporalio::Api::OperatorService::V1::AddSearchAttributesRequest, ::Temporalio::Api::OperatorService::V1::AddSearchAttributesResponse + # RemoveSearchAttributes removes custom search attributes. + # + # Returns NOT_FOUND status code if a Search Attribute with any of the specified names is not registered + rpc :RemoveSearchAttributes, ::Temporalio::Api::OperatorService::V1::RemoveSearchAttributesRequest, ::Temporalio::Api::OperatorService::V1::RemoveSearchAttributesResponse + # ListSearchAttributes returns comprehensive information about search attributes. + rpc :ListSearchAttributes, ::Temporalio::Api::OperatorService::V1::ListSearchAttributesRequest, ::Temporalio::Api::OperatorService::V1::ListSearchAttributesResponse + # DeleteNamespace synchronously deletes a namespace and asynchronously reclaims all namespace resources. + # (-- api-linter: core::0135::method-signature=disabled + # aip.dev/not-precedent: DeleteNamespace RPC doesn't follow Google API format. --) + # (-- api-linter: core::0135::response-message-name=disabled + # aip.dev/not-precedent: DeleteNamespace RPC doesn't follow Google API format. --) + rpc :DeleteNamespace, ::Temporalio::Api::OperatorService::V1::DeleteNamespaceRequest, ::Temporalio::Api::OperatorService::V1::DeleteNamespaceResponse + # AddOrUpdateRemoteCluster adds or updates remote cluster. + rpc :AddOrUpdateRemoteCluster, ::Temporalio::Api::OperatorService::V1::AddOrUpdateRemoteClusterRequest, ::Temporalio::Api::OperatorService::V1::AddOrUpdateRemoteClusterResponse + # RemoveRemoteCluster removes remote cluster. + rpc :RemoveRemoteCluster, ::Temporalio::Api::OperatorService::V1::RemoveRemoteClusterRequest, ::Temporalio::Api::OperatorService::V1::RemoveRemoteClusterResponse + # ListClusters returns information about Temporal clusters. + rpc :ListClusters, ::Temporalio::Api::OperatorService::V1::ListClustersRequest, ::Temporalio::Api::OperatorService::V1::ListClustersResponse + end + + Stub = Service.rpc_stub_class + end + # (-- Search Attribute --) + end + end + end +end diff --git a/lib/gen/temporal/api/protocol/v1/message_pb.rb b/lib/gen/temporal/api/protocol/v1/message_pb.rb new file mode 100644 index 00000000..577b0c11 --- /dev/null +++ b/lib/gen/temporal/api/protocol/v1/message_pb.rb @@ -0,0 +1,30 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporal/api/protocol/v1/message.proto + +require 'google/protobuf' + +require 'google/protobuf/any_pb' + +Google::Protobuf::DescriptorPool.generated_pool.build do + add_file("temporal/api/protocol/v1/message.proto", :syntax => :proto3) do + add_message "temporal.api.protocol.v1.Message" do + optional :id, :string, 1 + optional :protocol_instance_id, :string, 2 + optional :body, :message, 5, "google.protobuf.Any" + oneof :sequencing_id do + optional :event_id, :int64, 3 + optional :command_index, :int64, 4 + end + end + end +end + +module Temporalio + module Api + module Protocol + module V1 + Message = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.protocol.v1.Message").msgclass + end + end + end +end diff --git a/lib/gen/temporal/api/query/v1/message_pb.rb b/lib/gen/temporal/api/query/v1/message_pb.rb index b3848ce5..652b77c1 100644 --- a/lib/gen/temporal/api/query/v1/message_pb.rb +++ b/lib/gen/temporal/api/query/v1/message_pb.rb @@ -6,11 +6,13 @@ require 'temporal/api/enums/v1/query_pb' require 'temporal/api/enums/v1/workflow_pb' require 'temporal/api/common/v1/message_pb' + Google::Protobuf::DescriptorPool.generated_pool.build do add_file("temporal/api/query/v1/message.proto", :syntax => :proto3) do add_message "temporal.api.query.v1.WorkflowQuery" do optional :query_type, :string, 1 optional :query_args, :message, 2, "temporal.api.common.v1.Payloads" + optional :header, :message, 3, "temporal.api.common.v1.Header" end add_message "temporal.api.query.v1.WorkflowQueryResult" do optional :result_type, :enum, 1, "temporal.api.enums.v1.QueryResultType" @@ -23,7 +25,7 @@ end end -module Temporal +module Temporalio module Api module Query module V1 diff --git a/lib/gen/temporal/api/replication/v1/message_pb.rb b/lib/gen/temporal/api/replication/v1/message_pb.rb index 6e964bc2..5336b7de 100644 --- a/lib/gen/temporal/api/replication/v1/message_pb.rb +++ b/lib/gen/temporal/api/replication/v1/message_pb.rb @@ -3,6 +3,10 @@ require 'google/protobuf' +require 'google/protobuf/timestamp_pb' +require 'dependencies/gogoproto/gogo_pb' +require 'temporal/api/enums/v1/namespace_pb' + Google::Protobuf::DescriptorPool.generated_pool.build do add_file("temporal/api/replication/v1/message.proto", :syntax => :proto3) do add_message "temporal.api.replication.v1.ClusterReplicationConfig" do @@ -11,16 +15,22 @@ add_message "temporal.api.replication.v1.NamespaceReplicationConfig" do optional :active_cluster_name, :string, 1 repeated :clusters, :message, 2, "temporal.api.replication.v1.ClusterReplicationConfig" + optional :state, :enum, 3, "temporal.api.enums.v1.ReplicationState" + end + add_message "temporal.api.replication.v1.FailoverStatus" do + optional :failover_time, :message, 1, "google.protobuf.Timestamp" + optional :failover_version, :int64, 2 end end end -module Temporal +module Temporalio module Api module Replication module V1 ClusterReplicationConfig = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.replication.v1.ClusterReplicationConfig").msgclass NamespaceReplicationConfig = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.replication.v1.NamespaceReplicationConfig").msgclass + FailoverStatus = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.replication.v1.FailoverStatus").msgclass end end end diff --git a/lib/gen/temporal/api/schedule/v1/message_pb.rb b/lib/gen/temporal/api/schedule/v1/message_pb.rb new file mode 100644 index 00000000..1d2f5383 --- /dev/null +++ b/lib/gen/temporal/api/schedule/v1/message_pb.rb @@ -0,0 +1,149 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporal/api/schedule/v1/message.proto + +require 'google/protobuf' + +require 'google/protobuf/duration_pb' +require 'google/protobuf/timestamp_pb' +require 'dependencies/gogoproto/gogo_pb' +require 'temporal/api/common/v1/message_pb' +require 'temporal/api/enums/v1/schedule_pb' +require 'temporal/api/workflow/v1/message_pb' + +Google::Protobuf::DescriptorPool.generated_pool.build do + add_file("temporal/api/schedule/v1/message.proto", :syntax => :proto3) do + add_message "temporal.api.schedule.v1.CalendarSpec" do + optional :second, :string, 1 + optional :minute, :string, 2 + optional :hour, :string, 3 + optional :day_of_month, :string, 4 + optional :month, :string, 5 + optional :year, :string, 6 + optional :day_of_week, :string, 7 + optional :comment, :string, 8 + end + add_message "temporal.api.schedule.v1.Range" do + optional :start, :int32, 1 + optional :end, :int32, 2 + optional :step, :int32, 3 + end + add_message "temporal.api.schedule.v1.StructuredCalendarSpec" do + repeated :second, :message, 1, "temporal.api.schedule.v1.Range" + repeated :minute, :message, 2, "temporal.api.schedule.v1.Range" + repeated :hour, :message, 3, "temporal.api.schedule.v1.Range" + repeated :day_of_month, :message, 4, "temporal.api.schedule.v1.Range" + repeated :month, :message, 5, "temporal.api.schedule.v1.Range" + repeated :year, :message, 6, "temporal.api.schedule.v1.Range" + repeated :day_of_week, :message, 7, "temporal.api.schedule.v1.Range" + optional :comment, :string, 8 + end + add_message "temporal.api.schedule.v1.IntervalSpec" do + optional :interval, :message, 1, "google.protobuf.Duration" + optional :phase, :message, 2, "google.protobuf.Duration" + end + add_message "temporal.api.schedule.v1.ScheduleSpec" do + repeated :structured_calendar, :message, 7, "temporal.api.schedule.v1.StructuredCalendarSpec" + repeated :cron_string, :string, 8 + repeated :calendar, :message, 1, "temporal.api.schedule.v1.CalendarSpec" + repeated :interval, :message, 2, "temporal.api.schedule.v1.IntervalSpec" + repeated :exclude_calendar, :message, 3, "temporal.api.schedule.v1.CalendarSpec" + repeated :exclude_structured_calendar, :message, 9, "temporal.api.schedule.v1.StructuredCalendarSpec" + optional :start_time, :message, 4, "google.protobuf.Timestamp" + optional :end_time, :message, 5, "google.protobuf.Timestamp" + optional :jitter, :message, 6, "google.protobuf.Duration" + optional :timezone_name, :string, 10 + optional :timezone_data, :bytes, 11 + end + add_message "temporal.api.schedule.v1.SchedulePolicies" do + optional :overlap_policy, :enum, 1, "temporal.api.enums.v1.ScheduleOverlapPolicy" + optional :catchup_window, :message, 2, "google.protobuf.Duration" + optional :pause_on_failure, :bool, 3 + end + add_message "temporal.api.schedule.v1.ScheduleAction" do + oneof :action do + optional :start_workflow, :message, 1, "temporal.api.workflow.v1.NewWorkflowExecutionInfo" + end + end + add_message "temporal.api.schedule.v1.ScheduleActionResult" do + optional :schedule_time, :message, 1, "google.protobuf.Timestamp" + optional :actual_time, :message, 2, "google.protobuf.Timestamp" + optional :start_workflow_result, :message, 11, "temporal.api.common.v1.WorkflowExecution" + end + add_message "temporal.api.schedule.v1.ScheduleState" do + optional :notes, :string, 1 + optional :paused, :bool, 2 + optional :limited_actions, :bool, 3 + optional :remaining_actions, :int64, 4 + end + add_message "temporal.api.schedule.v1.TriggerImmediatelyRequest" do + optional :overlap_policy, :enum, 1, "temporal.api.enums.v1.ScheduleOverlapPolicy" + end + add_message "temporal.api.schedule.v1.BackfillRequest" do + optional :start_time, :message, 1, "google.protobuf.Timestamp" + optional :end_time, :message, 2, "google.protobuf.Timestamp" + optional :overlap_policy, :enum, 3, "temporal.api.enums.v1.ScheduleOverlapPolicy" + end + add_message "temporal.api.schedule.v1.SchedulePatch" do + optional :trigger_immediately, :message, 1, "temporal.api.schedule.v1.TriggerImmediatelyRequest" + repeated :backfill_request, :message, 2, "temporal.api.schedule.v1.BackfillRequest" + optional :pause, :string, 3 + optional :unpause, :string, 4 + end + add_message "temporal.api.schedule.v1.ScheduleInfo" do + optional :action_count, :int64, 1 + optional :missed_catchup_window, :int64, 2 + optional :overlap_skipped, :int64, 3 + repeated :running_workflows, :message, 9, "temporal.api.common.v1.WorkflowExecution" + repeated :recent_actions, :message, 4, "temporal.api.schedule.v1.ScheduleActionResult" + repeated :future_action_times, :message, 5, "google.protobuf.Timestamp" + optional :create_time, :message, 6, "google.protobuf.Timestamp" + optional :update_time, :message, 7, "google.protobuf.Timestamp" + optional :invalid_schedule_error, :string, 8 + end + add_message "temporal.api.schedule.v1.Schedule" do + optional :spec, :message, 1, "temporal.api.schedule.v1.ScheduleSpec" + optional :action, :message, 2, "temporal.api.schedule.v1.ScheduleAction" + optional :policies, :message, 3, "temporal.api.schedule.v1.SchedulePolicies" + optional :state, :message, 4, "temporal.api.schedule.v1.ScheduleState" + end + add_message "temporal.api.schedule.v1.ScheduleListInfo" do + optional :spec, :message, 1, "temporal.api.schedule.v1.ScheduleSpec" + optional :workflow_type, :message, 2, "temporal.api.common.v1.WorkflowType" + optional :notes, :string, 3 + optional :paused, :bool, 4 + repeated :recent_actions, :message, 5, "temporal.api.schedule.v1.ScheduleActionResult" + repeated :future_action_times, :message, 6, "google.protobuf.Timestamp" + end + add_message "temporal.api.schedule.v1.ScheduleListEntry" do + optional :schedule_id, :string, 1 + optional :memo, :message, 2, "temporal.api.common.v1.Memo" + optional :search_attributes, :message, 3, "temporal.api.common.v1.SearchAttributes" + optional :info, :message, 4, "temporal.api.schedule.v1.ScheduleListInfo" + end + end +end + +module Temporalio + module Api + module Schedule + module V1 + CalendarSpec = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.schedule.v1.CalendarSpec").msgclass + Range = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.schedule.v1.Range").msgclass + StructuredCalendarSpec = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.schedule.v1.StructuredCalendarSpec").msgclass + IntervalSpec = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.schedule.v1.IntervalSpec").msgclass + ScheduleSpec = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.schedule.v1.ScheduleSpec").msgclass + SchedulePolicies = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.schedule.v1.SchedulePolicies").msgclass + ScheduleAction = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.schedule.v1.ScheduleAction").msgclass + ScheduleActionResult = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.schedule.v1.ScheduleActionResult").msgclass + ScheduleState = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.schedule.v1.ScheduleState").msgclass + TriggerImmediatelyRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.schedule.v1.TriggerImmediatelyRequest").msgclass + BackfillRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.schedule.v1.BackfillRequest").msgclass + SchedulePatch = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.schedule.v1.SchedulePatch").msgclass + ScheduleInfo = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.schedule.v1.ScheduleInfo").msgclass + Schedule = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.schedule.v1.Schedule").msgclass + ScheduleListInfo = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.schedule.v1.ScheduleListInfo").msgclass + ScheduleListEntry = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.schedule.v1.ScheduleListEntry").msgclass + end + end + end +end diff --git a/lib/gen/temporal/api/taskqueue/v1/message_pb.rb b/lib/gen/temporal/api/taskqueue/v1/message_pb.rb index 839cd994..648e3e2b 100644 --- a/lib/gen/temporal/api/taskqueue/v1/message_pb.rb +++ b/lib/gen/temporal/api/taskqueue/v1/message_pb.rb @@ -6,7 +6,9 @@ require 'google/protobuf/duration_pb' require 'google/protobuf/timestamp_pb' require 'google/protobuf/wrappers_pb' +require 'dependencies/gogoproto/gogo_pb' require 'temporal/api/enums/v1/task_queue_pb' + Google::Protobuf::DescriptorPool.generated_pool.build do add_file("temporal/api/taskqueue/v1/message.proto", :syntax => :proto3) do add_message "temporal.api.taskqueue.v1.TaskQueue" do @@ -35,15 +37,24 @@ optional :last_access_time, :message, 1, "google.protobuf.Timestamp" optional :identity, :string, 2 optional :rate_per_second, :double, 3 + optional :worker_versioning_id, :message, 4, "temporal.api.taskqueue.v1.VersionId" end add_message "temporal.api.taskqueue.v1.StickyExecutionAttributes" do optional :worker_task_queue, :message, 1, "temporal.api.taskqueue.v1.TaskQueue" optional :schedule_to_start_timeout, :message, 2, "google.protobuf.Duration" end + add_message "temporal.api.taskqueue.v1.VersionIdNode" do + optional :version, :message, 1, "temporal.api.taskqueue.v1.VersionId" + optional :previous_compatible, :message, 2, "temporal.api.taskqueue.v1.VersionIdNode" + optional :previous_incompatible, :message, 3, "temporal.api.taskqueue.v1.VersionIdNode" + end + add_message "temporal.api.taskqueue.v1.VersionId" do + optional :worker_build_id, :string, 1 + end end end -module Temporal +module Temporalio module Api module TaskQueue module V1 @@ -54,6 +65,8 @@ module V1 TaskQueuePartitionMetadata = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.taskqueue.v1.TaskQueuePartitionMetadata").msgclass PollerInfo = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.taskqueue.v1.PollerInfo").msgclass StickyExecutionAttributes = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.taskqueue.v1.StickyExecutionAttributes").msgclass + VersionIdNode = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.taskqueue.v1.VersionIdNode").msgclass + VersionId = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.taskqueue.v1.VersionId").msgclass end end end diff --git a/lib/gen/temporal/api/update/v1/message_pb.rb b/lib/gen/temporal/api/update/v1/message_pb.rb new file mode 100644 index 00000000..f438bc27 --- /dev/null +++ b/lib/gen/temporal/api/update/v1/message_pb.rb @@ -0,0 +1,72 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporal/api/update/v1/message.proto + +require 'google/protobuf' + +require 'temporal/api/common/v1/message_pb' +require 'temporal/api/enums/v1/update_pb' +require 'temporal/api/failure/v1/message_pb' + +Google::Protobuf::DescriptorPool.generated_pool.build do + add_file("temporal/api/update/v1/message.proto", :syntax => :proto3) do + add_message "temporal.api.update.v1.WaitPolicy" do + optional :lifecycle_stage, :enum, 1, "temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage" + end + add_message "temporal.api.update.v1.UpdateRef" do + optional :workflow_execution, :message, 1, "temporal.api.common.v1.WorkflowExecution" + optional :update_id, :string, 2 + end + add_message "temporal.api.update.v1.Outcome" do + oneof :value do + optional :success, :message, 1, "temporal.api.common.v1.Payloads" + optional :failure, :message, 2, "temporal.api.failure.v1.Failure" + end + end + add_message "temporal.api.update.v1.Meta" do + optional :update_id, :string, 1 + optional :identity, :string, 2 + end + add_message "temporal.api.update.v1.Input" do + optional :header, :message, 1, "temporal.api.common.v1.Header" + optional :name, :string, 2 + optional :args, :message, 3, "temporal.api.common.v1.Payloads" + end + add_message "temporal.api.update.v1.Request" do + optional :meta, :message, 1, "temporal.api.update.v1.Meta" + optional :input, :message, 2, "temporal.api.update.v1.Input" + end + add_message "temporal.api.update.v1.Rejection" do + optional :rejected_request_message_id, :string, 1 + optional :rejected_request_sequencing_event_id, :int64, 2 + optional :rejected_request, :message, 3, "temporal.api.update.v1.Request" + optional :failure, :message, 4, "temporal.api.failure.v1.Failure" + end + add_message "temporal.api.update.v1.Acceptance" do + optional :accepted_request_message_id, :string, 1 + optional :accepted_request_sequencing_event_id, :int64, 2 + optional :accepted_request, :message, 3, "temporal.api.update.v1.Request" + end + add_message "temporal.api.update.v1.Response" do + optional :meta, :message, 1, "temporal.api.update.v1.Meta" + optional :outcome, :message, 2, "temporal.api.update.v1.Outcome" + end + end +end + +module Temporalio + module Api + module Update + module V1 + WaitPolicy = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.update.v1.WaitPolicy").msgclass + UpdateRef = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.update.v1.UpdateRef").msgclass + Outcome = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.update.v1.Outcome").msgclass + Meta = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.update.v1.Meta").msgclass + Input = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.update.v1.Input").msgclass + Request = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.update.v1.Request").msgclass + Rejection = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.update.v1.Rejection").msgclass + Acceptance = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.update.v1.Acceptance").msgclass + Response = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.update.v1.Response").msgclass + end + end + end +end diff --git a/lib/gen/temporal/api/version/v1/message_pb.rb b/lib/gen/temporal/api/version/v1/message_pb.rb index f166d07d..02302766 100644 --- a/lib/gen/temporal/api/version/v1/message_pb.rb +++ b/lib/gen/temporal/api/version/v1/message_pb.rb @@ -4,7 +4,9 @@ require 'google/protobuf' require 'google/protobuf/timestamp_pb' +require 'dependencies/gogoproto/gogo_pb' require 'temporal/api/enums/v1/common_pb' + Google::Protobuf::DescriptorPool.generated_pool.build do add_file("temporal/api/version/v1/message.proto", :syntax => :proto3) do add_message "temporal.api.version.v1.ReleaseInfo" do @@ -26,7 +28,7 @@ end end -module Temporal +module Temporalio module Api module Version module V1 diff --git a/lib/gen/temporal/api/workflow/v1/message_pb.rb b/lib/gen/temporal/api/workflow/v1/message_pb.rb index c02b89ca..f6405daa 100644 --- a/lib/gen/temporal/api/workflow/v1/message_pb.rb +++ b/lib/gen/temporal/api/workflow/v1/message_pb.rb @@ -5,10 +5,12 @@ require 'google/protobuf/duration_pb' require 'google/protobuf/timestamp_pb' +require 'dependencies/gogoproto/gogo_pb' require 'temporal/api/enums/v1/workflow_pb' require 'temporal/api/common/v1/message_pb' require 'temporal/api/failure/v1/message_pb' require 'temporal/api/taskqueue/v1/message_pb' + Google::Protobuf::DescriptorPool.generated_pool.build do add_file("temporal/api/workflow/v1/message.proto", :syntax => :proto3) do add_message "temporal.api.workflow.v1.WorkflowExecutionInfo" do @@ -25,6 +27,8 @@ optional :search_attributes, :message, 11, "temporal.api.common.v1.SearchAttributes" optional :auto_reset_points, :message, 12, "temporal.api.workflow.v1.ResetPoints" optional :task_queue, :string, 13 + optional :state_transition_count, :int64, 14 + optional :history_size_bytes, :int64, 15 end add_message "temporal.api.workflow.v1.WorkflowExecutionConfig" do optional :task_queue, :message, 1, "temporal.api.taskqueue.v1.TaskQueue" @@ -53,6 +57,13 @@ optional :initiated_id, :int64, 4 optional :parent_close_policy, :enum, 5, "temporal.api.enums.v1.ParentClosePolicy" end + add_message "temporal.api.workflow.v1.PendingWorkflowTaskInfo" do + optional :state, :enum, 1, "temporal.api.enums.v1.PendingWorkflowTaskState" + optional :scheduled_time, :message, 2, "google.protobuf.Timestamp" + optional :original_scheduled_time, :message, 3, "google.protobuf.Timestamp" + optional :started_time, :message, 4, "google.protobuf.Timestamp" + optional :attempt, :int32, 5 + end add_message "temporal.api.workflow.v1.ResetPoints" do repeated :points, :message, 1, "temporal.api.workflow.v1.ResetPointInfo" end @@ -64,10 +75,25 @@ optional :expire_time, :message, 5, "google.protobuf.Timestamp" optional :resettable, :bool, 6 end + add_message "temporal.api.workflow.v1.NewWorkflowExecutionInfo" do + optional :workflow_id, :string, 1 + optional :workflow_type, :message, 2, "temporal.api.common.v1.WorkflowType" + optional :task_queue, :message, 3, "temporal.api.taskqueue.v1.TaskQueue" + optional :input, :message, 4, "temporal.api.common.v1.Payloads" + optional :workflow_execution_timeout, :message, 5, "google.protobuf.Duration" + optional :workflow_run_timeout, :message, 6, "google.protobuf.Duration" + optional :workflow_task_timeout, :message, 7, "google.protobuf.Duration" + optional :workflow_id_reuse_policy, :enum, 8, "temporal.api.enums.v1.WorkflowIdReusePolicy" + optional :retry_policy, :message, 9, "temporal.api.common.v1.RetryPolicy" + optional :cron_schedule, :string, 10 + optional :memo, :message, 11, "temporal.api.common.v1.Memo" + optional :search_attributes, :message, 12, "temporal.api.common.v1.SearchAttributes" + optional :header, :message, 13, "temporal.api.common.v1.Header" + end end end -module Temporal +module Temporalio module Api module Workflow module V1 @@ -75,8 +101,10 @@ module V1 WorkflowExecutionConfig = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflow.v1.WorkflowExecutionConfig").msgclass PendingActivityInfo = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflow.v1.PendingActivityInfo").msgclass PendingChildExecutionInfo = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflow.v1.PendingChildExecutionInfo").msgclass + PendingWorkflowTaskInfo = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflow.v1.PendingWorkflowTaskInfo").msgclass ResetPoints = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflow.v1.ResetPoints").msgclass ResetPointInfo = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflow.v1.ResetPointInfo").msgclass + NewWorkflowExecutionInfo = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflow.v1.NewWorkflowExecutionInfo").msgclass end end end diff --git a/lib/gen/temporal/api/workflowservice/v1/request_response_pb.rb b/lib/gen/temporal/api/workflowservice/v1/request_response_pb.rb index 57a2f735..7d8d6446 100644 --- a/lib/gen/temporal/api/workflowservice/v1/request_response_pb.rb +++ b/lib/gen/temporal/api/workflowservice/v1/request_response_pb.rb @@ -3,11 +3,13 @@ require 'google/protobuf' +require 'temporal/api/enums/v1/batch_operation_pb' require 'temporal/api/enums/v1/workflow_pb' require 'temporal/api/enums/v1/namespace_pb' require 'temporal/api/enums/v1/failed_cause_pb' require 'temporal/api/enums/v1/common_pb' require 'temporal/api/enums/v1/query_pb' +require 'temporal/api/enums/v1/reset_pb' require 'temporal/api/enums/v1/task_queue_pb' require 'temporal/api/common/v1/message_pb' require 'temporal/api/history/v1/message_pb' @@ -15,13 +17,19 @@ require 'temporal/api/command/v1/message_pb' require 'temporal/api/failure/v1/message_pb' require 'temporal/api/filter/v1/message_pb' +require 'temporal/api/protocol/v1/message_pb' require 'temporal/api/namespace/v1/message_pb' require 'temporal/api/query/v1/message_pb' require 'temporal/api/replication/v1/message_pb' +require 'temporal/api/schedule/v1/message_pb' require 'temporal/api/taskqueue/v1/message_pb' +require 'temporal/api/update/v1/message_pb' require 'temporal/api/version/v1/message_pb' +require 'temporal/api/batch/v1/message_pb' require 'google/protobuf/duration_pb' require 'google/protobuf/timestamp_pb' +require 'dependencies/gogoproto/gogo_pb' + Google::Protobuf::DescriptorPool.generated_pool.build do add_file("temporal/api/workflowservice/v1/request_response.proto", :syntax => :proto3) do add_message "temporal.api.workflowservice.v1.RegisterNamespaceRequest" do @@ -44,6 +52,7 @@ add_message "temporal.api.workflowservice.v1.ListNamespacesRequest" do optional :page_size, :int32, 1 optional :next_page_token, :bytes, 2 + optional :namespace_filter, :message, 3, "temporal.api.namespace.v1.NamespaceFilter" end add_message "temporal.api.workflowservice.v1.ListNamespacesResponse" do repeated :namespaces, :message, 1, "temporal.api.workflowservice.v1.DescribeNamespaceResponse" @@ -59,6 +68,7 @@ optional :replication_config, :message, 3, "temporal.api.replication.v1.NamespaceReplicationConfig" optional :failover_version, :int64, 4 optional :is_global_namespace, :bool, 5 + repeated :failover_history, :message, 6, "temporal.api.replication.v1.FailoverStatus" end add_message "temporal.api.workflowservice.v1.UpdateNamespaceRequest" do optional :namespace, :string, 1 @@ -67,6 +77,7 @@ optional :replication_config, :message, 4, "temporal.api.replication.v1.NamespaceReplicationConfig" optional :security_token, :string, 5 optional :delete_bad_binary, :string, 6 + optional :promote_namespace, :bool, 7 end add_message "temporal.api.workflowservice.v1.UpdateNamespaceResponse" do optional :namespace_info, :message, 1, "temporal.api.namespace.v1.NamespaceInfo" @@ -98,9 +109,11 @@ optional :memo, :message, 14, "temporal.api.common.v1.Memo" optional :search_attributes, :message, 15, "temporal.api.common.v1.SearchAttributes" optional :header, :message, 16, "temporal.api.common.v1.Header" + optional :request_eager_execution, :bool, 17 end add_message "temporal.api.workflowservice.v1.StartWorkflowExecutionResponse" do optional :run_id, :string, 1 + optional :eager_workflow_task, :message, 2, "temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse" end add_message "temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryRequest" do optional :namespace, :string, 1 @@ -117,11 +130,22 @@ optional :next_page_token, :bytes, 3 optional :archived, :bool, 4 end + add_message "temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryReverseRequest" do + optional :namespace, :string, 1 + optional :execution, :message, 2, "temporal.api.common.v1.WorkflowExecution" + optional :maximum_page_size, :int32, 3 + optional :next_page_token, :bytes, 4 + end + add_message "temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryReverseResponse" do + optional :history, :message, 1, "temporal.api.history.v1.History" + optional :next_page_token, :bytes, 3 + end add_message "temporal.api.workflowservice.v1.PollWorkflowTaskQueueRequest" do optional :namespace, :string, 1 optional :task_queue, :message, 2, "temporal.api.taskqueue.v1.TaskQueue" optional :identity, :string, 3 optional :binary_checksum, :string, 4 + optional :worker_versioning_id, :message, 5, "temporal.api.taskqueue.v1.VersionId" end add_message "temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse" do optional :task_token, :bytes, 1 @@ -138,6 +162,7 @@ optional :scheduled_time, :message, 12, "google.protobuf.Timestamp" optional :started_time, :message, 13, "google.protobuf.Timestamp" map :queries, :string, :message, 14, "temporal.api.query.v1.WorkflowQuery" + repeated :messages, :message, 15, "temporal.api.protocol.v1.Message" end add_message "temporal.api.workflowservice.v1.RespondWorkflowTaskCompletedRequest" do optional :task_token, :bytes, 1 @@ -149,9 +174,13 @@ optional :binary_checksum, :string, 7 map :query_results, :string, :message, 8, "temporal.api.query.v1.WorkflowQueryResult" optional :namespace, :string, 9 + optional :worker_versioning_id, :message, 10, "temporal.api.taskqueue.v1.VersionId" + repeated :messages, :message, 11, "temporal.api.protocol.v1.Message" end add_message "temporal.api.workflowservice.v1.RespondWorkflowTaskCompletedResponse" do optional :workflow_task, :message, 1, "temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse" + repeated :activity_tasks, :message, 2, "temporal.api.workflowservice.v1.PollActivityTaskQueueResponse" + optional :reset_history_event_id, :int64, 3 end add_message "temporal.api.workflowservice.v1.RespondWorkflowTaskFailedRequest" do optional :task_token, :bytes, 1 @@ -160,6 +189,7 @@ optional :identity, :string, 4 optional :binary_checksum, :string, 5 optional :namespace, :string, 6 + repeated :messages, :message, 7, "temporal.api.protocol.v1.Message" end add_message "temporal.api.workflowservice.v1.RespondWorkflowTaskFailedResponse" do end @@ -168,6 +198,7 @@ optional :task_queue, :message, 2, "temporal.api.taskqueue.v1.TaskQueue" optional :identity, :string, 3 optional :task_queue_metadata, :message, 4, "temporal.api.taskqueue.v1.TaskQueueMetadata" + optional :worker_versioning_id, :message, 5, "temporal.api.taskqueue.v1.VersionId" end add_message "temporal.api.workflowservice.v1.PollActivityTaskQueueResponse" do optional :task_token, :bytes, 1 @@ -231,8 +262,10 @@ optional :failure, :message, 2, "temporal.api.failure.v1.Failure" optional :identity, :string, 3 optional :namespace, :string, 4 + optional :last_heartbeat_details, :message, 5, "temporal.api.common.v1.Payloads" end add_message "temporal.api.workflowservice.v1.RespondActivityTaskFailedResponse" do + repeated :failures, :message, 1, "temporal.api.failure.v1.Failure" end add_message "temporal.api.workflowservice.v1.RespondActivityTaskFailedByIdRequest" do optional :namespace, :string, 1 @@ -241,8 +274,10 @@ optional :activity_id, :string, 4 optional :failure, :message, 5, "temporal.api.failure.v1.Failure" optional :identity, :string, 6 + optional :last_heartbeat_details, :message, 7, "temporal.api.common.v1.Payloads" end add_message "temporal.api.workflowservice.v1.RespondActivityTaskFailedByIdResponse" do + repeated :failures, :message, 1, "temporal.api.failure.v1.Failure" end add_message "temporal.api.workflowservice.v1.RespondActivityTaskCanceledRequest" do optional :task_token, :bytes, 1 @@ -268,6 +303,7 @@ optional :identity, :string, 3 optional :request_id, :string, 4 optional :first_execution_run_id, :string, 5 + optional :reason, :string, 6 end add_message "temporal.api.workflowservice.v1.RequestCancelWorkflowExecutionResponse" do end @@ -279,6 +315,7 @@ optional :identity, :string, 5 optional :request_id, :string, 6 optional :control, :string, 7 + optional :header, :message, 8, "temporal.api.common.v1.Header" end add_message "temporal.api.workflowservice.v1.SignalWorkflowExecutionResponse" do end @@ -312,6 +349,7 @@ optional :reason, :string, 3 optional :workflow_task_finish_event_id, :int64, 4 optional :request_id, :string, 5 + optional :reset_reapply_type, :enum, 6, "temporal.api.enums.v1.ResetReapplyType" end add_message "temporal.api.workflowservice.v1.ResetWorkflowExecutionResponse" do optional :run_id, :string, 1 @@ -326,6 +364,12 @@ end add_message "temporal.api.workflowservice.v1.TerminateWorkflowExecutionResponse" do end + add_message "temporal.api.workflowservice.v1.DeleteWorkflowExecutionRequest" do + optional :namespace, :string, 1 + optional :workflow_execution, :message, 2, "temporal.api.common.v1.WorkflowExecution" + end + add_message "temporal.api.workflowservice.v1.DeleteWorkflowExecutionResponse" do + end add_message "temporal.api.workflowservice.v1.ListOpenWorkflowExecutionsRequest" do optional :namespace, :string, 1 optional :maximum_page_size, :int32, 2 @@ -431,6 +475,7 @@ optional :workflow_execution_info, :message, 2, "temporal.api.workflow.v1.WorkflowExecutionInfo" repeated :pending_activities, :message, 3, "temporal.api.workflow.v1.PendingActivityInfo" repeated :pending_children, :message, 4, "temporal.api.workflow.v1.PendingChildExecutionInfo" + optional :pending_workflow_task, :message, 5, "temporal.api.workflow.v1.PendingWorkflowTaskInfo" end add_message "temporal.api.workflowservice.v1.DescribeTaskQueueRequest" do optional :namespace, :string, 1 @@ -451,6 +496,24 @@ optional :version_info, :message, 4, "temporal.api.version.v1.VersionInfo" optional :cluster_name, :string, 5 optional :history_shard_count, :int32, 6 + optional :persistence_store, :string, 7 + optional :visibility_store, :string, 8 + end + add_message "temporal.api.workflowservice.v1.GetSystemInfoRequest" do + end + add_message "temporal.api.workflowservice.v1.GetSystemInfoResponse" do + optional :server_version, :string, 1 + optional :capabilities, :message, 2, "temporal.api.workflowservice.v1.GetSystemInfoResponse.Capabilities" + end + add_message "temporal.api.workflowservice.v1.GetSystemInfoResponse.Capabilities" do + optional :signal_and_query_header, :bool, 1 + optional :internal_error_differentiation, :bool, 2 + optional :activity_failure_include_heartbeat, :bool, 3 + optional :supports_schedules, :bool, 4 + optional :encoded_failure_attributes, :bool, 5 + optional :build_id_based_versioning, :bool, 6 + optional :upsert_memo, :bool, 7 + optional :eager_workflow_start, :bool, 8 end add_message "temporal.api.workflowservice.v1.ListTaskQueuePartitionsRequest" do optional :namespace, :string, 1 @@ -460,10 +523,155 @@ repeated :activity_task_queue_partitions, :message, 1, "temporal.api.taskqueue.v1.TaskQueuePartitionMetadata" repeated :workflow_task_queue_partitions, :message, 2, "temporal.api.taskqueue.v1.TaskQueuePartitionMetadata" end + add_message "temporal.api.workflowservice.v1.CreateScheduleRequest" do + optional :namespace, :string, 1 + optional :schedule_id, :string, 2 + optional :schedule, :message, 3, "temporal.api.schedule.v1.Schedule" + optional :initial_patch, :message, 4, "temporal.api.schedule.v1.SchedulePatch" + optional :identity, :string, 5 + optional :request_id, :string, 6 + optional :memo, :message, 7, "temporal.api.common.v1.Memo" + optional :search_attributes, :message, 8, "temporal.api.common.v1.SearchAttributes" + end + add_message "temporal.api.workflowservice.v1.CreateScheduleResponse" do + optional :conflict_token, :bytes, 1 + end + add_message "temporal.api.workflowservice.v1.DescribeScheduleRequest" do + optional :namespace, :string, 1 + optional :schedule_id, :string, 2 + end + add_message "temporal.api.workflowservice.v1.DescribeScheduleResponse" do + optional :schedule, :message, 1, "temporal.api.schedule.v1.Schedule" + optional :info, :message, 2, "temporal.api.schedule.v1.ScheduleInfo" + optional :memo, :message, 3, "temporal.api.common.v1.Memo" + optional :search_attributes, :message, 4, "temporal.api.common.v1.SearchAttributes" + optional :conflict_token, :bytes, 5 + end + add_message "temporal.api.workflowservice.v1.UpdateScheduleRequest" do + optional :namespace, :string, 1 + optional :schedule_id, :string, 2 + optional :schedule, :message, 3, "temporal.api.schedule.v1.Schedule" + optional :conflict_token, :bytes, 4 + optional :identity, :string, 5 + optional :request_id, :string, 6 + end + add_message "temporal.api.workflowservice.v1.UpdateScheduleResponse" do + end + add_message "temporal.api.workflowservice.v1.PatchScheduleRequest" do + optional :namespace, :string, 1 + optional :schedule_id, :string, 2 + optional :patch, :message, 3, "temporal.api.schedule.v1.SchedulePatch" + optional :identity, :string, 4 + optional :request_id, :string, 5 + end + add_message "temporal.api.workflowservice.v1.PatchScheduleResponse" do + end + add_message "temporal.api.workflowservice.v1.ListScheduleMatchingTimesRequest" do + optional :namespace, :string, 1 + optional :schedule_id, :string, 2 + optional :start_time, :message, 3, "google.protobuf.Timestamp" + optional :end_time, :message, 4, "google.protobuf.Timestamp" + end + add_message "temporal.api.workflowservice.v1.ListScheduleMatchingTimesResponse" do + repeated :start_time, :message, 1, "google.protobuf.Timestamp" + end + add_message "temporal.api.workflowservice.v1.DeleteScheduleRequest" do + optional :namespace, :string, 1 + optional :schedule_id, :string, 2 + optional :identity, :string, 3 + end + add_message "temporal.api.workflowservice.v1.DeleteScheduleResponse" do + end + add_message "temporal.api.workflowservice.v1.ListSchedulesRequest" do + optional :namespace, :string, 1 + optional :maximum_page_size, :int32, 2 + optional :next_page_token, :bytes, 3 + end + add_message "temporal.api.workflowservice.v1.ListSchedulesResponse" do + repeated :schedules, :message, 1, "temporal.api.schedule.v1.ScheduleListEntry" + optional :next_page_token, :bytes, 2 + end + add_message "temporal.api.workflowservice.v1.UpdateWorkerBuildIdOrderingRequest" do + optional :namespace, :string, 1 + optional :task_queue, :string, 2 + optional :version_id, :message, 3, "temporal.api.taskqueue.v1.VersionId" + optional :previous_compatible, :message, 4, "temporal.api.taskqueue.v1.VersionId" + optional :become_default, :bool, 5 + end + add_message "temporal.api.workflowservice.v1.UpdateWorkerBuildIdOrderingResponse" do + end + add_message "temporal.api.workflowservice.v1.GetWorkerBuildIdOrderingRequest" do + optional :namespace, :string, 1 + optional :task_queue, :string, 2 + optional :max_depth, :int32, 3 + end + add_message "temporal.api.workflowservice.v1.GetWorkerBuildIdOrderingResponse" do + optional :current_default, :message, 1, "temporal.api.taskqueue.v1.VersionIdNode" + repeated :compatible_leaves, :message, 2, "temporal.api.taskqueue.v1.VersionIdNode" + end + add_message "temporal.api.workflowservice.v1.UpdateWorkflowExecutionRequest" do + optional :namespace, :string, 1 + optional :workflow_execution, :message, 2, "temporal.api.common.v1.WorkflowExecution" + optional :first_execution_run_id, :string, 3 + optional :wait_policy, :message, 4, "temporal.api.update.v1.WaitPolicy" + optional :request, :message, 5, "temporal.api.update.v1.Request" + end + add_message "temporal.api.workflowservice.v1.UpdateWorkflowExecutionResponse" do + optional :update_ref, :message, 1, "temporal.api.update.v1.UpdateRef" + optional :outcome, :message, 2, "temporal.api.update.v1.Outcome" + end + add_message "temporal.api.workflowservice.v1.StartBatchOperationRequest" do + optional :namespace, :string, 1 + optional :visibility_query, :string, 2 + optional :job_id, :string, 3 + optional :reason, :string, 4 + repeated :executions, :message, 5, "temporal.api.common.v1.WorkflowExecution" + oneof :operation do + optional :termination_operation, :message, 10, "temporal.api.batch.v1.BatchOperationTermination" + optional :signal_operation, :message, 11, "temporal.api.batch.v1.BatchOperationSignal" + optional :cancellation_operation, :message, 12, "temporal.api.batch.v1.BatchOperationCancellation" + optional :deletion_operation, :message, 13, "temporal.api.batch.v1.BatchOperationDeletion" + end + end + add_message "temporal.api.workflowservice.v1.StartBatchOperationResponse" do + end + add_message "temporal.api.workflowservice.v1.StopBatchOperationRequest" do + optional :namespace, :string, 1 + optional :job_id, :string, 2 + optional :reason, :string, 3 + optional :identity, :string, 4 + end + add_message "temporal.api.workflowservice.v1.StopBatchOperationResponse" do + end + add_message "temporal.api.workflowservice.v1.DescribeBatchOperationRequest" do + optional :namespace, :string, 1 + optional :job_id, :string, 2 + end + add_message "temporal.api.workflowservice.v1.DescribeBatchOperationResponse" do + optional :operation_type, :enum, 1, "temporal.api.enums.v1.BatchOperationType" + optional :job_id, :string, 2 + optional :state, :enum, 3, "temporal.api.enums.v1.BatchOperationState" + optional :start_time, :message, 4, "google.protobuf.Timestamp" + optional :close_time, :message, 5, "google.protobuf.Timestamp" + optional :total_operation_count, :int64, 6 + optional :complete_operation_count, :int64, 7 + optional :failure_operation_count, :int64, 8 + optional :identity, :string, 9 + optional :reason, :string, 10 + end + add_message "temporal.api.workflowservice.v1.ListBatchOperationsRequest" do + optional :namespace, :string, 1 + optional :page_size, :int32, 2 + optional :next_page_token, :bytes, 3 + end + add_message "temporal.api.workflowservice.v1.ListBatchOperationsResponse" do + repeated :operation_info, :message, 1, "temporal.api.batch.v1.BatchOperationInfo" + optional :next_page_token, :bytes, 2 + end end end -module Temporal +module Temporalio module Api module WorkflowService module V1 @@ -481,6 +689,8 @@ module V1 StartWorkflowExecutionResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.StartWorkflowExecutionResponse").msgclass GetWorkflowExecutionHistoryRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryRequest").msgclass GetWorkflowExecutionHistoryResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryResponse").msgclass + GetWorkflowExecutionHistoryReverseRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryReverseRequest").msgclass + GetWorkflowExecutionHistoryReverseResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryReverseResponse").msgclass PollWorkflowTaskQueueRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.PollWorkflowTaskQueueRequest").msgclass PollWorkflowTaskQueueResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse").msgclass RespondWorkflowTaskCompletedRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.RespondWorkflowTaskCompletedRequest").msgclass @@ -515,6 +725,8 @@ module V1 ResetWorkflowExecutionResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.ResetWorkflowExecutionResponse").msgclass TerminateWorkflowExecutionRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.TerminateWorkflowExecutionRequest").msgclass TerminateWorkflowExecutionResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.TerminateWorkflowExecutionResponse").msgclass + DeleteWorkflowExecutionRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.DeleteWorkflowExecutionRequest").msgclass + DeleteWorkflowExecutionResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.DeleteWorkflowExecutionResponse").msgclass ListOpenWorkflowExecutionsRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.ListOpenWorkflowExecutionsRequest").msgclass ListOpenWorkflowExecutionsResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.ListOpenWorkflowExecutionsResponse").msgclass ListClosedWorkflowExecutionsRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.ListClosedWorkflowExecutionsRequest").msgclass @@ -541,8 +753,39 @@ module V1 DescribeTaskQueueResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.DescribeTaskQueueResponse").msgclass GetClusterInfoRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.GetClusterInfoRequest").msgclass GetClusterInfoResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.GetClusterInfoResponse").msgclass + GetSystemInfoRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.GetSystemInfoRequest").msgclass + GetSystemInfoResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.GetSystemInfoResponse").msgclass + GetSystemInfoResponse::Capabilities = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.GetSystemInfoResponse.Capabilities").msgclass ListTaskQueuePartitionsRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.ListTaskQueuePartitionsRequest").msgclass ListTaskQueuePartitionsResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.ListTaskQueuePartitionsResponse").msgclass + CreateScheduleRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.CreateScheduleRequest").msgclass + CreateScheduleResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.CreateScheduleResponse").msgclass + DescribeScheduleRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.DescribeScheduleRequest").msgclass + DescribeScheduleResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.DescribeScheduleResponse").msgclass + UpdateScheduleRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.UpdateScheduleRequest").msgclass + UpdateScheduleResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.UpdateScheduleResponse").msgclass + PatchScheduleRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.PatchScheduleRequest").msgclass + PatchScheduleResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.PatchScheduleResponse").msgclass + ListScheduleMatchingTimesRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.ListScheduleMatchingTimesRequest").msgclass + ListScheduleMatchingTimesResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.ListScheduleMatchingTimesResponse").msgclass + DeleteScheduleRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.DeleteScheduleRequest").msgclass + DeleteScheduleResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.DeleteScheduleResponse").msgclass + ListSchedulesRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.ListSchedulesRequest").msgclass + ListSchedulesResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.ListSchedulesResponse").msgclass + UpdateWorkerBuildIdOrderingRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.UpdateWorkerBuildIdOrderingRequest").msgclass + UpdateWorkerBuildIdOrderingResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.UpdateWorkerBuildIdOrderingResponse").msgclass + GetWorkerBuildIdOrderingRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.GetWorkerBuildIdOrderingRequest").msgclass + GetWorkerBuildIdOrderingResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.GetWorkerBuildIdOrderingResponse").msgclass + UpdateWorkflowExecutionRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.UpdateWorkflowExecutionRequest").msgclass + UpdateWorkflowExecutionResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.UpdateWorkflowExecutionResponse").msgclass + StartBatchOperationRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.StartBatchOperationRequest").msgclass + StartBatchOperationResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.StartBatchOperationResponse").msgclass + StopBatchOperationRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.StopBatchOperationRequest").msgclass + StopBatchOperationResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.StopBatchOperationResponse").msgclass + DescribeBatchOperationRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.DescribeBatchOperationRequest").msgclass + DescribeBatchOperationResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.DescribeBatchOperationResponse").msgclass + ListBatchOperationsRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.ListBatchOperationsRequest").msgclass + ListBatchOperationsResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.ListBatchOperationsResponse").msgclass end end end diff --git a/lib/gen/temporal/api/workflowservice/v1/service_pb.rb b/lib/gen/temporal/api/workflowservice/v1/service_pb.rb index a5e87578..667b8800 100644 --- a/lib/gen/temporal/api/workflowservice/v1/service_pb.rb +++ b/lib/gen/temporal/api/workflowservice/v1/service_pb.rb @@ -4,12 +4,13 @@ require 'google/protobuf' require 'temporal/api/workflowservice/v1/request_response_pb' + Google::Protobuf::DescriptorPool.generated_pool.build do add_file("temporal/api/workflowservice/v1/service.proto", :syntax => :proto3) do end end -module Temporal +module Temporalio module Api module WorkflowService module V1 diff --git a/lib/gen/temporal/api/workflowservice/v1/service_services_pb.rb b/lib/gen/temporal/api/workflowservice/v1/service_services_pb.rb index 8a3b1dab..bb93223e 100644 --- a/lib/gen/temporal/api/workflowservice/v1/service_services_pb.rb +++ b/lib/gen/temporal/api/workflowservice/v1/service_services_pb.rb @@ -1,5 +1,5 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! -# Source: temporal/api/workflowservice/v1/service.proto for package 'Temporal.Api.WorkflowService.V1' +# Source: temporal/api/workflowservice/v1/service.proto for package 'Temporalio.Api.WorkflowService.V1' # Original file comments: # The MIT License # @@ -27,189 +27,289 @@ require 'grpc' require 'temporal/api/workflowservice/v1/service_pb' -module Temporal +module Temporalio module Api module WorkflowService module V1 module WorkflowService - # WorkflowService API is exposed to provide support for long running applications. Application is expected to call - # StartWorkflowExecution to create an instance for each instance of long running workflow. Such applications are expected - # to have a worker which regularly polls for WorkflowTask and ActivityTask from the WorkflowService. For each - # WorkflowTask, application is expected to process the history of events for that session and respond back with next - # commands. For each ActivityTask, application is expected to execute the actual logic for that task and respond back - # with completion or failure. Worker is expected to regularly heartbeat while activity task is running. + # WorkflowService API defines how Temporal SDKs and other clients interact with the Temporal server + # to create and interact with workflows and activities. + # + # Users are expected to call `StartWorkflowExecution` to create a new workflow execution. + # + # To drive workflows, a worker using a Temporal SDK must exist which regularly polls for workflow + # and activity tasks from the service. For each workflow task, the sdk must process the + # (incremental or complete) event history and respond back with any newly generated commands. + # + # For each activity task, the worker is expected to execute the user's code which implements that + # activity, responding with completion or failure. class Service - include GRPC::GenericService + include ::GRPC::GenericService self.marshal_class_method = :encode self.unmarshal_class_method = :decode self.service_name = 'temporal.api.workflowservice.v1.WorkflowService' - # RegisterNamespace creates a new namespace which can be used as a container for all resources. Namespace is a top level - # entity within Temporal, used as a container for all resources like workflow executions, task queues, etc. Namespace - # acts as a sandbox and provides isolation for all resources within the namespace. All resources belongs to exactly one + # RegisterNamespace creates a new namespace which can be used as a container for all resources. + # + # A Namespace is a top level entity within Temporal, and is used as a container for resources + # like workflow executions, task queues, etc. A Namespace acts as a sandbox and provides + # isolation for all resources within the namespace. All resources belongs to exactly one # namespace. - rpc :RegisterNamespace, ::Temporal::Api::WorkflowService::V1::RegisterNamespaceRequest, ::Temporal::Api::WorkflowService::V1::RegisterNamespaceResponse + rpc :RegisterNamespace, ::Temporalio::Api::WorkflowService::V1::RegisterNamespaceRequest, ::Temporalio::Api::WorkflowService::V1::RegisterNamespaceResponse # DescribeNamespace returns the information and configuration for a registered namespace. - rpc :DescribeNamespace, ::Temporal::Api::WorkflowService::V1::DescribeNamespaceRequest, ::Temporal::Api::WorkflowService::V1::DescribeNamespaceResponse + rpc :DescribeNamespace, ::Temporalio::Api::WorkflowService::V1::DescribeNamespaceRequest, ::Temporalio::Api::WorkflowService::V1::DescribeNamespaceResponse # ListNamespaces returns the information and configuration for all namespaces. - rpc :ListNamespaces, ::Temporal::Api::WorkflowService::V1::ListNamespacesRequest, ::Temporal::Api::WorkflowService::V1::ListNamespacesResponse + rpc :ListNamespaces, ::Temporalio::Api::WorkflowService::V1::ListNamespacesRequest, ::Temporalio::Api::WorkflowService::V1::ListNamespacesResponse + # UpdateNamespace is used to update the information and configuration of a registered + # namespace. + # # (-- api-linter: core::0134::method-signature=disabled # aip.dev/not-precedent: UpdateNamespace RPC doesn't follow Google API format. --) # (-- api-linter: core::0134::response-message-name=disabled # aip.dev/not-precedent: UpdateNamespace RPC doesn't follow Google API format. --) - # UpdateNamespace is used to update the information and configuration for a registered namespace. - rpc :UpdateNamespace, ::Temporal::Api::WorkflowService::V1::UpdateNamespaceRequest, ::Temporal::Api::WorkflowService::V1::UpdateNamespaceResponse - # DeprecateNamespace is used to update state of a registered namespace to DEPRECATED. Once the namespace is deprecated - # it cannot be used to start new workflow executions. Existing workflow executions will continue to run on - # deprecated namespaces. - rpc :DeprecateNamespace, ::Temporal::Api::WorkflowService::V1::DeprecateNamespaceRequest, ::Temporal::Api::WorkflowService::V1::DeprecateNamespaceResponse - # StartWorkflowExecution starts a new long running workflow instance. It will create the instance with - # 'WorkflowExecutionStarted' event in history and also schedule the first WorkflowTask for the worker to make the - # first command for this instance. It will return 'WorkflowExecutionAlreadyStartedFailure', if an instance already - # exists with same workflowId. - rpc :StartWorkflowExecution, ::Temporal::Api::WorkflowService::V1::StartWorkflowExecutionRequest, ::Temporal::Api::WorkflowService::V1::StartWorkflowExecutionResponse - # GetWorkflowExecutionHistory returns the history of specified workflow execution. It fails with 'NotFoundFailure' if specified workflow - # execution in unknown to the service. - rpc :GetWorkflowExecutionHistory, ::Temporal::Api::WorkflowService::V1::GetWorkflowExecutionHistoryRequest, ::Temporal::Api::WorkflowService::V1::GetWorkflowExecutionHistoryResponse - # PollWorkflowTaskQueue is called by application worker to process WorkflowTask from a specific task queue. A - # WorkflowTask is dispatched to callers for active workflow executions, with pending workflow tasks. - # Application is then expected to call 'RespondWorkflowTaskCompleted' API when it is done processing the WorkflowTask. - # It will also create a 'WorkflowTaskStarted' event in the history for that session before handing off WorkflowTask to - # application worker. - rpc :PollWorkflowTaskQueue, ::Temporal::Api::WorkflowService::V1::PollWorkflowTaskQueueRequest, ::Temporal::Api::WorkflowService::V1::PollWorkflowTaskQueueResponse - # RespondWorkflowTaskCompleted is called by application worker to complete a WorkflowTask handed as a result of - # 'PollWorkflowTaskQueue' API call. Completing a WorkflowTask will result in new events for the workflow execution and - # potentially new ActivityTask being created for corresponding commands. It will also create a WorkflowTaskCompleted - # event in the history for that session. Use the 'taskToken' provided as response of PollWorkflowTaskQueue API call - # for completing the WorkflowTask. - # The response could contain a new workflow task if there is one or if the request asking for one. - rpc :RespondWorkflowTaskCompleted, ::Temporal::Api::WorkflowService::V1::RespondWorkflowTaskCompletedRequest, ::Temporal::Api::WorkflowService::V1::RespondWorkflowTaskCompletedResponse - # RespondWorkflowTaskFailed is called by application worker to indicate failure. This results in - # WorkflowTaskFailedEvent written to the history and a new WorkflowTask created. This API can be used by client to - # either clear sticky task queue or report any panics during WorkflowTask processing. Temporal will only append first - # WorkflowTaskFailed event to the history of workflow execution for consecutive failures. - rpc :RespondWorkflowTaskFailed, ::Temporal::Api::WorkflowService::V1::RespondWorkflowTaskFailedRequest, ::Temporal::Api::WorkflowService::V1::RespondWorkflowTaskFailedResponse - # PollActivityTaskQueue is called by application worker to process ActivityTask from a specific task queue. ActivityTask - # is dispatched to callers whenever a ScheduleTask command is made for a workflow execution. - # Application is expected to call 'RespondActivityTaskCompleted' or 'RespondActivityTaskFailed' once it is done + rpc :UpdateNamespace, ::Temporalio::Api::WorkflowService::V1::UpdateNamespaceRequest, ::Temporalio::Api::WorkflowService::V1::UpdateNamespaceResponse + # DeprecateNamespace is used to update the state of a registered namespace to DEPRECATED. + # + # Once the namespace is deprecated it cannot be used to start new workflow executions. Existing + # workflow executions will continue to run on deprecated namespaces. + # Deprecated. + rpc :DeprecateNamespace, ::Temporalio::Api::WorkflowService::V1::DeprecateNamespaceRequest, ::Temporalio::Api::WorkflowService::V1::DeprecateNamespaceResponse + # StartWorkflowExecution starts a new workflow execution. + # + # It will create the execution with a `WORKFLOW_EXECUTION_STARTED` event in its history and + # also schedule the first workflow task. Returns `WorkflowExecutionAlreadyStarted`, if an + # instance already exists with same workflow id. + rpc :StartWorkflowExecution, ::Temporalio::Api::WorkflowService::V1::StartWorkflowExecutionRequest, ::Temporalio::Api::WorkflowService::V1::StartWorkflowExecutionResponse + # GetWorkflowExecutionHistory returns the history of specified workflow execution. Fails with + # `NotFound` if the specified workflow execution is unknown to the service. + rpc :GetWorkflowExecutionHistory, ::Temporalio::Api::WorkflowService::V1::GetWorkflowExecutionHistoryRequest, ::Temporalio::Api::WorkflowService::V1::GetWorkflowExecutionHistoryResponse + # GetWorkflowExecutionHistoryReverse returns the history of specified workflow execution in reverse + # order (starting from last event). Fails with`NotFound` if the specified workflow execution is + # unknown to the service. + rpc :GetWorkflowExecutionHistoryReverse, ::Temporalio::Api::WorkflowService::V1::GetWorkflowExecutionHistoryReverseRequest, ::Temporalio::Api::WorkflowService::V1::GetWorkflowExecutionHistoryReverseResponse + # PollWorkflowTaskQueue is called by workers to make progress on workflows. + # + # A WorkflowTask is dispatched to callers for active workflow executions with pending workflow + # tasks. The worker is expected to call `RespondWorkflowTaskCompleted` when it is done + # processing the task. The service will create a `WorkflowTaskStarted` event in the history for + # this task before handing it to the worker. + rpc :PollWorkflowTaskQueue, ::Temporalio::Api::WorkflowService::V1::PollWorkflowTaskQueueRequest, ::Temporalio::Api::WorkflowService::V1::PollWorkflowTaskQueueResponse + # RespondWorkflowTaskCompleted is called by workers to successfully complete workflow tasks + # they received from `PollWorkflowTaskQueue`. + # + # Completing a WorkflowTask will write a `WORKFLOW_TASK_COMPLETED` event to the workflow's + # history, along with events corresponding to whatever commands the SDK generated while + # executing the task (ex timer started, activity task scheduled, etc). + rpc :RespondWorkflowTaskCompleted, ::Temporalio::Api::WorkflowService::V1::RespondWorkflowTaskCompletedRequest, ::Temporalio::Api::WorkflowService::V1::RespondWorkflowTaskCompletedResponse + # RespondWorkflowTaskFailed is called by workers to indicate the processing of a workflow task + # failed. + # + # This results in a `WORKFLOW_TASK_FAILED` event written to the history, and a new workflow + # task will be scheduled. This API can be used to report unhandled failures resulting from + # applying the workflow task. + # + # Temporal will only append first WorkflowTaskFailed event to the history of workflow execution + # for consecutive failures. + rpc :RespondWorkflowTaskFailed, ::Temporalio::Api::WorkflowService::V1::RespondWorkflowTaskFailedRequest, ::Temporalio::Api::WorkflowService::V1::RespondWorkflowTaskFailedResponse + # PollActivityTaskQueue is called by workers to process activity tasks from a specific task + # queue. + # + # The worker is expected to call one of the `RespondActivityTaskXXX` methods when it is done # processing the task. - # Application also needs to call 'RecordActivityTaskHeartbeat' API within 'heartbeatTimeoutSeconds' interval to - # prevent the task from getting timed out. An event 'ActivityTaskStarted' event is also written to workflow execution - # history before the ActivityTask is dispatched to application worker. - rpc :PollActivityTaskQueue, ::Temporal::Api::WorkflowService::V1::PollActivityTaskQueueRequest, ::Temporal::Api::WorkflowService::V1::PollActivityTaskQueueResponse - # RecordActivityTaskHeartbeat is called by application worker while it is processing an ActivityTask. If worker fails - # to heartbeat within 'heartbeatTimeoutSeconds' interval for the ActivityTask, then it will be marked as timedout and - # 'ActivityTaskTimedOut' event will be written to the workflow history. Calling 'RecordActivityTaskHeartbeat' will - # fail with 'NotFoundFailure' in such situations. Use the 'taskToken' provided as response of - # PollActivityTaskQueue API call for heart beating. - rpc :RecordActivityTaskHeartbeat, ::Temporal::Api::WorkflowService::V1::RecordActivityTaskHeartbeatRequest, ::Temporal::Api::WorkflowService::V1::RecordActivityTaskHeartbeatResponse + # + # An activity task is dispatched whenever a `SCHEDULE_ACTIVITY_TASK` command is produced during + # workflow execution. An in memory `ACTIVITY_TASK_STARTED` event is written to mutable state + # before the task is dispatched to the worker. The started event, and the final event + # (`ACTIVITY_TASK_COMPLETED` / `ACTIVITY_TASK_FAILED` / `ACTIVITY_TASK_TIMED_OUT`) will both be + # written permanently to Workflow execution history when Activity is finished. This is done to + # avoid writing many events in the case of a failure/retry loop. + rpc :PollActivityTaskQueue, ::Temporalio::Api::WorkflowService::V1::PollActivityTaskQueueRequest, ::Temporalio::Api::WorkflowService::V1::PollActivityTaskQueueResponse + # RecordActivityTaskHeartbeat is optionally called by workers while they execute activities. + # + # If worker fails to heartbeat within the `heartbeat_timeout` interval for the activity task, + # then it will be marked as timed out and an `ACTIVITY_TASK_TIMED_OUT` event will be written to + # the workflow history. Calling `RecordActivityTaskHeartbeat` will fail with `NotFound` in + # such situations, in that event, the SDK should request cancellation of the activity. + rpc :RecordActivityTaskHeartbeat, ::Temporalio::Api::WorkflowService::V1::RecordActivityTaskHeartbeatRequest, ::Temporalio::Api::WorkflowService::V1::RecordActivityTaskHeartbeatResponse + # See `RecordActivityTaskHeartbeat`. This version allows clients to record heartbeats by + # namespace/workflow id/activity id instead of task token. + # # (-- api-linter: core::0136::prepositions=disabled # aip.dev/not-precedent: "By" is used to indicate request type. --) - # RecordActivityTaskHeartbeatById is called by application worker while it is processing an ActivityTask. If worker fails - # to heartbeat within 'heartbeatTimeoutSeconds' interval for the ActivityTask, then it will be marked as timed out and - # 'ActivityTaskTimedOut' event will be written to the workflow history. Calling 'RecordActivityTaskHeartbeatById' will - # fail with 'NotFoundFailure' in such situations. Instead of using 'taskToken' like in RecordActivityTaskHeartbeat, - # use Namespace, WorkflowId and ActivityId - rpc :RecordActivityTaskHeartbeatById, ::Temporal::Api::WorkflowService::V1::RecordActivityTaskHeartbeatByIdRequest, ::Temporal::Api::WorkflowService::V1::RecordActivityTaskHeartbeatByIdResponse - # RespondActivityTaskCompleted is called by application worker when it is done processing an ActivityTask. It will - # result in a new 'ActivityTaskCompleted' event being written to the workflow history and a new WorkflowTask - # created for the workflow so new commands could be made. Use the 'taskToken' provided as response of - # PollActivityTaskQueue API call for completion. It fails with 'NotFoundFailure' if the taskToken is not valid - # anymore due to activity timeout. - rpc :RespondActivityTaskCompleted, ::Temporal::Api::WorkflowService::V1::RespondActivityTaskCompletedRequest, ::Temporal::Api::WorkflowService::V1::RespondActivityTaskCompletedResponse + rpc :RecordActivityTaskHeartbeatById, ::Temporalio::Api::WorkflowService::V1::RecordActivityTaskHeartbeatByIdRequest, ::Temporalio::Api::WorkflowService::V1::RecordActivityTaskHeartbeatByIdResponse + # RespondActivityTaskCompleted is called by workers when they successfully complete an activity + # task. + # + # This results in a new `ACTIVITY_TASK_COMPLETED` event being written to the workflow history + # and a new workflow task created for the workflow. Fails with `NotFound` if the task token is + # no longer valid due to activity timeout, already being completed, or never having existed. + rpc :RespondActivityTaskCompleted, ::Temporalio::Api::WorkflowService::V1::RespondActivityTaskCompletedRequest, ::Temporalio::Api::WorkflowService::V1::RespondActivityTaskCompletedResponse + # See `RecordActivityTaskCompleted`. This version allows clients to record completions by + # namespace/workflow id/activity id instead of task token. + # # (-- api-linter: core::0136::prepositions=disabled # aip.dev/not-precedent: "By" is used to indicate request type. --) - # RespondActivityTaskCompletedById is called by application worker when it is done processing an ActivityTask. - # It will result in a new 'ActivityTaskCompleted' event being written to the workflow history and a new WorkflowTask - # created for the workflow so new commands could be made. Similar to RespondActivityTaskCompleted but use Namespace, - # WorkflowId and ActivityId instead of 'taskToken' for completion. It fails with 'NotFoundFailure' - # if the these Ids are not valid anymore due to activity timeout. - rpc :RespondActivityTaskCompletedById, ::Temporal::Api::WorkflowService::V1::RespondActivityTaskCompletedByIdRequest, ::Temporal::Api::WorkflowService::V1::RespondActivityTaskCompletedByIdResponse - # RespondActivityTaskFailed is called by application worker when it is done processing an ActivityTask. It will - # result in a new 'ActivityTaskFailed' event being written to the workflow history and a new WorkflowTask - # created for the workflow instance so new commands could be made. Use the 'taskToken' provided as response of - # PollActivityTaskQueue API call for completion. It fails with 'NotFoundFailure' if the taskToken is not valid - # anymore due to activity timeout. - rpc :RespondActivityTaskFailed, ::Temporal::Api::WorkflowService::V1::RespondActivityTaskFailedRequest, ::Temporal::Api::WorkflowService::V1::RespondActivityTaskFailedResponse + rpc :RespondActivityTaskCompletedById, ::Temporalio::Api::WorkflowService::V1::RespondActivityTaskCompletedByIdRequest, ::Temporalio::Api::WorkflowService::V1::RespondActivityTaskCompletedByIdResponse + # RespondActivityTaskFailed is called by workers when processing an activity task fails. + # + # This results in a new `ACTIVITY_TASK_FAILED` event being written to the workflow history and + # a new workflow task created for the workflow. Fails with `NotFound` if the task token is no + # longer valid due to activity timeout, already being completed, or never having existed. + rpc :RespondActivityTaskFailed, ::Temporalio::Api::WorkflowService::V1::RespondActivityTaskFailedRequest, ::Temporalio::Api::WorkflowService::V1::RespondActivityTaskFailedResponse + # See `RecordActivityTaskFailed`. This version allows clients to record failures by + # namespace/workflow id/activity id instead of task token. + # # (-- api-linter: core::0136::prepositions=disabled # aip.dev/not-precedent: "By" is used to indicate request type. --) - # RespondActivityTaskFailedById is called by application worker when it is done processing an ActivityTask. - # It will result in a new 'ActivityTaskFailed' event being written to the workflow history and a new WorkflowTask - # created for the workflow instance so new commands could be made. Similar to RespondActivityTaskFailed but use - # Namespace, WorkflowId and ActivityId instead of 'taskToken' for completion. It fails with 'NotFoundFailure' - # if the these Ids are not valid anymore due to activity timeout. - rpc :RespondActivityTaskFailedById, ::Temporal::Api::WorkflowService::V1::RespondActivityTaskFailedByIdRequest, ::Temporal::Api::WorkflowService::V1::RespondActivityTaskFailedByIdResponse - # RespondActivityTaskCanceled is called by application worker when it is successfully canceled an ActivityTask. It will - # result in a new 'ActivityTaskCanceled' event being written to the workflow history and a new WorkflowTask - # created for the workflow instance so new commands could be made. Use the 'taskToken' provided as response of - # PollActivityTaskQueue API call for completion. It fails with 'NotFoundFailure' if the taskToken is not valid - # anymore due to activity timeout. - rpc :RespondActivityTaskCanceled, ::Temporal::Api::WorkflowService::V1::RespondActivityTaskCanceledRequest, ::Temporal::Api::WorkflowService::V1::RespondActivityTaskCanceledResponse + rpc :RespondActivityTaskFailedById, ::Temporalio::Api::WorkflowService::V1::RespondActivityTaskFailedByIdRequest, ::Temporalio::Api::WorkflowService::V1::RespondActivityTaskFailedByIdResponse + # RespondActivityTaskFailed is called by workers when processing an activity task fails. + # + # This results in a new `ACTIVITY_TASK_CANCELED` event being written to the workflow history + # and a new workflow task created for the workflow. Fails with `NotFound` if the task token is + # no longer valid due to activity timeout, already being completed, or never having existed. + rpc :RespondActivityTaskCanceled, ::Temporalio::Api::WorkflowService::V1::RespondActivityTaskCanceledRequest, ::Temporalio::Api::WorkflowService::V1::RespondActivityTaskCanceledResponse + # See `RecordActivityTaskCanceled`. This version allows clients to record failures by + # namespace/workflow id/activity id instead of task token. + # # (-- api-linter: core::0136::prepositions=disabled # aip.dev/not-precedent: "By" is used to indicate request type. --) - # RespondActivityTaskCanceledById is called by application worker when it is successfully canceled an ActivityTask. - # It will result in a new 'ActivityTaskCanceled' event being written to the workflow history and a new WorkflowTask - # created for the workflow instance so new commands could be made. Similar to RespondActivityTaskCanceled but use - # Namespace, WorkflowId and ActivityId instead of 'taskToken' for completion. It fails with 'NotFoundFailure' - # if the these Ids are not valid anymore due to activity timeout. - rpc :RespondActivityTaskCanceledById, ::Temporal::Api::WorkflowService::V1::RespondActivityTaskCanceledByIdRequest, ::Temporal::Api::WorkflowService::V1::RespondActivityTaskCanceledByIdResponse - # RequestCancelWorkflowExecution is called by application worker when it wants to request cancellation of a workflow instance. - # It will result in a new 'WorkflowExecutionCancelRequested' event being written to the workflow history and a new WorkflowTask - # created for the workflow instance so new commands could be made. It fails with 'NotFoundFailure' if the workflow is not valid - # anymore due to completion or doesn't exist. - rpc :RequestCancelWorkflowExecution, ::Temporal::Api::WorkflowService::V1::RequestCancelWorkflowExecutionRequest, ::Temporal::Api::WorkflowService::V1::RequestCancelWorkflowExecutionResponse - # SignalWorkflowExecution is used to send a signal event to running workflow execution. This results in - # WorkflowExecutionSignaled event recorded in the history and a workflow task being created for the execution. - rpc :SignalWorkflowExecution, ::Temporal::Api::WorkflowService::V1::SignalWorkflowExecutionRequest, ::Temporal::Api::WorkflowService::V1::SignalWorkflowExecutionResponse + rpc :RespondActivityTaskCanceledById, ::Temporalio::Api::WorkflowService::V1::RespondActivityTaskCanceledByIdRequest, ::Temporalio::Api::WorkflowService::V1::RespondActivityTaskCanceledByIdResponse + # RequestCancelWorkflowExecution is called by workers when they want to request cancellation of + # a workflow execution. + # + # This results in a new `WORKFLOW_EXECUTION_CANCEL_REQUESTED` event being written to the + # workflow history and a new workflow task created for the workflow. It returns success if the requested + # workflow is already closed. It fails with 'NotFound' if the requested workflow doesn't exist. + rpc :RequestCancelWorkflowExecution, ::Temporalio::Api::WorkflowService::V1::RequestCancelWorkflowExecutionRequest, ::Temporalio::Api::WorkflowService::V1::RequestCancelWorkflowExecutionResponse + # SignalWorkflowExecution is used to send a signal to a running workflow execution. + # + # This results in a `WORKFLOW_EXECUTION_SIGNALED` event recorded in the history and a workflow + # task being created for the execution. + rpc :SignalWorkflowExecution, ::Temporalio::Api::WorkflowService::V1::SignalWorkflowExecutionRequest, ::Temporalio::Api::WorkflowService::V1::SignalWorkflowExecutionResponse + # SignalWithStartWorkflowExecution is used to ensure a signal is sent to a workflow, even if + # it isn't yet started. + # + # If the workflow is running, a `WORKFLOW_EXECUTION_SIGNALED` event is recorded in the history + # and a workflow task is generated. + # + # If the workflow is not running or not found, then the workflow is created with + # `WORKFLOW_EXECUTION_STARTED` and `WORKFLOW_EXECUTION_SIGNALED` events in its history, and a + # workflow task is generated. + # # (-- api-linter: core::0136::prepositions=disabled # aip.dev/not-precedent: "With" is used to indicate combined operation. --) - # SignalWithStartWorkflowExecution is used to ensure sending signal to a workflow. - # If the workflow is running, this results in WorkflowExecutionSignaled event being recorded in the history - # and a workflow task being created for the execution. - # If the workflow is not running or not found, this results in WorkflowExecutionStarted and WorkflowExecutionSignaled - # events being recorded in history, and a workflow task being created for the execution - rpc :SignalWithStartWorkflowExecution, ::Temporal::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionRequest, ::Temporal::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionResponse - # ResetWorkflowExecution reset an existing workflow execution to WorkflowTaskCompleted event(exclusive). - # And it will immediately terminating the current execution instance. - rpc :ResetWorkflowExecution, ::Temporal::Api::WorkflowService::V1::ResetWorkflowExecutionRequest, ::Temporal::Api::WorkflowService::V1::ResetWorkflowExecutionResponse - # TerminateWorkflowExecution terminates an existing workflow execution by recording WorkflowExecutionTerminated event - # in the history and immediately terminating the execution instance. - rpc :TerminateWorkflowExecution, ::Temporal::Api::WorkflowService::V1::TerminateWorkflowExecutionRequest, ::Temporal::Api::WorkflowService::V1::TerminateWorkflowExecutionResponse + rpc :SignalWithStartWorkflowExecution, ::Temporalio::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionRequest, ::Temporalio::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionResponse + # ResetWorkflowExecution will reset an existing workflow execution to a specified + # `WORKFLOW_TASK_COMPLETED` event (exclusive). It will immediately terminate the current + # execution instance. + # TODO: Does exclusive here mean *just* the completed event, or also WFT started? Otherwise the task is doomed to time out? + rpc :ResetWorkflowExecution, ::Temporalio::Api::WorkflowService::V1::ResetWorkflowExecutionRequest, ::Temporalio::Api::WorkflowService::V1::ResetWorkflowExecutionResponse + # TerminateWorkflowExecution terminates an existing workflow execution by recording a + # `WORKFLOW_EXECUTION_TERMINATED` event in the history and immediately terminating the + # execution instance. + rpc :TerminateWorkflowExecution, ::Temporalio::Api::WorkflowService::V1::TerminateWorkflowExecutionRequest, ::Temporalio::Api::WorkflowService::V1::TerminateWorkflowExecutionResponse + # DeleteWorkflowExecution asynchronously deletes a specific Workflow Execution (when + # WorkflowExecution.run_id is provided) or the latest Workflow Execution (when + # WorkflowExecution.run_id is not provided). If the Workflow Execution is Running, it will be + # terminated before deletion. + # (-- api-linter: core::0135::method-signature=disabled + # aip.dev/not-precedent: DeleteNamespace RPC doesn't follow Google API format. --) + # (-- api-linter: core::0135::response-message-name=disabled + # aip.dev/not-precedent: DeleteNamespace RPC doesn't follow Google API format. --) + rpc :DeleteWorkflowExecution, ::Temporalio::Api::WorkflowService::V1::DeleteWorkflowExecutionRequest, ::Temporalio::Api::WorkflowService::V1::DeleteWorkflowExecutionResponse # ListOpenWorkflowExecutions is a visibility API to list the open executions in a specific namespace. - rpc :ListOpenWorkflowExecutions, ::Temporal::Api::WorkflowService::V1::ListOpenWorkflowExecutionsRequest, ::Temporal::Api::WorkflowService::V1::ListOpenWorkflowExecutionsResponse + rpc :ListOpenWorkflowExecutions, ::Temporalio::Api::WorkflowService::V1::ListOpenWorkflowExecutionsRequest, ::Temporalio::Api::WorkflowService::V1::ListOpenWorkflowExecutionsResponse # ListClosedWorkflowExecutions is a visibility API to list the closed executions in a specific namespace. - rpc :ListClosedWorkflowExecutions, ::Temporal::Api::WorkflowService::V1::ListClosedWorkflowExecutionsRequest, ::Temporal::Api::WorkflowService::V1::ListClosedWorkflowExecutionsResponse + rpc :ListClosedWorkflowExecutions, ::Temporalio::Api::WorkflowService::V1::ListClosedWorkflowExecutionsRequest, ::Temporalio::Api::WorkflowService::V1::ListClosedWorkflowExecutionsResponse # ListWorkflowExecutions is a visibility API to list workflow executions in a specific namespace. - rpc :ListWorkflowExecutions, ::Temporal::Api::WorkflowService::V1::ListWorkflowExecutionsRequest, ::Temporal::Api::WorkflowService::V1::ListWorkflowExecutionsResponse + rpc :ListWorkflowExecutions, ::Temporalio::Api::WorkflowService::V1::ListWorkflowExecutionsRequest, ::Temporalio::Api::WorkflowService::V1::ListWorkflowExecutionsResponse # ListArchivedWorkflowExecutions is a visibility API to list archived workflow executions in a specific namespace. - rpc :ListArchivedWorkflowExecutions, ::Temporal::Api::WorkflowService::V1::ListArchivedWorkflowExecutionsRequest, ::Temporal::Api::WorkflowService::V1::ListArchivedWorkflowExecutionsResponse + rpc :ListArchivedWorkflowExecutions, ::Temporalio::Api::WorkflowService::V1::ListArchivedWorkflowExecutionsRequest, ::Temporalio::Api::WorkflowService::V1::ListArchivedWorkflowExecutionsResponse # ScanWorkflowExecutions is a visibility API to list large amount of workflow executions in a specific namespace without order. - rpc :ScanWorkflowExecutions, ::Temporal::Api::WorkflowService::V1::ScanWorkflowExecutionsRequest, ::Temporal::Api::WorkflowService::V1::ScanWorkflowExecutionsResponse + rpc :ScanWorkflowExecutions, ::Temporalio::Api::WorkflowService::V1::ScanWorkflowExecutionsRequest, ::Temporalio::Api::WorkflowService::V1::ScanWorkflowExecutionsResponse # CountWorkflowExecutions is a visibility API to count of workflow executions in a specific namespace. - rpc :CountWorkflowExecutions, ::Temporal::Api::WorkflowService::V1::CountWorkflowExecutionsRequest, ::Temporal::Api::WorkflowService::V1::CountWorkflowExecutionsResponse + rpc :CountWorkflowExecutions, ::Temporalio::Api::WorkflowService::V1::CountWorkflowExecutionsRequest, ::Temporalio::Api::WorkflowService::V1::CountWorkflowExecutionsResponse # GetSearchAttributes is a visibility API to get all legal keys that could be used in list APIs - rpc :GetSearchAttributes, ::Temporal::Api::WorkflowService::V1::GetSearchAttributesRequest, ::Temporal::Api::WorkflowService::V1::GetSearchAttributesResponse - # RespondQueryTaskCompleted is called by application worker to complete a QueryTask (which is a WorkflowTask for query) - # as a result of 'PollWorkflowTaskQueue' API call. Completing a QueryTask will unblock the client call to 'QueryWorkflow' - # API and return the query result to client as a response to 'QueryWorkflow' API call. - rpc :RespondQueryTaskCompleted, ::Temporal::Api::WorkflowService::V1::RespondQueryTaskCompletedRequest, ::Temporal::Api::WorkflowService::V1::RespondQueryTaskCompletedResponse - # ResetStickyTaskQueue resets the sticky task queue related information in mutable state of a given workflow. + rpc :GetSearchAttributes, ::Temporalio::Api::WorkflowService::V1::GetSearchAttributesRequest, ::Temporalio::Api::WorkflowService::V1::GetSearchAttributesResponse + # RespondQueryTaskCompleted is called by workers to complete queries which were delivered on + # the `query` (not `queries`) field of a `PollWorkflowTaskQueueResponse`. + # + # Completing the query will unblock the corresponding client call to `QueryWorkflow` and return + # the query result a response. + rpc :RespondQueryTaskCompleted, ::Temporalio::Api::WorkflowService::V1::RespondQueryTaskCompletedRequest, ::Temporalio::Api::WorkflowService::V1::RespondQueryTaskCompletedResponse + # ResetStickyTaskQueue resets the sticky task queue related information in the mutable state of + # a given workflow. This is prudent for workers to perform if a workflow has been paged out of + # their cache. + # # Things cleared are: # 1. StickyTaskQueue # 2. StickyScheduleToStartTimeout - rpc :ResetStickyTaskQueue, ::Temporal::Api::WorkflowService::V1::ResetStickyTaskQueueRequest, ::Temporal::Api::WorkflowService::V1::ResetStickyTaskQueueResponse - # QueryWorkflow returns query result for a specified workflow execution - rpc :QueryWorkflow, ::Temporal::Api::WorkflowService::V1::QueryWorkflowRequest, ::Temporal::Api::WorkflowService::V1::QueryWorkflowResponse + rpc :ResetStickyTaskQueue, ::Temporalio::Api::WorkflowService::V1::ResetStickyTaskQueueRequest, ::Temporalio::Api::WorkflowService::V1::ResetStickyTaskQueueResponse + # QueryWorkflow requests a query be executed for a specified workflow execution. + rpc :QueryWorkflow, ::Temporalio::Api::WorkflowService::V1::QueryWorkflowRequest, ::Temporalio::Api::WorkflowService::V1::QueryWorkflowResponse # DescribeWorkflowExecution returns information about the specified workflow execution. - rpc :DescribeWorkflowExecution, ::Temporal::Api::WorkflowService::V1::DescribeWorkflowExecutionRequest, ::Temporal::Api::WorkflowService::V1::DescribeWorkflowExecutionResponse - # DescribeTaskQueue returns information about the target task queue, right now this API returns the - # pollers which polled this task queue in last few minutes. - rpc :DescribeTaskQueue, ::Temporal::Api::WorkflowService::V1::DescribeTaskQueueRequest, ::Temporal::Api::WorkflowService::V1::DescribeTaskQueueResponse + rpc :DescribeWorkflowExecution, ::Temporalio::Api::WorkflowService::V1::DescribeWorkflowExecutionRequest, ::Temporalio::Api::WorkflowService::V1::DescribeWorkflowExecutionResponse + # DescribeTaskQueue returns information about the target task queue. + rpc :DescribeTaskQueue, ::Temporalio::Api::WorkflowService::V1::DescribeTaskQueueRequest, ::Temporalio::Api::WorkflowService::V1::DescribeTaskQueueResponse # GetClusterInfo returns information about temporal cluster - rpc :GetClusterInfo, ::Temporal::Api::WorkflowService::V1::GetClusterInfoRequest, ::Temporal::Api::WorkflowService::V1::GetClusterInfoResponse - rpc :ListTaskQueuePartitions, ::Temporal::Api::WorkflowService::V1::ListTaskQueuePartitionsRequest, ::Temporal::Api::WorkflowService::V1::ListTaskQueuePartitionsResponse + rpc :GetClusterInfo, ::Temporalio::Api::WorkflowService::V1::GetClusterInfoRequest, ::Temporalio::Api::WorkflowService::V1::GetClusterInfoResponse + # GetSystemInfo returns information about the system. + rpc :GetSystemInfo, ::Temporalio::Api::WorkflowService::V1::GetSystemInfoRequest, ::Temporalio::Api::WorkflowService::V1::GetSystemInfoResponse + rpc :ListTaskQueuePartitions, ::Temporalio::Api::WorkflowService::V1::ListTaskQueuePartitionsRequest, ::Temporalio::Api::WorkflowService::V1::ListTaskQueuePartitionsResponse + # Creates a new schedule. + # (-- api-linter: core::0133::method-signature=disabled + # aip.dev/not-precedent: CreateSchedule doesn't follow Google API format --) + # (-- api-linter: core::0133::response-message-name=disabled + # aip.dev/not-precedent: CreateSchedule doesn't follow Google API format --) + # (-- api-linter: core::0133::http-uri-parent=disabled + # aip.dev/not-precedent: CreateSchedule doesn't follow Google API format --) + rpc :CreateSchedule, ::Temporalio::Api::WorkflowService::V1::CreateScheduleRequest, ::Temporalio::Api::WorkflowService::V1::CreateScheduleResponse + # Returns the schedule description and current state of an existing schedule. + rpc :DescribeSchedule, ::Temporalio::Api::WorkflowService::V1::DescribeScheduleRequest, ::Temporalio::Api::WorkflowService::V1::DescribeScheduleResponse + # Changes the configuration or state of an existing schedule. + # (-- api-linter: core::0134::response-message-name=disabled + # aip.dev/not-precedent: UpdateSchedule RPC doesn't follow Google API format. --) + # (-- api-linter: core::0134::method-signature=disabled + # aip.dev/not-precedent: UpdateSchedule RPC doesn't follow Google API format. --) + rpc :UpdateSchedule, ::Temporalio::Api::WorkflowService::V1::UpdateScheduleRequest, ::Temporalio::Api::WorkflowService::V1::UpdateScheduleResponse + # Makes a specific change to a schedule or triggers an immediate action. + # (-- api-linter: core::0134::synonyms=disabled + # aip.dev/not-precedent: we have both patch and update. --) + rpc :PatchSchedule, ::Temporalio::Api::WorkflowService::V1::PatchScheduleRequest, ::Temporalio::Api::WorkflowService::V1::PatchScheduleResponse + # Lists matching times within a range. + rpc :ListScheduleMatchingTimes, ::Temporalio::Api::WorkflowService::V1::ListScheduleMatchingTimesRequest, ::Temporalio::Api::WorkflowService::V1::ListScheduleMatchingTimesResponse + # Deletes a schedule, removing it from the system. + # (-- api-linter: core::0135::method-signature=disabled + # aip.dev/not-precedent: DeleteSchedule doesn't follow Google API format --) + # (-- api-linter: core::0135::response-message-name=disabled + # aip.dev/not-precedent: DeleteSchedule doesn't follow Google API format --) + rpc :DeleteSchedule, ::Temporalio::Api::WorkflowService::V1::DeleteScheduleRequest, ::Temporalio::Api::WorkflowService::V1::DeleteScheduleResponse + # List all schedules in a namespace. + rpc :ListSchedules, ::Temporalio::Api::WorkflowService::V1::ListSchedulesRequest, ::Temporalio::Api::WorkflowService::V1::ListSchedulesResponse + # Allows users to specify a graph of worker build id based versions on a + # per task queue basis. Versions are ordered, and may be either compatible + # with some extant version, or a new incompatible version. + # (-- api-linter: core::0134::response-message-name=disabled + # aip.dev/not-precedent: UpdateWorkerBuildIdOrdering RPC doesn't follow Google API format. --) + # (-- api-linter: core::0134::method-signature=disabled + # aip.dev/not-precedent: UpdateWorkerBuildIdOrdering RPC doesn't follow Google API format. --) + rpc :UpdateWorkerBuildIdOrdering, ::Temporalio::Api::WorkflowService::V1::UpdateWorkerBuildIdOrderingRequest, ::Temporalio::Api::WorkflowService::V1::UpdateWorkerBuildIdOrderingResponse + # Fetches the worker build id versioning graph for some task queue. + rpc :GetWorkerBuildIdOrdering, ::Temporalio::Api::WorkflowService::V1::GetWorkerBuildIdOrderingRequest, ::Temporalio::Api::WorkflowService::V1::GetWorkerBuildIdOrderingResponse + # Invokes the specified update function on user workflow code. + # (-- api-linter: core::0134=disabled + # aip.dev/not-precedent: UpdateWorkflowExecution doesn't follow Google API format --) + rpc :UpdateWorkflowExecution, ::Temporalio::Api::WorkflowService::V1::UpdateWorkflowExecutionRequest, ::Temporalio::Api::WorkflowService::V1::UpdateWorkflowExecutionResponse + # StartBatchOperation starts a new batch operation + rpc :StartBatchOperation, ::Temporalio::Api::WorkflowService::V1::StartBatchOperationRequest, ::Temporalio::Api::WorkflowService::V1::StartBatchOperationResponse + # StopBatchOperation stops a batch operation + rpc :StopBatchOperation, ::Temporalio::Api::WorkflowService::V1::StopBatchOperationRequest, ::Temporalio::Api::WorkflowService::V1::StopBatchOperationResponse + # DescribeBatchOperation returns the information about a batch operation + rpc :DescribeBatchOperation, ::Temporalio::Api::WorkflowService::V1::DescribeBatchOperationRequest, ::Temporalio::Api::WorkflowService::V1::DescribeBatchOperationResponse + # ListBatchOperations returns a list of batch operations + rpc :ListBatchOperations, ::Temporalio::Api::WorkflowService::V1::ListBatchOperationsRequest, ::Temporalio::Api::WorkflowService::V1::ListBatchOperationsResponse end Stub = Service.rpc_stub_class diff --git a/lib/temporal/client.rb b/lib/temporal/client.rb index 912a63d0..ea991faa 100644 --- a/lib/temporal/client.rb +++ b/lib/temporal/client.rb @@ -149,7 +149,7 @@ def register_namespace(name, description = nil, is_global: false, retention_peri # Fetches metadata for a namespace. # @param name [String] name of the namespace - # @return [Hash] info deserialized from Temporal::Api::WorkflowService::V1::DescribeNamespaceResponse + # @return [Hash] info deserialized from Temporalio::Api::WorkflowService::V1::DescribeNamespaceResponse def describe_namespace(name) connection.describe_namespace(name: name) end diff --git a/lib/temporal/connection/converter/base.rb b/lib/temporal/connection/converter/base.rb index 93b09b2b..17669983 100644 --- a/lib/temporal/connection/converter/base.rb +++ b/lib/temporal/connection/converter/base.rb @@ -17,7 +17,7 @@ def from_payload(payload) def to_payloads(data) return nil if data.nil? - Temporal::Api::Common::V1::Payloads.new( + Temporalio::Api::Common::V1::Payloads.new( payloads: data.map(&method(:to_payload)) ) end diff --git a/lib/temporal/connection/converter/payload/bytes.rb b/lib/temporal/connection/converter/payload/bytes.rb index 16b157c8..2b8da7e6 100644 --- a/lib/temporal/connection/converter/payload/bytes.rb +++ b/lib/temporal/connection/converter/payload/bytes.rb @@ -18,7 +18,7 @@ def from_payload(payload) def to_payload(data) return nil unless data.is_a?(String) && data.encoding == Encoding::ASCII_8BIT - Temporal::Api::Common::V1::Payload.new( + Temporalio::Api::Common::V1::Payload.new( metadata: { 'encoding' => ENCODING }, data: data ) diff --git a/lib/temporal/connection/converter/payload/json.rb b/lib/temporal/connection/converter/payload/json.rb index 0e1665e0..1cd7b4d1 100644 --- a/lib/temporal/connection/converter/payload/json.rb +++ b/lib/temporal/connection/converter/payload/json.rb @@ -16,7 +16,7 @@ def from_payload(payload) end def to_payload(data) - Temporal::Api::Common::V1::Payload.new( + Temporalio::Api::Common::V1::Payload.new( metadata: { 'encoding' => ENCODING }, data: Temporal::JSON.serialize(data).b ) diff --git a/lib/temporal/connection/converter/payload/nil.rb b/lib/temporal/connection/converter/payload/nil.rb index 7337520f..856aa012 100644 --- a/lib/temporal/connection/converter/payload/nil.rb +++ b/lib/temporal/connection/converter/payload/nil.rb @@ -16,7 +16,7 @@ def from_payload(payload) def to_payload(data) return nil unless data.nil? - Temporal::Api::Common::V1::Payload.new( + Temporalio::Api::Common::V1::Payload.new( metadata: { 'encoding' => ENCODING } ) end diff --git a/lib/temporal/connection/converter/payload/proto_json.rb b/lib/temporal/connection/converter/payload/proto_json.rb index 994c1e24..6d952a60 100644 --- a/lib/temporal/connection/converter/payload/proto_json.rb +++ b/lib/temporal/connection/converter/payload/proto_json.rb @@ -20,7 +20,7 @@ def from_payload(payload) def to_payload(data) return unless data.is_a?(Google::Protobuf::MessageExts) - Temporal::Api::Common::V1::Payload.new( + Temporalio::Api::Common::V1::Payload.new( metadata: { 'encoding' => ENCODING, 'messageType' => data.class.descriptor.name, diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index 9335e51b..0240ab23 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -17,14 +17,14 @@ class GRPC include Concerns::Payloads HISTORY_EVENT_FILTER = { - all: Temporal::Api::Enums::V1::HistoryEventFilterType::HISTORY_EVENT_FILTER_TYPE_ALL_EVENT, - close: Temporal::Api::Enums::V1::HistoryEventFilterType::HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT, + all: Temporalio::Api::Enums::V1::HistoryEventFilterType::HISTORY_EVENT_FILTER_TYPE_ALL_EVENT, + close: Temporalio::Api::Enums::V1::HistoryEventFilterType::HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT, }.freeze QUERY_REJECT_CONDITION = { - none: Temporal::Api::Enums::V1::QueryRejectCondition::QUERY_REJECT_CONDITION_NONE, - not_open: Temporal::Api::Enums::V1::QueryRejectCondition::QUERY_REJECT_CONDITION_NOT_OPEN, - not_completed_cleanly: Temporal::Api::Enums::V1::QueryRejectCondition::QUERY_REJECT_CONDITION_NOT_COMPLETED_CLEANLY + none: Temporalio::Api::Enums::V1::QueryRejectCondition::QUERY_REJECT_CONDITION_NONE, + not_open: Temporalio::Api::Enums::V1::QueryRejectCondition::QUERY_REJECT_CONDITION_NOT_OPEN, + not_completed_cleanly: Temporalio::Api::Enums::V1::QueryRejectCondition::QUERY_REJECT_CONDITION_NOT_COMPLETED_CLEANLY }.freeze DEFAULT_OPTIONS = { @@ -42,7 +42,7 @@ def initialize(host, port, identity, credentials, options = {}) end def register_namespace(name:, description: nil, is_global: false, retention_period: 10, data: nil) - request = Temporal::Api::WorkflowService::V1::RegisterNamespaceRequest.new( + request = Temporalio::Api::WorkflowService::V1::RegisterNamespaceRequest.new( namespace: name, description: description, is_global_namespace: is_global, @@ -57,19 +57,19 @@ def register_namespace(name:, description: nil, is_global: false, retention_peri end def describe_namespace(name:) - request = Temporal::Api::WorkflowService::V1::DescribeNamespaceRequest.new(namespace: name) + request = Temporalio::Api::WorkflowService::V1::DescribeNamespaceRequest.new(namespace: name) client.describe_namespace(request) end def list_namespaces(page_size:, next_page_token: "") - request = Temporal::Api::WorkflowService::V1::ListNamespacesRequest.new(page_size: page_size, next_page_token: next_page_token) + request = Temporalio::Api::WorkflowService::V1::ListNamespacesRequest.new(page_size: page_size, next_page_token: next_page_token) client.list_namespaces(request) end def update_namespace(name:, description:) - request = Temporal::Api::WorkflowService::V1::UpdateNamespaceRequest.new( + request = Temporalio::Api::WorkflowService::V1::UpdateNamespaceRequest.new( namespace: name, - update_info: Temporal::Api::WorkflowService::V1::UpdateNamespaceInfo.new( + update_info: Temporalio::Api::WorkflowService::V1::UpdateNamespaceInfo.new( description: description ) ) @@ -77,7 +77,7 @@ def update_namespace(name:, description:) end def deprecate_namespace(name:) - request = Temporal::Api::WorkflowService::V1::DeprecateNamespaceRequest.new(namespace: name) + request = Temporalio::Api::WorkflowService::V1::DeprecateNamespaceRequest.new(namespace: name) client.deprecate_namespace(request) end @@ -96,15 +96,15 @@ def start_workflow_execution( memo: nil, search_attributes: nil ) - request = Temporal::Api::WorkflowService::V1::StartWorkflowExecutionRequest.new( + request = Temporalio::Api::WorkflowService::V1::StartWorkflowExecutionRequest.new( identity: identity, namespace: namespace, - workflow_type: Temporal::Api::Common::V1::WorkflowType.new( + workflow_type: Temporalio::Api::Common::V1::WorkflowType.new( name: workflow_name ), workflow_id: workflow_id, workflow_id_reuse_policy: Temporal::Connection::Serializer::WorkflowIdReusePolicy.new(workflow_id_reuse_policy).to_proto, - task_queue: Temporal::Api::TaskQueue::V1::TaskQueue.new( + task_queue: Temporalio::Api::TaskQueue::V1::TaskQueue.new( name: task_queue ), input: to_payloads(input), @@ -112,14 +112,14 @@ def start_workflow_execution( workflow_run_timeout: run_timeout, workflow_task_timeout: task_timeout, request_id: SecureRandom.uuid, - header: Temporal::Api::Common::V1::Header.new( + header: Temporalio::Api::Common::V1::Header.new( fields: to_payload_map(headers || {}) ), cron_schedule: cron_schedule, - memo: Temporal::Api::Common::V1::Memo.new( + memo: Temporalio::Api::Common::V1::Memo.new( fields: to_payload_map(memo || {}) ), - search_attributes: Temporal::Api::Common::V1::SearchAttributes.new( + search_attributes: Temporalio::Api::Common::V1::SearchAttributes.new( indexed_fields: to_payload_map(search_attributes || {}) ), ) @@ -152,9 +152,9 @@ def get_workflow_execution_history( ) end end - request = Temporal::Api::WorkflowService::V1::GetWorkflowExecutionHistoryRequest.new( + request = Temporalio::Api::WorkflowService::V1::GetWorkflowExecutionHistoryRequest.new( namespace: namespace, - execution: Temporal::Api::Common::V1::WorkflowExecution.new( + execution: Temporalio::Api::Common::V1::WorkflowExecution.new( workflow_id: workflow_id, run_id: run_id ), @@ -167,10 +167,10 @@ def get_workflow_execution_history( end def poll_workflow_task_queue(namespace:, task_queue:, binary_checksum:) - request = Temporal::Api::WorkflowService::V1::PollWorkflowTaskQueueRequest.new( + request = Temporalio::Api::WorkflowService::V1::PollWorkflowTaskQueueRequest.new( identity: identity, namespace: namespace, - task_queue: Temporal::Api::TaskQueue::V1::TaskQueue.new( + task_queue: Temporalio::Api::TaskQueue::V1::TaskQueue.new( name: task_queue ), binary_checksum: binary_checksum @@ -186,7 +186,7 @@ def poll_workflow_task_queue(namespace:, task_queue:, binary_checksum:) def respond_query_task_completed(namespace:, task_token:, query_result:) query_result_proto = Serializer.serialize(query_result) - request = Temporal::Api::WorkflowService::V1::RespondQueryTaskCompletedRequest.new( + request = Temporalio::Api::WorkflowService::V1::RespondQueryTaskCompletedRequest.new( task_token: task_token, namespace: namespace, completed_type: query_result_proto.result_type, @@ -198,7 +198,7 @@ def respond_query_task_completed(namespace:, task_token:, query_result:) end def respond_workflow_task_completed(namespace:, task_token:, commands:, binary_checksum:, query_results: {}) - request = Temporal::Api::WorkflowService::V1::RespondWorkflowTaskCompletedRequest.new( + request = Temporalio::Api::WorkflowService::V1::RespondWorkflowTaskCompletedRequest.new( namespace: namespace, identity: identity, task_token: task_token, @@ -211,7 +211,7 @@ def respond_workflow_task_completed(namespace:, task_token:, commands:, binary_c end def respond_workflow_task_failed(namespace:, task_token:, cause:, exception:, binary_checksum:) - request = Temporal::Api::WorkflowService::V1::RespondWorkflowTaskFailedRequest.new( + request = Temporalio::Api::WorkflowService::V1::RespondWorkflowTaskFailedRequest.new( namespace: namespace, identity: identity, task_token: task_token, @@ -223,10 +223,10 @@ def respond_workflow_task_failed(namespace:, task_token:, cause:, exception:, bi end def poll_activity_task_queue(namespace:, task_queue:) - request = Temporal::Api::WorkflowService::V1::PollActivityTaskQueueRequest.new( + request = Temporalio::Api::WorkflowService::V1::PollActivityTaskQueueRequest.new( identity: identity, namespace: namespace, - task_queue: Temporal::Api::TaskQueue::V1::TaskQueue.new( + task_queue: Temporalio::Api::TaskQueue::V1::TaskQueue.new( name: task_queue ) ) @@ -240,7 +240,7 @@ def poll_activity_task_queue(namespace:, task_queue:) end def record_activity_task_heartbeat(namespace:, task_token:, details: nil) - request = Temporal::Api::WorkflowService::V1::RecordActivityTaskHeartbeatRequest.new( + request = Temporalio::Api::WorkflowService::V1::RecordActivityTaskHeartbeatRequest.new( namespace: namespace, task_token: task_token, details: to_details_payloads(details), @@ -254,7 +254,7 @@ def record_activity_task_heartbeat_by_id end def respond_activity_task_completed(namespace:, task_token:, result:) - request = Temporal::Api::WorkflowService::V1::RespondActivityTaskCompletedRequest.new( + request = Temporalio::Api::WorkflowService::V1::RespondActivityTaskCompletedRequest.new( namespace: namespace, identity: identity, task_token: task_token, @@ -264,7 +264,7 @@ def respond_activity_task_completed(namespace:, task_token:, result:) end def respond_activity_task_completed_by_id(namespace:, activity_id:, workflow_id:, run_id:, result:) - request = Temporal::Api::WorkflowService::V1::RespondActivityTaskCompletedByIdRequest.new( + request = Temporalio::Api::WorkflowService::V1::RespondActivityTaskCompletedByIdRequest.new( identity: identity, namespace: namespace, workflow_id: workflow_id, @@ -277,7 +277,7 @@ def respond_activity_task_completed_by_id(namespace:, activity_id:, workflow_id: def respond_activity_task_failed(namespace:, task_token:, exception:) serialize_whole_error = Temporal.configuration.use_error_serialization_v2 - request = Temporal::Api::WorkflowService::V1::RespondActivityTaskFailedRequest.new( + request = Temporalio::Api::WorkflowService::V1::RespondActivityTaskFailedRequest.new( namespace: namespace, identity: identity, task_token: task_token, @@ -287,7 +287,7 @@ def respond_activity_task_failed(namespace:, task_token:, exception:) end def respond_activity_task_failed_by_id(namespace:, activity_id:, workflow_id:, run_id:, exception:) - request = Temporal::Api::WorkflowService::V1::RespondActivityTaskFailedByIdRequest.new( + request = Temporalio::Api::WorkflowService::V1::RespondActivityTaskFailedByIdRequest.new( identity: identity, namespace: namespace, workflow_id: workflow_id, @@ -299,7 +299,7 @@ def respond_activity_task_failed_by_id(namespace:, activity_id:, workflow_id:, r end def respond_activity_task_canceled(namespace:, task_token:, details: nil) - request = Temporal::Api::WorkflowService::V1::RespondActivityTaskCanceledRequest.new( + request = Temporalio::Api::WorkflowService::V1::RespondActivityTaskCanceledRequest.new( namespace: namespace, task_token: task_token, details: to_details_payloads(details), @@ -317,9 +317,9 @@ def request_cancel_workflow_execution end def signal_workflow_execution(namespace:, workflow_id:, run_id:, signal:, input: nil) - request = Temporal::Api::WorkflowService::V1::SignalWorkflowExecutionRequest.new( + request = Temporalio::Api::WorkflowService::V1::SignalWorkflowExecutionRequest.new( namespace: namespace, - workflow_execution: Temporal::Api::Common::V1::WorkflowExecution.new( + workflow_execution: Temporalio::Api::Common::V1::WorkflowExecution.new( workflow_id: workflow_id, run_id: run_id ), @@ -357,15 +357,15 @@ def signal_with_start_workflow_execution( headers end - request = Temporal::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionRequest.new( + request = Temporalio::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionRequest.new( identity: identity, namespace: namespace, - workflow_type: Temporal::Api::Common::V1::WorkflowType.new( + workflow_type: Temporalio::Api::Common::V1::WorkflowType.new( name: workflow_name ), workflow_id: workflow_id, workflow_id_reuse_policy: Temporal::Connection::Serializer::WorkflowIdReusePolicy.new(workflow_id_reuse_policy).to_proto, - task_queue: Temporal::Api::TaskQueue::V1::TaskQueue.new( + task_queue: Temporalio::Api::TaskQueue::V1::TaskQueue.new( name: task_queue ), input: to_payloads(input), @@ -373,16 +373,16 @@ def signal_with_start_workflow_execution( workflow_run_timeout: run_timeout, workflow_task_timeout: task_timeout, request_id: SecureRandom.uuid, - header: Temporal::Api::Common::V1::Header.new( + header: Temporalio::Api::Common::V1::Header.new( fields: proto_header_fields, ), cron_schedule: cron_schedule, signal_name: signal_name, signal_input: to_signal_payloads(signal_input), - memo: Temporal::Api::Common::V1::Memo.new( + memo: Temporalio::Api::Common::V1::Memo.new( fields: to_payload_map(memo || {}) ), - search_attributes: Temporal::Api::Common::V1::SearchAttributes.new( + search_attributes: Temporalio::Api::Common::V1::SearchAttributes.new( indexed_fields: to_payload_map(search_attributes || {}) ), ) @@ -391,9 +391,9 @@ def signal_with_start_workflow_execution( end def reset_workflow_execution(namespace:, workflow_id:, run_id:, reason:, workflow_task_event_id:) - request = Temporal::Api::WorkflowService::V1::ResetWorkflowExecutionRequest.new( + request = Temporalio::Api::WorkflowService::V1::ResetWorkflowExecutionRequest.new( namespace: namespace, - workflow_execution: Temporal::Api::Common::V1::WorkflowExecution.new( + workflow_execution: Temporalio::Api::Common::V1::WorkflowExecution.new( workflow_id: workflow_id, run_id: run_id, ), @@ -410,10 +410,10 @@ def terminate_workflow_execution( reason: nil, details: nil ) - request = Temporal::Api::WorkflowService::V1::TerminateWorkflowExecutionRequest.new( + request = Temporalio::Api::WorkflowService::V1::TerminateWorkflowExecutionRequest.new( identity: identity, namespace: namespace, - workflow_execution: Temporal::Api::Common::V1::WorkflowExecution.new( + workflow_execution: Temporalio::Api::Common::V1::WorkflowExecution.new( workflow_id: workflow_id, run_id: run_id, ), @@ -425,7 +425,7 @@ def terminate_workflow_execution( end def list_open_workflow_executions(namespace:, from:, to:, next_page_token: nil, workflow_id: nil, workflow: nil, max_page_size: nil) - request = Temporal::Api::WorkflowService::V1::ListOpenWorkflowExecutionsRequest.new( + request = Temporalio::Api::WorkflowService::V1::ListOpenWorkflowExecutionsRequest.new( namespace: namespace, maximum_page_size: max_page_size.nil? ? options[:max_page_size] : max_page_size, next_page_token: next_page_token, @@ -437,7 +437,7 @@ def list_open_workflow_executions(namespace:, from:, to:, next_page_token: nil, end def list_closed_workflow_executions(namespace:, from:, to:, next_page_token: nil, workflow_id: nil, workflow: nil, status: nil, max_page_size: nil) - request = Temporal::Api::WorkflowService::V1::ListClosedWorkflowExecutionsRequest.new( + request = Temporalio::Api::WorkflowService::V1::ListClosedWorkflowExecutionsRequest.new( namespace: namespace, maximum_page_size: max_page_size.nil? ? options[:max_page_size] : max_page_size, next_page_token: next_page_token, @@ -450,7 +450,7 @@ def list_closed_workflow_executions(namespace:, from:, to:, next_page_token: nil end def list_workflow_executions(namespace:, query:, next_page_token: nil, max_page_size: nil) - request = Temporal::Api::WorkflowService::V1::ListWorkflowExecutionsRequest.new( + request = Temporalio::Api::WorkflowService::V1::ListWorkflowExecutionsRequest.new( namespace: namespace, page_size: max_page_size.nil? ? options[:max_page_size] : max_page_size, next_page_token: next_page_token, @@ -468,7 +468,7 @@ def scan_workflow_executions end def count_workflow_executions(namespace:, query:) - request = Temporal::Api::WorkflowService::V1::CountWorkflowExecutionsRequest.new( + request = Temporalio::Api::WorkflowService::V1::CountWorkflowExecutionsRequest.new( namespace: namespace, query: query ) @@ -484,13 +484,13 @@ def reset_sticky_task_queue end def query_workflow(namespace:, workflow_id:, run_id:, query:, args: nil, query_reject_condition: nil) - request = Temporal::Api::WorkflowService::V1::QueryWorkflowRequest.new( + request = Temporalio::Api::WorkflowService::V1::QueryWorkflowRequest.new( namespace: namespace, - execution: Temporal::Api::Common::V1::WorkflowExecution.new( + execution: Temporalio::Api::Common::V1::WorkflowExecution.new( workflow_id: workflow_id, run_id: run_id ), - query: Temporal::Api::Query::V1::WorkflowQuery.new( + query: Temporalio::Api::Query::V1::WorkflowQuery.new( query_type: query, query_args: to_query_payloads(args) ) @@ -519,9 +519,9 @@ def query_workflow(namespace:, workflow_id:, run_id:, query:, args: nil, query_r end def describe_workflow_execution(namespace:, workflow_id:, run_id:) - request = Temporal::Api::WorkflowService::V1::DescribeWorkflowExecutionRequest.new( + request = Temporalio::Api::WorkflowService::V1::DescribeWorkflowExecutionRequest.new( namespace: namespace, - execution: Temporal::Api::Common::V1::WorkflowExecution.new( + execution: Temporalio::Api::Common::V1::WorkflowExecution.new( workflow_id: workflow_id, run_id: run_id ) @@ -530,12 +530,12 @@ def describe_workflow_execution(namespace:, workflow_id:, run_id:) end def describe_task_queue(namespace:, task_queue:) - request = Temporal::Api::WorkflowService::V1::DescribeTaskQueueRequest.new( + request = Temporalio::Api::WorkflowService::V1::DescribeTaskQueueRequest.new( namespace: namespace, - task_queue: Temporal::Api::TaskQueue::V1::TaskQueue.new( + task_queue: Temporalio::Api::TaskQueue::V1::TaskQueue.new( name: task_queue ), - task_queue_type: Temporal::Api::Enums::V1::TaskQueueType::Workflow, + task_queue_type: Temporalio::Api::Enums::V1::TaskQueueType::Workflow, include_task_queue_status: true ) client.describe_task_queue(request) @@ -553,7 +553,7 @@ def cancel_polling_request attr_reader :url, :identity, :credentials, :options, :poll_mutex, :poll_request def client - @client ||= Temporal::Api::WorkflowService::V1::WorkflowService::Stub.new( + @client ||= Temporalio::Api::WorkflowService::V1::WorkflowService::Stub.new( url, credentials, timeout: 60 @@ -565,7 +565,7 @@ def can_poll? end def serialize_time_filter(from, to) - Temporal::Api::Filter::V1::StartTimeFilter.new( + Temporalio::Api::Filter::V1::StartTimeFilter.new( earliest_time: from&.to_time, latest_time: to&.to_time ) @@ -574,22 +574,22 @@ def serialize_time_filter(from, to) def serialize_execution_filter(value) return unless value - Temporal::Api::Filter::V1::WorkflowExecutionFilter.new(workflow_id: value) + Temporalio::Api::Filter::V1::WorkflowExecutionFilter.new(workflow_id: value) end def serialize_type_filter(value) return unless value - Temporal::Api::Filter::V1::WorkflowTypeFilter.new(name: value) + Temporalio::Api::Filter::V1::WorkflowTypeFilter.new(name: value) end def serialize_status_filter(value) return unless value sym = Temporal::Workflow::Status::API_STATUS_MAP.invert[value] - status = Temporal::Api::Enums::V1::WorkflowExecutionStatus.resolve(sym) + status = Temporalio::Api::Enums::V1::WorkflowExecutionStatus.resolve(sym) - Temporal::Api::Filter::V1::StatusFilter.new(status: status) + Temporalio::Api::Filter::V1::StatusFilter.new(status: status) end end end diff --git a/lib/temporal/connection/serializer/cancel_timer.rb b/lib/temporal/connection/serializer/cancel_timer.rb index a51eceb7..99215f04 100644 --- a/lib/temporal/connection/serializer/cancel_timer.rb +++ b/lib/temporal/connection/serializer/cancel_timer.rb @@ -5,10 +5,10 @@ module Connection module Serializer class CancelTimer < Base def to_proto - Temporal::Api::Command::V1::Command.new( - command_type: Temporal::Api::Enums::V1::CommandType::COMMAND_TYPE_CANCEL_TIMER, + Temporalio::Api::Command::V1::Command.new( + command_type: Temporalio::Api::Enums::V1::CommandType::COMMAND_TYPE_CANCEL_TIMER, cancel_timer_command_attributes: - Temporal::Api::Command::V1::CancelTimerCommandAttributes.new( + Temporalio::Api::Command::V1::CancelTimerCommandAttributes.new( timer_id: object.timer_id.to_s ) ) diff --git a/lib/temporal/connection/serializer/complete_workflow.rb b/lib/temporal/connection/serializer/complete_workflow.rb index f228dbee..beb3b0ed 100644 --- a/lib/temporal/connection/serializer/complete_workflow.rb +++ b/lib/temporal/connection/serializer/complete_workflow.rb @@ -8,10 +8,10 @@ class CompleteWorkflow < Base include Concerns::Payloads def to_proto - Temporal::Api::Command::V1::Command.new( - command_type: Temporal::Api::Enums::V1::CommandType::COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, + Temporalio::Api::Command::V1::Command.new( + command_type: Temporalio::Api::Enums::V1::CommandType::COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, complete_workflow_execution_command_attributes: - Temporal::Api::Command::V1::CompleteWorkflowExecutionCommandAttributes.new( + Temporalio::Api::Command::V1::CompleteWorkflowExecutionCommandAttributes.new( result: to_result_payloads(object.result) ) ) diff --git a/lib/temporal/connection/serializer/continue_as_new.rb b/lib/temporal/connection/serializer/continue_as_new.rb index 2dc5d174..9a6a7ecf 100644 --- a/lib/temporal/connection/serializer/continue_as_new.rb +++ b/lib/temporal/connection/serializer/continue_as_new.rb @@ -9,12 +9,12 @@ class ContinueAsNew < Base include Concerns::Payloads def to_proto - Temporal::Api::Command::V1::Command.new( - command_type: Temporal::Api::Enums::V1::CommandType::COMMAND_TYPE_CONTINUE_AS_NEW_WORKFLOW_EXECUTION, + Temporalio::Api::Command::V1::Command.new( + command_type: Temporalio::Api::Enums::V1::CommandType::COMMAND_TYPE_CONTINUE_AS_NEW_WORKFLOW_EXECUTION, continue_as_new_workflow_execution_command_attributes: - Temporal::Api::Command::V1::ContinueAsNewWorkflowExecutionCommandAttributes.new( - workflow_type: Temporal::Api::Common::V1::WorkflowType.new(name: object.workflow_type), - task_queue: Temporal::Api::TaskQueue::V1::TaskQueue.new(name: object.task_queue), + Temporalio::Api::Command::V1::ContinueAsNewWorkflowExecutionCommandAttributes.new( + workflow_type: Temporalio::Api::Common::V1::WorkflowType.new(name: object.workflow_type), + task_queue: Temporalio::Api::TaskQueue::V1::TaskQueue.new(name: object.task_queue), input: to_payloads(object.input), workflow_run_timeout: object.timeouts[:execution], workflow_task_timeout: object.timeouts[:task], @@ -31,19 +31,19 @@ def to_proto def serialize_headers(headers) return unless headers - Temporal::Api::Common::V1::Header.new(fields: to_payload_map(headers)) + Temporalio::Api::Common::V1::Header.new(fields: to_payload_map(headers)) end def serialize_memo(memo) return unless memo - Temporal::Api::Common::V1::Memo.new(fields: to_payload_map(memo)) + Temporalio::Api::Common::V1::Memo.new(fields: to_payload_map(memo)) end def serialize_search_attributes(search_attributes) return unless search_attributes - Temporal::Api::Common::V1::SearchAttributes.new(indexed_fields: to_payload_map(search_attributes)) + Temporalio::Api::Common::V1::SearchAttributes.new(indexed_fields: to_payload_map(search_attributes)) end end end diff --git a/lib/temporal/connection/serializer/fail_workflow.rb b/lib/temporal/connection/serializer/fail_workflow.rb index 0cc79725..a6ef9ea0 100644 --- a/lib/temporal/connection/serializer/fail_workflow.rb +++ b/lib/temporal/connection/serializer/fail_workflow.rb @@ -6,10 +6,10 @@ module Connection module Serializer class FailWorkflow < Base def to_proto - Temporal::Api::Command::V1::Command.new( - command_type: Temporal::Api::Enums::V1::CommandType::COMMAND_TYPE_FAIL_WORKFLOW_EXECUTION, + Temporalio::Api::Command::V1::Command.new( + command_type: Temporalio::Api::Enums::V1::CommandType::COMMAND_TYPE_FAIL_WORKFLOW_EXECUTION, fail_workflow_execution_command_attributes: - Temporal::Api::Command::V1::FailWorkflowExecutionCommandAttributes.new( + Temporalio::Api::Command::V1::FailWorkflowExecutionCommandAttributes.new( failure: Failure.new(object.exception).to_proto ) ) diff --git a/lib/temporal/connection/serializer/failure.rb b/lib/temporal/connection/serializer/failure.rb index dbba5fb5..0a94a72f 100644 --- a/lib/temporal/connection/serializer/failure.rb +++ b/lib/temporal/connection/serializer/failure.rb @@ -18,10 +18,10 @@ def to_proto else to_details_payloads(object.message) end - Temporal::Api::Failure::V1::Failure.new( + Temporalio::Api::Failure::V1::Failure.new( message: object.message, stack_trace: stack_trace_from(object.backtrace), - application_failure_info: Temporal::Api::Failure::V1::ApplicationFailureInfo.new( + application_failure_info: Temporalio::Api::Failure::V1::ApplicationFailureInfo.new( type: object.class.name, details: details ) diff --git a/lib/temporal/connection/serializer/query_answer.rb b/lib/temporal/connection/serializer/query_answer.rb index 7f28ec06..746c50c0 100644 --- a/lib/temporal/connection/serializer/query_answer.rb +++ b/lib/temporal/connection/serializer/query_answer.rb @@ -8,8 +8,8 @@ class QueryAnswer < Base include Concerns::Payloads def to_proto - Temporal::Api::Query::V1::WorkflowQueryResult.new( - result_type: Temporal::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_ANSWERED, + Temporalio::Api::Query::V1::WorkflowQueryResult.new( + result_type: Temporalio::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_ANSWERED, answer: to_query_payloads(object.result) ) end diff --git a/lib/temporal/connection/serializer/query_failure.rb b/lib/temporal/connection/serializer/query_failure.rb index 0a2fca21..28256ed5 100644 --- a/lib/temporal/connection/serializer/query_failure.rb +++ b/lib/temporal/connection/serializer/query_failure.rb @@ -5,8 +5,8 @@ module Connection module Serializer class QueryFailure < Base def to_proto - Temporal::Api::Query::V1::WorkflowQueryResult.new( - result_type: Temporal::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_FAILED, + Temporalio::Api::Query::V1::WorkflowQueryResult.new( + result_type: Temporalio::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_FAILED, error_message: object.error.message ) end diff --git a/lib/temporal/connection/serializer/record_marker.rb b/lib/temporal/connection/serializer/record_marker.rb index 133d79dc..b29040f3 100644 --- a/lib/temporal/connection/serializer/record_marker.rb +++ b/lib/temporal/connection/serializer/record_marker.rb @@ -8,10 +8,10 @@ class RecordMarker < Base include Concerns::Payloads def to_proto - Temporal::Api::Command::V1::Command.new( - command_type: Temporal::Api::Enums::V1::CommandType::COMMAND_TYPE_RECORD_MARKER, + Temporalio::Api::Command::V1::Command.new( + command_type: Temporalio::Api::Enums::V1::CommandType::COMMAND_TYPE_RECORD_MARKER, record_marker_command_attributes: - Temporal::Api::Command::V1::RecordMarkerCommandAttributes.new( + Temporalio::Api::Command::V1::RecordMarkerCommandAttributes.new( marker_name: object.name, details: { 'data' => to_details_payloads(object.details) diff --git a/lib/temporal/connection/serializer/request_activity_cancellation.rb b/lib/temporal/connection/serializer/request_activity_cancellation.rb index 2cf51a65..fb341270 100644 --- a/lib/temporal/connection/serializer/request_activity_cancellation.rb +++ b/lib/temporal/connection/serializer/request_activity_cancellation.rb @@ -5,10 +5,10 @@ module Connection module Serializer class RequestActivityCancellation < Base def to_proto - Temporal::Api::Command::V1::Command.new( - command_type: Temporal::Api::Enums::V1::CommandType::COMMAND_TYPE_REQUEST_CANCEL_ACTIVITY_TASK, + Temporalio::Api::Command::V1::Command.new( + command_type: Temporalio::Api::Enums::V1::CommandType::COMMAND_TYPE_REQUEST_CANCEL_ACTIVITY_TASK, request_cancel_activity_task_command_attributes: - Temporal::Api::Command::V1::RequestCancelActivityTaskCommandAttributes.new( + Temporalio::Api::Command::V1::RequestCancelActivityTaskCommandAttributes.new( scheduled_event_id: object.activity_id.to_i ) ) diff --git a/lib/temporal/connection/serializer/retry_policy.rb b/lib/temporal/connection/serializer/retry_policy.rb index 58d42ab1..bea53786 100644 --- a/lib/temporal/connection/serializer/retry_policy.rb +++ b/lib/temporal/connection/serializer/retry_policy.rb @@ -16,7 +16,7 @@ def to_proto non_retryable_error_types: non_retriable_errors, }.compact - Temporal::Api::Common::V1::RetryPolicy.new(options) + Temporalio::Api::Common::V1::RetryPolicy.new(options) end end end diff --git a/lib/temporal/connection/serializer/schedule_activity.rb b/lib/temporal/connection/serializer/schedule_activity.rb index 93d3a207..bf9120f5 100644 --- a/lib/temporal/connection/serializer/schedule_activity.rb +++ b/lib/temporal/connection/serializer/schedule_activity.rb @@ -9,15 +9,14 @@ class ScheduleActivity < Base include Concerns::Payloads def to_proto - Temporal::Api::Command::V1::Command.new( - command_type: Temporal::Api::Enums::V1::CommandType::COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, + Temporalio::Api::Command::V1::Command.new( + command_type: Temporalio::Api::Enums::V1::CommandType::COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, schedule_activity_task_command_attributes: - Temporal::Api::Command::V1::ScheduleActivityTaskCommandAttributes.new( + Temporalio::Api::Command::V1::ScheduleActivityTaskCommandAttributes.new( activity_id: object.activity_id.to_s, - activity_type: Temporal::Api::Common::V1::ActivityType.new(name: object.activity_type), + activity_type: Temporalio::Api::Common::V1::ActivityType.new(name: object.activity_type), input: to_payloads(object.input), - namespace: object.namespace, - task_queue: Temporal::Api::TaskQueue::V1::TaskQueue.new(name: object.task_queue), + task_queue: Temporalio::Api::TaskQueue::V1::TaskQueue.new(name: object.task_queue), schedule_to_close_timeout: object.timeouts[:schedule_to_close], schedule_to_start_timeout: object.timeouts[:schedule_to_start], start_to_close_timeout: object.timeouts[:start_to_close], @@ -33,7 +32,7 @@ def to_proto def serialize_headers(headers) return unless headers - Temporal::Api::Common::V1::Header.new(fields: object.headers) + Temporalio::Api::Common::V1::Header.new(fields: object.headers) end end end diff --git a/lib/temporal/connection/serializer/signal_external_workflow.rb b/lib/temporal/connection/serializer/signal_external_workflow.rb index 91907edd..5cc640fd 100644 --- a/lib/temporal/connection/serializer/signal_external_workflow.rb +++ b/lib/temporal/connection/serializer/signal_external_workflow.rb @@ -8,10 +8,10 @@ class SignalExternalWorkflow < Base include Concerns::Payloads def to_proto - Temporal::Api::Command::V1::Command.new( - command_type: Temporal::Api::Enums::V1::CommandType::COMMAND_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION, + Temporalio::Api::Command::V1::Command.new( + command_type: Temporalio::Api::Enums::V1::CommandType::COMMAND_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION, signal_external_workflow_execution_command_attributes: - Temporal::Api::Command::V1::SignalExternalWorkflowExecutionCommandAttributes.new( + Temporalio::Api::Command::V1::SignalExternalWorkflowExecutionCommandAttributes.new( namespace: object.namespace, execution: serialize_execution(object.execution), signal_name: object.signal_name, @@ -25,7 +25,7 @@ def to_proto private def serialize_execution(execution) - Temporal::Api::Common::V1::WorkflowExecution.new(workflow_id: execution[:workflow_id], run_id: execution[:run_id]) + Temporalio::Api::Common::V1::WorkflowExecution.new(workflow_id: execution[:workflow_id], run_id: execution[:run_id]) end end end diff --git a/lib/temporal/connection/serializer/start_child_workflow.rb b/lib/temporal/connection/serializer/start_child_workflow.rb index 7d1c03e4..3cc3a0aa 100644 --- a/lib/temporal/connection/serializer/start_child_workflow.rb +++ b/lib/temporal/connection/serializer/start_child_workflow.rb @@ -10,20 +10,20 @@ class StartChildWorkflow < Base include Concerns::Payloads PARENT_CLOSE_POLICY = { - terminate: Temporal::Api::Enums::V1::ParentClosePolicy::PARENT_CLOSE_POLICY_TERMINATE, - abandon: Temporal::Api::Enums::V1::ParentClosePolicy::PARENT_CLOSE_POLICY_ABANDON, - request_cancel: Temporal::Api::Enums::V1::ParentClosePolicy::PARENT_CLOSE_POLICY_REQUEST_CANCEL, + terminate: Temporalio::Api::Enums::V1::ParentClosePolicy::PARENT_CLOSE_POLICY_TERMINATE, + abandon: Temporalio::Api::Enums::V1::ParentClosePolicy::PARENT_CLOSE_POLICY_ABANDON, + request_cancel: Temporalio::Api::Enums::V1::ParentClosePolicy::PARENT_CLOSE_POLICY_REQUEST_CANCEL, }.freeze def to_proto - Temporal::Api::Command::V1::Command.new( - command_type: Temporal::Api::Enums::V1::CommandType::COMMAND_TYPE_START_CHILD_WORKFLOW_EXECUTION, + Temporalio::Api::Command::V1::Command.new( + command_type: Temporalio::Api::Enums::V1::CommandType::COMMAND_TYPE_START_CHILD_WORKFLOW_EXECUTION, start_child_workflow_execution_command_attributes: - Temporal::Api::Command::V1::StartChildWorkflowExecutionCommandAttributes.new( + Temporalio::Api::Command::V1::StartChildWorkflowExecutionCommandAttributes.new( namespace: object.namespace, workflow_id: object.workflow_id.to_s, - workflow_type: Temporal::Api::Common::V1::WorkflowType.new(name: object.workflow_type), - task_queue: Temporal::Api::TaskQueue::V1::TaskQueue.new(name: object.task_queue), + workflow_type: Temporalio::Api::Common::V1::WorkflowType.new(name: object.workflow_type), + task_queue: Temporalio::Api::TaskQueue::V1::TaskQueue.new(name: object.task_queue), input: to_payloads(object.input), workflow_execution_timeout: object.timeouts[:execution], workflow_run_timeout: object.timeouts[:run], @@ -44,13 +44,13 @@ def to_proto def serialize_headers(headers) return unless headers - Temporal::Api::Common::V1::Header.new(fields: to_payload_map(headers)) + Temporalio::Api::Common::V1::Header.new(fields: to_payload_map(headers)) end def serialize_memo(memo) return unless memo - Temporal::Api::Common::V1::Memo.new(fields: to_payload_map(memo)) + Temporalio::Api::Common::V1::Memo.new(fields: to_payload_map(memo)) end def serialize_parent_close_policy(parent_close_policy) @@ -66,7 +66,7 @@ def serialize_parent_close_policy(parent_close_policy) def serialize_search_attributes(search_attributes) return unless search_attributes - Temporal::Api::Common::V1::SearchAttributes.new(indexed_fields: to_payload_map(search_attributes)) + Temporalio::Api::Common::V1::SearchAttributes.new(indexed_fields: to_payload_map(search_attributes)) end end end diff --git a/lib/temporal/connection/serializer/start_timer.rb b/lib/temporal/connection/serializer/start_timer.rb index 9ec313ea..6869dcb1 100644 --- a/lib/temporal/connection/serializer/start_timer.rb +++ b/lib/temporal/connection/serializer/start_timer.rb @@ -5,10 +5,10 @@ module Connection module Serializer class StartTimer < Base def to_proto - Temporal::Api::Command::V1::Command.new( - command_type: Temporal::Api::Enums::V1::CommandType::COMMAND_TYPE_START_TIMER, + Temporalio::Api::Command::V1::Command.new( + command_type: Temporalio::Api::Enums::V1::CommandType::COMMAND_TYPE_START_TIMER, start_timer_command_attributes: - Temporal::Api::Command::V1::StartTimerCommandAttributes.new( + Temporalio::Api::Command::V1::StartTimerCommandAttributes.new( timer_id: object.timer_id.to_s, start_to_fire_timeout: object.timeout ) diff --git a/lib/temporal/connection/serializer/upsert_search_attributes.rb b/lib/temporal/connection/serializer/upsert_search_attributes.rb index c11a8a0a..0af6b79f 100644 --- a/lib/temporal/connection/serializer/upsert_search_attributes.rb +++ b/lib/temporal/connection/serializer/upsert_search_attributes.rb @@ -8,11 +8,11 @@ class UpsertSearchAttributes < Base include Concerns::Payloads def to_proto - Temporal::Api::Command::V1::Command.new( - command_type: Temporal::Api::Enums::V1::CommandType::COMMAND_TYPE_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES, + Temporalio::Api::Command::V1::Command.new( + command_type: Temporalio::Api::Enums::V1::CommandType::COMMAND_TYPE_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES, upsert_workflow_search_attributes_command_attributes: - Temporal::Api::Command::V1::UpsertWorkflowSearchAttributesCommandAttributes.new( - search_attributes: Temporal::Api::Common::V1::SearchAttributes.new( + Temporalio::Api::Command::V1::UpsertWorkflowSearchAttributesCommandAttributes.new( + search_attributes: Temporalio::Api::Common::V1::SearchAttributes.new( indexed_fields: to_payload_map(object.search_attributes || {}) ), ) diff --git a/lib/temporal/connection/serializer/workflow_id_reuse_policy.rb b/lib/temporal/connection/serializer/workflow_id_reuse_policy.rb index b3040197..22ac6c51 100644 --- a/lib/temporal/connection/serializer/workflow_id_reuse_policy.rb +++ b/lib/temporal/connection/serializer/workflow_id_reuse_policy.rb @@ -6,9 +6,9 @@ module Serializer class WorkflowIdReusePolicy < Base WORKFLOW_ID_REUSE_POLICY = { - allow_failed: Temporal::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY, - allow: Temporal::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, - reject: Temporal::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE + allow_failed: Temporalio::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY, + allow: Temporalio::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, + reject: Temporalio::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE }.freeze def to_proto diff --git a/lib/temporal/metadata.rb b/lib/temporal/metadata.rb index 5cfb7a00..38f003b2 100644 --- a/lib/temporal/metadata.rb +++ b/lib/temporal/metadata.rb @@ -27,7 +27,7 @@ def generate_activity_metadata(task, namespace) ) end - # @param task [Temporal::Api::WorkflowService::V1::PollWorkflowTaskQueueResponse] + # @param task [Temporalio::Api::WorkflowService::V1::PollWorkflowTaskQueueResponse] # @param namespace [String] def generate_workflow_task_metadata(task, namespace) Metadata::WorkflowTask.new( diff --git a/lib/temporal/version.rb b/lib/temporal/version.rb index baa4f079..22474736 100644 --- a/lib/temporal/version.rb +++ b/lib/temporal/version.rb @@ -1,3 +1,3 @@ module Temporal - VERSION = '0.0.1'.freeze + VERSION = '0.0.2'.freeze end diff --git a/lib/temporal/workflow/command.rb b/lib/temporal/workflow/command.rb index b81f2deb..9d33aaea 100644 --- a/lib/temporal/workflow/command.rb +++ b/lib/temporal/workflow/command.rb @@ -2,7 +2,7 @@ module Temporal class Workflow module Command # TODO: Move these classes into their own directories under workflow/command/* - ScheduleActivity = Struct.new(:activity_type, :activity_id, :input, :namespace, :task_queue, :retry_policy, :timeouts, :headers, keyword_init: true) + ScheduleActivity = Struct.new(:activity_type, :activity_id, :input, :task_queue, :retry_policy, :timeouts, :headers, keyword_init: true) StartChildWorkflow = Struct.new(:workflow_type, :workflow_id, :input, :namespace, :task_queue, :retry_policy, :parent_close_policy, :timeouts, :headers, :cron_schedule, :memo, :workflow_id_reuse_policy, :search_attributes, keyword_init: true) ContinueAsNew = Struct.new(:workflow_type, :task_queue, :input, :timeouts, :retry_policy, :headers, :memo, :search_attributes, keyword_init: true) RequestActivityCancellation = Struct.new(:activity_id, keyword_init: true) diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index bbd5cb0b..7de79bb9 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -69,7 +69,6 @@ def execute_activity(activity_class, *input, **args) activity_id: options[:activity_id], activity_type: execution_options.name, input: input, - namespace: execution_options.namespace, task_queue: execution_options.task_queue, retry_policy: execution_options.retry_policy, timeouts: execution_options.timeouts, diff --git a/lib/temporal/workflow/errors.rb b/lib/temporal/workflow/errors.rb index d8194359..aea40170 100644 --- a/lib/temporal/workflow/errors.rb +++ b/lib/temporal/workflow/errors.rb @@ -6,7 +6,7 @@ class Errors extend Concerns::Payloads # Convert a failure returned from the server to an Error to raise to the client - # failure: Temporal::Api::Failure::V1::Failure + # failure: Temporalio::Api::Failure::V1::Failure def self.generate_error(failure, default_exception_class = StandardError) case failure.failure_info when :application_failure_info @@ -63,8 +63,8 @@ def self.generate_error(failure, default_exception_class = StandardError) end end - WORKFLOW_ALREADY_EXISTS_SYM = Temporal::Api::Enums::V1::StartChildWorkflowExecutionFailedCause.lookup( - Temporal::Api::Enums::V1::StartChildWorkflowExecutionFailedCause::START_CHILD_WORKFLOW_EXECUTION_FAILED_CAUSE_WORKFLOW_ALREADY_EXISTS + WORKFLOW_ALREADY_EXISTS_SYM = Temporalio::Api::Enums::V1::StartChildWorkflowExecutionFailedCause.lookup( + Temporalio::Api::Enums::V1::StartChildWorkflowExecutionFailedCause::START_CHILD_WORKFLOW_EXECUTION_FAILED_CAUSE_WORKFLOW_ALREADY_EXISTS ) def self.generate_error_for_child_workflow_start(cause, workflow_id) diff --git a/lib/temporal/workflow/task_processor.rb b/lib/temporal/workflow/task_processor.rb index 56057f5d..e0b867da 100644 --- a/lib/temporal/workflow/task_processor.rb +++ b/lib/temporal/workflow/task_processor.rb @@ -168,7 +168,7 @@ def fail_task(error) connection.respond_workflow_task_failed( namespace: namespace, task_token: task_token, - cause: Temporal::Api::Enums::V1::WorkflowTaskFailedCause::WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE, + cause: Temporalio::Api::Enums::V1::WorkflowTaskFailedCause::WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE, exception: error, binary_checksum: binary_checksum ) diff --git a/proto b/proto index 4c2f6a28..e4246bbd 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 4c2f6a281fa3fde8b0a24447de3e0d0f47d230b4 +Subproject commit e4246bbd59fd1f850bdd5be6a59d6d2f8e532d76 diff --git a/spec/fabricators/grpc/activity_task_fabricator.rb b/spec/fabricators/grpc/activity_task_fabricator.rb index 8e940a96..b0305f99 100644 --- a/spec/fabricators/grpc/activity_task_fabricator.rb +++ b/spec/fabricators/grpc/activity_task_fabricator.rb @@ -1,6 +1,6 @@ require 'securerandom' -Fabricator(:api_activity_task, from: Temporal::Api::WorkflowService::V1::PollActivityTaskQueueResponse) do +Fabricator(:api_activity_task, from: Temporalio::Api::WorkflowService::V1::PollActivityTaskQueueResponse) do transient :task_token, :activity_name, :headers activity_id { SecureRandom.uuid } @@ -17,6 +17,6 @@ fields = (attrs[:headers] || {}).each_with_object({}) do |(field, value), h| h[field] = Temporal.configuration.converter.to_payload(value) end - Temporal::Api::Common::V1::Header.new(fields: fields) + Temporalio::Api::Common::V1::Header.new(fields: fields) end end diff --git a/spec/fabricators/grpc/activity_type_fabricator.rb b/spec/fabricators/grpc/activity_type_fabricator.rb index eace3c89..b1e232ff 100644 --- a/spec/fabricators/grpc/activity_type_fabricator.rb +++ b/spec/fabricators/grpc/activity_type_fabricator.rb @@ -1,3 +1,3 @@ -Fabricator(:api_activity_type, from: Temporal::Api::Common::V1::ActivityType) do +Fabricator(:api_activity_type, from: Temporalio::Api::Common::V1::ActivityType) do name 'TestActivity' end diff --git a/spec/fabricators/grpc/application_failure_fabricator.rb b/spec/fabricators/grpc/application_failure_fabricator.rb index edf90c82..9d1396d8 100644 --- a/spec/fabricators/grpc/application_failure_fabricator.rb +++ b/spec/fabricators/grpc/application_failure_fabricator.rb @@ -3,12 +3,12 @@ class TestDeserializer include Temporal::Concerns::Payloads end # Simulates Temporal::Connection::Serializer::Failure -Fabricator(:api_application_failure, from: Temporal::Api::Failure::V1::Failure) do +Fabricator(:api_application_failure, from: Temporalio::Api::Failure::V1::Failure) do transient :error_class, :backtrace message { |attrs| attrs[:message] } stack_trace { |attrs| attrs[:backtrace].join("\n") } application_failure_info do |attrs| - Temporal::Api::Failure::V1::ApplicationFailureInfo.new( + Temporalio::Api::Failure::V1::ApplicationFailureInfo.new( type: attrs[:error_class], details: TestDeserializer.new.to_details_payloads(attrs[:message]), ) diff --git a/spec/fabricators/grpc/header_fabricator.rb b/spec/fabricators/grpc/header_fabricator.rb index c70c886d..c641e0c2 100644 --- a/spec/fabricators/grpc/header_fabricator.rb +++ b/spec/fabricators/grpc/header_fabricator.rb @@ -1,3 +1,3 @@ -Fabricator(:api_header, from: Temporal::Api::Common::V1::Header) do - fields { Google::Protobuf::Map.new(:string, :message, Temporal::Api::Common::V1::Payload) } +Fabricator(:api_header, from: Temporalio::Api::Common::V1::Header) do + fields { Google::Protobuf::Map.new(:string, :message, Temporalio::Api::Common::V1::Payload) } end diff --git a/spec/fabricators/grpc/history_event_fabricator.rb b/spec/fabricators/grpc/history_event_fabricator.rb index 957f717d..41b3a191 100644 --- a/spec/fabricators/grpc/history_event_fabricator.rb +++ b/spec/fabricators/grpc/history_event_fabricator.rb @@ -4,22 +4,22 @@ class TestSerializer extend Temporal::Concerns::Payloads end -Fabricator(:api_history_event, from: Temporal::Api::History::V1::HistoryEvent) do +Fabricator(:api_history_event, from: Temporalio::Api::History::V1::HistoryEvent) do event_id { 1 } event_time { Time.now } end Fabricator(:api_workflow_execution_started_event, from: :api_history_event) do transient :headers - event_type { Temporal::Api::Enums::V1::EventType::EVENT_TYPE_WORKFLOW_EXECUTION_STARTED } + event_type { Temporalio::Api::Enums::V1::EventType::EVENT_TYPE_WORKFLOW_EXECUTION_STARTED } event_time { Time.now } workflow_execution_started_event_attributes do |attrs| header_fields = (attrs[:headers] || {}).each_with_object({}) do |(field, value), h| h[field] = Temporal.configuration.converter.to_payload(value) end - header = Temporal::Api::Common::V1::Header.new(fields: header_fields) + header = Temporalio::Api::Common::V1::Header.new(fields: header_fields) - Temporal::Api::History::V1::WorkflowExecutionStartedEventAttributes.new( + Temporalio::Api::History::V1::WorkflowExecutionStartedEventAttributes.new( workflow_type: Fabricate(:api_workflow_type), task_queue: Fabricate(:api_task_queue), input: nil, @@ -36,9 +36,9 @@ class TestSerializer end Fabricator(:api_workflow_execution_completed_event, from: :api_history_event) do - event_type { Temporal::Api::Enums::V1::EventType::EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED } + event_type { Temporalio::Api::Enums::V1::EventType::EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED } workflow_execution_completed_event_attributes do |attrs| - Temporal::Api::History::V1::WorkflowExecutionCompletedEventAttributes.new( + Temporalio::Api::History::V1::WorkflowExecutionCompletedEventAttributes.new( result: nil, workflow_task_completed_event_id: attrs[:event_id] - 1 ) @@ -46,9 +46,9 @@ class TestSerializer end Fabricator(:api_workflow_task_scheduled_event, from: :api_history_event) do - event_type { Temporal::Api::Enums::V1::EventType::EVENT_TYPE_WORKFLOW_TASK_SCHEDULED } + event_type { Temporalio::Api::Enums::V1::EventType::EVENT_TYPE_WORKFLOW_TASK_SCHEDULED } workflow_task_scheduled_event_attributes do |attrs| - Temporal::Api::History::V1::WorkflowTaskScheduledEventAttributes.new( + Temporalio::Api::History::V1::WorkflowTaskScheduledEventAttributes.new( task_queue: Fabricate(:api_task_queue), start_to_close_timeout: 15, attempt: 0 @@ -57,9 +57,9 @@ class TestSerializer end Fabricator(:api_workflow_task_started_event, from: :api_history_event) do - event_type { Temporal::Api::Enums::V1::EventType::EVENT_TYPE_WORKFLOW_TASK_STARTED } + event_type { Temporalio::Api::Enums::V1::EventType::EVENT_TYPE_WORKFLOW_TASK_STARTED } workflow_task_started_event_attributes do |attrs| - Temporal::Api::History::V1::WorkflowTaskStartedEventAttributes.new( + Temporalio::Api::History::V1::WorkflowTaskStartedEventAttributes.new( scheduled_event_id: attrs[:event_id] - 1, identity: 'test-worker@test-host', request_id: SecureRandom.uuid @@ -68,9 +68,9 @@ class TestSerializer end Fabricator(:api_workflow_task_completed_event, from: :api_history_event) do - event_type { Temporal::Api::Enums::V1::EventType::EVENT_TYPE_WORKFLOW_TASK_COMPLETED } + event_type { Temporalio::Api::Enums::V1::EventType::EVENT_TYPE_WORKFLOW_TASK_COMPLETED } workflow_task_completed_event_attributes do |attrs| - Temporal::Api::History::V1::WorkflowTaskCompletedEventAttributes.new( + Temporalio::Api::History::V1::WorkflowTaskCompletedEventAttributes.new( scheduled_event_id: attrs[:event_id] - 2, started_event_id: attrs[:event_id] - 1, identity: 'test-worker@test-host', @@ -80,22 +80,21 @@ class TestSerializer end Fabricator(:api_activity_task_scheduled_event, from: :api_history_event) do - event_type { Temporal::Api::Enums::V1::EventType::EVENT_TYPE_ACTIVITY_TASK_SCHEDULED } + event_type { Temporalio::Api::Enums::V1::EventType::EVENT_TYPE_ACTIVITY_TASK_SCHEDULED } activity_task_scheduled_event_attributes do |attrs| - Temporal::Api::History::V1::ActivityTaskScheduledEventAttributes.new( + Temporalio::Api::History::V1::ActivityTaskScheduledEventAttributes.new( activity_id: attrs[:event_id].to_s, - activity_type: Temporal::Api::Common::V1::ActivityType.new(name: 'TestActivity'), + activity_type: Temporalio::Api::Common::V1::ActivityType.new(name: 'TestActivity'), workflow_task_completed_event_id: attrs[:event_id] - 1, - namespace: 'test-namespace', task_queue: Fabricate(:api_task_queue) ) end end Fabricator(:api_activity_task_started_event, from: :api_history_event) do - event_type { Temporal::Api::Enums::V1::EventType::EVENT_TYPE_ACTIVITY_TASK_STARTED } + event_type { Temporalio::Api::Enums::V1::EventType::EVENT_TYPE_ACTIVITY_TASK_STARTED } activity_task_started_event_attributes do |attrs| - Temporal::Api::History::V1::ActivityTaskStartedEventAttributes.new( + Temporalio::Api::History::V1::ActivityTaskStartedEventAttributes.new( scheduled_event_id: attrs[:event_id] - 1, identity: 'test-worker@test-host', request_id: SecureRandom.uuid @@ -104,9 +103,9 @@ class TestSerializer end Fabricator(:api_activity_task_completed_event, from: :api_history_event) do - event_type { Temporal::Api::Enums::V1::EventType::EVENT_TYPE_ACTIVITY_TASK_COMPLETED } + event_type { Temporalio::Api::Enums::V1::EventType::EVENT_TYPE_ACTIVITY_TASK_COMPLETED } activity_task_completed_event_attributes do |attrs| - Temporal::Api::History::V1::ActivityTaskCompletedEventAttributes.new( + Temporalio::Api::History::V1::ActivityTaskCompletedEventAttributes.new( result: nil, scheduled_event_id: attrs[:event_id] - 2, started_event_id: attrs[:event_id] - 1, @@ -116,10 +115,10 @@ class TestSerializer end Fabricator(:api_activity_task_failed_event, from: :api_history_event) do - event_type { Temporal::Api::Enums::V1::EventType::EVENT_TYPE_ACTIVITY_TASK_FAILED } + event_type { Temporalio::Api::Enums::V1::EventType::EVENT_TYPE_ACTIVITY_TASK_FAILED } activity_task_failed_event_attributes do |attrs| - Temporal::Api::History::V1::ActivityTaskFailedEventAttributes.new( - failure: Temporal::Api::Failure::V1::Failure.new(message: "Activity failed"), + Temporalio::Api::History::V1::ActivityTaskFailedEventAttributes.new( + failure: Temporalio::Api::Failure::V1::Failure.new(message: "Activity failed"), scheduled_event_id: attrs[:event_id] - 2, started_event_id: attrs[:event_id] - 1, identity: 'test-worker@test-host' @@ -128,9 +127,9 @@ class TestSerializer end Fabricator(:api_activity_task_canceled_event, from: :api_history_event) do - event_type { Temporal::Api::Enums::V1::EventType::EVENT_TYPE_ACTIVITY_TASK_CANCELED } + event_type { Temporalio::Api::Enums::V1::EventType::EVENT_TYPE_ACTIVITY_TASK_CANCELED } activity_task_canceled_event_attributes do |attrs| - Temporal::Api::History::V1::ActivityTaskCanceledEventAttributes.new( + Temporalio::Api::History::V1::ActivityTaskCanceledEventAttributes.new( details: TestSerializer.to_details_payloads('ACTIVITY_ID_NOT_STARTED'), scheduled_event_id: attrs[:event_id] - 2, started_event_id: nil, @@ -140,9 +139,9 @@ class TestSerializer end Fabricator(:api_activity_task_cancel_requested_event, from: :api_history_event) do - event_type { Temporal::Api::Enums::V1::EventType::EVENT_TYPE_ACTIVITY_TASK_CANCEL_REQUESTED } + event_type { Temporalio::Api::Enums::V1::EventType::EVENT_TYPE_ACTIVITY_TASK_CANCEL_REQUESTED } activity_task_cancel_requested_event_attributes do |attrs| - Temporal::Api::History::V1::ActivityTaskCancelRequestedEventAttributes.new( + Temporalio::Api::History::V1::ActivityTaskCancelRequestedEventAttributes.new( scheduled_event_id: attrs[:event_id] - 1, workflow_task_completed_event_id: attrs[:event_id] - 2, ) @@ -150,9 +149,9 @@ class TestSerializer end Fabricator(:api_timer_started_event, from: :api_history_event) do - event_type { Temporal::Api::Enums::V1::EventType::EVENT_TYPE_TIMER_STARTED } + event_type { Temporalio::Api::Enums::V1::EventType::EVENT_TYPE_TIMER_STARTED } timer_started_event_attributes do |attrs| - Temporal::Api::History::V1::TimerStartedEventAttributes.new( + Temporalio::Api::History::V1::TimerStartedEventAttributes.new( timer_id: attrs[:event_id].to_s, start_to_fire_timeout: 10, workflow_task_completed_event_id: attrs[:event_id] - 1 @@ -161,9 +160,9 @@ class TestSerializer end Fabricator(:api_timer_fired_event, from: :api_history_event) do - event_type { Temporal::Api::Enums::V1::EventType::EVENT_TYPE_TIMER_FIRED } + event_type { Temporalio::Api::Enums::V1::EventType::EVENT_TYPE_TIMER_FIRED } timer_fired_event_attributes do |attrs| - Temporal::Api::History::V1::TimerFiredEventAttributes.new( + Temporalio::Api::History::V1::TimerFiredEventAttributes.new( timer_id: attrs[:event_id].to_s, started_event_id: attrs[:event_id] - 4 ) @@ -171,9 +170,9 @@ class TestSerializer end Fabricator(:api_timer_canceled_event, from: :api_history_event) do - event_type { Temporal::Api::Enums::V1::EventType::EVENT_TYPE_TIMER_CANCELED } + event_type { Temporalio::Api::Enums::V1::EventType::EVENT_TYPE_TIMER_CANCELED } timer_canceled_event_attributes do |attrs| - Temporal::Api::History::V1::TimerCanceledEventAttributes.new( + Temporalio::Api::History::V1::TimerCanceledEventAttributes.new( timer_id: attrs[:event_id].to_s, started_event_id: attrs[:event_id] - 4, workflow_task_completed_event_id: attrs[:event_id] - 1, diff --git a/spec/fabricators/grpc/memo_fabricator.rb b/spec/fabricators/grpc/memo_fabricator.rb index 6c9fd726..38f764f2 100644 --- a/spec/fabricators/grpc/memo_fabricator.rb +++ b/spec/fabricators/grpc/memo_fabricator.rb @@ -1,6 +1,6 @@ -Fabricator(:memo, from: Temporal::Api::Common::V1::Memo) do +Fabricator(:memo, from: Temporalio::Api::Common::V1::Memo) do fields do - Google::Protobuf::Map.new(:string, :message, Temporal::Api::Common::V1::Payload).tap do |m| + Google::Protobuf::Map.new(:string, :message, Temporalio::Api::Common::V1::Payload).tap do |m| m['foo'] = Temporal.configuration.converter.to_payload('bar') end end diff --git a/spec/fabricators/grpc/payload_fabricator.rb b/spec/fabricators/grpc/payload_fabricator.rb index d6476915..badd8f36 100644 --- a/spec/fabricators/grpc/payload_fabricator.rb +++ b/spec/fabricators/grpc/payload_fabricator.rb @@ -1,3 +1,3 @@ -Fabricator(:api_payload, from: Temporal::Api::Common::V1::Payload) do +Fabricator(:api_payload, from: Temporalio::Api::Common::V1::Payload) do metadata { Google::Protobuf::Map.new(:string, :bytes) } end diff --git a/spec/fabricators/grpc/search_attributes_fabricator.rb b/spec/fabricators/grpc/search_attributes_fabricator.rb index f201abd7..16a33675 100644 --- a/spec/fabricators/grpc/search_attributes_fabricator.rb +++ b/spec/fabricators/grpc/search_attributes_fabricator.rb @@ -1,6 +1,6 @@ -Fabricator(:search_attributes, from: Temporal::Api::Common::V1::SearchAttributes) do +Fabricator(:search_attributes, from: Temporalio::Api::Common::V1::SearchAttributes) do indexed_fields do - Google::Protobuf::Map.new(:string, :message, Temporal::Api::Common::V1::Payload).tap do |m| + Google::Protobuf::Map.new(:string, :message, Temporalio::Api::Common::V1::Payload).tap do |m| m['foo'] = Temporal.configuration.converter.to_payload('bar') end end diff --git a/spec/fabricators/grpc/task_queue_fabricator.rb b/spec/fabricators/grpc/task_queue_fabricator.rb index 635f7aff..761ef867 100644 --- a/spec/fabricators/grpc/task_queue_fabricator.rb +++ b/spec/fabricators/grpc/task_queue_fabricator.rb @@ -1,3 +1,3 @@ -Fabricator(:api_task_queue, from: Temporal::Api::TaskQueue::V1::TaskQueue) do +Fabricator(:api_task_queue, from: Temporalio::Api::TaskQueue::V1::TaskQueue) do name 'test-task-queue' end diff --git a/spec/fabricators/grpc/workflow_execution_fabricator.rb b/spec/fabricators/grpc/workflow_execution_fabricator.rb index 385cd508..32150daa 100644 --- a/spec/fabricators/grpc/workflow_execution_fabricator.rb +++ b/spec/fabricators/grpc/workflow_execution_fabricator.rb @@ -1,6 +1,6 @@ require 'securerandom' -Fabricator(:api_workflow_execution, from: Temporal::Api::Common::V1::WorkflowExecution) do +Fabricator(:api_workflow_execution, from: Temporalio::Api::Common::V1::WorkflowExecution) do run_id { SecureRandom.uuid } workflow_id { SecureRandom.uuid } end diff --git a/spec/fabricators/grpc/workflow_execution_info_fabricator.rb b/spec/fabricators/grpc/workflow_execution_info_fabricator.rb index cbd6a916..efba5cea 100644 --- a/spec/fabricators/grpc/workflow_execution_info_fabricator.rb +++ b/spec/fabricators/grpc/workflow_execution_info_fabricator.rb @@ -1,11 +1,11 @@ -Fabricator(:api_workflow_execution_info, from: Temporal::Api::Workflow::V1::WorkflowExecutionInfo) do +Fabricator(:api_workflow_execution_info, from: Temporalio::Api::Workflow::V1::WorkflowExecutionInfo) do transient :workflow_id, :workflow execution { |attrs| Fabricate(:api_workflow_execution, workflow_id: attrs[:workflow_id]) } type { |attrs| Fabricate(:api_workflow_type, name: attrs[:workflow]) } start_time { Google::Protobuf::Timestamp.new.tap { |t| t.from_time(Time.now) } } close_time { Google::Protobuf::Timestamp.new.tap { |t| t.from_time(Time.now) } } - status { Temporal::Api::Enums::V1::WorkflowExecutionStatus::WORKFLOW_EXECUTION_STATUS_COMPLETED } + status { Temporalio::Api::Enums::V1::WorkflowExecutionStatus::WORKFLOW_EXECUTION_STATUS_COMPLETED } history_length { rand(100) } memo { Fabricate(:memo) } search_attributes { Fabricate(:search_attributes) } diff --git a/spec/fabricators/grpc/workflow_execution_started_event_attributes_fabricator.rb b/spec/fabricators/grpc/workflow_execution_started_event_attributes_fabricator.rb index 0cc19e16..172bd7a5 100644 --- a/spec/fabricators/grpc/workflow_execution_started_event_attributes_fabricator.rb +++ b/spec/fabricators/grpc/workflow_execution_started_event_attributes_fabricator.rb @@ -2,7 +2,7 @@ Fabricator( :api_workflow_execution_started_event_attributes, - from: Temporal::Api::History::V1::WorkflowExecutionStartedEventAttributes + from: Temporalio::Api::History::V1::WorkflowExecutionStartedEventAttributes ) do transient :headers @@ -14,6 +14,6 @@ fields = (attrs[:headers] || {}).each_with_object({}) do |(field, value), h| h[field] = Temporal.configuration.converter.to_payload(value) end - Temporal::Api::Common::V1::Header.new(fields: fields) + Temporalio::Api::Common::V1::Header.new(fields: fields) end end diff --git a/spec/fabricators/grpc/workflow_query_fabricator.rb b/spec/fabricators/grpc/workflow_query_fabricator.rb index dcabbb24..024cdd59 100644 --- a/spec/fabricators/grpc/workflow_query_fabricator.rb +++ b/spec/fabricators/grpc/workflow_query_fabricator.rb @@ -1,4 +1,4 @@ -Fabricator(:api_workflow_query, from: Temporal::Api::Query::V1::WorkflowQuery) do +Fabricator(:api_workflow_query, from: Temporalio::Api::Query::V1::WorkflowQuery) do query_type { 'state' } query_args { Temporal.configuration.converter.to_payloads(['']) } end diff --git a/spec/fabricators/grpc/workflow_task_fabricator.rb b/spec/fabricators/grpc/workflow_task_fabricator.rb index d1470c04..855baeb8 100644 --- a/spec/fabricators/grpc/workflow_task_fabricator.rb +++ b/spec/fabricators/grpc/workflow_task_fabricator.rb @@ -1,6 +1,6 @@ require 'securerandom' -Fabricator(:api_workflow_task, from: Temporal::Api::WorkflowService::V1::PollWorkflowTaskQueueResponse) do +Fabricator(:api_workflow_task, from: Temporalio::Api::WorkflowService::V1::PollWorkflowTaskQueueResponse) do transient :task_token, :activity_name, :headers, :events started_event_id { rand(100) } @@ -9,7 +9,7 @@ workflow_execution { Fabricate(:api_workflow_execution) } scheduled_time { Google::Protobuf::Timestamp.new.tap { |t| t.from_time(Time.now) } } started_time { Google::Protobuf::Timestamp.new.tap { |t| t.from_time(Time.now) } } - history { |attrs| Temporal::Api::History::V1::History.new(events: attrs[:events]) } + history { |attrs| Temporalio::Api::History::V1::History.new(events: attrs[:events]) } query { nil } end diff --git a/spec/fabricators/grpc/workflow_type_fabricator.rb b/spec/fabricators/grpc/workflow_type_fabricator.rb index f72ba1de..f7661a30 100644 --- a/spec/fabricators/grpc/workflow_type_fabricator.rb +++ b/spec/fabricators/grpc/workflow_type_fabricator.rb @@ -1,3 +1,3 @@ -Fabricator(:api_workflow_type, from: Temporal::Api::Common::V1::WorkflowType) do +Fabricator(:api_workflow_type, from: Temporalio::Api::Common::V1::WorkflowType) do name 'TestWorkflow' end diff --git a/spec/fabricators/workflow_canceled_event_fabricator.rb b/spec/fabricators/workflow_canceled_event_fabricator.rb index 666beb7e..57181242 100644 --- a/spec/fabricators/workflow_canceled_event_fabricator.rb +++ b/spec/fabricators/workflow_canceled_event_fabricator.rb @@ -1,7 +1,7 @@ -Fabricator(:workflow_canceled_event, from: Temporal::Api::History::V1::HistoryEvent) do +Fabricator(:workflow_canceled_event, from: Temporalio::Api::History::V1::HistoryEvent) do event_time { Google::Protobuf::Timestamp.new.tap { |t| t.from_time(Time.now) } } event_type { :EVENT_TYPE_WORKFLOW_EXECUTION_CANCELED } workflow_execution_canceled_event_attributes do - Temporal::Api::History::V1::WorkflowExecutionCanceledEventAttributes.new + Temporalio::Api::History::V1::WorkflowExecutionCanceledEventAttributes.new end end diff --git a/spec/fabricators/workflow_completed_event_fabricator.rb b/spec/fabricators/workflow_completed_event_fabricator.rb index 02eb5226..57331f45 100644 --- a/spec/fabricators/workflow_completed_event_fabricator.rb +++ b/spec/fabricators/workflow_completed_event_fabricator.rb @@ -1,10 +1,10 @@ -Fabricator(:workflow_completed_event, from: Temporal::Api::History::V1::HistoryEvent) do +Fabricator(:workflow_completed_event, from: Temporalio::Api::History::V1::HistoryEvent) do transient :result event_time { Google::Protobuf::Timestamp.new.tap { |t| t.from_time(Time.now) } } event_type { :EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED } workflow_execution_completed_event_attributes do |attrs| - Temporal::Api::History::V1::WorkflowExecutionCompletedEventAttributes.new( + Temporalio::Api::History::V1::WorkflowExecutionCompletedEventAttributes.new( result: attrs[:result] ) end diff --git a/spec/fabricators/workflow_execution_history_fabricator.rb b/spec/fabricators/workflow_execution_history_fabricator.rb index bf13f965..8e033a80 100644 --- a/spec/fabricators/workflow_execution_history_fabricator.rb +++ b/spec/fabricators/workflow_execution_history_fabricator.rb @@ -1,5 +1,5 @@ -Fabricator(:workflow_execution_history, from: Temporal::Api::WorkflowService::V1::GetWorkflowExecutionHistoryResponse) do +Fabricator(:workflow_execution_history, from: Temporalio::Api::WorkflowService::V1::GetWorkflowExecutionHistoryResponse) do transient :events - history { |attrs| Temporal::Api::History::V1::History.new(events: attrs[:events]) } + history { |attrs| Temporalio::Api::History::V1::History.new(events: attrs[:events]) } next_page_token '' end diff --git a/spec/unit/lib/temporal/client_spec.rb b/spec/unit/lib/temporal/client_spec.rb index 73b9b3fb..5d8019bc 100644 --- a/spec/unit/lib/temporal/client_spec.rb +++ b/spec/unit/lib/temporal/client_spec.rb @@ -34,7 +34,7 @@ class TestStartWorkflow < Temporal::Workflow describe '#start_workflow' do let(:temporal_response) do - Temporal::Api::WorkflowService::V1::StartWorkflowExecutionResponse.new(run_id: 'xxx') + Temporalio::Api::WorkflowService::V1::StartWorkflowExecutionResponse.new(run_id: 'xxx') end before { allow(connection).to receive(:start_workflow_execution).and_return(temporal_response) } @@ -202,7 +202,7 @@ class TestStartWorkflow < Temporal::Workflow describe '#start_workflow with a signal' do let(:temporal_response) do - Temporal::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionResponse.new(run_id: 'xxx') + Temporalio::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionResponse.new(run_id: 'xxx') end before { allow(connection).to receive(:signal_with_start_workflow_execution).and_return(temporal_response) } @@ -281,7 +281,7 @@ def expect_signal_with_start(expected_arguments, expected_signal_argument) describe '#schedule_workflow' do let(:temporal_response) do - Temporal::Api::WorkflowService::V1::StartWorkflowExecutionResponse.new(run_id: 'xxx') + Temporalio::Api::WorkflowService::V1::StartWorkflowExecutionResponse.new(run_id: 'xxx') end before { allow(connection).to receive(:start_workflow_execution).and_return(temporal_response) } @@ -330,7 +330,7 @@ def expect_signal_with_start(expected_arguments, expected_signal_argument) end describe '#describe_namespace' do - before { allow(connection).to receive(:describe_namespace).and_return(Temporal::Api::WorkflowService::V1::DescribeNamespaceResponse.new) } + before { allow(connection).to receive(:describe_namespace).and_return(Temporalio::Api::WorkflowService::V1::DescribeNamespaceResponse.new) } it 'passes the namespace to the connection' do result = subject.describe_namespace('new-namespace') @@ -450,7 +450,7 @@ class NamespacedWorkflow < Temporal::Workflow ['string', 'a result'], ].each do |(type, expected_result)| it "completes and returns a #{type}" do - payload = Temporal::Api::Common::V1::Payloads.new( + payload = Temporalio::Api::Common::V1::Payloads.new( payloads: [ Temporal.configuration.converter.to_payload(expected_result) ], @@ -530,7 +530,7 @@ class NamespacedWorkflow < Temporal::Workflow describe '#reset_workflow' do let(:temporal_response) do - Temporal::Api::WorkflowService::V1::ResetWorkflowExecutionResponse.new(run_id: 'xxx') + Temporalio::Api::WorkflowService::V1::ResetWorkflowExecutionResponse.new(run_id: 'xxx') end let(:history) do Temporal::Workflow::History.new([ @@ -681,7 +681,7 @@ class NamespacedWorkflow < Temporal::Workflow describe '#terminate_workflow' do let(:temporal_response) do - Temporal::Api::WorkflowService::V1::TerminateWorkflowExecutionResponse.new + Temporalio::Api::WorkflowService::V1::TerminateWorkflowExecutionResponse.new end before { allow(connection).to receive(:terminate_workflow_execution).and_return(temporal_response) } @@ -703,7 +703,7 @@ class NamespacedWorkflow < Temporal::Workflow describe '#fetch_workflow_execution_info' do let(:response) do - Temporal::Api::WorkflowService::V1::DescribeWorkflowExecutionResponse.new( + Temporalio::Api::WorkflowService::V1::DescribeWorkflowExecutionResponse.new( workflow_execution_info: api_info ) end @@ -794,7 +794,7 @@ class NamespacedWorkflow < Temporal::Workflow Fabricate(:api_workflow_execution_info, workflow: 'TestWorkflow', workflow_id: '') end let(:response) do - Temporal::Api::WorkflowService::V1::ListOpenWorkflowExecutionsResponse.new( + Temporalio::Api::WorkflowService::V1::ListOpenWorkflowExecutionsResponse.new( executions: [api_execution_info], next_page_token: '' ) @@ -815,19 +815,19 @@ class NamespacedWorkflow < Temporal::Workflow context 'when history is paginated' do let(:response_1) do - Temporal::Api::WorkflowService::V1::ListOpenWorkflowExecutionsResponse.new( + Temporalio::Api::WorkflowService::V1::ListOpenWorkflowExecutionsResponse.new( executions: [api_execution_info], next_page_token: 'a' ) end let(:response_2) do - Temporal::Api::WorkflowService::V1::ListOpenWorkflowExecutionsResponse.new( + Temporalio::Api::WorkflowService::V1::ListOpenWorkflowExecutionsResponse.new( executions: [api_execution_info], next_page_token: 'b' ) end let(:response_3) do - Temporal::Api::WorkflowService::V1::ListOpenWorkflowExecutionsResponse.new( + Temporalio::Api::WorkflowService::V1::ListOpenWorkflowExecutionsResponse.new( executions: [api_execution_info], next_page_token: '' ) diff --git a/spec/unit/lib/temporal/connection/converter/composite_spec.rb b/spec/unit/lib/temporal/connection/converter/composite_spec.rb index 9f74393b..fd4bee56 100644 --- a/spec/unit/lib/temporal/connection/converter/composite_spec.rb +++ b/spec/unit/lib/temporal/connection/converter/composite_spec.rb @@ -10,11 +10,11 @@ describe 'encoding' do it 'tries converters until it finds a match' do payloads = [ - Temporal::Api::Common::V1::Payload.new( + Temporalio::Api::Common::V1::Payload.new( metadata: { 'encoding' => Temporal::Connection::Converter::Payload::Bytes::ENCODING }, data: 'test'.b ), - Temporal::Api::Common::V1::Payload.new( + Temporalio::Api::Common::V1::Payload.new( metadata: { 'encoding' => Temporal::Connection::Converter::Payload::JSON::ENCODING }, data: '"test"' ), @@ -32,11 +32,11 @@ describe 'decoding' do it 'uses metadata to pick a converter' do payloads = [ - Temporal::Api::Common::V1::Payload.new( + Temporalio::Api::Common::V1::Payload.new( metadata: { 'encoding' => Temporal::Connection::Converter::Payload::Bytes::ENCODING }, data: 'test'.b ), - Temporal::Api::Common::V1::Payload.new( + Temporalio::Api::Common::V1::Payload.new( metadata: { 'encoding' => Temporal::Connection::Converter::Payload::JSON::ENCODING }, data: '"test"' ), @@ -50,7 +50,7 @@ end it 'raises if there is no converter for an encoding' do - payload = Temporal::Api::Common::V1::Payload.new( + payload = Temporalio::Api::Common::V1::Payload.new( metadata: { 'encoding' => 'fake' } ) diff --git a/spec/unit/lib/temporal/connection/converter/payload/bytes_spec.rb b/spec/unit/lib/temporal/connection/converter/payload/bytes_spec.rb index 8a9391fb..e8fe42ff 100644 --- a/spec/unit/lib/temporal/connection/converter/payload/bytes_spec.rb +++ b/spec/unit/lib/temporal/connection/converter/payload/bytes_spec.rb @@ -5,7 +5,7 @@ describe 'round trip' do it 'encodes to a binary/plain payload' do - payload = Temporal::Api::Common::V1::Payload.new( + payload = Temporalio::Api::Common::V1::Payload.new( metadata: { 'encoding' => described_class::ENCODING }, data: 'test'.b ) @@ -14,7 +14,7 @@ end it 'decodes a binary/plain payload to a byte string' do - payload = Temporal::Api::Common::V1::Payload.new( + payload = Temporalio::Api::Common::V1::Payload.new( metadata: { 'encoding' => described_class::ENCODING }, data: 'test'.b ) diff --git a/spec/unit/lib/temporal/connection/converter/payload/nil_spec.rb b/spec/unit/lib/temporal/connection/converter/payload/nil_spec.rb index 3779d27b..78e8d7f7 100644 --- a/spec/unit/lib/temporal/connection/converter/payload/nil_spec.rb +++ b/spec/unit/lib/temporal/connection/converter/payload/nil_spec.rb @@ -4,7 +4,7 @@ subject { described_class.new } it 'encodes a null payload' do - payload = Temporal::Api::Common::V1::Payload.new( + payload = Temporalio::Api::Common::V1::Payload.new( metadata: { 'encoding' => described_class::ENCODING } ) @@ -12,7 +12,7 @@ end it 'decodes a null payload' do - payload = Temporal::Api::Common::V1::Payload.new( + payload = Temporalio::Api::Common::V1::Payload.new( metadata: { 'encoding' => described_class::ENCODING } ) diff --git a/spec/unit/lib/temporal/connection/converter/payload/proto_json_spec.rb b/spec/unit/lib/temporal/connection/converter/payload/proto_json_spec.rb index 5570d9df..21111d5f 100644 --- a/spec/unit/lib/temporal/connection/converter/payload/proto_json_spec.rb +++ b/spec/unit/lib/temporal/connection/converter/payload/proto_json_spec.rb @@ -5,10 +5,10 @@ describe 'round trip' do it 'converts' do - # Temporal::Api::Common::V1::Payload is a protobuf. + # Temporalio::Api::Common::V1::Payload is a protobuf. # Using it as the "input" here to show the roundtrip. # #to_payload will return a wrapped Payload around this one. - input = Temporal::Api::Common::V1::Payload.new( + input = Temporalio::Api::Common::V1::Payload.new( metadata: { 'hello' => 'world' }, data: 'hello world', ) diff --git a/spec/unit/lib/temporal/connection/serializer/continue_as_new_spec.rb b/spec/unit/lib/temporal/connection/serializer/continue_as_new_spec.rb index 38166828..e2d44893 100644 --- a/spec/unit/lib/temporal/connection/serializer/continue_as_new_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/continue_as_new_spec.rb @@ -16,7 +16,7 @@ result = described_class.new(command).to_proto - expect(result).to be_an_instance_of(Temporal::Api::Command::V1::Command) + expect(result).to be_an_instance_of(Temporalio::Api::Command::V1::Command) expect(result.command_type).to eql( :COMMAND_TYPE_CONTINUE_AS_NEW_WORKFLOW_EXECUTION ) diff --git a/spec/unit/lib/temporal/connection/serializer/failure_spec.rb b/spec/unit/lib/temporal/connection/serializer/failure_spec.rb index 2dd32370..b492556f 100644 --- a/spec/unit/lib/temporal/connection/serializer/failure_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/failure_spec.rb @@ -10,7 +10,7 @@ class TestDeserializer it 'produces a protobuf' do result = described_class.new(StandardError.new('test')).to_proto - expect(result).to be_an_instance_of(Temporal::Api::Failure::V1::Failure) + expect(result).to be_an_instance_of(Temporalio::Api::Failure::V1::Failure) end class NaughtyClass; end diff --git a/spec/unit/lib/temporal/connection/serializer/query_answer_spec.rb b/spec/unit/lib/temporal/connection/serializer/query_answer_spec.rb index 8876bbd5..62028824 100644 --- a/spec/unit/lib/temporal/connection/serializer/query_answer_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/query_answer_spec.rb @@ -13,9 +13,9 @@ class TestDeserializer it 'produces a protobuf' do result = described_class.new(query_result).to_proto - expect(result).to be_a(Temporal::Api::Query::V1::WorkflowQueryResult) - expect(result.result_type).to eq(Temporal::Api::Enums::V1::QueryResultType.lookup( - Temporal::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_ANSWERED) + expect(result).to be_a(Temporalio::Api::Query::V1::WorkflowQueryResult) + expect(result.result_type).to eq(Temporalio::Api::Enums::V1::QueryResultType.lookup( + Temporalio::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_ANSWERED) ) expect(result.answer).to eq(TestDeserializer.to_query_payloads(42)) end diff --git a/spec/unit/lib/temporal/connection/serializer/query_failure_spec.rb b/spec/unit/lib/temporal/connection/serializer/query_failure_spec.rb index 7e948f4d..0590c0c4 100644 --- a/spec/unit/lib/temporal/connection/serializer/query_failure_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/query_failure_spec.rb @@ -9,9 +9,9 @@ it 'produces a protobuf' do result = described_class.new(query_result).to_proto - expect(result).to be_a(Temporal::Api::Query::V1::WorkflowQueryResult) - expect(result.result_type).to eq(Temporal::Api::Enums::V1::QueryResultType.lookup( - Temporal::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_FAILED) + expect(result).to be_a(Temporalio::Api::Query::V1::WorkflowQueryResult) + expect(result.result_type).to eq(Temporalio::Api::Enums::V1::QueryResultType.lookup( + Temporalio::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_FAILED) ) expect(result.error_message).to eq('Test query failure') end diff --git a/spec/unit/lib/temporal/connection/serializer/upsert_search_attributes_spec.rb b/spec/unit/lib/temporal/connection/serializer/upsert_search_attributes_spec.rb index 21816b6c..da5d8879 100644 --- a/spec/unit/lib/temporal/connection/serializer/upsert_search_attributes_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/upsert_search_attributes_spec.rb @@ -23,7 +23,7 @@ class TestDeserializer ) result = described_class.new(command).to_proto - expect(result).to be_an_instance_of(Temporal::Api::Command::V1::Command) + expect(result).to be_an_instance_of(Temporalio::Api::Command::V1::Command) expect(result.command_type).to eql( :COMMAND_TYPE_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES ) diff --git a/spec/unit/lib/temporal/connection/serializer/workflow_id_reuse_policy_spec.rb b/spec/unit/lib/temporal/connection/serializer/workflow_id_reuse_policy_spec.rb index 21c05302..950b8a04 100644 --- a/spec/unit/lib/temporal/connection/serializer/workflow_id_reuse_policy_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/workflow_id_reuse_policy_spec.rb @@ -4,9 +4,9 @@ describe Temporal::Connection::Serializer::WorkflowIdReusePolicy do describe 'to_proto' do SYM_TO_PROTO = { - allow_failed: Temporal::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY, - allow: Temporal::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, - reject: Temporal::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE + allow_failed: Temporalio::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY, + allow: Temporalio::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, + reject: Temporalio::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE }.freeze def self.test_valid_policy(policy_sym) diff --git a/spec/unit/lib/temporal/grpc_spec.rb b/spec/unit/lib/temporal/grpc_spec.rb index 92e37e28..42f0f3a3 100644 --- a/spec/unit/lib/temporal/grpc_spec.rb +++ b/spec/unit/lib/temporal/grpc_spec.rb @@ -48,7 +48,7 @@ class TestDeserializer end it 'starts a workflow with scalar arguments' do - allow(grpc_stub).to receive(:start_workflow_execution).and_return(Temporal::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionResponse.new(run_id: 'xxx')) + allow(grpc_stub).to receive(:start_workflow_execution).and_return(Temporalio::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionResponse.new(run_id: 'xxx')) datetime_attribute_value = Time.now subject.start_workflow_execution( @@ -75,7 +75,7 @@ class TestDeserializer ) expect(grpc_stub).to have_received(:start_workflow_execution) do |request| - expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::StartWorkflowExecutionRequest) + expect(request).to be_an_instance_of(Temporalio::Api::WorkflowService::V1::StartWorkflowExecutionRequest) expect(request.namespace).to eq(namespace) expect(request.workflow_id).to eq(workflow_id) expect(request.workflow_type.name).to eq('workflow_name') @@ -86,11 +86,11 @@ class TestDeserializer expect(request.workflow_task_timeout.seconds).to eq(3) expect(request.workflow_id_reuse_policy).to eq(:WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE) expect(request.search_attributes.indexed_fields).to eq({ - 'foo-int-attribute' => Temporal::Api::Common::V1::Payload.new(data: '256', metadata: { 'encoding' => 'json/plain' }), - 'foo-string-attribute' => Temporal::Api::Common::V1::Payload.new(data: '"bar"', metadata: { 'encoding' => 'json/plain' }), - 'foo-double-attribute' => Temporal::Api::Common::V1::Payload.new(data: '6.28', metadata: { 'encoding' => 'json/plain' }), - 'foo-bool-attribute' => Temporal::Api::Common::V1::Payload.new(data: 'false', metadata: { 'encoding' => 'json/plain' }), - 'foo-datetime-attribute' => Temporal::Api::Common::V1::Payload.new(data: "\"#{datetime_attribute_value.utc.iso8601}\"", metadata: { 'encoding' => 'json/plain' }), + 'foo-int-attribute' => Temporalio::Api::Common::V1::Payload.new(data: '256', metadata: { 'encoding' => 'json/plain' }), + 'foo-string-attribute' => Temporalio::Api::Common::V1::Payload.new(data: '"bar"', metadata: { 'encoding' => 'json/plain' }), + 'foo-double-attribute' => Temporalio::Api::Common::V1::Payload.new(data: '6.28', metadata: { 'encoding' => 'json/plain' }), + 'foo-bool-attribute' => Temporalio::Api::Common::V1::Payload.new(data: 'false', metadata: { 'encoding' => 'json/plain' }), + 'foo-datetime-attribute' => Temporalio::Api::Common::V1::Payload.new(data: "\"#{datetime_attribute_value.utc.iso8601}\"", metadata: { 'encoding' => 'json/plain' }), }) end end @@ -117,7 +117,7 @@ class TestDeserializer describe '#signal_with_start_workflow' do let(:temporal_response) do - Temporal::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionResponse.new(run_id: 'xxx') + Temporalio::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionResponse.new(run_id: 'xxx') end before { allow(grpc_stub).to receive(:signal_with_start_workflow_execution).and_return(temporal_response) } @@ -138,7 +138,7 @@ class TestDeserializer ) expect(grpc_stub).to have_received(:signal_with_start_workflow_execution) do |request| - expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionRequest) + expect(request).to be_an_instance_of(Temporalio::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionRequest) expect(request.namespace).to eq(namespace) expect(request.workflow_id).to eq(workflow_id) expect(request.workflow_type.name).to eq('workflow_name') @@ -177,8 +177,8 @@ class TestDeserializer describe "#list_namespaces" do let (:response) do - Temporal::Api::WorkflowService::V1::ListNamespacesResponse.new( - namespaces: [Temporal::Api::WorkflowService::V1::DescribeNamespaceResponse.new], + Temporalio::Api::WorkflowService::V1::ListNamespacesResponse.new( + namespaces: [Temporalio::Api::WorkflowService::V1::DescribeNamespaceResponse.new], next_page_token: "" ) end @@ -194,7 +194,7 @@ class TestDeserializer ) expect(grpc_stub).to have_received(:list_namespaces) do |request| - expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListNamespacesRequest) + expect(request).to be_an_instance_of(Temporalio::Api::WorkflowService::V1::ListNamespacesRequest) expect(request.page_size).to eq(10) expect(request.next_page_token).to eq(next_page_token) end @@ -203,8 +203,8 @@ class TestDeserializer describe '#get_workflow_execution_history' do let(:response) do - Temporal::Api::WorkflowService::V1::GetWorkflowExecutionHistoryResponse.new( - history: Temporal::Api::History::V1::History.new, + Temporalio::Api::WorkflowService::V1::GetWorkflowExecutionHistoryResponse.new( + history: Temporalio::Api::History::V1::History.new, next_page_token: nil ) end @@ -219,15 +219,15 @@ class TestDeserializer ) expect(grpc_stub).to have_received(:get_workflow_execution_history) do |request| - expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::GetWorkflowExecutionHistoryRequest) + expect(request).to be_an_instance_of(Temporalio::Api::WorkflowService::V1::GetWorkflowExecutionHistoryRequest) expect(request.namespace).to eq(namespace) expect(request.execution.workflow_id).to eq(workflow_id) expect(request.execution.run_id).to eq(run_id) expect(request.next_page_token).to be_empty expect(request.wait_new_event).to eq(false) expect(request.history_event_filter_type).to eq( - Temporal::Api::Enums::V1::HistoryEventFilterType.lookup( - Temporal::Api::Enums::V1::HistoryEventFilterType::HISTORY_EVENT_FILTER_TYPE_ALL_EVENT + Temporalio::Api::Enums::V1::HistoryEventFilterType.lookup( + Temporalio::Api::Enums::V1::HistoryEventFilterType::HISTORY_EVENT_FILTER_TYPE_ALL_EVENT ) ) end @@ -289,8 +289,8 @@ class TestDeserializer expect(grpc_stub).to have_received(:get_workflow_execution_history) do |request| expect(request.history_event_filter_type).to eq( - Temporal::Api::Enums::V1::HistoryEventFilterType.lookup( - Temporal::Api::Enums::V1::HistoryEventFilterType::HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT + Temporalio::Api::Enums::V1::HistoryEventFilterType.lookup( + Temporalio::Api::Enums::V1::HistoryEventFilterType::HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT ) ) end @@ -303,7 +303,7 @@ class TestDeserializer let(:to) { Time.now } let(:args) { { namespace: namespace, from: from, to: to } } let(:temporal_response) do - Temporal::Api::WorkflowService::V1::ListOpenWorkflowExecutionsResponse.new(executions: [], next_page_token: '') + Temporalio::Api::WorkflowService::V1::ListOpenWorkflowExecutionsResponse.new(executions: [], next_page_token: '') end before do @@ -314,10 +314,10 @@ class TestDeserializer subject.list_open_workflow_executions(**args) expect(grpc_stub).to have_received(:list_open_workflow_executions) do |request| - expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListOpenWorkflowExecutionsRequest) + expect(request).to be_an_instance_of(Temporalio::Api::WorkflowService::V1::ListOpenWorkflowExecutionsRequest) expect(request.maximum_page_size).to eq(described_class::DEFAULT_OPTIONS[:max_page_size]) expect(request.next_page_token).to eq('') - expect(request.start_time_filter).to be_an_instance_of(Temporal::Api::Filter::V1::StartTimeFilter) + expect(request.start_time_filter).to be_an_instance_of(Temporalio::Api::Filter::V1::StartTimeFilter) expect(request.start_time_filter.earliest_time.to_time) .to eq(from) expect(request.start_time_filter.latest_time.to_time) @@ -332,7 +332,7 @@ class TestDeserializer subject.list_open_workflow_executions(**args.merge(next_page_token: 'x')) expect(grpc_stub).to have_received(:list_open_workflow_executions) do |request| - expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListOpenWorkflowExecutionsRequest) + expect(request).to be_an_instance_of(Temporalio::Api::WorkflowService::V1::ListOpenWorkflowExecutionsRequest) expect(request.next_page_token).to eq('x') end end @@ -343,9 +343,9 @@ class TestDeserializer subject.list_open_workflow_executions(**args.merge(workflow_id: 'xxx')) expect(grpc_stub).to have_received(:list_open_workflow_executions) do |request| - expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListOpenWorkflowExecutionsRequest) + expect(request).to be_an_instance_of(Temporalio::Api::WorkflowService::V1::ListOpenWorkflowExecutionsRequest) expect(request.execution_filter) - .to be_an_instance_of(Temporal::Api::Filter::V1::WorkflowExecutionFilter) + .to be_an_instance_of(Temporalio::Api::Filter::V1::WorkflowExecutionFilter) expect(request.execution_filter.workflow_id).to eq('xxx') end end @@ -356,8 +356,8 @@ class TestDeserializer subject.list_open_workflow_executions(**args.merge(workflow: 'TestWorkflow')) expect(grpc_stub).to have_received(:list_open_workflow_executions) do |request| - expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListOpenWorkflowExecutionsRequest) - expect(request.type_filter).to be_an_instance_of(Temporal::Api::Filter::V1::WorkflowTypeFilter) + expect(request).to be_an_instance_of(Temporalio::Api::WorkflowService::V1::ListOpenWorkflowExecutionsRequest) + expect(request.type_filter).to be_an_instance_of(Temporalio::Api::Filter::V1::WorkflowTypeFilter) expect(request.type_filter.name).to eq('TestWorkflow') end end @@ -369,7 +369,7 @@ class TestDeserializer let(:query) { 'StartDate < 2022-04-07T20:48:20Z order by StartTime desc' } let(:args) { { namespace: namespace, query: query } } let(:temporal_response) do - Temporal::Api::WorkflowService::V1::CountWorkflowExecutionsResponse.new(count: 0) + Temporalio::Api::WorkflowService::V1::CountWorkflowExecutionsResponse.new(count: 0) end before do @@ -380,7 +380,7 @@ class TestDeserializer subject.count_workflow_executions(**args) expect(grpc_stub).to have_received(:count_workflow_executions) do |request| - expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::CountWorkflowExecutionsRequest) + expect(request).to be_an_instance_of(Temporalio::Api::WorkflowService::V1::CountWorkflowExecutionsRequest) expect(request.namespace).to eq(namespace) expect(request.query).to eq(query) end @@ -392,10 +392,10 @@ class TestDeserializer let(:query) { 'StartDate < 2022-04-07T20:48:20Z order by StartTime desc' } let(:args) { { namespace: namespace, query: query } } let(:temporal_response) do - Temporal::Api::WorkflowService::V1::ListWorkflowExecutionsResponse.new(executions: [], next_page_token: '') + Temporalio::Api::WorkflowService::V1::ListWorkflowExecutionsResponse.new(executions: [], next_page_token: '') end let(:temporal_paginated_response) do - Temporal::Api::WorkflowService::V1::ListWorkflowExecutionsResponse.new(executions: [], next_page_token: 'more-results') + Temporalio::Api::WorkflowService::V1::ListWorkflowExecutionsResponse.new(executions: [], next_page_token: 'more-results') end before do @@ -406,7 +406,7 @@ class TestDeserializer subject.list_workflow_executions(**args) expect(grpc_stub).to have_received(:list_workflow_executions) do |request| - expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListWorkflowExecutionsRequest) + expect(request).to be_an_instance_of(Temporalio::Api::WorkflowService::V1::ListWorkflowExecutionsRequest) expect(request.page_size).to eq(described_class::DEFAULT_OPTIONS[:max_page_size]) expect(request.next_page_token).to eq('') expect(request.namespace).to eq(namespace) @@ -419,7 +419,7 @@ class TestDeserializer subject.list_workflow_executions(**args.merge(next_page_token: 'x')) expect(grpc_stub).to have_received(:list_workflow_executions) do |request| - expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListWorkflowExecutionsRequest) + expect(request).to be_an_instance_of(Temporalio::Api::WorkflowService::V1::ListWorkflowExecutionsRequest) expect(request.next_page_token).to eq('x') end end @@ -432,7 +432,7 @@ class TestDeserializer let(:to) { Time.now } let(:args) { { namespace: namespace, from: from, to: to } } let(:temporal_response) do - Temporal::Api::WorkflowService::V1::ListClosedWorkflowExecutionsResponse.new(executions: [], next_page_token: '') + Temporalio::Api::WorkflowService::V1::ListClosedWorkflowExecutionsResponse.new(executions: [], next_page_token: '') end before do @@ -443,10 +443,10 @@ class TestDeserializer subject.list_closed_workflow_executions(**args) expect(grpc_stub).to have_received(:list_closed_workflow_executions) do |request| - expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListClosedWorkflowExecutionsRequest) + expect(request).to be_an_instance_of(Temporalio::Api::WorkflowService::V1::ListClosedWorkflowExecutionsRequest) expect(request.maximum_page_size).to eq(described_class::DEFAULT_OPTIONS[:max_page_size]) expect(request.next_page_token).to eq('') - expect(request.start_time_filter).to be_an_instance_of(Temporal::Api::Filter::V1::StartTimeFilter) + expect(request.start_time_filter).to be_an_instance_of(Temporalio::Api::Filter::V1::StartTimeFilter) expect(request.start_time_filter.earliest_time.to_time) .to eq(from) expect(request.start_time_filter.latest_time.to_time) @@ -462,14 +462,14 @@ class TestDeserializer subject.list_closed_workflow_executions(**args.merge(next_page_token: 'x')) expect(grpc_stub).to have_received(:list_closed_workflow_executions) do |request| - expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListClosedWorkflowExecutionsRequest) + expect(request).to be_an_instance_of(Temporalio::Api::WorkflowService::V1::ListClosedWorkflowExecutionsRequest) expect(request.next_page_token).to eq('x') end end end context 'when status is supplied' do - let(:api_completed_status) { Temporal::Api::Enums::V1::WorkflowExecutionStatus::WORKFLOW_EXECUTION_STATUS_COMPLETED } + let(:api_completed_status) { Temporalio::Api::Enums::V1::WorkflowExecutionStatus::WORKFLOW_EXECUTION_STATUS_COMPLETED } it 'makes an API request' do subject.list_closed_workflow_executions( @@ -477,8 +477,8 @@ class TestDeserializer ) expect(grpc_stub).to have_received(:list_closed_workflow_executions) do |request| - expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListClosedWorkflowExecutionsRequest) - expect(request.status_filter).to eq(Temporal::Api::Filter::V1::StatusFilter.new(status: api_completed_status)) + expect(request).to be_an_instance_of(Temporalio::Api::WorkflowService::V1::ListClosedWorkflowExecutionsRequest) + expect(request.status_filter).to eq(Temporalio::Api::Filter::V1::StatusFilter.new(status: api_completed_status)) end end end @@ -488,9 +488,9 @@ class TestDeserializer subject.list_closed_workflow_executions(**args.merge(workflow_id: 'xxx')) expect(grpc_stub).to have_received(:list_closed_workflow_executions) do |request| - expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListClosedWorkflowExecutionsRequest) + expect(request).to be_an_instance_of(Temporalio::Api::WorkflowService::V1::ListClosedWorkflowExecutionsRequest) expect(request.execution_filter) - .to be_an_instance_of(Temporal::Api::Filter::V1::WorkflowExecutionFilter) + .to be_an_instance_of(Temporalio::Api::Filter::V1::WorkflowExecutionFilter) expect(request.execution_filter.workflow_id).to eq('xxx') end end @@ -501,8 +501,8 @@ class TestDeserializer subject.list_closed_workflow_executions(**args.merge(workflow: 'TestWorkflow')) expect(grpc_stub).to have_received(:list_closed_workflow_executions) do |request| - expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::ListClosedWorkflowExecutionsRequest) - expect(request.type_filter).to be_an_instance_of(Temporal::Api::Filter::V1::WorkflowTypeFilter) + expect(request).to be_an_instance_of(Temporalio::Api::WorkflowService::V1::ListClosedWorkflowExecutionsRequest) + expect(request.type_filter).to be_an_instance_of(Temporalio::Api::Filter::V1::WorkflowTypeFilter) expect(request.type_filter.name).to eq('TestWorkflow') end end @@ -516,7 +516,7 @@ class TestDeserializer before do allow(grpc_stub) .to receive(:respond_query_task_completed) - .and_return(Temporal::Api::WorkflowService::V1::RespondQueryTaskCompletedResponse.new) + .and_return(Temporalio::Api::WorkflowService::V1::RespondQueryTaskCompletedResponse.new) end context 'when query result is an answer' do @@ -530,11 +530,11 @@ class TestDeserializer ) expect(grpc_stub).to have_received(:respond_query_task_completed) do |request| - expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::RespondQueryTaskCompletedRequest) + expect(request).to be_an_instance_of(Temporalio::Api::WorkflowService::V1::RespondQueryTaskCompletedRequest) expect(request.task_token).to eq(task_token) expect(request.namespace).to eq(namespace) - expect(request.completed_type).to eq(Temporal::Api::Enums::V1::QueryResultType.lookup( - Temporal::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_ANSWERED) + expect(request.completed_type).to eq(Temporalio::Api::Enums::V1::QueryResultType.lookup( + Temporalio::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_ANSWERED) ) expect(request.query_result).to eq(TestDeserializer.to_query_payloads(42)) expect(request.error_message).to eq('') @@ -553,11 +553,11 @@ class TestDeserializer ) expect(grpc_stub).to have_received(:respond_query_task_completed) do |request| - expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::RespondQueryTaskCompletedRequest) + expect(request).to be_an_instance_of(Temporalio::Api::WorkflowService::V1::RespondQueryTaskCompletedRequest) expect(request.task_token).to eq(task_token) expect(request.namespace).to eq(namespace) - expect(request.completed_type).to eq(Temporal::Api::Enums::V1::QueryResultType.lookup( - Temporal::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_FAILED) + expect(request.completed_type).to eq(Temporalio::Api::Enums::V1::QueryResultType.lookup( + Temporalio::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_FAILED) ) expect(request.query_result).to eq(nil) expect(request.error_message).to eq('Test query failure') @@ -572,7 +572,7 @@ class TestDeserializer before do allow(grpc_stub) .to receive(:respond_workflow_task_completed) - .and_return(Temporal::Api::WorkflowService::V1::RespondWorkflowTaskCompletedResponse.new) + .and_return(Temporalio::Api::WorkflowService::V1::RespondWorkflowTaskCompletedResponse.new) end context 'when responding with query results' do @@ -593,7 +593,7 @@ class TestDeserializer ) expect(grpc_stub).to have_received(:respond_workflow_task_completed) do |request| - expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::RespondWorkflowTaskCompletedRequest) + expect(request).to be_an_instance_of(Temporalio::Api::WorkflowService::V1::RespondWorkflowTaskCompletedRequest) expect(request.task_token).to eq(task_token) expect(request.namespace).to eq(namespace) expect(request.commands).to be_empty @@ -602,15 +602,15 @@ class TestDeserializer expect(request.query_results.length).to eq(2) - expect(request.query_results['1']).to be_a(Temporal::Api::Query::V1::WorkflowQueryResult) - expect(request.query_results['1'].result_type).to eq(Temporal::Api::Enums::V1::QueryResultType.lookup( - Temporal::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_ANSWERED) + expect(request.query_results['1']).to be_a(Temporalio::Api::Query::V1::WorkflowQueryResult) + expect(request.query_results['1'].result_type).to eq(Temporalio::Api::Enums::V1::QueryResultType.lookup( + Temporalio::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_ANSWERED) ) expect(request.query_results['1'].answer).to eq(TestDeserializer.to_query_payloads(42)) - expect(request.query_results['2']).to be_a(Temporal::Api::Query::V1::WorkflowQueryResult) - expect(request.query_results['2'].result_type).to eq(Temporal::Api::Enums::V1::QueryResultType.lookup( - Temporal::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_FAILED) + expect(request.query_results['2']).to be_a(Temporalio::Api::Query::V1::WorkflowQueryResult) + expect(request.query_results['2'].result_type).to eq(Temporalio::Api::Enums::V1::QueryResultType.lookup( + Temporalio::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_FAILED) ) expect(request.query_results['2'].error_message).to eq('Test query failure') end @@ -620,7 +620,7 @@ class TestDeserializer describe '#respond_workflow_task_failed' do let(:task_token) { 'task-token' } - let(:cause) { Temporal::Api::Enums::V1::WorkflowTaskFailedCause::WORKFLOW_TASK_FAILED_CAUSE_UNHANDLED_COMMAND } + let(:cause) { Temporalio::Api::Enums::V1::WorkflowTaskFailedCause::WORKFLOW_TASK_FAILED_CAUSE_UNHANDLED_COMMAND } before { allow(grpc_stub).to receive(:respond_workflow_task_failed) } @@ -634,10 +634,10 @@ class TestDeserializer ) expect(grpc_stub).to have_received(:respond_workflow_task_failed) do |request| - expect(request).to be_an_instance_of(Temporal::Api::WorkflowService::V1::RespondWorkflowTaskFailedRequest) + expect(request).to be_an_instance_of(Temporalio::Api::WorkflowService::V1::RespondWorkflowTaskFailedRequest) expect(request.namespace).to eq(namespace) expect(request.task_token).to eq(task_token) - expect(request.cause).to be(Temporal::Api::Enums::V1::WorkflowTaskFailedCause.lookup(cause)) + expect(request.cause).to be(Temporalio::Api::Enums::V1::WorkflowTaskFailedCause.lookup(cause)) expect(request.identity).to eq(identity) expect(request.binary_checksum).to eq(binary_checksum) end diff --git a/spec/unit/lib/temporal/testing/temporal_override_spec.rb b/spec/unit/lib/temporal/testing/temporal_override_spec.rb index 81a4345b..4d8bef64 100644 --- a/spec/unit/lib/temporal/testing/temporal_override_spec.rb +++ b/spec/unit/lib/temporal/testing/temporal_override_spec.rb @@ -25,7 +25,7 @@ def execute context 'when testing mode is disabled' do describe 'Temporal.start_workflow' do let(:connection) { instance_double('Temporal::Connection::GRPC') } - let(:response) { Temporal::Api::WorkflowService::V1::StartWorkflowExecutionResponse.new(run_id: 'xxx') } + let(:response) { Temporalio::Api::WorkflowService::V1::StartWorkflowExecutionResponse.new(run_id: 'xxx') } before { allow(Temporal::Connection).to receive(:generate).and_return(connection) } after { client.remove_instance_variable(:@connection) rescue NameError } diff --git a/spec/unit/lib/temporal/workflow/context_spec.rb b/spec/unit/lib/temporal/workflow/context_spec.rb index 76e99dc1..90a3cb14 100644 --- a/spec/unit/lib/temporal/workflow/context_spec.rb +++ b/spec/unit/lib/temporal/workflow/context_spec.rb @@ -94,7 +94,7 @@ class MyTestWorkflow < Temporal::Workflow; end started_proc.call(child_workflow_execution) expect(child_workflow_future.finished?).to be false expect(child_workflow_future.child_workflow_execution_future.finished?).to be true - expect(child_workflow_future.child_workflow_execution_future.get).to be_instance_of(Temporal::Api::Common::V1::WorkflowExecution) + expect(child_workflow_future.child_workflow_execution_future.get).to be_instance_of(Temporalio::Api::Common::V1::WorkflowExecution) # complete the workflow via dispatch and check if the child workflow future is finished completed_proc.call('finished result') diff --git a/spec/unit/lib/temporal/workflow/execution_info_spec.rb b/spec/unit/lib/temporal/workflow/execution_info_spec.rb index c40dfd98..ad3368f2 100644 --- a/spec/unit/lib/temporal/workflow/execution_info_spec.rb +++ b/spec/unit/lib/temporal/workflow/execution_info_spec.rb @@ -36,7 +36,7 @@ :api_workflow_execution_info, workflow: 'TestWorkflow', workflow_id: '', - status: Temporal::Api::Enums::V1::WorkflowExecutionStatus::WORKFLOW_EXECUTION_STATUS_TERMINATED + status: Temporalio::Api::Enums::V1::WorkflowExecutionStatus::WORKFLOW_EXECUTION_STATUS_TERMINATED ) end @@ -70,7 +70,7 @@ :api_workflow_execution_info, workflow: 'TestWorkflow', workflow_id: '', - status: Temporal::Api::Enums::V1::WorkflowExecutionStatus.resolve(status) + status: Temporalio::Api::Enums::V1::WorkflowExecutionStatus.resolve(status) ) end it { is_expected.to be_closed } @@ -83,7 +83,7 @@ :api_workflow_execution_info, workflow: 'TestWorkflow', workflow_id: '', - status: Temporal::Api::Enums::V1::WorkflowExecutionStatus.resolve(:WORKFLOW_EXECUTION_STATUS_RUNNING) + status: Temporalio::Api::Enums::V1::WorkflowExecutionStatus.resolve(:WORKFLOW_EXECUTION_STATUS_RUNNING) ) end diff --git a/spec/unit/lib/temporal/workflow/executor_spec.rb b/spec/unit/lib/temporal/workflow/executor_spec.rb index 0808d3f6..84f31cff 100644 --- a/spec/unit/lib/temporal/workflow/executor_spec.rb +++ b/spec/unit/lib/temporal/workflow/executor_spec.rb @@ -53,12 +53,12 @@ def execute it 'generates workflow metadata' do allow(Temporal::Metadata::Workflow).to receive(:new).and_call_original - payload = Temporal::Api::Common::V1::Payload.new( + payload = Temporalio::Api::Common::V1::Payload.new( metadata: { 'encoding' => 'json/plain' }, data: '"bar"'.b ) header = - Google::Protobuf::Map.new(:string, :message, Temporal::Api::Common::V1::Payload, { 'Foo' => payload }) + Google::Protobuf::Map.new(:string, :message, Temporalio::Api::Common::V1::Payload, { 'Foo' => payload }) workflow_started_event.workflow_execution_started_event_attributes.header = Fabricate(:api_header, fields: header) diff --git a/spec/unit/lib/temporal/workflow/task_processor_spec.rb b/spec/unit/lib/temporal/workflow/task_processor_spec.rb index 00d1d745..ac5c31e0 100644 --- a/spec/unit/lib/temporal/workflow/task_processor_spec.rb +++ b/spec/unit/lib/temporal/workflow/task_processor_spec.rb @@ -107,7 +107,7 @@ let(:query_id) { SecureRandom.uuid } let(:query_result) { Temporal::Workflow::QueryResult.answer(42) } let(:queries) do - Google::Protobuf::Map.new(:string, :message, Temporal::Api::Query::V1::WorkflowQuery).tap do |map| + Google::Protobuf::Map.new(:string, :message, Temporalio::Api::Query::V1::WorkflowQuery).tap do |map| map[query_id] = Fabricate(:api_workflow_query) end end @@ -221,7 +221,7 @@ .with( namespace: namespace, task_token: task.task_token, - cause: Temporal::Api::Enums::V1::WorkflowTaskFailedCause::WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE, + cause: Temporalio::Api::Enums::V1::WorkflowTaskFailedCause::WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE, exception: exception, binary_checksum: binary_checksum ) @@ -249,7 +249,7 @@ .with( namespace: namespace, task_token: task.task_token, - cause: Temporal::Api::Enums::V1::WorkflowTaskFailedCause::WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE, + cause: Temporalio::Api::Enums::V1::WorkflowTaskFailedCause::WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE, exception: exception, binary_checksum: binary_checksum ) @@ -374,7 +374,7 @@ .with( namespace: namespace, task_token: task.task_token, - cause: Temporal::Api::Enums::V1::WorkflowTaskFailedCause::WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE, + cause: Temporalio::Api::Enums::V1::WorkflowTaskFailedCause::WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE, exception: an_instance_of(Temporal::UnexpectedResponse), binary_checksum: binary_checksum ) From ed2e90554e4dd2aa2dfb61ef63a11aa80e9a490d Mon Sep 17 00:00:00 2001 From: Jeff Schoner Date: Mon, 27 Feb 2023 12:11:18 -0800 Subject: [PATCH 073/125] Test for executable concerns with dup only for String (#219) --- lib/temporal/execution_options.rb | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/temporal/execution_options.rb b/lib/temporal/execution_options.rb index 681cac83..65f0031c 100644 --- a/lib/temporal/execution_options.rb +++ b/lib/temporal/execution_options.rb @@ -51,9 +51,21 @@ def task_list private def has_executable_concern?(object) - # NOTE: When object is a String .dup is needed since Object#singleton_class mutates - # it and screws up C extension class detection (used by Protobufs) - object.dup.singleton_class.included_modules.include?(Concerns::Executable) + if object.is_a?(String) + # NOTE: When object is a String, Object#singleton_class mutates it and + # screws up C extension class detection used in older versions of + # the protobuf library. This was fixed in protobuf 3.20.0-rc1 + # via https://github.com/protocolbuffers/protobuf/pull/9342. + # + # Creating a duplicate of this object prevents the mutation of + # the original object which will be put into a protobuf payload + # before being sent to Temporal server. Because duplication fails + # when Sorbet final classes are used, duplication is limited only + # to String classes. + object = object.dup + end + + object.singleton_class.included_modules.include?(Concerns::Executable) rescue TypeError false end From 868e4929a7dd4786a2edc607ec5529748b538462 Mon Sep 17 00:00:00 2001 From: Jeff Schoner Date: Mon, 27 Feb 2023 12:11:32 -0800 Subject: [PATCH 074/125] APIs for managing custom search attributes (#218) * Methods for operating on custom search attributes * Example test for custom search attribute APIs * Unit tests --- .../integration/search_attributes_spec.rb | 88 +++++++++ lib/temporal.rb | 3 + lib/temporal/client.rb | 15 ++ lib/temporal/connection/grpc.rb | 69 ++++++- lib/temporal/errors.rb | 4 + spec/unit/lib/temporal/client_spec.rb | 42 +++- spec/unit/lib/temporal/grpc_spec.rb | 184 ++++++++++++++++++ 7 files changed, 401 insertions(+), 4 deletions(-) create mode 100644 examples/spec/integration/search_attributes_spec.rb diff --git a/examples/spec/integration/search_attributes_spec.rb b/examples/spec/integration/search_attributes_spec.rb new file mode 100644 index 00000000..6bdd8bf9 --- /dev/null +++ b/examples/spec/integration/search_attributes_spec.rb @@ -0,0 +1,88 @@ +require 'temporal/errors' + +describe 'search attributes' do + let(:attribute_1) { 'Age' } + let(:attribute_2) { 'Name' } + + def cleanup + custom_attributes = Temporal.list_custom_search_attributes + Temporal.remove_custom_search_attributes(attribute_1) if custom_attributes.include?(attribute_1) + Temporal.remove_custom_search_attributes(attribute_2) if custom_attributes.include?(attribute_2) + end + + before do + cleanup + end + + after do + cleanup + end + + it 'add' do + Temporal.add_custom_search_attributes( + { + attribute_1 => :int, + attribute_2 => :keyword + } + ) + + custom_attributes = Temporal.list_custom_search_attributes + expect(custom_attributes).to include(attribute_1 => :int) + expect(custom_attributes).to include(attribute_2 => :keyword) + end + + it 'add duplicate fails' do + Temporal.add_custom_search_attributes( + { + attribute_1 => :int + } + ) + + expect do + Temporal.add_custom_search_attributes( + { + attribute_1 => :int + } + ) + end.to raise_error(Temporal::SearchAttributeAlreadyExistsFailure) + end + + it 'type change fails' do + Temporal.add_custom_search_attributes( + { + attribute_1 => :int + } + ) + + Temporal.remove_custom_search_attributes(attribute_1) + + expect do + Temporal.add_custom_search_attributes( + { + attribute_1 => :keyword + } + ) + end.to raise_error(an_instance_of(Temporal::SearchAttributeFailure)) + end + + it 'remove' do + Temporal.add_custom_search_attributes( + { + attribute_1 => :int, + attribute_2 => :keyword + } + ) + + Temporal.remove_custom_search_attributes(attribute_1, attribute_2) + + custom_attributes = Temporal.list_custom_search_attributes + expect(custom_attributes).not_to include(attribute_1 => :int) + expect(custom_attributes).not_to include(attribute_2 => :keyword) + end + + it 'remove non-existent fails' do + expect do + Temporal.remove_custom_search_attributes(attribute_1, attribute_2) + end.to raise_error(Temporal::NotFoundFailure) + end +end diff --git a/lib/temporal.rb b/lib/temporal.rb index 90baf264..ec1ca412 100644 --- a/lib/temporal.rb +++ b/lib/temporal.rb @@ -30,6 +30,9 @@ module Temporal :list_open_workflow_executions, :list_closed_workflow_executions, :query_workflow_executions, + :add_custom_search_attributes, + :list_custom_search_attributes, + :remove_custom_search_attributes, :connection class << self diff --git a/lib/temporal/client.rb b/lib/temporal/client.rb index ea991faa..da92b9a1 100644 --- a/lib/temporal/client.rb +++ b/lib/temporal/client.rb @@ -409,6 +409,21 @@ def query_workflow_executions(namespace, query, next_page_token: nil, max_page_s Temporal::Workflow::Executions.new(connection: connection, status: :all, request_options: { namespace: namespace, query: query, next_page_token: next_page_token, max_page_size: max_page_size }.merge(filter)) end + # @param attributes [Hash[String, Symbol]] name to symbol for type, see INDEXED_VALUE_TYPE above + def add_custom_search_attributes(attributes) + connection.add_custom_search_attributes(attributes) + end + + # @return Hash[String, Symbol] name to symbol for type, see INDEXED_VALUE_TYPE above + def list_custom_search_attributes + connection.list_custom_search_attributes + end + + # @param attribute_names [Array[String]] Attributes to remove + def remove_custom_search_attributes(*attribute_names) + connection.remove_custom_search_attributes(attribute_names) + end + def connection @connection ||= Temporal::Connection.generate(config.for_connection) end diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index 0240ab23..52c440fa 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -4,7 +4,9 @@ require 'securerandom' require 'gen/temporal/api/filter/v1/message_pb' require 'gen/temporal/api/workflowservice/v1/service_services_pb' +require 'gen/temporal/api/operatorservice/v1/service_services_pb' require 'gen/temporal/api/enums/v1/workflow_pb' +require 'gen/temporal/api/enums/v1/common_pb' require 'temporal/connection/errors' require 'temporal/connection/serializer' require 'temporal/connection/serializer/failure' @@ -27,10 +29,26 @@ class GRPC not_completed_cleanly: Temporalio::Api::Enums::V1::QueryRejectCondition::QUERY_REJECT_CONDITION_NOT_COMPLETED_CLEANLY }.freeze + SYMBOL_TO_INDEXED_VALUE_TYPE = { + text: Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_TEXT, + keyword: Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_KEYWORD, + int: Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_INT, + double: Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_DOUBLE, + bool: Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_BOOL, + datetime: Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_DATETIME, + keyword_list: Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_KEYWORD_LIST, + }.freeze + + INDEXED_VALUE_TYPE_TO_SYMBOL = SYMBOL_TO_INDEXED_VALUE_TYPE.map do |symbol, int_value| + [Temporalio::Api::Enums::V1::IndexedValueType.lookup(int_value), symbol] + end.to_h.freeze + DEFAULT_OPTIONS = { max_page_size: 100 }.freeze + CONNECTION_TIMEOUT_SECONDS = 60 + def initialize(host, port, identity, credentials, options = {}) @url = "#{host}:#{port}" @identity = identity @@ -475,8 +493,45 @@ def count_workflow_executions(namespace:, query:) client.count_workflow_executions(request) end - def get_search_attributes - raise NotImplementedError + def add_custom_search_attributes(attributes) + attributes.each_value do |symbol_type| + next if SYMBOL_TO_INDEXED_VALUE_TYPE.include?(symbol_type) + + raise Temporal::InvalidSearchAttributeTypeFailure.new( + "Cannot add search attributes (#{attributes}): unknown search attribute type :#{symbol_type}, supported types: #{SYMBOL_TO_INDEXED_VALUE_TYPE.keys}" + ) + end + + request = Temporalio::Api::OperatorService::V1::AddSearchAttributesRequest.new( + search_attributes: attributes.map { |name, type| [name, SYMBOL_TO_INDEXED_VALUE_TYPE[type]] }.to_h + ) + begin + operator_client.add_search_attributes(request) + rescue ::GRPC::AlreadyExists => e + raise Temporal::SearchAttributeAlreadyExistsFailure.new(e) + rescue ::GRPC::Internal => e + # The internal workflow that adds search attributes can fail for a variety of reasons such + # as recreating a removed attribute with a new type. Wrap these all up into a fall through + # exception. + raise Temporal::SearchAttributeFailure.new(e) + end + end + + def list_custom_search_attributes + request = Temporalio::Api::OperatorService::V1::ListSearchAttributesRequest.new + response = operator_client.list_search_attributes(request) + response.custom_attributes.map { |name, type| [name, INDEXED_VALUE_TYPE_TO_SYMBOL[type]] }.to_h + end + + def remove_custom_search_attributes(attribute_names) + request = Temporalio::Api::OperatorService::V1::RemoveSearchAttributesRequest.new( + search_attributes: attribute_names + ) + begin + operator_client.remove_search_attributes(request) + rescue ::GRPC::NotFound => e + raise Temporal::NotFoundFailure.new(e) + end end def reset_sticky_task_queue @@ -556,7 +611,15 @@ def client @client ||= Temporalio::Api::WorkflowService::V1::WorkflowService::Stub.new( url, credentials, - timeout: 60 + timeout: CONNECTION_TIMEOUT_SECONDS + ) + end + + def operator_client + @operator_client ||= Temporalio::Api::OperatorService::V1::OperatorService::Stub.new( + url, + credentials, + timeout: CONNECTION_TIMEOUT_SECONDS ) end diff --git a/lib/temporal/errors.rb b/lib/temporal/errors.rb index 84da25f2..b2144658 100644 --- a/lib/temporal/errors.rb +++ b/lib/temporal/errors.rb @@ -78,4 +78,8 @@ class NamespaceAlreadyExistsFailure < ApiError; end class CancellationAlreadyRequestedFailure < ApiError; end class QueryFailed < ApiError; end class UnexpectedResponse < ApiError; end + + class SearchAttributeAlreadyExistsFailure < ApiError; end + class SearchAttributeFailure < ApiError; end + class InvalidSearchAttributeTypeFailure < ClientError; end end diff --git a/spec/unit/lib/temporal/client_spec.rb b/spec/unit/lib/temporal/client_spec.rb index 5d8019bc..52919d46 100644 --- a/spec/unit/lib/temporal/client_spec.rb +++ b/spec/unit/lib/temporal/client_spec.rb @@ -787,6 +787,47 @@ class NamespacedWorkflow < Temporal::Workflow end end + describe '#add_custom_search_attributes' do + before { allow(connection).to receive(:add_custom_search_attributes) } + + let(:attributes) { { SomeTextField: :text, SomeIntField: :int } } + + it 'passes through to connection' do + subject.add_custom_search_attributes(attributes) + + expect(connection) + .to have_received(:add_custom_search_attributes) + .with(attributes) + end + end + + describe '#list_custom_search_attributes' do + let(:attributes) { { 'SomeIntField' => :int, 'SomeBoolField' => :bool } } + + before { allow(connection).to receive(:list_custom_search_attributes).and_return(attributes) } + + it 'passes through to connection' do + response = subject.list_custom_search_attributes + + expect(response).to eq(attributes) + + expect(connection) + .to have_received(:list_custom_search_attributes) + end + end + + describe '#remove_custom_search_attributes' do + before { allow(connection).to receive(:remove_custom_search_attributes) } + + it 'passes through to connection' do + subject.remove_custom_search_attributes(:SomeTextField, :SomeIntField) + + expect(connection) + .to have_received(:remove_custom_search_attributes) + .with(%i[SomeTextField SomeIntField]) + end + end + describe '#list_open_workflow_executions' do let(:from) { Time.now - 600 } let(:now) { Time.now } @@ -931,7 +972,6 @@ class NamespacedWorkflow < Temporal::Workflow .to have_received(:list_open_workflow_executions) .with(namespace: namespace, from: from, to: now, next_page_token: 'a', max_page_size: 10) .once - end end diff --git a/spec/unit/lib/temporal/grpc_spec.rb b/spec/unit/lib/temporal/grpc_spec.rb index 42f0f3a3..09552231 100644 --- a/spec/unit/lib/temporal/grpc_spec.rb +++ b/spec/unit/lib/temporal/grpc_spec.rb @@ -5,6 +5,7 @@ let(:identity) { 'my-identity' } let(:binary_checksum) { 'v1.0.0' } let(:grpc_stub) { double('grpc stub') } + let(:grpc_operator_stub) { double('grpc stub') } let(:namespace) { 'test-namespace' } let(:workflow_id) { SecureRandom.uuid } let(:run_id) { SecureRandom.uuid } @@ -18,6 +19,7 @@ class TestDeserializer before do allow(subject).to receive(:client).and_return(grpc_stub) + allow(subject).to receive(:operator_client).and_return(grpc_operator_stub) allow(Time).to receive(:now).and_return(now) end @@ -643,4 +645,186 @@ class TestDeserializer end end end + + describe '#add_custom_search_attributes' do + it 'calls GRPC service with supplied arguments' do + allow(grpc_operator_stub).to receive(:add_search_attributes) + subject.add_custom_search_attributes( + { + 'SomeTextField' => :text, + 'SomeKeywordField' => :keyword, + 'SomeIntField' => :int, + 'SomeDoubleField' => :double, + 'SomeBoolField' => :bool, + 'SomeDatetimeField' => :datetime, + 'SomeKeywordListField' => :keyword_list + } + ) + + expect(grpc_operator_stub).to have_received(:add_search_attributes) do |request| + expect(request).to be_an_instance_of(Temporalio::Api::OperatorService::V1::AddSearchAttributesRequest) + expect(request.search_attributes).to eq( + { + 'SomeTextField' => Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_TEXT, + 'SomeKeywordField' => Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_KEYWORD, + 'SomeIntField' => Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_INT, + 'SomeDoubleField' => Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_DOUBLE, + 'SomeBoolField' => Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_BOOL, + 'SomeDatetimeField' => Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_DATETIME, + 'SomeKeywordListField' => Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_KEYWORD_LIST + } + ) + end + end + + it 'attribute already exists' do + allow(grpc_operator_stub).to receive(:add_search_attributes).and_raise(GRPC::AlreadyExists.new('')) + expect do + subject.add_custom_search_attributes( + { + 'SomeTextField' => :text + } + ) + end.to raise_error(Temporal::SearchAttributeAlreadyExistsFailure) + end + + it 'failed to add attribute' do + allow(grpc_operator_stub).to receive(:add_search_attributes).and_raise(GRPC::Internal.new('')) + expect do + subject.add_custom_search_attributes( + { + 'SomeTextField' => :text + } + ) + end.to raise_error(Temporal::SearchAttributeFailure) + end + + it 'attributes can be symbols' do + allow(grpc_operator_stub).to receive(:add_search_attributes) + subject.add_custom_search_attributes( + { + SomeTextField: :text + } + ) + + expect(grpc_operator_stub).to have_received(:add_search_attributes) do |request| + expect(request).to be_an_instance_of(Temporalio::Api::OperatorService::V1::AddSearchAttributesRequest) + expect(request.search_attributes).to eq( + { + 'SomeTextField' => Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_TEXT + } + ) + end + end + + it 'invalid attribute type' do + expect do + subject.add_custom_search_attributes( + { + 'SomeBadField' => :foo + } + ) + end.to raise_error(Temporal::InvalidSearchAttributeTypeFailure) do |e| + expect(e.to_s).to eq('Cannot add search attributes ({"SomeBadField"=>:foo}): unknown search attribute type :foo, supported types: [:text, :keyword, :int, :double, :bool, :datetime, :keyword_list]') + end + end + end + + describe '#list_custom_search_attributes' do + it 'calls GRPC service with supplied arguments' do + allow(grpc_operator_stub).to receive(:list_search_attributes).and_return( + Temporalio::Api::OperatorService::V1::ListSearchAttributesResponse.new( + custom_attributes: { + 'SomeTextField' => Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_TEXT, + 'SomeKeywordField' => Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_KEYWORD, + 'SomeIntField' => Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_INT, + 'SomeDoubleField' => Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_DOUBLE, + 'SomeBoolField' => Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_BOOL, + 'SomeDatetimeField' => Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_DATETIME, + 'SomeKeywordListField' => Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_KEYWORD_LIST + } + ) + ) + + response = subject.list_custom_search_attributes + + expect(response).to eq( + { + 'SomeTextField' => :text, + 'SomeKeywordField' => :keyword, + 'SomeIntField' => :int, + 'SomeDoubleField' => :double, + 'SomeBoolField' => :bool, + 'SomeDatetimeField' => :datetime, + 'SomeKeywordListField' => :keyword_list + } + ) + + expect(grpc_operator_stub).to have_received(:list_search_attributes) do |request| + expect(request).to be_an_instance_of(Temporalio::Api::OperatorService::V1::ListSearchAttributesRequest) + end + end + + it 'unknown attribute type becomes nil' do + allow(grpc_operator_stub).to receive(:list_search_attributes).and_return( + Temporalio::Api::OperatorService::V1::ListSearchAttributesResponse.new( + custom_attributes: { + 'SomeTextField' => Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_TEXT, + 'SomeUnknownField' => 100 # simulate some new type being added in the proto in the future + } + ) + ) + + response = subject.list_custom_search_attributes + + expect(response).to eq( + { + 'SomeTextField' => :text, + 'SomeUnknownField' => nil + } + ) + end + end + + describe '#remove_custom_search_attributes' do + it 'calls GRPC service with supplied arguments' do + allow(grpc_operator_stub).to receive(:remove_search_attributes) + + attributes = ['SomeTextField', 'SomeIntField'] + + subject.remove_custom_search_attributes( + attributes + ) + + expect(grpc_operator_stub).to have_received(:remove_search_attributes) do |request| + expect(request).to be_an_instance_of(Temporalio::Api::OperatorService::V1::RemoveSearchAttributesRequest) + expect(request.search_attributes).to eq(attributes) + end + end + + it 'cannot remove non-existent attribute' do + allow(grpc_operator_stub).to receive(:remove_search_attributes).and_raise(GRPC::NotFound.new) + + attributes = ['SomeTextField', 'SomeIntField'] + + expect do + subject.remove_custom_search_attributes( + attributes + ) + end.to raise_error(Temporal::NotFoundFailure) + end + + it 'attribute names can be symbols' do + allow(grpc_operator_stub).to receive(:remove_search_attributes) + + subject.remove_custom_search_attributes( + %i[SomeTextField SomeIntField] + ) + + expect(grpc_operator_stub).to have_received(:remove_search_attributes) do |request| + expect(request).to be_an_instance_of(Temporalio::Api::OperatorService::V1::RemoveSearchAttributesRequest) + expect(request.search_attributes).to eq(%w[SomeTextField SomeIntField]) + end + end + end end From d0cf921cfede0346ae7cfdb188c2d52a900ff2e6 Mon Sep 17 00:00:00 2001 From: Drew Hoskins <37816070+drewhoskins-stripe@users.noreply.github.com> Date: Wed, 8 Mar 2023 08:26:41 -0800 Subject: [PATCH 075/125] dynamic Workflow (#221) * Dynamic Workflows * Feedback --- examples/bin/worker | 1 + .../spec/integration/dynamic_workflow_spec.rb | 37 +++++++++++++++++++ examples/workflows/delegator_workflow.rb | 33 +++++++++++++++++ lib/temporal/errors.rb | 1 + lib/temporal/worker.rb | 36 +++++++++++++----- lib/temporal/workflow/context.rb | 4 ++ spec/unit/lib/temporal/worker_spec.rb | 36 ++++++++++++++++++ .../lib/temporal/workflow/context_spec.rb | 12 +++++- 8 files changed, 149 insertions(+), 11 deletions(-) create mode 100644 examples/spec/integration/dynamic_workflow_spec.rb create mode 100644 examples/workflows/delegator_workflow.rb diff --git a/examples/bin/worker b/examples/bin/worker index 29e55b69..2c6fc4e3 100755 --- a/examples/bin/worker +++ b/examples/bin/worker @@ -67,6 +67,7 @@ worker.register_workflow(UpsertSearchAttributesWorkflow) worker.register_workflow(WaitForWorkflow) worker.register_workflow(WaitForExternalSignalWorkflow) worker.register_workflow(WaitForNamedSignalWorkflow) +worker.register_dynamic_workflow(DelegatorWorkflow) worker.register_activity(AsyncActivity) worker.register_activity(EchoActivity) diff --git a/examples/spec/integration/dynamic_workflow_spec.rb b/examples/spec/integration/dynamic_workflow_spec.rb new file mode 100644 index 00000000..39cff9c7 --- /dev/null +++ b/examples/spec/integration/dynamic_workflow_spec.rb @@ -0,0 +1,37 @@ +require 'workflows/delegator_workflow' + +describe 'Dynamic workflows' do + let(:workflow_id) { SecureRandom.uuid } + + it 'can delegate to other classes' do + # PlusExecutor and TimesExecutor do not subclass Workflow + run_id = Temporal.start_workflow( + PlusExecutor, + {a: 5, b: 3}, + options: { + workflow_id: workflow_id + }) + + result = Temporal.await_workflow_result( + PlusExecutor, + workflow_id: workflow_id, + run_id: run_id, + ) + expect(result[:computation]).to eq(8) + + run_id = Temporal.start_workflow( + TimesExecutor, + {a: 5, b: 3}, + options: { + workflow_id: workflow_id + }) + + result = Temporal.await_workflow_result( + TimesExecutor, + workflow_id: workflow_id, + run_id: run_id, + ) + expect(result[:computation]).to eq(15) + + end +end diff --git a/examples/workflows/delegator_workflow.rb b/examples/workflows/delegator_workflow.rb new file mode 100644 index 00000000..4d4c6cb1 --- /dev/null +++ b/examples/workflows/delegator_workflow.rb @@ -0,0 +1,33 @@ +# This sample illustrates using a dynamic Activity to delegate to another set of non-activity +# classes. This is an advanced use case, used, for example, for integrating with an existing framework +# that doesn't know about temporal. +# See Temporal::Worker#register_dynamic_activity for more info. + +# An example of another non-Activity class hierarchy. +class MyWorkflowExecutor + def do_it(_args) + raise NotImplementedError + end +end + +class PlusExecutor < MyWorkflowExecutor + def do_it(args) + args[:a] + args[:b] + end +end + +class TimesExecutor < MyWorkflowExecutor + def do_it(args) + args[:a] * args[:b] + end +end + +# Calls into our other class hierarchy. +class DelegatorWorkflow < Temporal::Workflow + def execute(input) + executor = Object.const_get(workflow.name).new + raise ArgumentError, "Unknown workflow: #{executor.class}" unless executor.is_a?(MyWorkflowExecutor) + + {computation: executor.do_it(input)} + end +end diff --git a/lib/temporal/errors.rb b/lib/temporal/errors.rb index b2144658..323e25cd 100644 --- a/lib/temporal/errors.rb +++ b/lib/temporal/errors.rb @@ -33,6 +33,7 @@ class ActivityCanceled < ActivityException; end class ActivityNotRegistered < ClientError; end class WorkflowNotRegistered < ClientError; end class SecondDynamicActivityError < ClientError; end + class SecondDynamicWorkflowError < ClientError; end class ApiError < Error; end diff --git a/lib/temporal/worker.rb b/lib/temporal/worker.rb index a051c2b2..1dab48fa 100644 --- a/lib/temporal/worker.rb +++ b/lib/temporal/worker.rb @@ -43,25 +43,41 @@ def initialize( end def register_workflow(workflow_class, options = {}) - execution_options = ExecutionOptions.new(workflow_class, options, config.default_execution_options) - key = [execution_options.namespace, execution_options.task_queue] + namespace_and_task_queue, execution_options = executable_registration(workflow_class, options) + + @workflows[namespace_and_task_queue].add(execution_options.name, workflow_class) + end + + # Register one special workflow that you want to intercept any unknown workflows, + # perhaps so you can delegate work to other classes, somewhat analogous to ruby's method_missing. + # Only one dynamic Workflow may be registered per task queue. + # Within Workflow#execute, you may retrieve the name of the unknown class via workflow.name. + def register_dynamic_workflow(workflow_class, options = {}) + namespace_and_task_queue, execution_options = executable_registration(workflow_class, options) - @workflows[key].add(execution_options.name, workflow_class) + begin + @workflows[namespace_and_task_queue].add_dynamic(execution_options.name, workflow_class) + rescue Temporal::ExecutableLookup::SecondDynamicExecutableError => e + raise Temporal::SecondDynamicWorkflowError, + "Temporal::Worker#register_dynamic_workflow: cannot register #{execution_options.name} "\ + "dynamically; #{e.previous_executable_name} was already registered dynamically for task queue "\ + "'#{execution_options.task_queue}', and there can be only one." + end end def register_activity(activity_class, options = {}) - key, execution_options = activity_registration(activity_class, options) - @activities[key].add(execution_options.name, activity_class) + namespace_and_task_queue, execution_options = executable_registration(activity_class, options) + @activities[namespace_and_task_queue].add(execution_options.name, activity_class) end - # Register one special activity that you want to intercept any unknown Activities, + # Register one special activity that you want to intercept any unknown activities, # perhaps so you can delegate work to other classes, somewhat analogous to ruby's method_missing. # Only one dynamic Activity may be registered per task queue. # Within Activity#execute, you may retrieve the name of the unknown class via activity.name. def register_dynamic_activity(activity_class, options = {}) - key, execution_options = activity_registration(activity_class, options) + namespace_and_task_queue, execution_options = executable_registration(activity_class, options) begin - @activities[key].add_dynamic(execution_options.name, activity_class) + @activities[namespace_and_task_queue].add_dynamic(execution_options.name, activity_class) rescue Temporal::ExecutableLookup::SecondDynamicExecutableError => e raise Temporal::SecondDynamicActivityError, "Temporal::Worker#register_dynamic_activity: cannot register #{execution_options.name} "\ @@ -126,8 +142,8 @@ def activity_poller_for(namespace, task_queue, lookup) Activity::Poller.new(namespace, task_queue, lookup.freeze, config, activity_middleware, activity_poller_options) end - def activity_registration(activity_class, options) - execution_options = ExecutionOptions.new(activity_class, options, config.default_execution_options) + def executable_registration(executable_class, options) + execution_options = ExecutionOptions.new(executable_class, options, config.default_execution_options) key = [execution_options.namespace, execution_options.task_queue] [key, execution_options] end diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index 7de79bb9..c9561780 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -212,6 +212,10 @@ def start_timer(timeout, timer_id = nil) future end + def name + @metadata.name + end + def cancel_timer(timer_id) command = Command::CancelTimer.new(timer_id: timer_id) schedule_command(command) diff --git a/spec/unit/lib/temporal/worker_spec.rb b/spec/unit/lib/temporal/worker_spec.rb index 6c153a88..bd8a1211 100644 --- a/spec/unit/lib/temporal/worker_spec.rb +++ b/spec/unit/lib/temporal/worker_spec.rb @@ -12,6 +12,11 @@ class TestWorkerWorkflow < Temporal::Workflow task_queue 'default-task-queue' end + class OtherTestWorkerWorkflow < Temporal::Workflow + namespace 'default-namespace' + task_queue 'default-task-queue' + end + class TestWorkerActivity < Temporal::Activity namespace 'default-namespace' task_queue 'default-task-queue' @@ -64,6 +69,37 @@ class OtherTestWorkerActivity < Temporal::Activity end end + describe '#register_dynamic_workflow' do + let(:workflow_keys) { subject.send(:workflows).keys } + + it 'registers a dynamic workflow with the provided config options' do + lookup = instance_double(Temporal::ExecutableLookup, add: nil) + expect(Temporal::ExecutableLookup).to receive(:new).and_return(lookup) + expect(lookup).to receive(:add_dynamic).with('test-dynamic-workflow', TestWorkerWorkflow) + + subject.register_dynamic_workflow( + TestWorkerWorkflow, + name: 'test-dynamic-workflow', + namespace: 'test-namespace', + task_queue: 'test-task-queue' + ) + + expect(workflow_keys).to include(['test-namespace', 'test-task-queue']) + end + + it 'cannot double-register a workflow' do + subject.register_dynamic_workflow(TestWorkerWorkflow) + expect do + subject.register_dynamic_workflow(OtherTestWorkerWorkflow) + end.to raise_error( + Temporal::SecondDynamicWorkflowError, + 'Temporal::Worker#register_dynamic_workflow: cannot register OtherTestWorkerWorkflow dynamically; ' \ + 'TestWorkerWorkflow was already registered dynamically for task queue \'default-task-queue\', ' \ + 'and there can be only one.' + ) + end + end + describe '#register_activity' do let(:lookup) { instance_double(Temporal::ExecutableLookup, add: nil) } let(:activity_keys) { subject.send(:activities).keys } diff --git a/spec/unit/lib/temporal/workflow/context_spec.rb b/spec/unit/lib/temporal/workflow/context_spec.rb index 90a3cb14..d3169e53 100644 --- a/spec/unit/lib/temporal/workflow/context_spec.rb +++ b/spec/unit/lib/temporal/workflow/context_spec.rb @@ -4,6 +4,7 @@ require 'temporal/workflow/future' require 'temporal/workflow/query_registry' require 'temporal/workflow/stack_trace_tracker' +require 'temporal/metadata/workflow' require 'time' class MyTestWorkflow < Temporal::Workflow; end @@ -16,7 +17,9 @@ class MyTestWorkflow < Temporal::Workflow; end allow(double).to receive(:register) double end - let(:metadata) { instance_double('Temporal::Metadata::Workflow') } + let(:metadata_hash) { Fabricate(:workflow_metadata).to_h } + let(:metadata) { Temporal::Metadata::Workflow.new(**metadata_hash) } + let(:workflow_context) do Temporal::Workflow::Context.new( state_manager, @@ -247,6 +250,13 @@ class MyTestWorkflow < Temporal::Workflow; end end end + describe '#name' do + it 'returns the name from the metadata' do + # Set in the :workflow_metadata Fabricator + expect(workflow_context.name).to eq("TestWorkflow") + end + end + describe '#wait_for_all' do let(:target_1) { 'target1' } let(:future_1) { Temporal::Workflow::Future.new(target_1, workflow_context) } From 5e9dd9eeff28f11bf7d506f0f3867cb5a0ac2067 Mon Sep 17 00:00:00 2001 From: Jeff Schoner Date: Fri, 24 Mar 2023 13:39:47 -0700 Subject: [PATCH 076/125] Read search attributes from inside workflow code (#222) * Read search attributes from inside workflow code * Add comment about how to set search attributes * Make nil handling consistent in state manager * Refactor and extend state manager specs * Refactor upsert_search_attributes_workflow * Make upserted search attributes available to read immediately --- .../initial_search_attributes_spec.rb | 4 +- .../upsert_search_attributes_workflow.rb | 12 +- lib/temporal/workflow/context.rb | 21 +++- lib/temporal/workflow/state_manager.rb | 13 +- .../grpc/history_event_fabricator.rb | 27 +++- .../lib/temporal/workflow/context_spec.rb | 6 + .../temporal/workflow/state_manager_spec.rb | 117 +++++++++++++++++- 7 files changed, 189 insertions(+), 11 deletions(-) diff --git a/examples/spec/integration/initial_search_attributes_spec.rb b/examples/spec/integration/initial_search_attributes_spec.rb index 29febed0..fae51adf 100644 --- a/examples/spec/integration/initial_search_attributes_spec.rb +++ b/examples/spec/integration/initial_search_attributes_spec.rb @@ -40,12 +40,12 @@ ) # UpsertSearchAttributesWorkflow returns the search attributes it upserted during its execution - added_attributes = Temporal.await_workflow_result( + attributes_at_end = Temporal.await_workflow_result( UpsertSearchAttributesWorkflow, workflow_id: workflow_id, run_id: run_id, ) - expect(added_attributes).to eq(upserted_search_attributes) + expect(attributes_at_end).to eq(expected_custom_attributes) # These attributes are set for the worker in bin/worker expected_attributes = { diff --git a/examples/workflows/upsert_search_attributes_workflow.rb b/examples/workflows/upsert_search_attributes_workflow.rb index fd92e108..4657628f 100644 --- a/examples/workflows/upsert_search_attributes_workflow.rb +++ b/examples/workflows/upsert_search_attributes_workflow.rb @@ -16,6 +16,10 @@ def execute(values) } attributes.compact! workflow.upsert_search_attributes(attributes) + # .dup because the same backing hash may be used throughout the workflow, causing + # the equality check at the end to succeed incorrectly + attributes_after_upsert = workflow.search_attributes.dup + # The following lines are extra complexity to test if upsert_search_attributes is tracked properly in the internal # state machine. future = HelloWorldActivity.execute("Moon") @@ -24,6 +28,12 @@ def execute(values) workflow.wait_for_all(future) HelloWorldActivity.execute!(name) - attributes + + attributes_at_end = workflow.search_attributes + if attributes_at_end != attributes_after_upsert + raise "Attributes at end #{attributes_at_end} don't match after upsert #{attributes_after_upsert}" + end + + attributes_at_end end end diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index c9561780..793d4377 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -55,6 +55,13 @@ def headers metadata.headers end + # Retrieves a hash of all current search attributes on this workflow run. Attributes + # can be set in a workflow by calling upsert_search_attributes or when starting a + # workflow by specifying the search_attributes option. + def search_attributes + state_manager.search_attributes + end + def has_release?(release_name) state_manager.release?(release_name.to_s) end @@ -256,7 +263,7 @@ def continue_as_new(*input, **args) retry_policy: execution_options.retry_policy, headers: execution_options.headers, memo: execution_options.memo, - search_attributes: Helpers.process_search_attributes(execution_options.search_attributes), + search_attributes: Helpers.process_search_attributes(execution_options.search_attributes) ) schedule_command(command) completed! @@ -416,9 +423,19 @@ def signal_external_workflow(workflow, signal, workflow_id, run_id = nil, input end # Replaces or adds the values of your custom search attributes specified during a workflow's execution. - # To use this your server must support Elasticsearch, and the attributes must be pre-configured + # To use this your server must enable advanced visibility using SQL starting with version 1.20 or + # Elasticsearch on all versions. The attributes must be pre-configured. # See https://docs.temporal.io/docs/concepts/what-is-a-search-attribute/ # + # Do be aware that non-deterministic upserting of search attributes can lead to "phantom" + # attributes that are available in code but not on Temporal server. For example, if your code + # upserted {"foo" => 1} then changed to upsert {"bar" => 2} without proper versioning, you + # will see {"foo" => 1, "bar" => 2} in search attributes in workflow code even though + # {"bar" => 2} was never upserted on Temporal server. When the same search attribute + # name is used with a different value, you will see a similar case where the new value will + # be present until the end of the history window, then change to the old version after that. This + # does at least match the "old" value that will be present on the server. + # # @param search_attributes [Hash] # If an attribute is registered as a Datetime, you can pass in a Time: e.g. # workflow.now diff --git a/lib/temporal/workflow/state_manager.rb b/lib/temporal/workflow/state_manager.rb index 9c391e90..6fbf8983 100644 --- a/lib/temporal/workflow/state_manager.rb +++ b/lib/temporal/workflow/state_manager.rb @@ -18,7 +18,7 @@ class StateManager class UnsupportedEvent < Temporal::InternalError; end class UnsupportedMarkerType < Temporal::InternalError; end - attr_reader :commands, :local_time + attr_reader :commands, :local_time, :search_attributes def initialize(dispatcher) @dispatcher = dispatcher @@ -30,6 +30,7 @@ def initialize(dispatcher) @last_event_id = 0 @local_time = nil @replay = false + @search_attributes = {} end def replay? @@ -52,6 +53,11 @@ def schedule(command) command.workflow_id ||= command_id when Command::StartTimer command.timer_id ||= command_id + when Command::UpsertSearchAttributes + # This allows newly upserted search attributes to be read + # immediately. Without this, attributes would not be available + # until the next history window is applied on replay. + search_attributes.merge!(command.search_attributes) end state_machine = command_tracker[command_id] @@ -123,6 +129,10 @@ def apply_event(event) case event.type when 'WORKFLOW_EXECUTION_STARTED' + unless event.attributes.search_attributes.nil? + search_attributes.merge!(from_payload_map(event.attributes.search_attributes&.indexed_fields || {})) + end + state_machine.start dispatch( History::EventTarget.workflow, @@ -290,6 +300,7 @@ def apply_event(event) dispatch(history_target, 'completed') when 'UPSERT_WORKFLOW_SEARCH_ATTRIBUTES' + search_attributes.merge!(from_payload_map(event.attributes.search_attributes&.indexed_fields || {})) # no need to track state; this is just a synchronous API call. discard_command(history_target) diff --git a/spec/fabricators/grpc/history_event_fabricator.rb b/spec/fabricators/grpc/history_event_fabricator.rb index 41b3a191..6f043b4d 100644 --- a/spec/fabricators/grpc/history_event_fabricator.rb +++ b/spec/fabricators/grpc/history_event_fabricator.rb @@ -1,23 +1,25 @@ require 'securerandom' +require 'temporal/concerns/payloads' class TestSerializer extend Temporal::Concerns::Payloads end +include Temporal::Concerns::Payloads + Fabricator(:api_history_event, from: Temporalio::Api::History::V1::HistoryEvent) do event_id { 1 } event_time { Time.now } end Fabricator(:api_workflow_execution_started_event, from: :api_history_event) do - transient :headers + transient :headers, :search_attributes event_type { Temporalio::Api::Enums::V1::EventType::EVENT_TYPE_WORKFLOW_EXECUTION_STARTED } event_time { Time.now } workflow_execution_started_event_attributes do |attrs| - header_fields = (attrs[:headers] || {}).each_with_object({}) do |(field, value), h| - h[field] = Temporal.configuration.converter.to_payload(value) - end + header_fields = to_payload_map(attrs[:headers] || {}) header = Temporalio::Api::Common::V1::Header.new(fields: header_fields) + indexed_fields = attrs[:search_attributes] ? to_payload_map(attrs[:search_attributes]) : nil Temporalio::Api::History::V1::WorkflowExecutionStartedEventAttributes.new( workflow_type: Fabricate(:api_workflow_type), @@ -31,6 +33,9 @@ class TestSerializer retry_policy: nil, attempt: 0, header: header, + search_attributes: Temporalio::Api::Common::V1::SearchAttributes.new( + indexed_fields: indexed_fields + ) ) end end @@ -180,3 +185,17 @@ class TestSerializer ) end end + +Fabricator(:api_upsert_search_attributes_event, from: :api_history_event) do + transient :search_attributes + event_type { Temporalio::Api::Enums::V1::EventType::EVENT_TYPE_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES } + upsert_workflow_search_attributes_event_attributes do |attrs| + indexed_fields = attrs[:search_attributes] ? to_payload_map(attrs[:search_attributes]) : nil + Temporalio::Api::History::V1::UpsertWorkflowSearchAttributesEventAttributes.new( + workflow_task_completed_event_id: attrs[:event_id] - 1, + search_attributes: Temporalio::Api::Common::V1::SearchAttributes.new( + indexed_fields: indexed_fields + ) + ) + end +end diff --git a/spec/unit/lib/temporal/workflow/context_spec.rb b/spec/unit/lib/temporal/workflow/context_spec.rb index d3169e53..6ca8e91f 100644 --- a/spec/unit/lib/temporal/workflow/context_spec.rb +++ b/spec/unit/lib/temporal/workflow/context_spec.rb @@ -248,6 +248,12 @@ class MyTestWorkflow < Temporal::Workflow; end workflow_context.upsert_search_attributes({'CustomDatetimeField' => time}) ).to eq({ 'CustomDatetimeField' => time.utc.iso8601 }) end + + it 'gets latest search attributes from state_manager' do + search_attributes = { 'CustomIntField' => 42 } + expect(state_manager).to receive(:search_attributes).and_return(search_attributes) + expect(workflow_context.search_attributes).to eq(search_attributes) + end end describe '#name' do diff --git a/spec/unit/lib/temporal/workflow/state_manager_spec.rb b/spec/unit/lib/temporal/workflow/state_manager_spec.rb index 57745b71..ce193bb1 100644 --- a/spec/unit/lib/temporal/workflow/state_manager_spec.rb +++ b/spec/unit/lib/temporal/workflow/state_manager_spec.rb @@ -1,5 +1,7 @@ require 'temporal/workflow' require 'temporal/workflow/dispatcher' +require 'temporal/workflow/history/event' +require 'temporal/workflow/history/window' require 'temporal/workflow/state_manager' require 'temporal/errors' @@ -36,4 +38,117 @@ class MyWorkflow < Temporal::Workflow; end end end end -end \ No newline at end of file + + describe '#search_attributes' do + let(:initial_search_attributes) do + { + 'CustomAttribute1' => 42, + 'CustomAttribute2' => 10 + } + end + let(:start_workflow_execution_event) do + Fabricate(:api_workflow_execution_started_event, search_attributes: initial_search_attributes) + end + let(:start_workflow_execution_event_no_search_attributes) do + Fabricate(:api_workflow_execution_started_event) + end + let(:workflow_task_started_event) { Fabricate(:api_workflow_task_started_event, event_id: 2) } + let(:upserted_attributes_1) do + { + 'CustomAttribute3' => 'foo', + 'CustomAttribute2' => 8 + } + end + let(:upsert_search_attribute_event_1) do + Fabricate(:api_upsert_search_attributes_event, search_attributes: upserted_attributes_1) + end + let(:usperted_attributes_2) do + { + 'CustomAttribute3' => 'bar', + 'CustomAttribute4' => 10 + } + end + let(:upsert_search_attribute_event_2) do + Fabricate(:api_upsert_search_attributes_event, + event_id: 4, + search_attributes: usperted_attributes_2) + end + let(:upsert_empty_search_attributes_event) do + Fabricate(:api_upsert_search_attributes_event, search_attributes: {}) + end + + it 'initial merges with upserted' do + state_manager = described_class.new(Temporal::Workflow::Dispatcher.new) + + window = Temporal::Workflow::History::Window.new + window.add(Temporal::Workflow::History::Event.new(start_workflow_execution_event)) + window.add(Temporal::Workflow::History::Event.new(upsert_search_attribute_event_1)) + + command = Temporal::Workflow::Command::UpsertSearchAttributes.new( + search_attributes: upserted_attributes_1 + ) + + state_manager.schedule(command) + # Attributes from command are applied immediately, then merged when + # history window is replayed below. This ensures newly upserted + # search attributes are available immediately in workflow code. + expect(state_manager.search_attributes).to eq(upserted_attributes_1) + + state_manager.apply(window) + + expect(state_manager.search_attributes).to eq( + { + 'CustomAttribute1' => 42, # from initial (not overridden) + 'CustomAttribute2' => 8, # only from upsert + 'CustomAttribute3' => 'foo', # overridden by upsert + } + ) + end + + it 'initial and upsert treated as empty hash' do + state_manager = described_class.new(Temporal::Workflow::Dispatcher.new) + + window = Temporal::Workflow::History::Window.new + window.add(Temporal::Workflow::History::Event.new(start_workflow_execution_event_no_search_attributes)) + window.add(Temporal::Workflow::History::Event.new(upsert_empty_search_attributes_event)) + + command = Temporal::Workflow::Command::UpsertSearchAttributes.new(search_attributes: {}) + expect(state_manager.search_attributes).to eq({}) + + state_manager.schedule(command) + state_manager.apply(window) + + expect(state_manager.search_attributes).to eq({}) + end + + + it 'multiple upserts merge' do + state_manager = described_class.new(Temporal::Workflow::Dispatcher.new) + + window_1 = Temporal::Workflow::History::Window.new + window_1.add(Temporal::Workflow::History::Event.new(workflow_task_started_event)) + window_1.add(Temporal::Workflow::History::Event.new(upsert_search_attribute_event_1)) + + command_1 = Temporal::Workflow::Command::UpsertSearchAttributes.new(search_attributes: upserted_attributes_1) + state_manager.schedule(command_1) + state_manager.apply(window_1) + + expect(state_manager.search_attributes).to eq(upserted_attributes_1) + + window_2 = Temporal::Workflow::History::Window.new + window_2.add(Temporal::Workflow::History::Event.new(upsert_search_attribute_event_2)) + + command_2 = Temporal::Workflow::Command::UpsertSearchAttributes.new(search_attributes: usperted_attributes_2) + state_manager.schedule(command_2) + state_manager.apply(window_2) + + expect(state_manager.search_attributes).to eq( + { + 'CustomAttribute2' => 8, + 'CustomAttribute3' => 'bar', + 'CustomAttribute4' => 10, + } + ) + end + end +end From d0f3ad96d20c60736e71b471e5055bbfdfae79af Mon Sep 17 00:00:00 2001 From: markchua Date: Tue, 28 Mar 2023 10:45:25 -0700 Subject: [PATCH 077/125] Add header propagation and workflow middleware. (#226) Co-authored-by: Mark Chua --- examples/bin/trigger | 5 ++ examples/bin/worker | 6 +++ examples/middleware/sample_propagator.rb | 10 ++++ lib/temporal/client.rb | 6 +-- lib/temporal/configuration.rb | 14 ++++- .../serializer/schedule_activity.rb | 2 +- .../middleware/header_propagator_chain.rb | 22 ++++++++ lib/temporal/worker.rb | 9 +++- lib/temporal/workflow/context.rb | 6 +-- lib/temporal/workflow/executor.rb | 10 ++-- lib/temporal/workflow/poller.rb | 8 +-- lib/temporal/workflow/task_processor.rb | 7 +-- spec/unit/lib/temporal/client_spec.rb | 29 +++++++++++ spec/unit/lib/temporal/configuration_spec.rb | 23 +++++++++ .../middleware/header_propagation_chain.rb | 51 +++++++++++++++++++ spec/unit/lib/temporal/worker_spec.rb | 15 ++++++ .../lib/temporal/workflow/context_spec.rb | 30 ++++++++++- .../lib/temporal/workflow/executor_spec.rb | 5 +- .../unit/lib/temporal/workflow/poller_spec.rb | 15 ++++-- .../temporal/workflow/task_processor_spec.rb | 3 +- 20 files changed, 251 insertions(+), 25 deletions(-) create mode 100644 examples/middleware/sample_propagator.rb create mode 100644 lib/temporal/middleware/header_propagator_chain.rb create mode 100644 spec/unit/lib/temporal/middleware/header_propagation_chain.rb diff --git a/examples/bin/trigger b/examples/bin/trigger index 725bd35e..34661068 100755 --- a/examples/bin/trigger +++ b/examples/bin/trigger @@ -2,6 +2,7 @@ require_relative '../init' Dir[File.expand_path('../workflows/*.rb', __dir__)].each { |f| require f } +Dir[File.expand_path('../middleware/*.rb', __dir__)].each { |f| require f } workflow_class_name, *args = ARGV workflow_class = Object.const_get(workflow_class_name) @@ -10,5 +11,9 @@ workflow_id = SecureRandom.uuid # Convert integer strings to integers input = args.map { |arg| Integer(arg) rescue arg } +Temporal.configure do |config| + config.add_header_propagator(SamplePropagator) +end + run_id = Temporal.start_workflow(workflow_class, *input, options: { workflow_id: workflow_id }) Temporal.logger.info "Started workflow", { workflow_id: workflow_id, run_id: run_id } diff --git a/examples/bin/worker b/examples/bin/worker index 2c6fc4e3..79b6313c 100755 --- a/examples/bin/worker +++ b/examples/bin/worker @@ -24,6 +24,10 @@ if !ENV['USE_ERROR_SERIALIZATION_V2'].nil? end end +Temporal.configure do |config| + config.add_header_propagator(SamplePropagator) +end + worker = Temporal::Worker.new(binary_checksum: `git show HEAD -s --format=%H`.strip) worker.register_workflow(AsyncActivityWorkflow) @@ -94,5 +98,7 @@ worker.register_dynamic_activity(DelegatorActivity) worker.add_workflow_task_middleware(LoggingMiddleware, 'EXAMPLE') worker.add_activity_middleware(LoggingMiddleware, 'EXAMPLE') +worker.add_activity_middleware(SamplePropagator) +worker.add_workflow_middleware(SamplePropagator) worker.start diff --git a/examples/middleware/sample_propagator.rb b/examples/middleware/sample_propagator.rb new file mode 100644 index 00000000..59bb59f0 --- /dev/null +++ b/examples/middleware/sample_propagator.rb @@ -0,0 +1,10 @@ +class SamplePropagator + def inject!(headers) + headers['test-header'] = 'test' + end + + def call(metadata) + Temporal.logger.info("Got headers!", headers: metadata.headers.to_h) + yield + end +end \ No newline at end of file diff --git a/lib/temporal/client.rb b/lib/temporal/client.rb index da92b9a1..975fd96d 100644 --- a/lib/temporal/client.rb +++ b/lib/temporal/client.rb @@ -61,7 +61,7 @@ def start_workflow(workflow, *input, options: {}, **args) run_timeout: compute_run_timeout(execution_options), task_timeout: execution_options.timeouts[:task], workflow_id_reuse_policy: options[:workflow_id_reuse_policy], - headers: execution_options.headers, + headers: config.header_propagator_chain.inject(execution_options.headers), memo: execution_options.memo, search_attributes: Workflow::Context::Helpers.process_search_attributes(execution_options.search_attributes), ) @@ -78,7 +78,7 @@ def start_workflow(workflow, *input, options: {}, **args) run_timeout: compute_run_timeout(execution_options), task_timeout: execution_options.timeouts[:task], workflow_id_reuse_policy: options[:workflow_id_reuse_policy], - headers: execution_options.headers, + headers: config.header_propagator_chain.inject(execution_options.headers), memo: execution_options.memo, search_attributes: Workflow::Context::Helpers.process_search_attributes(execution_options.search_attributes), signal_name: signal_name, @@ -127,7 +127,7 @@ def schedule_workflow(workflow, cron_schedule, *input, options: {}, **args) run_timeout: compute_run_timeout(execution_options), task_timeout: execution_options.timeouts[:task], workflow_id_reuse_policy: options[:workflow_id_reuse_policy], - headers: execution_options.headers, + headers: config.header_propagator_chain.inject(execution_options.headers), cron_schedule: cron_schedule, memo: execution_options.memo, search_attributes: Workflow::Context::Helpers.process_search_attributes(execution_options.search_attributes), diff --git a/lib/temporal/configuration.rb b/lib/temporal/configuration.rb index 820e5173..20fdd7cf 100644 --- a/lib/temporal/configuration.rb +++ b/lib/temporal/configuration.rb @@ -1,5 +1,7 @@ require 'temporal/logger' require 'temporal/metrics_adapters/null' +require 'temporal/middleware/header_propagator_chain' +require 'temporal/middleware/entry' require 'temporal/connection/converter/payload/nil' require 'temporal/connection/converter/payload/bytes' require 'temporal/connection/converter/payload/json' @@ -12,7 +14,7 @@ class Configuration Execution = Struct.new(:namespace, :task_queue, :timeouts, :headers, :search_attributes, keyword_init: true) attr_reader :timeouts, :error_handlers - attr_accessor :connection_type, :converter, :use_error_serialization_v2, :host, :port, :credentials, :identity, :logger, :metrics_adapter, :namespace, :task_queue, :headers, :search_attributes + attr_accessor :connection_type, :converter, :use_error_serialization_v2, :host, :port, :credentials, :identity, :logger, :metrics_adapter, :namespace, :task_queue, :headers, :search_attributes, :header_propagators # See https://docs.temporal.io/blog/activity-timeouts/ for general docs. # We want an infinite execution timeout for cron schedules and other perpetual workflows. @@ -58,6 +60,7 @@ def initialize @credentials = :this_channel_is_insecure @identity = nil @search_attributes = {} + @header_propagators = [] end def on_error(&block) @@ -96,6 +99,15 @@ def default_execution_options ).freeze end + def add_header_propagator(propagator_class, *args) + raise 'header propagator must implement `def inject!(headers)`' unless propagator_class.method_defined? :inject! + @header_propagators << Middleware::Entry.new(propagator_class, args) + end + + def header_propagator_chain + Middleware::HeaderPropagatorChain.new(header_propagators) + end + private def default_identity diff --git a/lib/temporal/connection/serializer/schedule_activity.rb b/lib/temporal/connection/serializer/schedule_activity.rb index bf9120f5..10b26570 100644 --- a/lib/temporal/connection/serializer/schedule_activity.rb +++ b/lib/temporal/connection/serializer/schedule_activity.rb @@ -32,7 +32,7 @@ def to_proto def serialize_headers(headers) return unless headers - Temporalio::Api::Common::V1::Header.new(fields: object.headers) + Temporalio::Api::Common::V1::Header.new(fields: to_payload_map(headers)) end end end diff --git a/lib/temporal/middleware/header_propagator_chain.rb b/lib/temporal/middleware/header_propagator_chain.rb new file mode 100644 index 00000000..39384438 --- /dev/null +++ b/lib/temporal/middleware/header_propagator_chain.rb @@ -0,0 +1,22 @@ +module Temporal + module Middleware + class HeaderPropagatorChain + def initialize(entries = []) + @propagators = entries.map(&:init_middleware) + end + + def inject(headers) + return headers if propagators.empty? + h = headers.dup + for propagator in propagators + propagator.inject!(h) + end + h + end + + private + + attr_reader :propagators + end + end +end \ No newline at end of file diff --git a/lib/temporal/worker.rb b/lib/temporal/worker.rb index 1dab48fa..18e3e3c7 100644 --- a/lib/temporal/worker.rb +++ b/lib/temporal/worker.rb @@ -31,6 +31,7 @@ def initialize( @activities = Hash.new { |hash, key| hash[key] = ExecutableLookup.new } @pollers = [] @workflow_task_middleware = [] + @workflow_middleware = [] @activity_middleware = [] @shutting_down = false @activity_poller_options = { @@ -90,6 +91,10 @@ def add_workflow_task_middleware(middleware_class, *args) @workflow_task_middleware << Middleware::Entry.new(middleware_class, args) end + def add_workflow_middleware(middleware_class, *args) + @workflow_middleware << Middleware::Entry.new(middleware_class, args) + end + def add_activity_middleware(middleware_class, *args) @activity_middleware << Middleware::Entry.new(middleware_class, args) end @@ -128,14 +133,14 @@ def stop attr_reader :config, :activity_poller_options, :workflow_poller_options, :activities, :workflows, :pollers, - :workflow_task_middleware, :activity_middleware + :workflow_task_middleware, :workflow_middleware, :activity_middleware def shutting_down? @shutting_down end def workflow_poller_for(namespace, task_queue, lookup) - Workflow::Poller.new(namespace, task_queue, lookup.freeze, config, workflow_task_middleware, workflow_poller_options) + Workflow::Poller.new(namespace, task_queue, lookup.freeze, config, workflow_task_middleware, workflow_middleware, workflow_poller_options) end def activity_poller_for(namespace, task_queue, lookup) diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index 793d4377..58f5dd17 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -79,7 +79,7 @@ def execute_activity(activity_class, *input, **args) task_queue: execution_options.task_queue, retry_policy: execution_options.retry_policy, timeouts: execution_options.timeouts, - headers: execution_options.headers + headers: config.header_propagator_chain.inject(execution_options.headers) ) target, cancelation_id = schedule_command(command) @@ -136,7 +136,7 @@ def execute_workflow(workflow_class, *input, **args) retry_policy: execution_options.retry_policy, parent_close_policy: parent_close_policy, timeouts: execution_options.timeouts, - headers: execution_options.headers, + headers: config.header_propagator_chain.inject(execution_options.headers), cron_schedule: cron_schedule, memo: execution_options.memo, workflow_id_reuse_policy: workflow_id_reuse_policy, @@ -261,7 +261,7 @@ def continue_as_new(*input, **args) input: input, timeouts: execution_options.timeouts, retry_policy: execution_options.retry_policy, - headers: execution_options.headers, + headers: config.header_propagator_chain.inject(execution_options.headers), memo: execution_options.memo, search_attributes: Helpers.process_search_attributes(execution_options.search_attributes) ) diff --git a/lib/temporal/workflow/executor.rb b/lib/temporal/workflow/executor.rb index 90b090f0..b11aa328 100644 --- a/lib/temporal/workflow/executor.rb +++ b/lib/temporal/workflow/executor.rb @@ -16,7 +16,7 @@ class Executor # @param task_metadata [Metadata::WorkflowTask] # @param config [Configuration] # @param track_stack_trace [Boolean] - def initialize(workflow_class, history, task_metadata, config, track_stack_trace) + def initialize(workflow_class, history, task_metadata, config, track_stack_trace, middleware_chain) @workflow_class = workflow_class @dispatcher = Dispatcher.new @query_registry = QueryRegistry.new @@ -25,6 +25,7 @@ def initialize(workflow_class, history, task_metadata, config, track_stack_trace @task_metadata = task_metadata @config = config @track_stack_trace = track_stack_trace + @middleware_chain = middleware_chain end def run @@ -55,7 +56,8 @@ def process_queries(queries) private - attr_reader :workflow_class, :dispatcher, :query_registry, :state_manager, :task_metadata, :history, :config, :track_stack_trace + attr_reader :workflow_class, :dispatcher, :query_registry, :state_manager, + :task_metadata, :history, :config, :track_stack_trace, :middleware_chain def process_query(query) result = query_registry.handle(query.query_type, query.query_args) @@ -70,7 +72,9 @@ def execute_workflow(input, workflow_started_event) context = Workflow::Context.new(state_manager, dispatcher, workflow_class, metadata, config, query_registry, track_stack_trace) Fiber.new do - workflow_class.execute_in_context(context, input) + middleware_chain.invoke(metadata) do + workflow_class.execute_in_context(context, input) + end end.resume end end diff --git a/lib/temporal/workflow/poller.rb b/lib/temporal/workflow/poller.rb index 5f1fa042..195f3a96 100644 --- a/lib/temporal/workflow/poller.rb +++ b/lib/temporal/workflow/poller.rb @@ -14,12 +14,13 @@ class Poller binary_checksum: nil }.freeze - def initialize(namespace, task_queue, workflow_lookup, config, middleware = [], options = {}) + def initialize(namespace, task_queue, workflow_lookup, config, middleware = [], workflow_middleware = [], options = {}) @namespace = namespace @task_queue = task_queue @workflow_lookup = workflow_lookup @config = config @middleware = middleware + @workflow_middleware = workflow_middleware @shutting_down = false @options = DEFAULT_OPTIONS.merge(options) end @@ -45,7 +46,7 @@ def wait private - attr_reader :namespace, :task_queue, :connection, :workflow_lookup, :config, :middleware, :options, :thread + attr_reader :namespace, :task_queue, :connection, :workflow_lookup, :config, :middleware, :workflow_middleware, :options, :thread def connection @connection ||= Temporal::Connection.generate(config.for_connection) @@ -96,8 +97,9 @@ def poll_for_task def process(task) middleware_chain = Middleware::Chain.new(middleware) + workflow_middleware_chain = Middleware::Chain.new(workflow_middleware) - TaskProcessor.new(task, namespace, workflow_lookup, middleware_chain, config, binary_checksum).process + TaskProcessor.new(task, namespace, workflow_lookup, middleware_chain, workflow_middleware_chain, config, binary_checksum).process end def thread_pool diff --git a/lib/temporal/workflow/task_processor.rb b/lib/temporal/workflow/task_processor.rb index e0b867da..fea45309 100644 --- a/lib/temporal/workflow/task_processor.rb +++ b/lib/temporal/workflow/task_processor.rb @@ -24,7 +24,7 @@ def query_args MAX_FAILED_ATTEMPTS = 1 LEGACY_QUERY_KEY = :legacy_query - def initialize(task, namespace, workflow_lookup, middleware_chain, config, binary_checksum) + def initialize(task, namespace, workflow_lookup, middleware_chain, workflow_middleware_chain, config, binary_checksum) @task = task @namespace = namespace @metadata = Metadata.generate_workflow_task_metadata(task, namespace) @@ -32,6 +32,7 @@ def initialize(task, namespace, workflow_lookup, middleware_chain, config, binar @workflow_name = task.workflow_type.name @workflow_class = workflow_lookup.find(workflow_name) @middleware_chain = middleware_chain + @workflow_middleware_chain = workflow_middleware_chain @config = config @binary_checksum = binary_checksum end @@ -53,7 +54,7 @@ def process track_stack_trace = queries.values.map(&:query_type).include?(StackTraceTracker::STACK_TRACE_QUERY_NAME) # TODO: For sticky workflows we need to cache the Executor instance - executor = Workflow::Executor.new(workflow_class, history, metadata, config, track_stack_trace) + executor = Workflow::Executor.new(workflow_class, history, metadata, config, track_stack_trace, workflow_middleware_chain) commands = middleware_chain.invoke(metadata) do executor.run @@ -79,7 +80,7 @@ def process private attr_reader :task, :namespace, :task_token, :workflow_name, :workflow_class, - :middleware_chain, :metadata, :config, :binary_checksum + :middleware_chain, :workflow_middleware_chain, :metadata, :config, :binary_checksum def connection @connection ||= Temporal::Connection.generate(config.for_connection) diff --git a/spec/unit/lib/temporal/client_spec.rb b/spec/unit/lib/temporal/client_spec.rb index 52919d46..97d8ddf8 100644 --- a/spec/unit/lib/temporal/client_spec.rb +++ b/spec/unit/lib/temporal/client_spec.rb @@ -39,6 +39,35 @@ class TestStartWorkflow < Temporal::Workflow before { allow(connection).to receive(:start_workflow_execution).and_return(temporal_response) } + context 'with header propagator' do + class TestHeaderPropagator + def inject!(header) + header['test'] = 'asdf' + end + end + + it 'updates the header' do + config.add_header_propagator(TestHeaderPropagator) + subject.start_workflow(TestStartWorkflow, 42) + expect(connection) + .to have_received(:start_workflow_execution) + .with( + namespace: 'default-test-namespace', + workflow_id: an_instance_of(String), + workflow_name: 'TestStartWorkflow', + task_queue: 'default-test-task-queue', + input: [42], + task_timeout: Temporal.configuration.timeouts[:task], + run_timeout: Temporal.configuration.timeouts[:run], + execution_timeout: Temporal.configuration.timeouts[:execution], + workflow_id_reuse_policy: nil, + headers: { 'test' => 'asdf' }, + memo: {}, + search_attributes: {}, + ) + end + end + context 'using a workflow class' do it 'returns run_id' do result = subject.start_workflow(TestStartWorkflow, 42) diff --git a/spec/unit/lib/temporal/configuration_spec.rb b/spec/unit/lib/temporal/configuration_spec.rb index c9ee44c0..c1024e34 100644 --- a/spec/unit/lib/temporal/configuration_spec.rb +++ b/spec/unit/lib/temporal/configuration_spec.rb @@ -1,6 +1,10 @@ require 'temporal/configuration' describe Temporal::Configuration do + class TestHeaderPropagator + def inject!(_); end + end + describe '#initialize' do it 'initializes proper default workflow timeouts' do timeouts = subject.timeouts @@ -27,6 +31,25 @@ end end + describe '#add_header_propagator' do + let(:header_propagators) { subject.send(:header_propagators) } + + it 'adds middleware entry to the list of middlewares' do + subject.add_header_propagator(TestHeaderPropagator) + subject.add_header_propagator(TestHeaderPropagator, 'arg1', 'arg2') + + expect(header_propagators.size).to eq(2) + + expect(header_propagators[0]).to be_an_instance_of(Temporal::Middleware::Entry) + expect(header_propagators[0].klass).to eq(TestHeaderPropagator) + expect(header_propagators[0].args).to eq([]) + + expect(header_propagators[1]).to be_an_instance_of(Temporal::Middleware::Entry) + expect(header_propagators[1].klass).to eq(TestHeaderPropagator) + expect(header_propagators[1].args).to eq(['arg1', 'arg2']) + end + end + describe '#for_connection' do let (:new_identity) { 'new_identity' } diff --git a/spec/unit/lib/temporal/middleware/header_propagation_chain.rb b/spec/unit/lib/temporal/middleware/header_propagation_chain.rb new file mode 100644 index 00000000..c7f8c522 --- /dev/null +++ b/spec/unit/lib/temporal/middleware/header_propagation_chain.rb @@ -0,0 +1,51 @@ +require 'temporal/middleware/header_propagator_chain' +require 'temporal/middleware/entry' + +describe Temporal::Middleware::HeaderPropagatorChain do + class TestHeaderPropagator + attr_reader :id + + def initialize(id) + @id = id + end + + def inject!(header) + header['first'] = id unless header.has_key? :first + header[id] = id + end + end + + describe '#inject' do + subject { described_class.new(propagators) } + let(:headers) { { 'test' => 'header' } } + + context 'with propagators' do + let(:propagators) do + [ + propagator_1, + propagator_2, + ] + end + let(:propagator_1) { Temporal::Middleware::Entry.new(TestHeaderPropagator, '1') } + let(:propagator_2) { Temporal::Middleware::Entry.new(TestHeaderPropagator, '2') } + + it 'calls each propagator in order' do + expected = { + 'test' => 'header', + 'first' => '1', + '1' => '1', + '2' => '2', + } + expect(subject.inject(headers)).to eq(expected) + end + end + + context 'without propagators' do + let(:propagators) { [] } + + it 'returns the result of the passed block' do + expect(subject.inject(headers)).to eq(headers) + end + end + end +end diff --git a/spec/unit/lib/temporal/worker_spec.rb b/spec/unit/lib/temporal/worker_spec.rb index bd8a1211..71b6bcfa 100644 --- a/spec/unit/lib/temporal/worker_spec.rb +++ b/spec/unit/lib/temporal/worker_spec.rb @@ -30,6 +30,10 @@ class TestWorkerActivityMiddleware def call(_); end end + class TestWorkerWorkflowMiddleware + def call(_); end + end + class OtherTestWorkerActivity < Temporal::Activity namespace 'default-namespace' task_queue 'default-task-queue' @@ -215,6 +219,7 @@ class OtherTestWorkerActivity < Temporal::Activity an_instance_of(Temporal::ExecutableLookup), config, [], + [], thread_pool_size: 10, binary_checksum: nil ) @@ -228,6 +233,7 @@ class OtherTestWorkerActivity < Temporal::Activity an_instance_of(Temporal::ExecutableLookup), config, [], + [], thread_pool_size: 10, binary_checksum: nil ) @@ -305,6 +311,7 @@ class OtherTestWorkerActivity < Temporal::Activity an_instance_of(Temporal::ExecutableLookup), an_instance_of(Temporal::Configuration), [], + [], thread_pool_size: 10, binary_checksum: binary_checksum ) @@ -323,6 +330,7 @@ class OtherTestWorkerActivity < Temporal::Activity context 'when middleware is configured' do let(:entry_1) { instance_double(Temporal::Middleware::Entry) } let(:entry_2) { instance_double(Temporal::Middleware::Entry) } + let(:entry_3) { instance_double(Temporal::Middleware::Entry) } before do allow(Temporal::Middleware::Entry) @@ -335,8 +343,14 @@ class OtherTestWorkerActivity < Temporal::Activity .with(TestWorkerActivityMiddleware, []) .and_return(entry_2) + allow(Temporal::Middleware::Entry) + .to receive(:new) + .with(TestWorkerWorkflowMiddleware, []) + .and_return(entry_3) + subject.add_workflow_task_middleware(TestWorkerWorkflowTaskMiddleware) subject.add_activity_middleware(TestWorkerActivityMiddleware) + subject.add_workflow_middleware(TestWorkerWorkflowMiddleware) end it 'starts pollers with correct middleware' do @@ -350,6 +364,7 @@ class OtherTestWorkerActivity < Temporal::Activity an_instance_of(Temporal::ExecutableLookup), config, [entry_1], + [entry_3], thread_pool_size: 10, binary_checksum: nil ) diff --git a/spec/unit/lib/temporal/workflow/context_spec.rb b/spec/unit/lib/temporal/workflow/context_spec.rb index 6ca8e91f..6559e277 100644 --- a/spec/unit/lib/temporal/workflow/context_spec.rb +++ b/spec/unit/lib/temporal/workflow/context_spec.rb @@ -1,3 +1,4 @@ +require 'temporal/activity' require 'temporal/workflow' require 'temporal/workflow/context' require 'temporal/workflow/dispatcher' @@ -8,6 +9,7 @@ require 'time' class MyTestWorkflow < Temporal::Workflow; end +class MyTestActivity < Temporal::Activity; end describe Temporal::Workflow::Context do let(:state_manager) { instance_double('Temporal::Workflow::StateManager') } @@ -19,6 +21,7 @@ class MyTestWorkflow < Temporal::Workflow; end end let(:metadata_hash) { Fabricate(:workflow_metadata).to_h } let(:metadata) { Temporal::Metadata::Workflow.new(**metadata_hash) } + let(:config) { Temporal.configuration } let(:workflow_context) do Temporal::Workflow::Context.new( @@ -26,7 +29,7 @@ class MyTestWorkflow < Temporal::Workflow; end dispatcher, MyTestWorkflow, metadata, - Temporal.configuration, + config, query_registry, track_stack_trace ) @@ -63,6 +66,31 @@ class MyTestWorkflow < Temporal::Workflow; end end end + describe '#execute_activity' do + context "with header propagation" do + class TestHeaderPropagator + def inject!(header) + header['test'] = 'asdf' + end + end + + it 'propagates the header' do + config.add_header_propagator(TestHeaderPropagator) + expect(state_manager).to receive(:schedule).with(Temporal::Workflow::Command::ScheduleActivity.new( + activity_id: nil, + activity_type: 'MyTestActivity', + input: [], + task_queue: 'default-task-queue', + retry_policy: nil, + timeouts: {:execution => 315360000, :run => 315360000, :task => 10, :schedule_to_close => nil, :schedule_to_start => nil, :start_to_close => 30, :heartbeat => nil}, + headers: { 'test' => 'asdf' } + )) + allow(dispatcher).to receive(:register_handler) + workflow_context.execute_activity(MyTestActivity) + end + end + end + describe '#execute_workflow' do it 'returns the correct futures when starting a child workflow' do allow(state_manager).to receive(:schedule) diff --git a/spec/unit/lib/temporal/workflow/executor_spec.rb b/spec/unit/lib/temporal/workflow/executor_spec.rb index 84f31cff..0730f70e 100644 --- a/spec/unit/lib/temporal/workflow/executor_spec.rb +++ b/spec/unit/lib/temporal/workflow/executor_spec.rb @@ -1,3 +1,4 @@ +require 'temporal/middleware/chain' require 'temporal/workflow/executor' require 'temporal/workflow/history' require 'temporal/workflow' @@ -5,7 +6,7 @@ require 'temporal/workflow/query_registry' describe Temporal::Workflow::Executor do - subject { described_class.new(workflow, history, workflow_metadata, config, false) } + subject { described_class.new(workflow, history, workflow_metadata, config, false, middleware_chain) } let(:workflow_started_event) { Fabricate(:api_workflow_execution_started_event, event_id: 1) } let(:history) do @@ -19,6 +20,7 @@ let(:workflow) { TestWorkflow } let(:workflow_metadata) { Fabricate(:workflow_metadata) } let(:config) { Temporal::Configuration.new } + let(:middleware_chain) { Temporal::Middleware::Chain.new } class TestWorkflow < Temporal::Workflow def execute @@ -29,6 +31,7 @@ def execute describe '#run' do it 'runs a workflow' do allow(workflow).to receive(:execute_in_context).and_call_original + expect(middleware_chain).to receive(:invoke).and_call_original subject.run diff --git a/spec/unit/lib/temporal/workflow/poller_spec.rb b/spec/unit/lib/temporal/workflow/poller_spec.rb index 083ec376..6481e418 100644 --- a/spec/unit/lib/temporal/workflow/poller_spec.rb +++ b/spec/unit/lib/temporal/workflow/poller_spec.rb @@ -11,6 +11,9 @@ let(:config) { Temporal::Configuration.new } let(:middleware_chain) { instance_double(Temporal::Middleware::Chain) } let(:middleware) { [] } + let(:workflow_middleware_chain) { instance_double(Temporal::Middleware::Chain) } + let(:workflow_middleware) { [] } + let(:empty_middleware_chain) { instance_double(Temporal::Middleware::Chain) } let(:binary_checksum) { 'v1.0.0' } subject do @@ -20,6 +23,7 @@ lookup, config, middleware, + workflow_middleware, { binary_checksum: binary_checksum } @@ -28,7 +32,9 @@ before do allow(Temporal::Connection).to receive(:generate).and_return(connection) - allow(Temporal::Middleware::Chain).to receive(:new).and_return(middleware_chain) + allow(Temporal::Middleware::Chain).to receive(:new).with(workflow_middleware).and_return(workflow_middleware_chain) + allow(Temporal::Middleware::Chain).to receive(:new).with(middleware).and_return(middleware_chain) + allow(Temporal::Middleware::Chain).to receive(:new).with([]).and_return(empty_middleware_chain) allow(Temporal.metrics).to receive(:timing) allow(Temporal.metrics).to receive(:increment) end @@ -109,7 +115,7 @@ expect(Temporal::Workflow::TaskProcessor) .to have_received(:new) - .with(task, namespace, lookup, middleware_chain, config, binary_checksum) + .with(task, namespace, lookup, empty_middleware_chain, empty_middleware_chain, config, binary_checksum) expect(task_processor).to have_received(:process) end @@ -137,10 +143,12 @@ def initialize(_); end def call(_); end end + let(:workflow_middleware) { [entry_1] } let(:middleware) { [entry_1, entry_2] } let(:entry_1) { Temporal::Middleware::Entry.new(TestPollerMiddleware, '1') } let(:entry_2) { Temporal::Middleware::Entry.new(TestPollerMiddleware, '2') } + it 'initializes middleware chain and passes it down to TaskProcessor' do subject.start @@ -148,9 +156,10 @@ def call(_); end subject.stop_polling; subject.wait expect(Temporal::Middleware::Chain).to have_received(:new).with(middleware) + expect(Temporal::Middleware::Chain).to have_received(:new).with(workflow_middleware) expect(Temporal::Workflow::TaskProcessor) .to have_received(:new) - .with(task, namespace, lookup, middleware_chain, config, binary_checksum) + .with(task, namespace, lookup, middleware_chain, workflow_middleware_chain, config, binary_checksum) end end end diff --git a/spec/unit/lib/temporal/workflow/task_processor_spec.rb b/spec/unit/lib/temporal/workflow/task_processor_spec.rb index ac5c31e0..9c6a1887 100644 --- a/spec/unit/lib/temporal/workflow/task_processor_spec.rb +++ b/spec/unit/lib/temporal/workflow/task_processor_spec.rb @@ -4,7 +4,7 @@ require 'temporal/workflow/task_processor' describe Temporal::Workflow::TaskProcessor do - subject { described_class.new(task, namespace, lookup, middleware_chain, config, binary_checksum) } + subject { described_class.new(task, namespace, lookup, middleware_chain, workflow_middleware_chain, config, binary_checksum) } let(:namespace) { 'test-namespace' } let(:lookup) { instance_double('Temporal::ExecutableLookup', find: nil) } @@ -15,6 +15,7 @@ let(:workflow_name) { 'TestWorkflow' } let(:connection) { instance_double('Temporal::Connection::GRPC') } let(:middleware_chain) { Temporal::Middleware::Chain.new } + let(:workflow_middleware_chain) { Temporal::Middleware::Chain.new } let(:input) { %w[arg1 arg2] } let(:config) { Temporal::Configuration.new } let(:binary_checksum) { 'v1.0.0' } From 86264adb3fe04581bb9ee39dac6d014521f17295 Mon Sep 17 00:00:00 2001 From: Drew Hoskins <37816070+drewhoskins-stripe@users.noreply.github.com> Date: Mon, 3 Apr 2023 08:10:13 -0700 Subject: [PATCH 078/125] Paginate through empty workflow history pages (#213) * Allow empty pages when paginating through history * Remove unused error * Fix unrelated search attributes test * Remove test for internal error that doesn't repro in some versions of temporal --- examples/bin/register_namespace | 17 ++++- .../integration/search_attributes_spec.rb | 18 ----- lib/temporal/errors.rb | 1 - lib/temporal/workflow/task_processor.rb | 7 +- .../grpc/workflow_task_fabricator.rb | 4 -- .../workflow_execution_history_fabricator.rb | 4 +- .../temporal/workflow/task_processor_spec.rb | 67 +++++++++++++++---- 7 files changed, 71 insertions(+), 47 deletions(-) diff --git a/examples/bin/register_namespace b/examples/bin/register_namespace index 9d008c51..672d73c0 100755 --- a/examples/bin/register_namespace +++ b/examples/bin/register_namespace @@ -4,7 +4,7 @@ require_relative '../init' namespace = ARGV[0] description = ARGV[1] -fail 'Missing namespace name, please run register_namespace ' unless namespace +raise 'Missing namespace name, please run register_namespace ' unless namespace begin Temporal.register_namespace(namespace, description) @@ -13,4 +13,17 @@ rescue Temporal::NamespaceAlreadyExistsFailure Temporal.logger.info 'Namespace already exists', { namespace: namespace } end - +# Register a variety of search attributes for ease of integration testing +attributes_to_add = { + 'CustomStringField' => :text, + 'CustomDoubleField' => :double, + 'CustomBoolField' => :bool, + 'CustomIntField' => :int, + 'CustomDatetimeField' => :datetime +} +begin + Temporal.add_custom_search_attributes(attributes_to_add) + Temporal.logger.info('Registered search attributes', { namespace: namespace, attributes: attributes_to_add }) +rescue Temporal::SearchAttributeAlreadyExistsFailure + Temporal.logger.info('Default search attributes already exist for namespace', { namespace: namespace }) +end diff --git a/examples/spec/integration/search_attributes_spec.rb b/examples/spec/integration/search_attributes_spec.rb index 6bdd8bf9..8db7848e 100644 --- a/examples/spec/integration/search_attributes_spec.rb +++ b/examples/spec/integration/search_attributes_spec.rb @@ -47,24 +47,6 @@ def cleanup end.to raise_error(Temporal::SearchAttributeAlreadyExistsFailure) end - it 'type change fails' do - Temporal.add_custom_search_attributes( - { - attribute_1 => :int - } - ) - - Temporal.remove_custom_search_attributes(attribute_1) - - expect do - Temporal.add_custom_search_attributes( - { - attribute_1 => :keyword - } - ) - end.to raise_error(an_instance_of(Temporal::SearchAttributeFailure)) - end - it 'remove' do Temporal.add_custom_search_attributes( { diff --git a/lib/temporal/errors.rb b/lib/temporal/errors.rb index 323e25cd..7aa11405 100644 --- a/lib/temporal/errors.rb +++ b/lib/temporal/errors.rb @@ -78,7 +78,6 @@ class FeatureVersionNotSupportedFailure < ApiError; end class NamespaceAlreadyExistsFailure < ApiError; end class CancellationAlreadyRequestedFailure < ApiError; end class QueryFailed < ApiError; end - class UnexpectedResponse < ApiError; end class SearchAttributeAlreadyExistsFailure < ApiError; end class SearchAttributeFailure < ApiError; end diff --git a/lib/temporal/workflow/task_processor.rb b/lib/temporal/workflow/task_processor.rb index fea45309..dbd14d05 100644 --- a/lib/temporal/workflow/task_processor.rb +++ b/lib/temporal/workflow/task_processor.rb @@ -95,7 +95,6 @@ def queue_time_ms def fetch_full_history events = task.history.events.to_a next_page_token = task.next_page_token - while !next_page_token.empty? do response = connection.get_workflow_execution_history( namespace: namespace, @@ -103,12 +102,8 @@ def fetch_full_history run_id: task.workflow_execution.run_id, next_page_token: next_page_token ) - - if response.history.events.empty? - raise Temporal::UnexpectedResponse, 'Received empty history page' - end - events += response.history.events.to_a + next_page_token = response.next_page_token end diff --git a/spec/fabricators/grpc/workflow_task_fabricator.rb b/spec/fabricators/grpc/workflow_task_fabricator.rb index 855baeb8..0fd5b7c5 100644 --- a/spec/fabricators/grpc/workflow_task_fabricator.rb +++ b/spec/fabricators/grpc/workflow_task_fabricator.rb @@ -12,7 +12,3 @@ history { |attrs| Temporalio::Api::History::V1::History.new(events: attrs[:events]) } query { nil } end - -Fabricator(:api_paginated_workflow_task, from: :api_workflow_task) do - next_page_token 'page-1' -end diff --git a/spec/fabricators/workflow_execution_history_fabricator.rb b/spec/fabricators/workflow_execution_history_fabricator.rb index 8e033a80..a969eb7f 100644 --- a/spec/fabricators/workflow_execution_history_fabricator.rb +++ b/spec/fabricators/workflow_execution_history_fabricator.rb @@ -1,5 +1,5 @@ Fabricator(:workflow_execution_history, from: Temporalio::Api::WorkflowService::V1::GetWorkflowExecutionHistoryResponse) do - transient :events + transient :events, :_next_page_token history { |attrs| Temporalio::Api::History::V1::History.new(events: attrs[:events]) } - next_page_token '' + next_page_token { |attrs| attrs[:_next_page_token] || '' } end diff --git a/spec/unit/lib/temporal/workflow/task_processor_spec.rb b/spec/unit/lib/temporal/workflow/task_processor_spec.rb index 9c6a1887..e20c1721 100644 --- a/spec/unit/lib/temporal/workflow/task_processor_spec.rb +++ b/spec/unit/lib/temporal/workflow/task_processor_spec.rb @@ -340,17 +340,17 @@ end context 'when history is paginated' do - let(:task) { Fabricate(:api_paginated_workflow_task, workflow_type: api_workflow_type) } + let(:page_one) { 'page-1' } + let(:task) { Fabricate(:api_workflow_task, workflow_type: api_workflow_type, next_page_token: page_one) } let(:event) { Fabricate(:api_workflow_execution_started_event) } - let(:history_response) { Fabricate(:workflow_execution_history, events: [event]) } - before do + it 'fetches additional pages' do + history_response = Fabricate(:workflow_execution_history, events: [event]) + allow(connection) .to receive(:get_workflow_execution_history) .and_return(history_response) - end - it 'fetches additional pages' do subject.process expect(connection) @@ -364,21 +364,60 @@ .once end + # Temporal server sometimes sends empty history pages but with a next_page_token. Best practice, used + # across the various SDKs, is to keep paginating. context 'when a page has no events' do - let(:history_response) { Fabricate(:workflow_execution_history, events: []) } + let(:page_two) { 'page-2' } + let(:page_three) { 'page-3' } + let(:first_history_response) { Fabricate(:workflow_execution_history, events: [event], _next_page_token: page_two) } - it 'fails a workflow task' do - subject.process + let(:empty_history_response) do + Fabricate(:workflow_execution_history, events: [], _next_page_token: page_three) + end + let(:final_event) { Fabricate(:api_workflow_execution_completed_event) } + let(:final_history_response) do + Fabricate(:workflow_execution_history, events: [final_event]) + end - expect(connection) - .to have_received(:respond_workflow_task_failed) + it 'continues asking for the next workflow task and populates all the events in the history' do + # page_one: [event], -> page_two + # page_two: [], -> page_three + # page_three: [final_event] + allow(connection) + .to receive(:get_workflow_execution_history) .with( namespace: namespace, - task_token: task.task_token, - cause: Temporalio::Api::Enums::V1::WorkflowTaskFailedCause::WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE, - exception: an_instance_of(Temporal::UnexpectedResponse), - binary_checksum: binary_checksum + workflow_id: task.workflow_execution.workflow_id, + run_id: task.workflow_execution.run_id, + next_page_token: page_one ) + .and_return(first_history_response) + + allow(connection) + .to receive(:get_workflow_execution_history) + .with( + namespace: namespace, + workflow_id: task.workflow_execution.workflow_id, + run_id: task.workflow_execution.run_id, + next_page_token: page_two + ) + .and_return(empty_history_response) + + allow(connection) + .to receive(:get_workflow_execution_history) + .with( + namespace: namespace, + workflow_id: task.workflow_execution.workflow_id, + run_id: task.workflow_execution.run_id, + next_page_token: page_three + ) + .and_return(final_history_response) + + allow(Temporal::Workflow::History).to receive(:new) + + subject.process + + expect(Temporal::Workflow::History).to have_received(:new).with([event, final_event]) end end end From 6e5fc34ec112380a5d8eea10b5a80c805d6b7a61 Mon Sep 17 00:00:00 2001 From: Drew Hoskins <37816070+drewhoskins-stripe@users.noreply.github.com> Date: Thu, 6 Apr 2023 06:29:56 -0700 Subject: [PATCH 079/125] Don't serialize huge exceptions (#227) * Don't serialize huge exceptions * Improve discoverability --- lib/temporal/connection/serializer/failure.rb | 24 +++++-- lib/temporal/workflow/errors.rb | 8 ++- .../connection/serializer/failure_spec.rb | 62 +++++++++++++++++++ .../unit/lib/temporal/workflow/errors_spec.rb | 18 +++--- 4 files changed, 95 insertions(+), 17 deletions(-) diff --git a/lib/temporal/connection/serializer/failure.rb b/lib/temporal/connection/serializer/failure.rb index 0a94a72f..ddfeb2e3 100644 --- a/lib/temporal/connection/serializer/failure.rb +++ b/lib/temporal/connection/serializer/failure.rb @@ -7,17 +7,29 @@ module Serializer class Failure < Base include Concerns::Payloads - def initialize(error, serialize_whole_error: false) + def initialize(error, serialize_whole_error: false, max_bytes: 200_000) @serialize_whole_error = serialize_whole_error + @max_bytes = max_bytes super(error) end def to_proto - details = if @serialize_whole_error - to_details_payloads(object) - else - to_details_payloads(object.message) - end + if @serialize_whole_error + details = to_details_payloads(object) + if details.payloads.first.data.size > @max_bytes + Temporal.logger.error( + "Could not serialize exception because it's too large, so we are using a fallback that may not "\ + "deserialize correctly on the client. First #{@max_bytes} bytes:\n" \ + "#{details.payloads.first.data[0..@max_bytes - 1]}", + {unserializable_error: object.class.name} + ) + # Fallback to a more conservative serialization if the payload is too big to avoid + # sending a huge amount of data to temporal and putting it in the history. + details = to_details_payloads(object.message) + end + else + details = to_details_payloads(object.message) + end Temporalio::Api::Failure::V1::Failure.new( message: object.message, stack_trace: stack_trace_from(object.backtrace), diff --git a/lib/temporal/workflow/errors.rb b/lib/temporal/workflow/errors.rb index aea40170..42157376 100644 --- a/lib/temporal/workflow/errors.rb +++ b/lib/temporal/workflow/errors.rb @@ -38,9 +38,11 @@ def self.generate_error(failure, default_exception_class = StandardError) message = "#{exception_class}: #{message}" exception = default_exception_class.new(message) Temporal.logger.error( - "Could not instantiate original error. Defaulting to StandardError. It's likely that your error's " \ - "initializer takes something more than just one positional argument. If so, make sure the worker running "\ - "your activities is setting Temporal.configuration.use_error_serialization_v2 to support this.", + "Could not instantiate original error. Defaulting to StandardError. Make sure the worker running " \ + "your activities is setting Temporal.configuration.use_error_serialization_v2. If so, make sure the " \ + "original error serialized by searching your logs for 'unserializable_error'. If not, you're using "\ + "legacy serialization, and it's likely that "\ + "your error's initializer takes something other than exactly one positional argument.", { original_error: error_type, serialized_error: details.payloads.first.data, diff --git a/spec/unit/lib/temporal/connection/serializer/failure_spec.rb b/spec/unit/lib/temporal/connection/serializer/failure_spec.rb index b492556f..4242554e 100644 --- a/spec/unit/lib/temporal/connection/serializer/failure_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/failure_spec.rb @@ -41,5 +41,67 @@ def initialize(foo, bar, bad_class:) expect(deserialized_error.bad_class).to eq(NaughtyClass) end + class MyBigError < StandardError + attr_reader :big_payload + def initialize(message) + super(message) + @big_payload = '123456789012345678901234567890123456789012345678901234567890' + end + end + + + it 'deals with too-large serialization using the old path' do + e = MyBigError.new('Uh oh!') + # Normal serialization path + failure_proto = described_class.new(e, serialize_whole_error: true, max_bytes: 1000).to_proto + expect(failure_proto.application_failure_info.type).to eq('MyBigError') + deserialized_error = TestDeserializer.new.from_details_payloads(failure_proto.application_failure_info.details) + expect(deserialized_error).to be_an_instance_of(MyBigError) + expect(deserialized_error.big_payload).to eq('123456789012345678901234567890123456789012345678901234567890') + + # Exercise legacy serialization mechanism + failure_proto = described_class.new(e, serialize_whole_error: false).to_proto + expect(failure_proto.application_failure_info.type).to eq('MyBigError') + old_style_deserialized_error = MyBigError.new(TestDeserializer.new.from_details_payloads(failure_proto.application_failure_info.details)) + expect(old_style_deserialized_error).to be_an_instance_of(MyBigError) + expect(old_style_deserialized_error.message).to eq('Uh oh!') + + # If the payload size exceeds the max_bytes, we fallback to the old-style serialization. + failure_proto = described_class.new(e, serialize_whole_error: true, max_bytes: 50).to_proto + expect(failure_proto.application_failure_info.type).to eq('MyBigError') + avoids_truncation_error = MyBigError.new(TestDeserializer.new.from_details_payloads(failure_proto.application_failure_info.details)) + expect(avoids_truncation_error).to be_an_instance_of(MyBigError) + expect(avoids_truncation_error.message).to eq('Uh oh!') + + # Fallback serialization should exactly match legacy serialization + expect(avoids_truncation_error).to eq(old_style_deserialized_error) + end + + it 'logs a helpful error when the payload is too large' do + e = MyBigError.new('Uh oh!') + + allow(Temporal.logger).to receive(:error) + max_bytes = 50 + described_class.new(e, serialize_whole_error: true, max_bytes: max_bytes).to_proto + expect(Temporal.logger) + .to have_received(:error) + .with( + "Could not serialize exception because it's too large, so we are using a fallback that may not deserialize "\ + "correctly on the client. First #{max_bytes} bytes:\n{\"^o\":\"MyBigError\",\"big_payload\":\"1234567890123456", + { unserializable_error: 'MyBigError' } + ) + + end + + class MyArglessError < RuntimeError + def initialize; end + end + + it 'successfully processes an error with no constructor arguments' do + e = MyArglessError.new + failure_proto = described_class.new(e, serialize_whole_error: true).to_proto + expect(failure_proto.application_failure_info.type).to eq('MyArglessError') + end + end end diff --git a/spec/unit/lib/temporal/workflow/errors_spec.rb b/spec/unit/lib/temporal/workflow/errors_spec.rb index 5947e245..53d86b68 100644 --- a/spec/unit/lib/temporal/workflow/errors_spec.rb +++ b/spec/unit/lib/temporal/workflow/errors_spec.rb @@ -101,10 +101,11 @@ def initialize(foo, bar) expect(Temporal.logger) .to have_received(:error) .with( - 'Could not instantiate original error. Defaulting to StandardError. ' \ - 'It\'s likely that your error\'s initializer takes something more than just one positional argument. '\ - 'If so, make sure the worker running your activities is setting '\ - 'Temporal.configuration.use_error_serialization_v2 to support this.', + "Could not instantiate original error. Defaulting to StandardError. "\ + "Make sure the worker running your activities is setting Temporal.configuration.use_error_serialization_v2. "\ + "If so, make sure the original error serialized by searching your logs for 'unserializable_error'. "\ + "If not, you're using legacy serialization, and it's likely that "\ + "your error's initializer takes something other than exactly one positional argument.", { original_error: "ErrorWithTwoArgs", serialized_error: '"An error message"', @@ -133,10 +134,11 @@ def initialize(foo, bar) expect(Temporal.logger) .to have_received(:error) .with( - 'Could not instantiate original error. Defaulting to StandardError. ' \ - 'It\'s likely that your error\'s initializer takes something more than just one positional argument. '\ - 'If so, make sure the worker running your activities is setting '\ - 'Temporal.configuration.use_error_serialization_v2 to support this.', + "Could not instantiate original error. Defaulting to StandardError. "\ + "Make sure the worker running your activities is setting Temporal.configuration.use_error_serialization_v2. "\ + "If so, make sure the original error serialized by searching your logs for 'unserializable_error'. "\ + "If not, you're using legacy serialization, and it's likely that "\ + "your error's initializer takes something other than exactly one positional argument.", { original_error: "ErrorThatRaisesInInitialize", serialized_error: '"An error message"', From 21252c498369350e80788bbeb0d294f4bc5b7595 Mon Sep 17 00:00:00 2001 From: calum-stripe <98350978+calum-stripe@users.noreply.github.com> Date: Fri, 7 Apr 2023 06:28:50 -0700 Subject: [PATCH 080/125] Adding payload codec pipeline (#224) * Revert signal_with_start (before taking a different approach) * Add signal arguments to start_workflow (to support signal_with_start) * Merge memo changes * Address PR feedback * Update method signature in temporal test fixture * Add detail to a few error messages * Update our FailWorkflowTask logic's call to ErrorHandler.handle * Workflow await * Make dispatch more generic * Fix race condition * Merge await into wait_for * Update sample workflow to use wait_for, rename to WaitForWorkflow * Reorganize and extend wait_for tests * Check for completed futures before setting dispatcher and yielding the fiber * Extend wait_for to take multiple futures and a condition block * Differentiate TARGET_WILDCARD and WILDCARD, allow comparison with EventTarget objects * Use Ruby 2.7 to be consistent with pay-server * Turn off schedule_to_start activity timeout by default * Refactor metadata generation * Make task queue available on workflow metadata, add example test * Expose workflow start time metadata * Add memos * Make error deserialization more resilient * Make temporal-ruby able to deserialize larger histories * Remove temporary test * Expose workflow name in activity metadata in temporal-ruby's unit tester * Add a workflow-level test * add namespace to emitted metrics * emit the workflow name tag during activity processing * fix typo * added failworkflowtaskerror * updated register namespace to accept new params * examples * fixed namespace test * empty * updated unit tests * removed unncessary code * updated seconds * Add replay flag to workflow context * fixed nits * Rename to replay? to history_replaying? * updated sleep to 0.5 * updated unit tests and nits * fixed unit tests * added link to comment * Merge fix * Fix upsert_search_attributes * added fix for nil search attributes * added unit test * updated unit test * added expect to be nil * Expose scheduled_time and current_attempt_scheduled_time on activity metadata * Implement ParentClosePolicy for child workflows * Add e2e test for child workflow execution * move serialization logic farther down the stack * Refactor serialize_parent_close_policy; add unit tests * Expose wait_for_start for child workflow execution * Remove future `workflow_id,run_id` annotations; simplify wait_for logic * Add parent_run_id, parent_id to workflow metadata * Allow opting out of child workflow futures * Remove duplicate describe block * Factor out workflow_id_reuse_policy serialization * Respect workflow_id_reuse_policy for child workflows * Add integration tests * Use a nicer exception type * Refactor wait_for into distinct wait_for_any and wait_for_condition methods * Order wildcard dispatch handlers * Remove finished handlers * Check finished? on wait_for_any, add more unit specs * More dispatch unit specs * Use hash instead of list for callbacks per target * Remove dead code, improve error messages in local workflow context * Correct swapped arguments * Eliminate unnecessary IDs for dispatcher handlers * Raise on duplicate ID * added paginated workflows * client spec * reset * Downstream the rest of [activity_metadata.workflow_name](https://github.com/coinbase/temporal-ruby/pull/130) * Downstream the rest of [activity_metadata.scheduled_time](https://github.com/coinbase/temporal-ruby/pull/164) * Fix an activity_metadata related test that doesn't exist upstream * Downstream the rest of [child workflow workflow_id_reuse_policy fixes](https://github.com/coinbase/temporal-ruby/pull/172) * Downstream [merge error fix](https://github.com/coinbase/temporal-ruby/pull/180) * added json protobuf * Update json_protobuf.rb * added unit test * Remove duplicate tests in context_spec * allow connection options to be set * add missing comma * add test for interceptors * remove unused variable * use new Temporal client in interceptor test to avoid test pollution * add option to specify search attributes when starting workflows * move empty check out of Temporal::Workflow::Context::Helpers.process_search_attributes * move process_search_attributes out of ExecutionOptions.initialize * allow default search attributes to be configured globally * fix tests, unit test global default search attributes * add unit test for gRPC serialization * clean up the integration test * Include remaining changes from upstream #188 (#98) ### Summary This PR is a follow-up to #97 that includes the changes made to the corresponding upstream PR ([github.com/coinbase/temporal-ruby#188](https://github.com/coinbase/temporal-ruby/pull/188)) after that downstream PR was eagerly merged (with a subset of the changes from upstream). The changes that were not included in the downstream PR when it was merged are: [compare](https://github.com/coinbase/temporal-ruby/pull/188/files/05b6eafeb43ac717c15ae683e6249c4de876ef3d..c6f614325695cc666e066aa218a329c9bf7504f3) (that should be identical to the changes in this PR). With the merging of the upstream PR, I also updated the [non-upstreamed changed doc](https://paper.dropbox.com/doc/Non-upstreamed-temporal-ruby-changes--Bm39qa99fshz6nbw9RG37pgFAg-AYkwiCfkcoM66adjZMwAZ) to remove the entry on Initial workflow search attributes. ### Motivation The intention of this PR is to synchronize upstream and downstream, with relation to the changeset applied in the upstream PR. ### Test plan N/A ### Rollout/monitoring/revert plan Safe to revert. * Remove dead code from previous messy merge * Separate wait_until handlers, execute at end * Modify signal_with_start_workflow to cover dwillett's repro * Disable running rubyfmt on save * Consolidate metrics constants * Add workflow task failure counter * Use metric_keys.rb for filename * Allow client identity to be configurable * Use PID instead of thread ID in default identity * Add poller completed metrics * Gauge metrics for queue size and available threads per thread pool * Add task queue and namespace to thread pool metrics * Fix tag * reverted proto_json changes * fixed import * remmoved test file * Revert "Merge pull request #108 from stripe-private-oss-forks/calum/fixing-import" This reverts commit 48cbe20d0a44c22d6351e1a09f0b69a36603bb24, reversing changes made to e805a38c58ee4469224486e99ada6a28d9eb3fca. * Revert "Merge pull request #107 from stripe-private-oss-forks/calum/reverting-json-proto-changes" This reverts commit e805a38c58ee4469224486e99ada6a28d9eb3fca, reversing changes made to 3deda64ab4516a79b40cd157bfcd1239b5a26a4d. * Exempt the necessary system workflows from normal JSON proto deserialization * Emit task queue for workflow task failures * Added task queue tags to workflow and activity task queue time metrics * DynamicActivity * Use const_get * Cleanup error message * Jeff feedback * do not fail workflow task if completing it errors It's possible for a workflow task to fail to complete even during normal operation. For example, if a signal is added to the workflow history concurrently with a workflow task attempting to complete the workflow then the `RespondWorkflowTaskCompleted` call will recieve an `InvalidArgument` error with an `UnhandledCommand` cause. If this happens then the Ruby SDK would get this error and then try and fail the workflow task due to how the `rescue` block encompassed the completion function. This would then lead to the `RespondWorkflowTaskFailed` call failing because the server doesn't recognize the task anymore. This is because the task was actually finalized when the original `RespondWorkflowTaskCompleted` call was made and so it should not be interacted with anymore. * add a comment * use more realistic error type in tests * Remove orphaned code post-merge * Allow empty pages when paginating through history * Remove unused error * Allow ActivityException to accept args * Improve deserialization code flow * Use the default converter to serialize errors when configured to do so * Get tests working * Cleanup * fix nit * Fix extraneous debug spew * added max page size param * Show bad data on Activity error serialization failure * Upgrade Temporal proto API to version 1.16 * Rename Temporal -> Temporalio * Remove deprecated namespace field for activity task scheduling * Methods for operating on custom search attributes * Example test for custom search attribute APIs * Unit tests * Treat missing history submessage on GetWorkflowExecutionHistoryResponse as timeout * Remove unnecessary &. * Dynamic Workflows * Add terminate-if-running workflow id reuse policy mappings * Add integration test for terminate-if-running * Move misplaced test * Feedback * Use terminate-if-running as policy for both invocations * added payload codecs * added search attribute payload methods * fixed tests * added codec tests * added chain tests * updated nits * Revert "Merge branch 'master' into calum/adding-payload-codecs" This reverts commit 145290ca283d5d5fa6a8652e1a62af68e758ad5d, reversing changes made to d8a42d8f4494d34c1d25f905532a251d9adb0ab8. * fixed tests * fixed * examples/lib/crypt_payload_codec.rb --------- Co-authored-by: Nathan Glass Co-authored-by: Jeff Schoner Co-authored-by: Andrew Hoskins Co-authored-by: Christopher Brown Co-authored-by: Arya Kumar Co-authored-by: Joseph Azevedo Co-authored-by: Jeff Schoner Co-authored-by: Bryton Shoffner --- examples/bin/worker | 8 ++- ...yptconverter.rb => crypt_payload_codec.rb} | 28 ++++---- examples/spec/integration/converter_spec.rb | 12 ++-- lib/temporal/concerns/payloads.rb | 23 +++++- lib/temporal/configuration.rb | 12 +++- .../connection/converter/codec/base.rb | 35 +++++++++ .../connection/converter/codec/chain.rb | 36 ++++++++++ lib/temporal/connection/grpc.rb | 4 +- .../connection/serializer/continue_as_new.rb | 2 +- .../serializer/start_child_workflow.rb | 2 +- .../serializer/upsert_search_attributes.rb | 2 +- lib/temporal/workflow/execution_info.rb | 11 +-- proto | 2 +- .../connection/converter/codec/base_spec.rb | 71 +++++++++++++++++++ .../connection/converter/codec/chain_spec.rb | 60 ++++++++++++++++ .../upsert_search_attributes_spec.rb | 2 +- 16 files changed, 272 insertions(+), 38 deletions(-) rename examples/lib/{cryptconverter.rb => crypt_payload_codec.rb} (76%) create mode 100644 lib/temporal/connection/converter/codec/base.rb create mode 100644 lib/temporal/connection/converter/codec/chain.rb create mode 100644 spec/unit/lib/temporal/connection/converter/codec/base_spec.rb create mode 100644 spec/unit/lib/temporal/connection/converter/codec/chain_spec.rb diff --git a/examples/bin/worker b/examples/bin/worker index 79b6313c..cead588d 100755 --- a/examples/bin/worker +++ b/examples/bin/worker @@ -1,6 +1,6 @@ #!/usr/bin/env ruby require_relative '../init' -require_relative '../lib/cryptconverter' +require_relative '../lib/crypt_payload_codec' require 'temporal/worker' @@ -11,8 +11,10 @@ Dir[File.expand_path('../middleware/*.rb', __dir__)].each { |f| require f } if !ENV['USE_ENCRYPTION'].nil? Temporal.configure do |config| config.task_queue = 'crypt' - config.converter = Temporal::CryptConverter.new( - payload_converter: Temporal::Configuration::DEFAULT_CONVERTER + config.payload_codec = Temporal::Connection::Converter::Codec::Chain.new( + payload_codecs: [ + Temporal::CryptPayloadCodec.new + ] ) end end diff --git a/examples/lib/cryptconverter.rb b/examples/lib/crypt_payload_codec.rb similarity index 76% rename from examples/lib/cryptconverter.rb rename to examples/lib/crypt_payload_codec.rb index c968bfc9..72e6769d 100644 --- a/examples/lib/cryptconverter.rb +++ b/examples/lib/crypt_payload_codec.rb @@ -1,7 +1,8 @@ require 'openssl' +require 'temporal/connection/converter/codec/base' module Temporal - class CryptConverter < Temporal::Connection::Converter::Base + class CryptPayloadCodec < Temporal::Connection::Converter::Codec::Base CIPHER = 'aes-256-gcm'.freeze GCM_NONCE_SIZE = 12 GCM_TAG_SIZE = 16 @@ -10,26 +11,23 @@ class CryptConverter < Temporal::Connection::Converter::Base METADATA_ENCODING_KEY = 'encoding'.freeze METADATA_ENCODING = 'binary/encrypted'.freeze - def to_payloads(data) + def encode(payload) + return nil if payload.nil? + key_id = get_key_id key = get_key(key_id) - payloads = super(data) - - Temporalio::Api::Common::V1::Payloads.new( - payloads: payloads.payloads.map { |payload| encrypt_payload(payload, key_id, key) } - ) + encrypt_payload(payload, key_id, key) end + + def decode(payload) + return nil if payload.nil? - def from_payloads(payloads) - return nil if payloads.nil? - - payloads.payloads.map do |payload| - if payload.metadata[METADATA_ENCODING_KEY] == METADATA_ENCODING - payload = decrypt_payload(payload) - end - from_payload(payload) + if payload.metadata[METADATA_ENCODING_KEY] == METADATA_ENCODING + payload = decrypt_payload(payload) end + + payload end private diff --git a/examples/spec/integration/converter_spec.rb b/examples/spec/integration/converter_spec.rb index bc3f78a6..21878c1f 100644 --- a/examples/spec/integration/converter_spec.rb +++ b/examples/spec/integration/converter_spec.rb @@ -1,5 +1,5 @@ require 'workflows/hello_world_workflow' -require 'lib/cryptconverter' +require 'lib/crypt_payload_codec' require 'grpc/errors' describe 'Converter', :integration do @@ -8,8 +8,10 @@ Temporal.configure do |config| config.task_queue = 'crypt' - config.converter = Temporal::CryptConverter.new( - payload_converter: Temporal::Configuration::DEFAULT_CONVERTER + config.payload_codec = Temporal::Connection::Converter::Codec::Chain.new( + payload_codecs: [ + Temporal::CryptPayloadCodec.new + ] ) end @@ -65,8 +67,8 @@ completion_event = events[:EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED].first result = completion_event.workflow_execution_completed_event_attributes.result - converter = Temporal.configuration.converter + payload_codec = Temporal.configuration.payload_codec - expect(converter.from_payloads(result)&.first).to eq('Hello World, Tom') + expect(payload_codec.decodes(result).payloads.first.data).to eq('"Hello World, Tom"') end end diff --git a/lib/temporal/concerns/payloads.rb b/lib/temporal/concerns/payloads.rb index ad703542..5c771e21 100644 --- a/lib/temporal/concerns/payloads.rb +++ b/lib/temporal/concerns/payloads.rb @@ -2,13 +2,19 @@ module Temporal module Concerns module Payloads def from_payloads(payloads) + payloads = payload_codec.decodes(payloads) payload_converter.from_payloads(payloads) end def from_payload(payload) + payload = payload_codec.decode(payload) payload_converter.from_payload(payload) end + def from_payload_map_without_codec(payload_map) + payload_map.map { |key, value| [key, payload_converter.from_payload(value)] }.to_h + end + def from_result_payloads(payloads) from_payloads(payloads)&.first end @@ -30,11 +36,20 @@ def from_payload_map(payload_map) end def to_payloads(data) - payload_converter.to_payloads(data) + payloads = payload_converter.to_payloads(data) + payload_codec.encodes(payloads) end def to_payload(data) - payload_converter.to_payload(data) + payload = payload_converter.to_payload(data) + payload_codec.encode(payload) + end + + def to_payload_map_without_codec(data) + # skips the payload_codec step because search attributes don't use this pipeline + data.transform_values do |value| + payload_converter.to_payload(value) + end end def to_result_payloads(data) @@ -62,6 +77,10 @@ def to_payload_map(data) def payload_converter Temporal.configuration.converter end + + def payload_codec + Temporal.configuration.payload_codec + end end end end diff --git a/lib/temporal/configuration.rb b/lib/temporal/configuration.rb index 20fdd7cf..7ba12f28 100644 --- a/lib/temporal/configuration.rb +++ b/lib/temporal/configuration.rb @@ -7,6 +7,7 @@ require 'temporal/connection/converter/payload/json' require 'temporal/connection/converter/payload/proto_json' require 'temporal/connection/converter/composite' +require 'temporal/connection/converter/codec/chain' module Temporal class Configuration @@ -14,7 +15,7 @@ class Configuration Execution = Struct.new(:namespace, :task_queue, :timeouts, :headers, :search_attributes, keyword_init: true) attr_reader :timeouts, :error_handlers - attr_accessor :connection_type, :converter, :use_error_serialization_v2, :host, :port, :credentials, :identity, :logger, :metrics_adapter, :namespace, :task_queue, :headers, :search_attributes, :header_propagators + attr_accessor :connection_type, :converter, :use_error_serialization_v2, :host, :port, :credentials, :identity, :logger, :metrics_adapter, :namespace, :task_queue, :headers, :search_attributes, :header_propagators, :payload_codec # See https://docs.temporal.io/blog/activity-timeouts/ for general docs. # We want an infinite execution timeout for cron schedules and other perpetual workflows. @@ -45,6 +46,14 @@ class Configuration Temporal::Connection::Converter::Payload::JSON.new ] ).freeze + + # The Payload Codec is an optional step that happens between the wire and the Payload Converter: + # Temporal Server <--> Wire <--> Payload Codec <--> Payload Converter <--> User code + # which can be useful for transformations such as compression and encryption + # more info at https://docs.temporal.io/security#payload-codec + DEFAULT_PAYLOAD_CODEC = Temporal::Connection::Converter::Codec::Chain.new( + payload_codecs: [] + ).freeze def initialize @connection_type = :grpc @@ -55,6 +64,7 @@ def initialize @task_queue = DEFAULT_TASK_QUEUE @headers = DEFAULT_HEADERS @converter = DEFAULT_CONVERTER + @payload_codec = DEFAULT_PAYLOAD_CODEC @use_error_serialization_v2 = false @error_handlers = [] @credentials = :this_channel_is_insecure diff --git a/lib/temporal/connection/converter/codec/base.rb b/lib/temporal/connection/converter/codec/base.rb new file mode 100644 index 00000000..d8748909 --- /dev/null +++ b/lib/temporal/connection/converter/codec/base.rb @@ -0,0 +1,35 @@ +module Temporal + module Connection + module Converter + module Codec + class Base + def encodes(payloads) + return nil if payloads.nil? + + Temporalio::Api::Common::V1::Payloads.new( + payloads: payloads.payloads.map(&method(:encode)) + ) + end + + def decodes(payloads) + return nil if payloads.nil? + + Temporalio::Api::Common::V1::Payloads.new( + payloads: payloads.payloads.map(&method(:decode)) + ) + end + + def encode(payload) + # should return Temporalio::Api::Common::V1::Payload + raise NotImplementedError, 'codec converter needs to implement encode' + end + + def decode(payload) + # should return Temporalio::Api::Common::V1::Payload + raise NotImplementedError, 'codec converter needs to implement decode' + end + end + end + end + end +end diff --git a/lib/temporal/connection/converter/codec/chain.rb b/lib/temporal/connection/converter/codec/chain.rb new file mode 100644 index 00000000..fc1a16f8 --- /dev/null +++ b/lib/temporal/connection/converter/codec/chain.rb @@ -0,0 +1,36 @@ +require 'temporal/connection/converter/codec/base' + +module Temporal + module Connection + module Converter + module Codec + # Performs encoding/decoding on the payloads via the given payload codecs. When encoding + # the codecs are applied last to first meaning the earlier encodings wrap the later ones. + # When decoding, the codecs are applied first to last to reverse the effect. + class Chain < Base + def initialize(payload_codecs:) + @payload_codecs = payload_codecs + end + + def encode(payload) + payload_codecs.reverse_each do |payload_codec| + payload = payload_codec.encode(payload) + end + payload + end + + def decode(payload) + payload_codecs.each do |payload_codec| + payload = payload_codec.decode(payload) + end + payload + end + + private + + attr_reader :payload_codecs + end + end + end + end +end diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index 52c440fa..e7b7e95e 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -138,7 +138,7 @@ def start_workflow_execution( fields: to_payload_map(memo || {}) ), search_attributes: Temporalio::Api::Common::V1::SearchAttributes.new( - indexed_fields: to_payload_map(search_attributes || {}) + indexed_fields: to_payload_map_without_codec(search_attributes || {}) ), ) @@ -401,7 +401,7 @@ def signal_with_start_workflow_execution( fields: to_payload_map(memo || {}) ), search_attributes: Temporalio::Api::Common::V1::SearchAttributes.new( - indexed_fields: to_payload_map(search_attributes || {}) + indexed_fields: to_payload_map_without_codec(search_attributes || {}) ), ) diff --git a/lib/temporal/connection/serializer/continue_as_new.rb b/lib/temporal/connection/serializer/continue_as_new.rb index 9a6a7ecf..c2b484bb 100644 --- a/lib/temporal/connection/serializer/continue_as_new.rb +++ b/lib/temporal/connection/serializer/continue_as_new.rb @@ -43,7 +43,7 @@ def serialize_memo(memo) def serialize_search_attributes(search_attributes) return unless search_attributes - Temporalio::Api::Common::V1::SearchAttributes.new(indexed_fields: to_payload_map(search_attributes)) + Temporalio::Api::Common::V1::SearchAttributes.new(indexed_fields: to_payload_map_without_codec(search_attributes)) end end end diff --git a/lib/temporal/connection/serializer/start_child_workflow.rb b/lib/temporal/connection/serializer/start_child_workflow.rb index 3cc3a0aa..90d08c79 100644 --- a/lib/temporal/connection/serializer/start_child_workflow.rb +++ b/lib/temporal/connection/serializer/start_child_workflow.rb @@ -66,7 +66,7 @@ def serialize_parent_close_policy(parent_close_policy) def serialize_search_attributes(search_attributes) return unless search_attributes - Temporalio::Api::Common::V1::SearchAttributes.new(indexed_fields: to_payload_map(search_attributes)) + Temporalio::Api::Common::V1::SearchAttributes.new(indexed_fields: to_payload_map_without_codec(search_attributes)) end end end diff --git a/lib/temporal/connection/serializer/upsert_search_attributes.rb b/lib/temporal/connection/serializer/upsert_search_attributes.rb index 0af6b79f..e8aa652c 100644 --- a/lib/temporal/connection/serializer/upsert_search_attributes.rb +++ b/lib/temporal/connection/serializer/upsert_search_attributes.rb @@ -13,7 +13,7 @@ def to_proto upsert_workflow_search_attributes_command_attributes: Temporalio::Api::Command::V1::UpsertWorkflowSearchAttributesCommandAttributes.new( search_attributes: Temporalio::Api::Common::V1::SearchAttributes.new( - indexed_fields: to_payload_map(object.search_attributes || {}) + indexed_fields: to_payload_map_without_codec(object.search_attributes || {}) ), ) ) diff --git a/lib/temporal/workflow/execution_info.rb b/lib/temporal/workflow/execution_info.rb index 88d27cd7..e3f70021 100644 --- a/lib/temporal/workflow/execution_info.rb +++ b/lib/temporal/workflow/execution_info.rb @@ -3,7 +3,8 @@ module Temporal class Workflow - class ExecutionInfo < Struct.new(:workflow, :workflow_id, :run_id, :start_time, :close_time, :status, :history_length, :memo, :search_attributes, keyword_init: true) + class ExecutionInfo < Struct.new(:workflow, :workflow_id, :run_id, :start_time, :close_time, :status, + :history_length, :memo, :search_attributes, keyword_init: true) extend Concerns::Payloads STATUSES = [ @@ -13,11 +14,11 @@ class ExecutionInfo < Struct.new(:workflow, :workflow_id, :run_id, :start_time, Temporal::Workflow::Status::CANCELED, Temporal::Workflow::Status::TERMINATED, Temporal::Workflow::Status::CONTINUED_AS_NEW, - Temporal::Workflow::Status::TIMED_OUT, + Temporal::Workflow::Status::TIMED_OUT ] def self.generate_from(response) - search_attributes = response.search_attributes.nil? ? {} : self.from_payload_map(response.search_attributes.indexed_fields) + search_attributes = response.search_attributes.nil? ? {} : from_payload_map_without_codec(response.search_attributes.indexed_fields) new( workflow: response.type.name, workflow_id: response.execution.workflow_id, @@ -26,8 +27,8 @@ def self.generate_from(response) close_time: response.close_time&.to_time, status: Temporal::Workflow::Status::API_STATUS_MAP.fetch(response.status), history_length: response.history_length, - memo: self.from_payload_map(response.memo.fields), - search_attributes: search_attributes, + memo: from_payload_map(response.memo.fields), + search_attributes: search_attributes ).freeze end diff --git a/proto b/proto index e4246bbd..4c2f6a28 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit e4246bbd59fd1f850bdd5be6a59d6d2f8e532d76 +Subproject commit 4c2f6a281fa3fde8b0a24447de3e0d0f47d230b4 diff --git a/spec/unit/lib/temporal/connection/converter/codec/base_spec.rb b/spec/unit/lib/temporal/connection/converter/codec/base_spec.rb new file mode 100644 index 00000000..22d0eecf --- /dev/null +++ b/spec/unit/lib/temporal/connection/converter/codec/base_spec.rb @@ -0,0 +1,71 @@ +require 'temporal/connection/converter/codec/chain' + +describe Temporal::Connection::Converter::Codec::Base do + let(:payloads) do + Temporalio::Api::Common::V1::Payloads.new( + payloads: [ + Temporalio::Api::Common::V1::Payload.new( + metadata: { 'encoding' => 'json/plain' }, + data: '{}'.b + ) + ] + ) + end + + let (:encoded_payload) do + Temporalio::Api::Common::V1::Payload.new( + metadata: { 'encoding' => 'binary/encrypted' }, + data: 'encrypted-payload'.b + ) + end + + let(:base_codec) { described_class.new } + + describe '#encodes' do + it 'returns nil if payloads is nil' do + expect(base_codec.encodes(nil)).to be_nil + end + + it 'encodes each payload in payloads' do + expect(base_codec).to receive(:encode).with(payloads.payloads[0]).and_return(encoded_payload) + base_codec.encodes(payloads) + end + + it 'returns a new Payloads object with the encoded payloads' do + encoded_payloads = Temporalio::Api::Common::V1::Payloads.new( + payloads: [Temporalio::Api::Common::V1::Payload.new( + metadata: { 'encoding' => 'json/plain' }, + data: 'encoded_payload'.b + )] + ) + + allow(base_codec).to receive(:encode).and_return(encoded_payloads.payloads[0]) + + expect(base_codec.encodes(payloads)).to eq(encoded_payloads) + end + end + + describe '#decodes' do + it 'returns nil if payloads is nil' do + expect(base_codec.decodes(nil)).to be_nil + end + + it 'decodes each payload in payloads' do + expect(base_codec).to receive(:decode).with(payloads.payloads[0]).and_return(payloads.payloads[0]) + base_codec.decodes(payloads) + end + + it 'returns a new Payloads object with the decoded payloads' do + decoded_payloads = Temporalio::Api::Common::V1::Payloads.new( + payloads: [Temporalio::Api::Common::V1::Payload.new( + metadata: { 'encoding' => 'json/plain' }, + data: 'decoded_payload'.b + )] + ) + + allow(base_codec).to receive(:decode).and_return(decoded_payloads.payloads[0]) + + expect(base_codec.decodes(payloads)).to eq(decoded_payloads) + end + end +end diff --git a/spec/unit/lib/temporal/connection/converter/codec/chain_spec.rb b/spec/unit/lib/temporal/connection/converter/codec/chain_spec.rb new file mode 100644 index 00000000..77c189db --- /dev/null +++ b/spec/unit/lib/temporal/connection/converter/codec/chain_spec.rb @@ -0,0 +1,60 @@ +require 'temporal/connection/converter/codec/chain' + +describe Temporal::Connection::Converter::Codec::Chain do + let(:codec1) { double('PayloadCodec1') } + let(:codec2) { double('PayloadCodec2') } + let(:codec3) { double('PayloadCodec3') } + + let(:payload_1) do + Temporalio::Api::Common::V1::Payload.new( + metadata: { 'encoding' => 'binary/plain' }, + data: 'payload_1'.b + ) + end + let(:payload_2) do + Temporalio::Api::Common::V1::Payload.new( + metadata: { 'encoding' => 'binary/plain' }, + data: 'payload_2'.b + ) + end + let(:payload_3) do + Temporalio::Api::Common::V1::Payload.new( + metadata: { 'encoding' => 'binary/plain' }, + data: 'payload_3'.b + ) + end + let(:payload_4) do + Temporalio::Api::Common::V1::Payload.new( + metadata: { 'encoding' => 'binary/plain' }, + data: 'payload_4'.b + ) + end + + subject { described_class.new(payload_codecs: [codec1, codec2, codec3]) } + + describe '#encode' do + it 'applies payload codecs in reverse order' do + expect(codec3).to receive(:encode).with(payload_1).and_return(payload_2) + expect(codec2).to receive(:encode).with(payload_2).and_return(payload_3) + expect(codec1).to receive(:encode).with(payload_3).and_return(payload_4) + + result = subject.encode(payload_1) + + expect(result.metadata).to eq(payload_4.metadata) + expect(result.data).to eq(payload_4.data) + end + end + + describe '#decode' do + it 'applies payload codecs in the original order' do + expect(codec1).to receive(:decode).with(payload_1).and_return(payload_2) + expect(codec2).to receive(:decode).with(payload_2).and_return(payload_3) + expect(codec3).to receive(:decode).with(payload_3).and_return(payload_4) + + result = subject.decode(payload_1) + + expect(result.metadata).to eq(payload_4.metadata) + expect(result.data).to eq(payload_4.data) + end + end +end diff --git a/spec/unit/lib/temporal/connection/serializer/upsert_search_attributes_spec.rb b/spec/unit/lib/temporal/connection/serializer/upsert_search_attributes_spec.rb index da5d8879..bc94128f 100644 --- a/spec/unit/lib/temporal/connection/serializer/upsert_search_attributes_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/upsert_search_attributes_spec.rb @@ -29,7 +29,7 @@ class TestDeserializer ) command_attributes = result.upsert_workflow_search_attributes_command_attributes expect(command_attributes).not_to be_nil - actual_attributes = TestDeserializer.from_payload_map(command_attributes&.search_attributes&.indexed_fields) + actual_attributes = TestDeserializer.from_payload_map_without_codec(command_attributes&.search_attributes&.indexed_fields) expect(actual_attributes).to eql(expected_attributes) end From 8077a87b40c80f713c02b6037eb49b20757a8038 Mon Sep 17 00:00:00 2001 From: markchua Date: Mon, 10 Apr 2023 11:30:21 -0700 Subject: [PATCH 081/125] Fix header in example integration test. (#230) Co-authored-by: Mark Chua --- examples/spec/integration/continue_as_new_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/spec/integration/continue_as_new_spec.rb b/examples/spec/integration/continue_as_new_spec.rb index 74b0771e..9efa0e2c 100644 --- a/examples/spec/integration/continue_as_new_spec.rb +++ b/examples/spec/integration/continue_as_new_spec.rb @@ -8,6 +8,7 @@ } headers = { 'my-header' => 'bar', + 'test-header' => 'test', } run_id = Temporal.start_workflow( LoopWorkflow, From 01da80cde27e29c101cabd8d324e12b82111805d Mon Sep 17 00:00:00 2001 From: Jeff Schoner Date: Mon, 10 Apr 2023 12:23:36 -0700 Subject: [PATCH 082/125] Update READMEs for running integration specs (#231) --- README.md | 3 ++- examples/README.md | 12 +++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7f5ff27d..b2818447 100644 --- a/README.md +++ b/README.md @@ -491,7 +491,8 @@ Temporal::Testing.local! do end ``` -Make sure to check out [example integration specs](examples/spec/integration) for more details. +Make sure to check out [example integration specs](examples/spec/integration) for more details. Instructions +for running these integration specs can be found in [examples/README.md](examples/README.md). ## TODO diff --git a/examples/README.md b/examples/README.md index bd9e064a..df644ef1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -12,10 +12,14 @@ bundle install Modify the `init.rb` file to point to your Temporal cluster. -Start a worker process: +Start the three worker processes. Each of these uses a different task queue because there are differences +in how their payloads are serialized. You typically want to do this by running each line in a separate +terminal or via tmux or similar. ```sh bin/worker +USE_ENCRYPTION=1 bin/worker +USE_ERROR_SERIALIZATION_V2=1 bin/worker ``` Use this command to trigger one of the example workflows from the `workflows` directory: @@ -25,11 +29,13 @@ bin/trigger NAME_OF_THE_WORKFLOW [argument_1, argument_2, ...] ``` ## Testing -To run tests, make sure the temporal server and the worker process are already running: +To run tests, make sure the temporal server is running: ```shell docker-compose up -bin/worker ``` + +Follow the instructions above to start the three worker proceses. + To execute the tests, run: ```shell bundle exec rspec From 5d1c08807ebf179c4f70ae1fa44b6e892fdb7694 Mon Sep 17 00:00:00 2001 From: Jeff Schoner Date: Wed, 12 Apr 2023 07:30:56 -0700 Subject: [PATCH 083/125] Fix broken encryption specs, update Temporal API proto to 1.20 (#233) * Regenerate proto code for v1.20.0 * Restore payload_codec instead of data coverter in spec * Improve error message when a payload converter is missing --- examples/spec/integration/converter_spec.rb | 2 +- lib/gen/temporal/api/common/v1/message_pb.rb | 13 +++ .../temporal/api/enums/v1/failed_cause_pb.rb | 1 + lib/gen/temporal/api/history/v1/message_pb.rb | 6 +- .../api/sdk/v1/task_complete_metadata_pb.rb | 23 ++++++ .../temporal/api/taskqueue/v1/message_pb.rb | 16 ++-- .../temporal/api/workflow/v1/message_pb.rb | 1 + .../workflowservice/v1/request_response_pb.rb | 79 +++++++++++++++---- .../workflowservice/v1/service_services_pb.rb | 25 ++++-- .../connection/converter/composite.rb | 2 +- proto | 2 +- .../connection/converter/composite_spec.rb | 6 +- 12 files changed, 136 insertions(+), 40 deletions(-) create mode 100644 lib/gen/temporal/api/sdk/v1/task_complete_metadata_pb.rb diff --git a/examples/spec/integration/converter_spec.rb b/examples/spec/integration/converter_spec.rb index 21878c1f..0a97f075 100644 --- a/examples/spec/integration/converter_spec.rb +++ b/examples/spec/integration/converter_spec.rb @@ -19,7 +19,7 @@ ensure Temporal.configure do |config| config.task_queue = task_queue - config.converter = Temporal::Configuration::DEFAULT_CONVERTER + config.payload_codec = Temporal::Configuration::DEFAULT_PAYLOAD_CODEC end end diff --git a/lib/gen/temporal/api/common/v1/message_pb.rb b/lib/gen/temporal/api/common/v1/message_pb.rb index ef18cc6e..52c50880 100644 --- a/lib/gen/temporal/api/common/v1/message_pb.rb +++ b/lib/gen/temporal/api/common/v1/message_pb.rb @@ -46,6 +46,16 @@ optional :maximum_attempts, :int32, 4 repeated :non_retryable_error_types, :string, 5 end + add_message "temporal.api.common.v1.MeteringMetadata" do + optional :nonfirst_local_activity_execution_attempts, :uint32, 13 + end + add_message "temporal.api.common.v1.WorkerVersionStamp" do + optional :build_id, :string, 1 + optional :bundle_id, :string, 2 + end + add_message "temporal.api.common.v1.WorkerVersionCapabilities" do + optional :build_id, :string, 1 + end end end @@ -63,6 +73,9 @@ module V1 WorkflowType = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.common.v1.WorkflowType").msgclass ActivityType = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.common.v1.ActivityType").msgclass RetryPolicy = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.common.v1.RetryPolicy").msgclass + MeteringMetadata = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.common.v1.MeteringMetadata").msgclass + WorkerVersionStamp = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.common.v1.WorkerVersionStamp").msgclass + WorkerVersionCapabilities = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.common.v1.WorkerVersionCapabilities").msgclass end end end diff --git a/lib/gen/temporal/api/enums/v1/failed_cause_pb.rb b/lib/gen/temporal/api/enums/v1/failed_cause_pb.rb index e0e21568..4986e3ac 100644 --- a/lib/gen/temporal/api/enums/v1/failed_cause_pb.rb +++ b/lib/gen/temporal/api/enums/v1/failed_cause_pb.rb @@ -53,6 +53,7 @@ value :SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_FAILED_CAUSE_UNSPECIFIED, 0 value :SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_FAILED_CAUSE_EXTERNAL_WORKFLOW_EXECUTION_NOT_FOUND, 1 value :SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_FAILED_CAUSE_NAMESPACE_NOT_FOUND, 2 + value :SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_FAILED_CAUSE_SIGNAL_COUNT_LIMIT_EXCEEDED, 3 end add_enum "temporal.api.enums.v1.ResourceExhaustedCause" do value :RESOURCE_EXHAUSTED_CAUSE_UNSPECIFIED, 0 diff --git a/lib/gen/temporal/api/history/v1/message_pb.rb b/lib/gen/temporal/api/history/v1/message_pb.rb index e2d6e2f4..2b0043ce 100644 --- a/lib/gen/temporal/api/history/v1/message_pb.rb +++ b/lib/gen/temporal/api/history/v1/message_pb.rb @@ -14,6 +14,7 @@ require 'temporal/api/taskqueue/v1/message_pb' require 'temporal/api/update/v1/message_pb' require 'temporal/api/workflow/v1/message_pb' +require 'temporal/api/sdk/v1/task_complete_metadata_pb' Google::Protobuf::DescriptorPool.generated_pool.build do add_file("temporal/api/history/v1/message.proto", :syntax => :proto3) do @@ -94,7 +95,9 @@ optional :started_event_id, :int64, 2 optional :identity, :string, 3 optional :binary_checksum, :string, 4 - optional :worker_versioning_id, :message, 5, "temporal.api.taskqueue.v1.VersionId" + optional :worker_version, :message, 5, "temporal.api.common.v1.WorkerVersionStamp" + optional :sdk_metadata, :message, 6, "temporal.api.sdk.v1.WorkflowTaskCompletedMetadata" + optional :metering_metadata, :message, 13, "temporal.api.common.v1.MeteringMetadata" end add_message "temporal.api.history.v1.WorkflowTaskTimedOutEventAttributes" do optional :scheduled_event_id, :int64, 1 @@ -199,6 +202,7 @@ optional :input, :message, 2, "temporal.api.common.v1.Payloads" optional :identity, :string, 3 optional :header, :message, 4, "temporal.api.common.v1.Header" + optional :skip_generate_workflow_task, :bool, 5 end add_message "temporal.api.history.v1.WorkflowExecutionTerminatedEventAttributes" do optional :reason, :string, 1 diff --git a/lib/gen/temporal/api/sdk/v1/task_complete_metadata_pb.rb b/lib/gen/temporal/api/sdk/v1/task_complete_metadata_pb.rb new file mode 100644 index 00000000..281bd518 --- /dev/null +++ b/lib/gen/temporal/api/sdk/v1/task_complete_metadata_pb.rb @@ -0,0 +1,23 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporal/api/sdk/v1/task_complete_metadata.proto + +require 'google/protobuf' + +Google::Protobuf::DescriptorPool.generated_pool.build do + add_file("temporal/api/sdk/v1/task_complete_metadata.proto", :syntax => :proto3) do + add_message "temporal.api.sdk.v1.WorkflowTaskCompletedMetadata" do + repeated :core_used_flags, :uint32, 1 + repeated :lang_used_flags, :uint32, 2 + end + end +end + +module Temporalio + module Api + module Sdk + module V1 + WorkflowTaskCompletedMetadata = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.sdk.v1.WorkflowTaskCompletedMetadata").msgclass + end + end + end +end diff --git a/lib/gen/temporal/api/taskqueue/v1/message_pb.rb b/lib/gen/temporal/api/taskqueue/v1/message_pb.rb index 648e3e2b..053e5f67 100644 --- a/lib/gen/temporal/api/taskqueue/v1/message_pb.rb +++ b/lib/gen/temporal/api/taskqueue/v1/message_pb.rb @@ -8,6 +8,7 @@ require 'google/protobuf/wrappers_pb' require 'dependencies/gogoproto/gogo_pb' require 'temporal/api/enums/v1/task_queue_pb' +require 'temporal/api/common/v1/message_pb' Google::Protobuf::DescriptorPool.generated_pool.build do add_file("temporal/api/taskqueue/v1/message.proto", :syntax => :proto3) do @@ -37,19 +38,15 @@ optional :last_access_time, :message, 1, "google.protobuf.Timestamp" optional :identity, :string, 2 optional :rate_per_second, :double, 3 - optional :worker_versioning_id, :message, 4, "temporal.api.taskqueue.v1.VersionId" + optional :worker_version_capabilities, :message, 4, "temporal.api.common.v1.WorkerVersionCapabilities" end add_message "temporal.api.taskqueue.v1.StickyExecutionAttributes" do optional :worker_task_queue, :message, 1, "temporal.api.taskqueue.v1.TaskQueue" optional :schedule_to_start_timeout, :message, 2, "google.protobuf.Duration" end - add_message "temporal.api.taskqueue.v1.VersionIdNode" do - optional :version, :message, 1, "temporal.api.taskqueue.v1.VersionId" - optional :previous_compatible, :message, 2, "temporal.api.taskqueue.v1.VersionIdNode" - optional :previous_incompatible, :message, 3, "temporal.api.taskqueue.v1.VersionIdNode" - end - add_message "temporal.api.taskqueue.v1.VersionId" do - optional :worker_build_id, :string, 1 + add_message "temporal.api.taskqueue.v1.CompatibleVersionSet" do + optional :version_set_id, :string, 1 + repeated :build_ids, :string, 2 end end end @@ -65,8 +62,7 @@ module V1 TaskQueuePartitionMetadata = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.taskqueue.v1.TaskQueuePartitionMetadata").msgclass PollerInfo = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.taskqueue.v1.PollerInfo").msgclass StickyExecutionAttributes = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.taskqueue.v1.StickyExecutionAttributes").msgclass - VersionIdNode = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.taskqueue.v1.VersionIdNode").msgclass - VersionId = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.taskqueue.v1.VersionId").msgclass + CompatibleVersionSet = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.taskqueue.v1.CompatibleVersionSet").msgclass end end end diff --git a/lib/gen/temporal/api/workflow/v1/message_pb.rb b/lib/gen/temporal/api/workflow/v1/message_pb.rb index f6405daa..470fd0a4 100644 --- a/lib/gen/temporal/api/workflow/v1/message_pb.rb +++ b/lib/gen/temporal/api/workflow/v1/message_pb.rb @@ -29,6 +29,7 @@ optional :task_queue, :string, 13 optional :state_transition_count, :int64, 14 optional :history_size_bytes, :int64, 15 + optional :most_recent_worker_version_stamp, :message, 16, "temporal.api.common.v1.WorkerVersionStamp" end add_message "temporal.api.workflow.v1.WorkflowExecutionConfig" do optional :task_queue, :message, 1, "temporal.api.taskqueue.v1.TaskQueue" diff --git a/lib/gen/temporal/api/workflowservice/v1/request_response_pb.rb b/lib/gen/temporal/api/workflowservice/v1/request_response_pb.rb index 7d8d6446..ab356996 100644 --- a/lib/gen/temporal/api/workflowservice/v1/request_response_pb.rb +++ b/lib/gen/temporal/api/workflowservice/v1/request_response_pb.rb @@ -26,6 +26,7 @@ require 'temporal/api/update/v1/message_pb' require 'temporal/api/version/v1/message_pb' require 'temporal/api/batch/v1/message_pb' +require 'temporal/api/sdk/v1/task_complete_metadata_pb' require 'google/protobuf/duration_pb' require 'google/protobuf/timestamp_pb' require 'dependencies/gogoproto/gogo_pb' @@ -110,6 +111,9 @@ optional :search_attributes, :message, 15, "temporal.api.common.v1.SearchAttributes" optional :header, :message, 16, "temporal.api.common.v1.Header" optional :request_eager_execution, :bool, 17 + optional :continued_failure, :message, 18, "temporal.api.failure.v1.Failure" + optional :last_completion_result, :message, 19, "temporal.api.common.v1.Payloads" + optional :workflow_start_delay, :message, 20, "google.protobuf.Duration" end add_message "temporal.api.workflowservice.v1.StartWorkflowExecutionResponse" do optional :run_id, :string, 1 @@ -145,7 +149,7 @@ optional :task_queue, :message, 2, "temporal.api.taskqueue.v1.TaskQueue" optional :identity, :string, 3 optional :binary_checksum, :string, 4 - optional :worker_versioning_id, :message, 5, "temporal.api.taskqueue.v1.VersionId" + optional :worker_version_capabilities, :message, 5, "temporal.api.common.v1.WorkerVersionCapabilities" end add_message "temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse" do optional :task_token, :bytes, 1 @@ -174,8 +178,10 @@ optional :binary_checksum, :string, 7 map :query_results, :string, :message, 8, "temporal.api.query.v1.WorkflowQueryResult" optional :namespace, :string, 9 - optional :worker_versioning_id, :message, 10, "temporal.api.taskqueue.v1.VersionId" + optional :worker_version_stamp, :message, 10, "temporal.api.common.v1.WorkerVersionStamp" repeated :messages, :message, 11, "temporal.api.protocol.v1.Message" + optional :sdk_metadata, :message, 12, "temporal.api.sdk.v1.WorkflowTaskCompletedMetadata" + optional :metering_metadata, :message, 13, "temporal.api.common.v1.MeteringMetadata" end add_message "temporal.api.workflowservice.v1.RespondWorkflowTaskCompletedResponse" do optional :workflow_task, :message, 1, "temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse" @@ -198,7 +204,7 @@ optional :task_queue, :message, 2, "temporal.api.taskqueue.v1.TaskQueue" optional :identity, :string, 3 optional :task_queue_metadata, :message, 4, "temporal.api.taskqueue.v1.TaskQueueMetadata" - optional :worker_versioning_id, :message, 5, "temporal.api.taskqueue.v1.VersionId" + optional :worker_version_capabilities, :message, 5, "temporal.api.common.v1.WorkerVersionCapabilities" end add_message "temporal.api.workflowservice.v1.PollActivityTaskQueueResponse" do optional :task_token, :bytes, 1 @@ -316,6 +322,7 @@ optional :request_id, :string, 6 optional :control, :string, 7 optional :header, :message, 8, "temporal.api.common.v1.Header" + optional :skip_generate_workflow_task, :bool, 9 end add_message "temporal.api.workflowservice.v1.SignalWorkflowExecutionResponse" do end @@ -339,6 +346,8 @@ optional :memo, :message, 17, "temporal.api.common.v1.Memo" optional :search_attributes, :message, 18, "temporal.api.common.v1.SearchAttributes" optional :header, :message, 19, "temporal.api.common.v1.Header" + optional :workflow_start_delay, :message, 20, "google.protobuf.Duration" + optional :skip_generate_workflow_task, :bool, 21 end add_message "temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse" do optional :run_id, :string, 1 @@ -514,6 +523,7 @@ optional :build_id_based_versioning, :bool, 6 optional :upsert_memo, :bool, 7 optional :eager_workflow_start, :bool, 8 + optional :sdk_metadata, :bool, 9 end add_message "temporal.api.workflowservice.v1.ListTaskQueuePartitionsRequest" do optional :namespace, :string, 1 @@ -591,23 +601,44 @@ repeated :schedules, :message, 1, "temporal.api.schedule.v1.ScheduleListEntry" optional :next_page_token, :bytes, 2 end - add_message "temporal.api.workflowservice.v1.UpdateWorkerBuildIdOrderingRequest" do + add_message "temporal.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest" do optional :namespace, :string, 1 optional :task_queue, :string, 2 - optional :version_id, :message, 3, "temporal.api.taskqueue.v1.VersionId" - optional :previous_compatible, :message, 4, "temporal.api.taskqueue.v1.VersionId" - optional :become_default, :bool, 5 + oneof :operation do + optional :add_new_build_id_in_new_default_set, :string, 3 + optional :add_new_compatible_build_id, :message, 4, "temporal.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest.AddNewCompatibleVersion" + optional :promote_set_by_build_id, :string, 5 + optional :promote_build_id_within_set, :string, 6 + end + end + add_message "temporal.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest.AddNewCompatibleVersion" do + optional :new_build_id, :string, 1 + optional :existing_compatible_build_id, :string, 2 + optional :make_set_default, :bool, 3 end - add_message "temporal.api.workflowservice.v1.UpdateWorkerBuildIdOrderingResponse" do + add_message "temporal.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityResponse" do + optional :version_set_id, :string, 1 end - add_message "temporal.api.workflowservice.v1.GetWorkerBuildIdOrderingRequest" do + add_message "temporal.api.workflowservice.v1.GetWorkerBuildIdCompatibilityRequest" do optional :namespace, :string, 1 optional :task_queue, :string, 2 - optional :max_depth, :int32, 3 + optional :max_sets, :int32, 3 + optional :include_retirement_candidates, :bool, 4 + optional :include_poller_compatibility, :bool, 5 + end + add_message "temporal.api.workflowservice.v1.GetWorkerBuildIdCompatibilityResponse" do + repeated :major_version_sets, :message, 1, "temporal.api.taskqueue.v1.CompatibleVersionSet" + repeated :retirement_candidates, :message, 2, "temporal.api.workflowservice.v1.GetWorkerBuildIdCompatibilityResponse.RetirementCandidate" + repeated :active_versions_and_pollers, :message, 3, "temporal.api.workflowservice.v1.GetWorkerBuildIdCompatibilityResponse.VersionsWithCompatiblePollers" + end + add_message "temporal.api.workflowservice.v1.GetWorkerBuildIdCompatibilityResponse.RetirementCandidate" do + optional :build_id, :string, 1 + optional :all_workflows_are_archived, :bool, 2 + repeated :pollers, :message, 3, "temporal.api.taskqueue.v1.PollerInfo" end - add_message "temporal.api.workflowservice.v1.GetWorkerBuildIdOrderingResponse" do - optional :current_default, :message, 1, "temporal.api.taskqueue.v1.VersionIdNode" - repeated :compatible_leaves, :message, 2, "temporal.api.taskqueue.v1.VersionIdNode" + add_message "temporal.api.workflowservice.v1.GetWorkerBuildIdCompatibilityResponse.VersionsWithCompatiblePollers" do + optional :most_recent_build_id, :string, 1 + repeated :pollers, :message, 2, "temporal.api.taskqueue.v1.PollerInfo" end add_message "temporal.api.workflowservice.v1.UpdateWorkflowExecutionRequest" do optional :namespace, :string, 1 @@ -668,6 +699,15 @@ repeated :operation_info, :message, 1, "temporal.api.batch.v1.BatchOperationInfo" optional :next_page_token, :bytes, 2 end + add_message "temporal.api.workflowservice.v1.PollWorkflowExecutionUpdateRequest" do + optional :namespace, :string, 1 + optional :update_ref, :message, 2, "temporal.api.update.v1.UpdateRef" + optional :identity, :string, 3 + optional :wait_policy, :message, 4, "temporal.api.update.v1.WaitPolicy" + end + add_message "temporal.api.workflowservice.v1.PollWorkflowExecutionUpdateResponse" do + optional :outcome, :message, 1, "temporal.api.update.v1.Outcome" + end end end @@ -772,10 +812,13 @@ module V1 DeleteScheduleResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.DeleteScheduleResponse").msgclass ListSchedulesRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.ListSchedulesRequest").msgclass ListSchedulesResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.ListSchedulesResponse").msgclass - UpdateWorkerBuildIdOrderingRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.UpdateWorkerBuildIdOrderingRequest").msgclass - UpdateWorkerBuildIdOrderingResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.UpdateWorkerBuildIdOrderingResponse").msgclass - GetWorkerBuildIdOrderingRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.GetWorkerBuildIdOrderingRequest").msgclass - GetWorkerBuildIdOrderingResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.GetWorkerBuildIdOrderingResponse").msgclass + UpdateWorkerBuildIdCompatibilityRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest").msgclass + UpdateWorkerBuildIdCompatibilityRequest::AddNewCompatibleVersion = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityRequest.AddNewCompatibleVersion").msgclass + UpdateWorkerBuildIdCompatibilityResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.UpdateWorkerBuildIdCompatibilityResponse").msgclass + GetWorkerBuildIdCompatibilityRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.GetWorkerBuildIdCompatibilityRequest").msgclass + GetWorkerBuildIdCompatibilityResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.GetWorkerBuildIdCompatibilityResponse").msgclass + GetWorkerBuildIdCompatibilityResponse::RetirementCandidate = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.GetWorkerBuildIdCompatibilityResponse.RetirementCandidate").msgclass + GetWorkerBuildIdCompatibilityResponse::VersionsWithCompatiblePollers = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.GetWorkerBuildIdCompatibilityResponse.VersionsWithCompatiblePollers").msgclass UpdateWorkflowExecutionRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.UpdateWorkflowExecutionRequest").msgclass UpdateWorkflowExecutionResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.UpdateWorkflowExecutionResponse").msgclass StartBatchOperationRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.StartBatchOperationRequest").msgclass @@ -786,6 +829,8 @@ module V1 DescribeBatchOperationResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.DescribeBatchOperationResponse").msgclass ListBatchOperationsRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.ListBatchOperationsRequest").msgclass ListBatchOperationsResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.ListBatchOperationsResponse").msgclass + PollWorkflowExecutionUpdateRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.PollWorkflowExecutionUpdateRequest").msgclass + PollWorkflowExecutionUpdateResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.workflowservice.v1.PollWorkflowExecutionUpdateResponse").msgclass end end end diff --git a/lib/gen/temporal/api/workflowservice/v1/service_services_pb.rb b/lib/gen/temporal/api/workflowservice/v1/service_services_pb.rb index bb93223e..3acb9d26 100644 --- a/lib/gen/temporal/api/workflowservice/v1/service_services_pb.rb +++ b/lib/gen/temporal/api/workflowservice/v1/service_services_pb.rb @@ -288,20 +288,29 @@ class Service rpc :DeleteSchedule, ::Temporalio::Api::WorkflowService::V1::DeleteScheduleRequest, ::Temporalio::Api::WorkflowService::V1::DeleteScheduleResponse # List all schedules in a namespace. rpc :ListSchedules, ::Temporalio::Api::WorkflowService::V1::ListSchedulesRequest, ::Temporalio::Api::WorkflowService::V1::ListSchedulesResponse - # Allows users to specify a graph of worker build id based versions on a - # per task queue basis. Versions are ordered, and may be either compatible - # with some extant version, or a new incompatible version. + # Allows users to specify sets of worker build id versions on a per task queue basis. Versions + # are ordered, and may be either compatible with some extant version, or a new incompatible + # version, forming sets of ids which are incompatible with each other, but whose contained + # members are compatible with one another. + # # (-- api-linter: core::0134::response-message-name=disabled - # aip.dev/not-precedent: UpdateWorkerBuildIdOrdering RPC doesn't follow Google API format. --) + # aip.dev/not-precedent: UpdateWorkerBuildIdCompatibility RPC doesn't follow Google API format. --) # (-- api-linter: core::0134::method-signature=disabled - # aip.dev/not-precedent: UpdateWorkerBuildIdOrdering RPC doesn't follow Google API format. --) - rpc :UpdateWorkerBuildIdOrdering, ::Temporalio::Api::WorkflowService::V1::UpdateWorkerBuildIdOrderingRequest, ::Temporalio::Api::WorkflowService::V1::UpdateWorkerBuildIdOrderingResponse - # Fetches the worker build id versioning graph for some task queue. - rpc :GetWorkerBuildIdOrdering, ::Temporalio::Api::WorkflowService::V1::GetWorkerBuildIdOrderingRequest, ::Temporalio::Api::WorkflowService::V1::GetWorkerBuildIdOrderingResponse + # aip.dev/not-precedent: UpdateWorkerBuildIdCompatibility RPC doesn't follow Google API format. --) + rpc :UpdateWorkerBuildIdCompatibility, ::Temporalio::Api::WorkflowService::V1::UpdateWorkerBuildIdCompatibilityRequest, ::Temporalio::Api::WorkflowService::V1::UpdateWorkerBuildIdCompatibilityResponse + # Fetches the worker build id versioning sets for some task queue and related metadata. + rpc :GetWorkerBuildIdCompatibility, ::Temporalio::Api::WorkflowService::V1::GetWorkerBuildIdCompatibilityRequest, ::Temporalio::Api::WorkflowService::V1::GetWorkerBuildIdCompatibilityResponse # Invokes the specified update function on user workflow code. # (-- api-linter: core::0134=disabled # aip.dev/not-precedent: UpdateWorkflowExecution doesn't follow Google API format --) rpc :UpdateWorkflowExecution, ::Temporalio::Api::WorkflowService::V1::UpdateWorkflowExecutionRequest, ::Temporalio::Api::WorkflowService::V1::UpdateWorkflowExecutionResponse + # Polls a workflow execution for the outcome of a workflow execution update + # previously issued through the UpdateWorkflowExecution RPC. The effective + # timeout on this call will be shorter of the the caller-supplied gRPC + # timeout and the server's configured long-poll timeout. + # (-- api-linter: core::0134=disabled + # aip.dev/not-precedent: UpdateWorkflowExecution doesn't follow Google API format --) + rpc :PollWorkflowExecutionUpdate, ::Temporalio::Api::WorkflowService::V1::PollWorkflowExecutionUpdateRequest, ::Temporalio::Api::WorkflowService::V1::PollWorkflowExecutionUpdateResponse # StartBatchOperation starts a new batch operation rpc :StartBatchOperation, ::Temporalio::Api::WorkflowService::V1::StartBatchOperationRequest, ::Temporalio::Api::WorkflowService::V1::StartBatchOperationResponse # StopBatchOperation stops a batch operation diff --git a/lib/temporal/connection/converter/composite.rb b/lib/temporal/connection/converter/composite.rb index 2b0d2f11..640ae4f7 100644 --- a/lib/temporal/connection/converter/composite.rb +++ b/lib/temporal/connection/converter/composite.rb @@ -25,7 +25,7 @@ def from_payload(payload) converter = payload_converters_by_encoding[encoding] if converter.nil? - raise ConverterNotFound + raise ConverterNotFound, "Could not find PayloadConverter for #{encoding}" end converter.from_payload(payload) diff --git a/proto b/proto index 4c2f6a28..ae312b07 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 4c2f6a281fa3fde8b0a24447de3e0d0f47d230b4 +Subproject commit ae312b0724003957b96fb966e3fe25a02abaade4 diff --git a/spec/unit/lib/temporal/connection/converter/composite_spec.rb b/spec/unit/lib/temporal/connection/converter/composite_spec.rb index fd4bee56..b78a62c0 100644 --- a/spec/unit/lib/temporal/connection/converter/composite_spec.rb +++ b/spec/unit/lib/temporal/connection/converter/composite_spec.rb @@ -54,7 +54,11 @@ metadata: { 'encoding' => 'fake' } ) - expect { subject.from_payload(payload) }.to raise_error(Temporal::Connection::Converter::Composite::ConverterNotFound) + expect do + subject.from_payload(payload) + end.to raise_error(Temporal::Connection::Converter::Composite::ConverterNotFound) do |e| + expect(e.message).to eq('Could not find PayloadConverter for fake') + end end end end From 89b0fa593baac99784ab5f5864b4f92f503bdd05 Mon Sep 17 00:00:00 2001 From: DeRauk Gibble Date: Thu, 13 Apr 2023 15:29:12 -0400 Subject: [PATCH 084/125] Integrate CI with Github Actions (#235) Co-authored-by: DeRauk Gibble --- .circleci/config.yml | 79 ----------------- .github/workflows/tests.yml | 85 +++++++++++++++++++ Gemfile | 2 - examples/docker-compose.yml | 1 - .../child_workflow_terminated_workflow.rb | 3 + spec/config/coveralls.rb | 3 - spec/unit/lib/temporal/worker_spec.rb | 19 ++++- 7 files changed, 104 insertions(+), 88 deletions(-) delete mode 100644 .circleci/config.yml create mode 100644 .github/workflows/tests.yml delete mode 100644 spec/config/coveralls.rb diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 72570bbd..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,79 +0,0 @@ -version: 2.1 -orbs: - ruby: circleci/ruby@0.1.2 - -jobs: - test_gem: - docker: - - image: cimg/ruby:3.0.3 - executor: ruby/default - steps: - - checkout - - ruby/bundle-install - - run: - name: Run RSpec - command: bundle exec rspec - - test_examples: - docker: - - image: cimg/ruby:3.0.3 - - image: circleci/postgres:alpine - name: postgres - environment: - POSTGRES_PASSWORD: temporal - - image: temporalio/auto-setup:latest - name: temporal - environment: - - DB=postgresql - - DB_PORT=5432 - - POSTGRES_USER=postgres - - POSTGRES_PWD=temporal - - POSTGRES_SEEDS=postgres - - environment: - - TEMPORAL_HOST=temporal - - steps: - - checkout - - - run: - name: Bundle Install - command: cd examples && bundle install --path vendor/bundle - - - run: - name: Register Namespace - command: cd examples && bin/register_namespace ruby-samples - - - run: - name: Wait for Namespace to settle - command: sleep 15 - - - run: - name: Boot up worker - command: cd examples && bin/worker - background: true - - - run: - name: Boot up crypt worker - command: cd examples && bin/worker - background: true - environment: - USE_ENCRYPTION: 1 - - - run: - name: Boot up worker for v2 error serialization tests - command: cd examples && bin/worker - background: true - environment: - USE_ERROR_SERIALIZATION_V2: 1 - - - run: - name: Run RSpec - command: cd examples && bundle exec rspec - -workflows: - version: 2 - test: - jobs: - - test_gem - - test_examples diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..2456cb5d --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,85 @@ +name: Tests + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + test_gem: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + + steps: + - uses: actions/checkout@v3 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.0.3 + bundler-cache: true + + - name: Run tests + run: | + bundle exec rspec + + test_examples: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + + steps: + - uses: actions/checkout@v3 + + - name: Start dependencies + run: | + docker-compose \ + -f examples/docker-compose.yml \ + up -d + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.0.3 + + - name: Bundle install + run: | + cd examples && bundle install --path vendor/bundle + + - name: Wait for dependencies to settle + run: | + sleep 10 + + - name: Register namespace + run: | + cd examples && bin/register_namespace ruby-samples + + - name: Wait for namespace to settle + run: | + sleep 10 + + - name: Boot up worker + run: | + cd examples && bin/worker & + + - name: Boot up crypt worker + env: + USE_ENCRYPTION: 1 + run: | + cd examples && bin/worker & + + - name: Boot up worker for v2 error serialization tests + env: + USE_ERROR_SERIALIZATION_V2: 1 + run: | + cd examples && bin/worker & + + - name: Run RSpec + env: + USE_ERROR_SERIALIZATION_V2: 1 + run: | + cd examples && bundle exec rspec \ No newline at end of file diff --git a/Gemfile b/Gemfile index f960e788..fa75df15 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,3 @@ source 'https://rubygems.org' gemspec - -gem 'coveralls', require: false diff --git a/examples/docker-compose.yml b/examples/docker-compose.yml index 0f98b630..77a7eb3b 100644 --- a/examples/docker-compose.yml +++ b/examples/docker-compose.yml @@ -7,7 +7,6 @@ services: - "7233:7233" environment: - "CASSANDRA_SEEDS=cassandra" - - "DYNAMIC_CONFIG_FILE_PATH=config/dynamicconfig/development.yaml" depends_on: - cassandra diff --git a/examples/workflows/child_workflow_terminated_workflow.rb b/examples/workflows/child_workflow_terminated_workflow.rb index 56e2d1ba..a64516a7 100644 --- a/examples/workflows/child_workflow_terminated_workflow.rb +++ b/examples/workflows/child_workflow_terminated_workflow.rb @@ -12,6 +12,9 @@ def execute child_workflow_execution.run_id ) + # Give time for termination to propagate + workflow.sleep(1) + # check that the result is now 'failed' { child_workflow_terminated: result.failed?, # terminated is represented as failed? with the Terminated Error diff --git a/spec/config/coveralls.rb b/spec/config/coveralls.rb deleted file mode 100644 index fe39ae25..00000000 --- a/spec/config/coveralls.rb +++ /dev/null @@ -1,3 +0,0 @@ -require 'coveralls' - -Coveralls.wear! diff --git a/spec/unit/lib/temporal/worker_spec.rb b/spec/unit/lib/temporal/worker_spec.rb index 71b6bcfa..ca5cf7d0 100644 --- a/spec/unit/lib/temporal/worker_spec.rb +++ b/spec/unit/lib/temporal/worker_spec.rb @@ -290,6 +290,11 @@ class OtherTestWorkerActivity < Temporal::Activity ) .and_return(activity_poller) + workflow_poller = instance_double(Temporal::Workflow::Poller, start: nil) + expect(Temporal::Workflow::Poller) + .to receive(:new) + .and_return(workflow_poller) + worker = Temporal::Worker.new(activity_thread_pool_size: 10) allow(worker).to receive(:shutting_down?).and_return(true) worker.register_workflow(TestWorkerWorkflow) @@ -301,6 +306,11 @@ class OtherTestWorkerActivity < Temporal::Activity end it 'can have a worklow poller with a binary checksum' do + activity_poller = instance_double(Temporal::Activity::Poller, start: nil) + expect(Temporal::Activity::Poller) + .to receive(:new) + .and_return(activity_poller) + workflow_poller = instance_double(Temporal::Workflow::Poller, start: nil) binary_checksum = 'abc123' expect(Temporal::Workflow::Poller) @@ -403,7 +413,10 @@ class OtherTestWorkerActivity < Temporal::Activity describe 'signal handling' do before do - @thread = Thread.new { subject.start } + @thread = Thread.new do + @worker_pid = Process.pid + subject.start + end sleep THREAD_SYNC_DELAY # give worker time to start end @@ -420,14 +433,14 @@ class OtherTestWorkerActivity < Temporal::Activity end it 'traps TERM signal' do - Process.kill('TERM', 0) + Process.kill('TERM', @worker_pid) sleep THREAD_SYNC_DELAY expect(@thread).not_to be_alive end it 'traps INT signal' do - Process.kill('INT', 0) + Process.kill('INT', @worker_pid) sleep THREAD_SYNC_DELAY expect(@thread).not_to be_alive From 9b4cc1634b4d7354598c016a573712285526ec00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelizaveta=20Leme=C5=A1eva?= Date: Mon, 17 Apr 2023 15:55:18 +0300 Subject: [PATCH 085/125] Add sleep after bad poll (#216) * Add sleep to pollers after bad poll * Add tests for sleep before retry polling * Add documentation for worker options * Remove redundant sleep_before_retry method, update tests * Fix tests after workflow poller constructor changes --- README.md | 12 ++++ lib/temporal/activity/poller.rb | 9 ++- lib/temporal/worker.rb | 8 ++- lib/temporal/workflow/poller.rb | 9 ++- .../unit/lib/temporal/activity/poller_spec.rb | 40 +++++++++++ spec/unit/lib/temporal/worker_spec.rb | 72 ++++++++++++++++--- .../unit/lib/temporal/workflow/poller_spec.rb | 42 +++++++++++ 7 files changed, 180 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index b2818447..04a1533b 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,18 @@ worker.register_activity(HelloActivity) worker.start # runs forever ``` +You can add several options when initializing worker (here defaults are provided as values): + +```ruby +Temporal::Worker.new( + activity_thread_pool_size: 20, # how many threads poll for activities + workflow_thread_pool_size: 10, # how many threads poll for workflows + binary_checksum: nil, # identifies the version of workflow worker code + activity_poll_retry_seconds: 0, # how many seconds to wait after unsuccessful poll for activities + workflow_poll_retry_seconds: 0 # how many seconds to wait after unsuccessful poll for workflows +) +``` + And finally start your workflow in another terminal shell: ```ruby diff --git a/lib/temporal/activity/poller.rb b/lib/temporal/activity/poller.rb index 767d11e2..329d2a8c 100644 --- a/lib/temporal/activity/poller.rb +++ b/lib/temporal/activity/poller.rb @@ -9,7 +9,8 @@ module Temporal class Activity class Poller DEFAULT_OPTIONS = { - thread_pool_size: 20 + thread_pool_size: 20, + poll_retry_seconds: 0 }.freeze def initialize(namespace, task_queue, activity_lookup, config, middleware = [], options = {}) @@ -90,6 +91,8 @@ def poll_for_task Temporal::ErrorHandler.handle(error, config) + sleep(poll_retry_seconds) + nil end @@ -109,6 +112,10 @@ def thread_pool } ) end + + def poll_retry_seconds + @options[:poll_retry_seconds] + end end end end diff --git a/lib/temporal/worker.rb b/lib/temporal/worker.rb index 18e3e3c7..70cfa398 100644 --- a/lib/temporal/worker.rb +++ b/lib/temporal/worker.rb @@ -24,7 +24,9 @@ def initialize( config = Temporal.configuration, activity_thread_pool_size: Temporal::Activity::Poller::DEFAULT_OPTIONS[:thread_pool_size], workflow_thread_pool_size: Temporal::Workflow::Poller::DEFAULT_OPTIONS[:thread_pool_size], - binary_checksum: Temporal::Workflow::Poller::DEFAULT_OPTIONS[:binary_checksum] + binary_checksum: Temporal::Workflow::Poller::DEFAULT_OPTIONS[:binary_checksum], + activity_poll_retry_seconds: Temporal::Activity::Poller::DEFAULT_OPTIONS[:poll_retry_seconds], + workflow_poll_retry_seconds: Temporal::Workflow::Poller::DEFAULT_OPTIONS[:poll_retry_seconds] ) @config = config @workflows = Hash.new { |hash, key| hash[key] = ExecutableLookup.new } @@ -36,10 +38,12 @@ def initialize( @shutting_down = false @activity_poller_options = { thread_pool_size: activity_thread_pool_size, + poll_retry_seconds: activity_poll_retry_seconds } @workflow_poller_options = { thread_pool_size: workflow_thread_pool_size, - binary_checksum: binary_checksum + binary_checksum: binary_checksum, + poll_retry_seconds: workflow_poll_retry_seconds } end diff --git a/lib/temporal/workflow/poller.rb b/lib/temporal/workflow/poller.rb index 195f3a96..c268f2b2 100644 --- a/lib/temporal/workflow/poller.rb +++ b/lib/temporal/workflow/poller.rb @@ -11,7 +11,8 @@ class Workflow class Poller DEFAULT_OPTIONS = { thread_pool_size: 10, - binary_checksum: nil + binary_checksum: nil, + poll_retry_seconds: 0 }.freeze def initialize(namespace, task_queue, workflow_lookup, config, middleware = [], workflow_middleware = [], options = {}) @@ -92,6 +93,8 @@ def poll_for_task Temporal.logger.error("Unable to poll Workflow task queue", { namespace: namespace, task_queue: task_queue, error: error.inspect }) Temporal::ErrorHandler.handle(error, config) + sleep(poll_retry_seconds) + nil end @@ -116,6 +119,10 @@ def thread_pool def binary_checksum @options[:binary_checksum] end + + def poll_retry_seconds + @options[:poll_retry_seconds] + end end end end diff --git a/spec/unit/lib/temporal/activity/poller_spec.rb b/spec/unit/lib/temporal/activity/poller_spec.rb index d066ae24..6c93adaf 100644 --- a/spec/unit/lib/temporal/activity/poller_spec.rb +++ b/spec/unit/lib/temporal/activity/poller_spec.rb @@ -159,6 +159,7 @@ def call(_); end before do allow(subject).to receive(:shutting_down?).and_return(false, true) allow(connection).to receive(:poll_activity_task_queue).and_raise(StandardError) + allow(subject).to receive(:sleep).and_return(nil) end it 'logs' do @@ -173,6 +174,45 @@ def call(_); end .to have_received(:error) .with('Unable to poll activity task queue', { namespace: 'test-namespace', task_queue: 'test-task-queue', error: '#' }) end + + it 'does not sleep' do + subject.start + + # stop poller before inspecting + subject.stop_polling; subject.wait + + expect(subject).to have_received(:sleep).with(0).once + end + end + end + + context 'when connection is unable to poll and poll_retry_seconds is set' do + subject do + described_class.new( + namespace, + task_queue, + lookup, + config, + middleware, + { + poll_retry_seconds: 5 + } + ) + end + + before do + allow(subject).to receive(:shutting_down?).and_return(false, true) + allow(connection).to receive(:poll_activity_task_queue).and_raise(StandardError) + allow(subject).to receive(:sleep).and_return(nil) + end + + it 'sleeps' do + subject.start + + # stop poller before inspecting + subject.stop_polling; subject.wait + + expect(subject).to have_received(:sleep).with(5).once end end diff --git a/spec/unit/lib/temporal/worker_spec.rb b/spec/unit/lib/temporal/worker_spec.rb index ca5cf7d0..bcd78084 100644 --- a/spec/unit/lib/temporal/worker_spec.rb +++ b/spec/unit/lib/temporal/worker_spec.rb @@ -221,7 +221,8 @@ class OtherTestWorkerActivity < Temporal::Activity [], [], thread_pool_size: 10, - binary_checksum: nil + binary_checksum: nil, + poll_retry_seconds: 0 ) .and_return(workflow_poller_1) @@ -235,7 +236,8 @@ class OtherTestWorkerActivity < Temporal::Activity [], [], thread_pool_size: 10, - binary_checksum: nil + binary_checksum: nil, + poll_retry_seconds: 0 ) .and_return(workflow_poller_2) @@ -247,7 +249,8 @@ class OtherTestWorkerActivity < Temporal::Activity an_instance_of(Temporal::ExecutableLookup), config, [], - thread_pool_size: 20 + thread_pool_size: 20, + poll_retry_seconds: 0 ) .and_return(activity_poller_1) @@ -259,7 +262,8 @@ class OtherTestWorkerActivity < Temporal::Activity an_instance_of(Temporal::ExecutableLookup), config, [], - thread_pool_size: 20 + thread_pool_size: 20, + poll_retry_seconds: 0 ) .and_return(activity_poller_2) @@ -286,7 +290,7 @@ class OtherTestWorkerActivity < Temporal::Activity an_instance_of(Temporal::ExecutableLookup), an_instance_of(Temporal::Configuration), [], - {thread_pool_size: 10} + {thread_pool_size: 10, poll_retry_seconds: 0} ) .and_return(activity_poller) @@ -323,7 +327,8 @@ class OtherTestWorkerActivity < Temporal::Activity [], [], thread_pool_size: 10, - binary_checksum: binary_checksum + binary_checksum: binary_checksum, + poll_retry_seconds: 0 ) .and_return(workflow_poller) @@ -337,6 +342,55 @@ class OtherTestWorkerActivity < Temporal::Activity expect(workflow_poller).to have_received(:start) end + it 'can have an activity poller that sleeps after unsuccessful poll' do + activity_poller = instance_double(Temporal::Activity::Poller, start: nil) + expect(Temporal::Activity::Poller) + .to receive(:new) + .with( + 'default-namespace', + 'default-task-queue', + an_instance_of(Temporal::ExecutableLookup), + an_instance_of(Temporal::Configuration), + [], + {thread_pool_size: 20, poll_retry_seconds: 10} + ) + .and_return(activity_poller) + + worker = Temporal::Worker.new(activity_poll_retry_seconds: 10) + allow(worker).to receive(:shutting_down?).and_return(true) + worker.register_workflow(TestWorkerWorkflow) + worker.register_activity(TestWorkerActivity) + + worker.start + + expect(activity_poller).to have_received(:start) + end + + it 'can have a workflow poller sleeping after unsuccessful poll' do + workflow_poller = instance_double(Temporal::Workflow::Poller, start: nil) + expect(Temporal::Workflow::Poller) + .to receive(:new) + .with( + 'default-namespace', + 'default-task-queue', + an_instance_of(Temporal::ExecutableLookup), + an_instance_of(Temporal::Configuration), + [], + [], + {binary_checksum: nil, poll_retry_seconds: 10, thread_pool_size: 10} + ) + .and_return(workflow_poller) + + worker = Temporal::Worker.new(workflow_poll_retry_seconds: 10) + allow(worker).to receive(:shutting_down?).and_return(true) + worker.register_workflow(TestWorkerWorkflow) + worker.register_activity(TestWorkerActivity) + + worker.start + + expect(workflow_poller).to have_received(:start) + end + context 'when middleware is configured' do let(:entry_1) { instance_double(Temporal::Middleware::Entry) } let(:entry_2) { instance_double(Temporal::Middleware::Entry) } @@ -376,7 +430,8 @@ class OtherTestWorkerActivity < Temporal::Activity [entry_1], [entry_3], thread_pool_size: 10, - binary_checksum: nil + binary_checksum: nil, + poll_retry_seconds: 0 ) .and_return(workflow_poller_1) @@ -388,7 +443,8 @@ class OtherTestWorkerActivity < Temporal::Activity an_instance_of(Temporal::ExecutableLookup), config, [entry_2], - thread_pool_size: 20 + thread_pool_size: 20, + poll_retry_seconds: 0 ) .and_return(activity_poller_1) diff --git a/spec/unit/lib/temporal/workflow/poller_spec.rb b/spec/unit/lib/temporal/workflow/poller_spec.rb index 6481e418..8c66a013 100644 --- a/spec/unit/lib/temporal/workflow/poller_spec.rb +++ b/spec/unit/lib/temporal/workflow/poller_spec.rb @@ -168,6 +168,7 @@ def call(_); end before do allow(subject).to receive(:shutting_down?).and_return(false, true) allow(connection).to receive(:poll_workflow_task_queue).and_raise(StandardError) + allow(subject).to receive(:sleep).and_return(nil) end it 'logs' do @@ -187,6 +188,47 @@ def call(_); end error: '#' ) end + + it 'does not sleep' do + subject.start + + # stop poller before inspecting + subject.stop_polling; subject.wait + + expect(subject).to have_received(:sleep).with(0).once + end + end + + context 'when connection is unable to poll and poll_retry_seconds is set' do + subject do + described_class.new( + namespace, + task_queue, + lookup, + config, + middleware, + workflow_middleware, + { + binary_checksum: binary_checksum, + poll_retry_seconds: 5 + } + ) + end + + before do + allow(subject).to receive(:shutting_down?).and_return(false, true) + allow(connection).to receive(:poll_workflow_task_queue).and_raise(StandardError) + allow(subject).to receive(:sleep).and_return(nil) + end + + it 'sleeps' do + subject.start + + # stop poller before inspecting + subject.stop_polling; subject.wait + + expect(subject).to have_received(:sleep).with(5).once + end end end end From 6256f56c315a865f05f9fcf9ba6fb3c2d448131e Mon Sep 17 00:00:00 2001 From: Jeff Schoner Date: Mon, 17 Apr 2023 10:46:52 -0700 Subject: [PATCH 086/125] Terminate-if-running workflow ID reuse policy (#220) * Add terminate-if-running workflow id reuse policy mappings * Integration test for terminate-if-running --- .../spec/integration/start_workflow_spec.rb | 25 +++++++++++++++++++ .../serializer/workflow_id_reuse_policy.rb | 3 ++- .../workflow_id_reuse_policy_spec.rb | 4 ++- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/examples/spec/integration/start_workflow_spec.rb b/examples/spec/integration/start_workflow_spec.rb index 46fb34a5..99d0d7c4 100644 --- a/examples/spec/integration/start_workflow_spec.rb +++ b/examples/spec/integration/start_workflow_spec.rb @@ -1,4 +1,5 @@ require 'workflows/hello_world_workflow' +require 'workflows/long_workflow' describe 'Temporal.start_workflow' do let(:workflow_id) { SecureRandom.uuid } @@ -68,4 +69,28 @@ }) end.to raise_error(Temporal::WorkflowExecutionAlreadyStartedFailure) end + + it 'terminates duplicate workflow ids based on workflow_id_reuse_policy' do + run_id_1 = Temporal.start_workflow(LongWorkflow, options: { + workflow_id: workflow_id, + workflow_id_reuse_policy: :terminate_if_running + }) + + run_id_2 = Temporal.start_workflow(LongWorkflow, options: { + workflow_id: workflow_id, + workflow_id_reuse_policy: :terminate_if_running + }) + + execution_1 = Temporal.fetch_workflow_execution_info( + Temporal.configuration.namespace, + workflow_id, + run_id_1) + execution_2 = Temporal.fetch_workflow_execution_info( + Temporal.configuration.namespace, + workflow_id, + run_id_2) + + expect(execution_1.status).to eq(Temporal::Workflow::Status::TERMINATED) + expect(execution_2.status).to eq(Temporal::Workflow::Status::RUNNING) + end end diff --git a/lib/temporal/connection/serializer/workflow_id_reuse_policy.rb b/lib/temporal/connection/serializer/workflow_id_reuse_policy.rb index 22ac6c51..0c6c71bf 100644 --- a/lib/temporal/connection/serializer/workflow_id_reuse_policy.rb +++ b/lib/temporal/connection/serializer/workflow_id_reuse_policy.rb @@ -8,7 +8,8 @@ class WorkflowIdReusePolicy < Base WORKFLOW_ID_REUSE_POLICY = { allow_failed: Temporalio::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY, allow: Temporalio::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, - reject: Temporalio::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE + reject: Temporalio::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE, + terminate_if_running: Temporalio::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING }.freeze def to_proto diff --git a/spec/unit/lib/temporal/connection/serializer/workflow_id_reuse_policy_spec.rb b/spec/unit/lib/temporal/connection/serializer/workflow_id_reuse_policy_spec.rb index 950b8a04..ce139325 100644 --- a/spec/unit/lib/temporal/connection/serializer/workflow_id_reuse_policy_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/workflow_id_reuse_policy_spec.rb @@ -6,7 +6,8 @@ SYM_TO_PROTO = { allow_failed: Temporalio::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY, allow: Temporalio::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, - reject: Temporalio::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE + reject: Temporalio::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE, + terminate_if_running: Temporalio::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING }.freeze def self.test_valid_policy(policy_sym) @@ -20,6 +21,7 @@ def self.test_valid_policy(policy_sym) test_valid_policy(:allow) test_valid_policy(:allow_failed) test_valid_policy(:reject) + test_valid_policy(:terminate_if_running) it "rejects invalid policies" do expect do From c2b1ec228b520725b1bf0d04a1433e75961cf3ce Mon Sep 17 00:00:00 2001 From: James Watkins-Harvey Date: Wed, 19 Apr 2023 16:57:44 -0400 Subject: [PATCH 087/125] Add client-name and client-version headers (#223) --- lib/temporal/connection/grpc.rb | 7 +++++-- .../interceptors/client_name_version_interceptor.rb | 13 +++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 lib/temporal/connection/interceptors/client_name_version_interceptor.rb diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index e7b7e95e..78e3f817 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -8,6 +8,7 @@ require 'gen/temporal/api/enums/v1/workflow_pb' require 'gen/temporal/api/enums/v1/common_pb' require 'temporal/connection/errors' +require 'temporal/connection/interceptors/client_name_version_interceptor' require 'temporal/connection/serializer' require 'temporal/connection/serializer/failure' require 'temporal/connection/serializer/workflow_id_reuse_policy' @@ -611,7 +612,8 @@ def client @client ||= Temporalio::Api::WorkflowService::V1::WorkflowService::Stub.new( url, credentials, - timeout: CONNECTION_TIMEOUT_SECONDS + timeout: CONNECTION_TIMEOUT_SECONDS, + interceptors: [ ClientNameVersionInterceptor.new() ] ) end @@ -619,7 +621,8 @@ def operator_client @operator_client ||= Temporalio::Api::OperatorService::V1::OperatorService::Stub.new( url, credentials, - timeout: CONNECTION_TIMEOUT_SECONDS + timeout: CONNECTION_TIMEOUT_SECONDS, + interceptors: [ ClientNameVersionInterceptor.new() ] ) end diff --git a/lib/temporal/connection/interceptors/client_name_version_interceptor.rb b/lib/temporal/connection/interceptors/client_name_version_interceptor.rb new file mode 100644 index 00000000..b81a7576 --- /dev/null +++ b/lib/temporal/connection/interceptors/client_name_version_interceptor.rb @@ -0,0 +1,13 @@ +require 'grpc' + +module Temporal + module Connection + class ClientNameVersionInterceptor < GRPC::ClientInterceptor + def request_response(request: nil, call: nil, method: nil, metadata: nil) + metadata['client-name'] = 'community-ruby' + metadata['client-version'] = Temporal::VERSION + yield + end + end + end +end \ No newline at end of file From 59ed5911850e6bcdda23af44d6908e5178568696 Mon Sep 17 00:00:00 2001 From: Jeff Schoner Date: Thu, 20 Apr 2023 12:36:08 -0700 Subject: [PATCH 088/125] Temporal CLI and SQL-based enhanced visibility compatibility (#236) * Fix tests that break on Windows * Make register namespace script more robust * Make search attributes APIs work with Temporalite --- examples/README.md | 6 +++ examples/bin/register_namespace | 29 ++++++++++---- .../integration/search_attributes_spec.rb | 38 +++++++++--------- lib/temporal/client.rb | 15 ++++--- lib/temporal/connection/grpc.rb | 16 +++++--- spec/unit/lib/temporal/client_spec.rb | 6 +-- spec/unit/lib/temporal/grpc_spec.rb | 40 +++++++++---------- .../lib/temporal/workflow/executor_spec.rb | 28 ++++++------- 8 files changed, 102 insertions(+), 76 deletions(-) diff --git a/examples/README.md b/examples/README.md index df644ef1..d7fd468e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -34,6 +34,12 @@ To run tests, make sure the temporal server is running: docker-compose up ``` +Run the register_namespace script to ensure the ruby-samples namespace and necessary +search attributes have been created: +```shell +bin/register_namespace +``` + Follow the instructions above to start the three worker proceses. To execute the tests, run: diff --git a/examples/bin/register_namespace b/examples/bin/register_namespace index 672d73c0..e241e95d 100755 --- a/examples/bin/register_namespace +++ b/examples/bin/register_namespace @@ -1,11 +1,9 @@ #!/usr/bin/env ruby require_relative '../init' -namespace = ARGV[0] +namespace = ARGV[0] || 'ruby-samples' description = ARGV[1] -raise 'Missing namespace name, please run register_namespace ' unless namespace - begin Temporal.register_namespace(namespace, description) Temporal.logger.info 'Namespace created', { namespace: namespace } @@ -13,6 +11,18 @@ rescue Temporal::NamespaceAlreadyExistsFailure Temporal.logger.info 'Namespace already exists', { namespace: namespace } end +loop do + begin + Temporal.list_custom_search_attributes(namespace: namespace) + Temporal.logger.info("Namespace is ready", { namespace: namespace }) + break + rescue GRPC::NotFound + Temporal.logger.info("Namespace not yet found, waiting and retrying", { namespace: namespace }) + sleep 1 + next + end +end + # Register a variety of search attributes for ease of integration testing attributes_to_add = { 'CustomStringField' => :text, @@ -21,9 +31,12 @@ attributes_to_add = { 'CustomIntField' => :int, 'CustomDatetimeField' => :datetime } -begin - Temporal.add_custom_search_attributes(attributes_to_add) - Temporal.logger.info('Registered search attributes', { namespace: namespace, attributes: attributes_to_add }) -rescue Temporal::SearchAttributeAlreadyExistsFailure - Temporal.logger.info('Default search attributes already exist for namespace', { namespace: namespace }) + +attributes_to_add.each do |name, type| + begin + Temporal.add_custom_search_attributes({name: type}) + Temporal.logger.info("Registered search attributes #{name} = #{type}", { namespace: namespace }) + rescue Temporal::SearchAttributeAlreadyExistsFailure + Temporal.logger.info("Default search attribute #{name} already exist for namespace", { namespace: namespace }) + end end diff --git a/examples/spec/integration/search_attributes_spec.rb b/examples/spec/integration/search_attributes_spec.rb index 8db7848e..b9f67da1 100644 --- a/examples/spec/integration/search_attributes_spec.rb +++ b/examples/spec/integration/search_attributes_spec.rb @@ -6,8 +6,9 @@ def cleanup custom_attributes = Temporal.list_custom_search_attributes - Temporal.remove_custom_search_attributes(attribute_1) if custom_attributes.include?(attribute_1) - Temporal.remove_custom_search_attributes(attribute_2) if custom_attributes.include?(attribute_2) + custom_attributes.keys.intersection([attribute_1, attribute_2]).each do |attribute| + Temporal.remove_custom_search_attributes(attribute) + end end before do @@ -18,13 +19,20 @@ def cleanup cleanup end + # Depending on the visibility storage backend of the server, recreating a search attribute + # is either ignored so long as the tpe is the same (Elastic Search) or it raises + # an error (SQL). This function ensures consistent state upon exit. + def safe_add(attributes) + begin + Temporal.add_custom_search_attributes(attributes) + rescue => e + # This won't always throw but when it does it needs to be of this type + expect(e).to be_instance_of(Temporal::SearchAttributeAlreadyExistsFailure) + end + end + it 'add' do - Temporal.add_custom_search_attributes( - { - attribute_1 => :int, - attribute_2 => :keyword - } - ) + safe_add({ attribute_1 => :int, attribute_2 => :keyword }) custom_attributes = Temporal.list_custom_search_attributes expect(custom_attributes).to include(attribute_1 => :int) @@ -32,12 +40,9 @@ def cleanup end it 'add duplicate fails' do - Temporal.add_custom_search_attributes( - { - attribute_1 => :int - } - ) + safe_add({ attribute_1 => :int }) + # This, however, will always throw expect do Temporal.add_custom_search_attributes( { @@ -48,12 +53,7 @@ def cleanup end it 'remove' do - Temporal.add_custom_search_attributes( - { - attribute_1 => :int, - attribute_2 => :keyword - } - ) + safe_add({ attribute_1 => :int, attribute_2 => :keyword }) Temporal.remove_custom_search_attributes(attribute_1, attribute_2) diff --git a/lib/temporal/client.rb b/lib/temporal/client.rb index 975fd96d..64b7c14b 100644 --- a/lib/temporal/client.rb +++ b/lib/temporal/client.rb @@ -410,18 +410,21 @@ def query_workflow_executions(namespace, query, next_page_token: nil, max_page_s end # @param attributes [Hash[String, Symbol]] name to symbol for type, see INDEXED_VALUE_TYPE above - def add_custom_search_attributes(attributes) - connection.add_custom_search_attributes(attributes) + # @param namespace String, required for SQL enhanced visibility, ignored for elastic search + def add_custom_search_attributes(attributes, namespace: nil) + connection.add_custom_search_attributes(attributes, namespace || config.default_execution_options.namespace) end + # @param namespace String, required for SQL enhanced visibility, ignored for elastic search # @return Hash[String, Symbol] name to symbol for type, see INDEXED_VALUE_TYPE above - def list_custom_search_attributes - connection.list_custom_search_attributes + def list_custom_search_attributes(namespace: nil) + connection.list_custom_search_attributes(namespace || config.default_execution_options.namespace) end # @param attribute_names [Array[String]] Attributes to remove - def remove_custom_search_attributes(*attribute_names) - connection.remove_custom_search_attributes(attribute_names) + # @param namespace String, required for SQL enhanced visibility, ignored for elastic search + def remove_custom_search_attributes(*attribute_names, namespace: nil) + connection.remove_custom_search_attributes(attribute_names, namespace || config.default_execution_options.namespace) end def connection diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index 78e3f817..169ba390 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -494,7 +494,7 @@ def count_workflow_executions(namespace:, query:) client.count_workflow_executions(request) end - def add_custom_search_attributes(attributes) + def add_custom_search_attributes(attributes, namespace) attributes.each_value do |symbol_type| next if SYMBOL_TO_INDEXED_VALUE_TYPE.include?(symbol_type) @@ -504,7 +504,8 @@ def add_custom_search_attributes(attributes) end request = Temporalio::Api::OperatorService::V1::AddSearchAttributesRequest.new( - search_attributes: attributes.map { |name, type| [name, SYMBOL_TO_INDEXED_VALUE_TYPE[type]] }.to_h + search_attributes: attributes.map { |name, type| [name, SYMBOL_TO_INDEXED_VALUE_TYPE[type]] }.to_h, + namespace: namespace ) begin operator_client.add_search_attributes(request) @@ -518,15 +519,18 @@ def add_custom_search_attributes(attributes) end end - def list_custom_search_attributes - request = Temporalio::Api::OperatorService::V1::ListSearchAttributesRequest.new + def list_custom_search_attributes(namespace) + request = Temporalio::Api::OperatorService::V1::ListSearchAttributesRequest.new( + namespace: namespace + ) response = operator_client.list_search_attributes(request) response.custom_attributes.map { |name, type| [name, INDEXED_VALUE_TYPE_TO_SYMBOL[type]] }.to_h end - def remove_custom_search_attributes(attribute_names) + def remove_custom_search_attributes(attribute_names, namespace) request = Temporalio::Api::OperatorService::V1::RemoveSearchAttributesRequest.new( - search_attributes: attribute_names + search_attributes: attribute_names, + namespace: namespace ) begin operator_client.remove_search_attributes(request) diff --git a/spec/unit/lib/temporal/client_spec.rb b/spec/unit/lib/temporal/client_spec.rb index 97d8ddf8..66ec3bf4 100644 --- a/spec/unit/lib/temporal/client_spec.rb +++ b/spec/unit/lib/temporal/client_spec.rb @@ -8,7 +8,7 @@ describe Temporal::Client do subject { described_class.new(config) } - let(:config) { Temporal::Configuration.new } + let(:config) { Temporal::Configuration.new.tap { |c| c.namespace = namespace } } let(:connection) { instance_double(Temporal::Connection::GRPC) } let(:namespace) { 'default-test-namespace' } let(:workflow_id) { SecureRandom.uuid } @@ -826,7 +826,7 @@ class NamespacedWorkflow < Temporal::Workflow expect(connection) .to have_received(:add_custom_search_attributes) - .with(attributes) + .with(attributes, namespace) end end @@ -853,7 +853,7 @@ class NamespacedWorkflow < Temporal::Workflow expect(connection) .to have_received(:remove_custom_search_attributes) - .with(%i[SomeTextField SomeIntField]) + .with(%i[SomeTextField SomeIntField], namespace) end end diff --git a/spec/unit/lib/temporal/grpc_spec.rb b/spec/unit/lib/temporal/grpc_spec.rb index 09552231..ce11e251 100644 --- a/spec/unit/lib/temporal/grpc_spec.rb +++ b/spec/unit/lib/temporal/grpc_spec.rb @@ -246,10 +246,7 @@ class TestDeserializer timeout: timeout ) - expect(grpc_stub).to have_received(:get_workflow_execution_history) do |request, deadline:| - expect(request.wait_new_event).to eq(true) - expect(deadline).to eq(now + timeout) - end + expect(grpc_stub).to have_received(:get_workflow_execution_history).with(anything, deadline: now + timeout) end it 'demands a timeout to be specified' do @@ -658,7 +655,8 @@ class TestDeserializer 'SomeBoolField' => :bool, 'SomeDatetimeField' => :datetime, 'SomeKeywordListField' => :keyword_list - } + }, + namespace ) expect(grpc_operator_stub).to have_received(:add_search_attributes) do |request| @@ -674,6 +672,7 @@ class TestDeserializer 'SomeKeywordListField' => Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_KEYWORD_LIST } ) + expect(request.namespace).to eq(namespace) end end @@ -683,7 +682,8 @@ class TestDeserializer subject.add_custom_search_attributes( { 'SomeTextField' => :text - } + }, + namespace ) end.to raise_error(Temporal::SearchAttributeAlreadyExistsFailure) end @@ -694,7 +694,8 @@ class TestDeserializer subject.add_custom_search_attributes( { 'SomeTextField' => :text - } + }, + namespace ) end.to raise_error(Temporal::SearchAttributeFailure) end @@ -704,7 +705,8 @@ class TestDeserializer subject.add_custom_search_attributes( { SomeTextField: :text - } + }, + namespace ) expect(grpc_operator_stub).to have_received(:add_search_attributes) do |request| @@ -714,6 +716,7 @@ class TestDeserializer 'SomeTextField' => Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_TEXT } ) + expect(request.namespace).to eq(namespace) end end @@ -722,7 +725,8 @@ class TestDeserializer subject.add_custom_search_attributes( { 'SomeBadField' => :foo - } + }, + namespace ) end.to raise_error(Temporal::InvalidSearchAttributeTypeFailure) do |e| expect(e.to_s).to eq('Cannot add search attributes ({"SomeBadField"=>:foo}): unknown search attribute type :foo, supported types: [:text, :keyword, :int, :double, :bool, :datetime, :keyword_list]') @@ -746,7 +750,7 @@ class TestDeserializer ) ) - response = subject.list_custom_search_attributes + response = subject.list_custom_search_attributes(namespace) expect(response).to eq( { @@ -762,6 +766,7 @@ class TestDeserializer expect(grpc_operator_stub).to have_received(:list_search_attributes) do |request| expect(request).to be_an_instance_of(Temporalio::Api::OperatorService::V1::ListSearchAttributesRequest) + expect(request.namespace).to eq(namespace) end end @@ -775,7 +780,7 @@ class TestDeserializer ) ) - response = subject.list_custom_search_attributes + response = subject.list_custom_search_attributes(namespace) expect(response).to eq( { @@ -792,13 +797,12 @@ class TestDeserializer attributes = ['SomeTextField', 'SomeIntField'] - subject.remove_custom_search_attributes( - attributes - ) + subject.remove_custom_search_attributes(attributes, namespace) expect(grpc_operator_stub).to have_received(:remove_search_attributes) do |request| expect(request).to be_an_instance_of(Temporalio::Api::OperatorService::V1::RemoveSearchAttributesRequest) expect(request.search_attributes).to eq(attributes) + expect(request.namespace).to eq(namespace) end end @@ -808,18 +812,14 @@ class TestDeserializer attributes = ['SomeTextField', 'SomeIntField'] expect do - subject.remove_custom_search_attributes( - attributes - ) + subject.remove_custom_search_attributes(attributes, namespace) end.to raise_error(Temporal::NotFoundFailure) end it 'attribute names can be symbols' do allow(grpc_operator_stub).to receive(:remove_search_attributes) - subject.remove_custom_search_attributes( - %i[SomeTextField SomeIntField] - ) + subject.remove_custom_search_attributes(%i[SomeTextField SomeIntField], namespace) expect(grpc_operator_stub).to have_received(:remove_search_attributes) do |request| expect(request).to be_an_instance_of(Temporalio::Api::OperatorService::V1::RemoveSearchAttributesRequest) diff --git a/spec/unit/lib/temporal/workflow/executor_spec.rb b/spec/unit/lib/temporal/workflow/executor_spec.rb index 0730f70e..47f10b86 100644 --- a/spec/unit/lib/temporal/workflow/executor_spec.rb +++ b/spec/unit/lib/temporal/workflow/executor_spec.rb @@ -55,7 +55,7 @@ def execute end it 'generates workflow metadata' do - allow(Temporal::Metadata::Workflow).to receive(:new).and_call_original + allow(Temporal::Metadata::Workflow).to receive(:new) payload = Temporalio::Api::Common::V1::Payload.new( metadata: { 'encoding' => 'json/plain' }, data: '"bar"'.b @@ -70,19 +70,19 @@ def execute event_attributes = workflow_started_event.workflow_execution_started_event_attributes expect(Temporal::Metadata::Workflow) .to have_received(:new) - .with( - namespace: workflow_metadata.namespace, - id: workflow_metadata.workflow_id, - name: event_attributes.workflow_type.name, - run_id: event_attributes.original_execution_run_id, - parent_id: nil, - parent_run_id: nil, - attempt: event_attributes.attempt, - task_queue: event_attributes.task_queue.name, - run_started_at: workflow_started_event.event_time.to_time, - memo: {}, - headers: {'Foo' => 'bar'} - ) + .with( + namespace: workflow_metadata.namespace, + id: workflow_metadata.workflow_id, + name: event_attributes.workflow_type.name, + run_id: event_attributes.original_execution_run_id, + parent_id: nil, + parent_run_id: nil, + attempt: event_attributes.attempt, + task_queue: event_attributes.task_queue.name, + headers: {'Foo' => 'bar'}, + run_started_at: workflow_started_event.event_time.to_time, + memo: {}, + ) end end From 146cfedd754b35e5ac8951889ebc65963ff27ead Mon Sep 17 00:00:00 2001 From: Jeff Schoner Date: Fri, 5 May 2023 09:42:05 -0700 Subject: [PATCH 089/125] Require version (#241) --- .../connection/interceptors/client_name_version_interceptor.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/temporal/connection/interceptors/client_name_version_interceptor.rb b/lib/temporal/connection/interceptors/client_name_version_interceptor.rb index b81a7576..5d70ff72 100644 --- a/lib/temporal/connection/interceptors/client_name_version_interceptor.rb +++ b/lib/temporal/connection/interceptors/client_name_version_interceptor.rb @@ -1,4 +1,5 @@ require 'grpc' +require 'temporal/version' module Temporal module Connection @@ -10,4 +11,4 @@ def request_response(request: nil, call: nil, method: nil, metadata: nil) end end end -end \ No newline at end of file +end From d972473931a4554ab45fede1f50d2cf651f51325 Mon Sep 17 00:00:00 2001 From: DeRauk Gibble Date: Fri, 5 May 2023 13:47:43 -0400 Subject: [PATCH 090/125] Add a dynamic config for the examples docker container and set system.forceSearchAttributesCacheRefreshOnRead to true (#242) Co-authored-by: DeRauk Gibble --- examples/docker-compose.yml | 3 +++ examples/dynamic-config.yml | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 examples/dynamic-config.yml diff --git a/examples/docker-compose.yml b/examples/docker-compose.yml index 77a7eb3b..4bff724c 100644 --- a/examples/docker-compose.yml +++ b/examples/docker-compose.yml @@ -7,6 +7,9 @@ services: - "7233:7233" environment: - "CASSANDRA_SEEDS=cassandra" + - "DYNAMIC_CONFIG_FILE_PATH=/etc/temporal/config/dynamicconfig/temporal-ruby.yaml" + volumes: + - ./dynamic-config.yml:/etc/temporal/config/dynamicconfig/temporal-ruby.yaml depends_on: - cassandra diff --git a/examples/dynamic-config.yml b/examples/dynamic-config.yml new file mode 100644 index 00000000..4481cb02 --- /dev/null +++ b/examples/dynamic-config.yml @@ -0,0 +1,2 @@ +system.forceSearchAttributesCacheRefreshOnRead: + - value: true \ No newline at end of file From 82666014e849e19c075e7ccf49bd72a0748da432 Mon Sep 17 00:00:00 2001 From: Jeff Schoner Date: Fri, 5 May 2023 20:04:13 -0700 Subject: [PATCH 091/125] Make worker.start and worker.stop threadsafe (#240) * make worker.start and worker.stop threadsafe * Eliminate unnecessary poll params * Remove unnecessary allow statements in poller specs * Rename _test_hook -> _hook --- lib/temporal/activity/poller.rb | 4 + lib/temporal/worker.rb | 42 ++++--- lib/temporal/workflow/poller.rb | 4 + .../unit/lib/temporal/activity/poller_spec.rb | 116 ++++++++++-------- spec/unit/lib/temporal/worker_spec.rb | 101 +++++++++------ .../unit/lib/temporal/workflow/poller_spec.rb | 113 ++++++++++------- 6 files changed, 233 insertions(+), 147 deletions(-) diff --git a/lib/temporal/activity/poller.rb b/lib/temporal/activity/poller.rb index 329d2a8c..f0e198d2 100644 --- a/lib/temporal/activity/poller.rb +++ b/lib/temporal/activity/poller.rb @@ -38,6 +38,10 @@ def cancel_pending_requests end def wait + if !shutting_down? + raise "Activity poller waiting for shutdown completion without being in shutting_down state!" + end + thread.join thread_pool.shutdown end diff --git a/lib/temporal/worker.rb b/lib/temporal/worker.rb index 70cfa398..2a232458 100644 --- a/lib/temporal/worker.rb +++ b/lib/temporal/worker.rb @@ -45,6 +45,7 @@ def initialize( binary_checksum: binary_checksum, poll_retry_seconds: workflow_poll_retry_seconds } + @start_stop_mutex = Mutex.new end def register_workflow(workflow_class, options = {}) @@ -104,17 +105,22 @@ def add_activity_middleware(middleware_class, *args) end def start - workflows.each_pair do |(namespace, task_queue), lookup| - pollers << workflow_poller_for(namespace, task_queue, lookup) - end + @start_stop_mutex.synchronize do + return if shutting_down? # Handle the case where stop method grabbed the mutex first - activities.each_pair do |(namespace, task_queue), lookup| - pollers << activity_poller_for(namespace, task_queue, lookup) - end + trap_signals - trap_signals + workflows.each_pair do |(namespace, task_queue), lookup| + pollers << workflow_poller_for(namespace, task_queue, lookup) + end - pollers.each(&:start) + activities.each_pair do |(namespace, task_queue), lookup| + pollers << activity_poller_for(namespace, task_queue, lookup) + end + + pollers.each(&:start) + end + on_started_hook # keep the main thread alive sleep 1 while !shutting_down? @@ -124,12 +130,16 @@ def stop @shutting_down = true Thread.new do - pollers.each(&:stop_polling) - # allow workers to drain in-transit tasks. - # https://github.com/temporalio/temporal/issues/1058 - sleep 1 - pollers.each(&:cancel_pending_requests) - pollers.each(&:wait) + @start_stop_mutex.synchronize do + pollers.each(&:stop_polling) + while_stopping_hook + # allow workers to drain in-transit tasks. + # https://github.com/temporalio/temporal/issues/1058 + sleep 1 + pollers.each(&:cancel_pending_requests) + pollers.each(&:wait) + end + on_stopped_hook end.join end @@ -143,6 +153,10 @@ def shutting_down? @shutting_down end + def on_started_hook; end + def while_stopping_hook; end + def on_stopped_hook; end + def workflow_poller_for(namespace, task_queue, lookup) Workflow::Poller.new(namespace, task_queue, lookup.freeze, config, workflow_task_middleware, workflow_middleware, workflow_poller_options) end diff --git a/lib/temporal/workflow/poller.rb b/lib/temporal/workflow/poller.rb index c268f2b2..07162ce1 100644 --- a/lib/temporal/workflow/poller.rb +++ b/lib/temporal/workflow/poller.rb @@ -41,6 +41,10 @@ def cancel_pending_requests end def wait + if !shutting_down? + raise "Workflow poller waiting for shutdown completion without being in shutting_down state!" + end + thread.join thread_pool.shutdown end diff --git a/spec/unit/lib/temporal/activity/poller_spec.rb b/spec/unit/lib/temporal/activity/poller_spec.rb index 6c93adaf..910d54d2 100644 --- a/spec/unit/lib/temporal/activity/poller_spec.rb +++ b/spec/unit/lib/temporal/activity/poller_spec.rb @@ -14,6 +14,7 @@ let(:config) { Temporal::Configuration.new } let(:middleware_chain) { instance_double(Temporal::Middleware::Chain) } let(:middleware) { [] } + let(:busy_wait_delay) {0.01} subject { described_class.new(namespace, task_queue, lookup, config, middleware) } @@ -25,30 +26,38 @@ allow(Temporal.metrics).to receive(:increment) end - describe '#start' do - it 'measures time between polls' do - allow(subject).to receive(:shutting_down?).and_return(false, false, true) - allow(connection).to receive(:poll_activity_task_queue).and_return(nil) + # poller will receive task times times, and nil thereafter. + # poller will be shut down after that + def poll(task, times: 1) + polled_times = 0 + allow(connection).to receive(:poll_activity_task_queue) do + polled_times += 1 + if polled_times <= times + task + else + nil + end + end - subject.start + subject.start - # stop poller before inspecting - subject.stop_polling; subject.wait + while polled_times < times + sleep(busy_wait_delay) + end + # stop poller before inspecting + subject.stop_polling; subject.wait + polled_times + end - expect(connection) - .to have_received(:poll_activity_task_queue) - .with(namespace: namespace, task_queue: task_queue) - .twice + describe '#start' do + it 'measures time between polls' do + # if it doesn't poll, this test will loop forever + times = poll(nil, times: 2) + expect(times).to be >= 2 end it 'reports time since last poll' do - allow(subject).to receive(:shutting_down?).and_return(false, false, true) - allow(connection).to receive(:poll_activity_task_queue).and_return(nil) - - subject.start - - # stop poller before inspecting - subject.stop_polling; subject.wait + poll(nil, times: 2) expect(Temporal.metrics) .to have_received(:timing) @@ -58,17 +67,11 @@ namespace: namespace, task_queue: task_queue ) - .twice + .at_least(:twice) end it 'reports polling completed with received_task false' do - allow(subject).to receive(:shutting_down?).and_return(false, false, true) - allow(connection).to receive(:poll_activity_task_queue).and_return(nil) - - subject.start - - # stop poller before inspecting - subject.stop_polling; subject.wait + poll(nil, times: 2) expect(Temporal.metrics) .to have_received(:increment) @@ -78,7 +81,7 @@ namespace: namespace, task_queue: task_queue ) - .twice + .at_least(:twice) end context 'when an activity task is received' do @@ -86,26 +89,18 @@ let(:task) { Fabricate(:api_activity_task) } before do - allow(subject).to receive(:shutting_down?).and_return(false, true) - allow(connection).to receive(:poll_activity_task_queue).and_return(task) allow(Temporal::Activity::TaskProcessor).to receive(:new).and_return(task_processor) allow(thread_pool).to receive(:schedule).and_yield end it 'schedules task processing using a ThreadPool' do - subject.start - - # stop poller before inspecting - subject.stop_polling; subject.wait + poll(task) expect(thread_pool).to have_received(:schedule) end it 'uses TaskProcessor to process tasks' do - subject.start - - # stop poller before inspecting - subject.stop_polling; subject.wait + poll(task) expect(Temporal::Activity::TaskProcessor) .to have_received(:new) @@ -114,10 +109,7 @@ end it 'reports polling completed with received_task true' do - subject.start - - # stop poller before inspecting - subject.stop_polling; subject.wait + poll(task) expect(Temporal.metrics) .to have_received(:increment) @@ -142,10 +134,7 @@ def call(_); end let(:entry_2) { Temporal::Middleware::Entry.new(TestPollerMiddleware, '2') } it 'initializes middleware chain and passes it down to TaskProcessor' do - subject.start - - # stop poller before inspecting - subject.stop_polling; subject.wait + poll(task) expect(Temporal::Middleware::Chain).to have_received(:new).with(middleware) expect(Temporal::Activity::TaskProcessor) @@ -157,15 +146,24 @@ def call(_); end context 'when connection is unable to poll' do before do - allow(subject).to receive(:shutting_down?).and_return(false, true) - allow(connection).to receive(:poll_activity_task_queue).and_raise(StandardError) allow(subject).to receive(:sleep).and_return(nil) end it 'logs' do allow(Temporal.logger).to receive(:error) + polled = false + allow(connection).to receive(:poll_activity_task_queue) do + if !polled + polled = true + raise StandardError + end + end + subject.start + while !polled + sleep(busy_wait_delay) + end # stop poller before inspecting subject.stop_polling; subject.wait @@ -176,7 +174,18 @@ def call(_); end end it 'does not sleep' do + polled = false + allow(connection).to receive(:poll_activity_task_queue) do + if !polled + polled = true + raise StandardError + end + end + subject.start + while !polled + sleep(busy_wait_delay) + end # stop poller before inspecting subject.stop_polling; subject.wait @@ -201,13 +210,22 @@ def call(_); end end before do - allow(subject).to receive(:shutting_down?).and_return(false, true) - allow(connection).to receive(:poll_activity_task_queue).and_raise(StandardError) allow(subject).to receive(:sleep).and_return(nil) end it 'sleeps' do + polled = false + allow(connection).to receive(:poll_activity_task_queue) do + if !polled + polled = true + raise StandardError + end + end + subject.start + while !polled + sleep(busy_wait_delay) + end # stop poller before inspecting subject.stop_polling; subject.wait diff --git a/spec/unit/lib/temporal/worker_spec.rb b/spec/unit/lib/temporal/worker_spec.rb index bcd78084..f8a74b21 100644 --- a/spec/unit/lib/temporal/worker_spec.rb +++ b/spec/unit/lib/temporal/worker_spec.rb @@ -202,15 +202,41 @@ class OtherTestWorkerActivity < Temporal::Activity end end + def start_and_stop(worker) + allow(worker).to receive(:on_started_hook) { + worker.stop + } + stopped = false + allow(worker).to receive(:on_stopped_hook) { + stopped = true + } + + thread = Thread.new {worker.start} + while !stopped + sleep(THREAD_SYNC_DELAY) + end + thread + end + + describe 'start and stop' do + it 'can stop before starting' do + expect(Temporal::Workflow::Poller) + .to_not receive(:new) + expect(Temporal::Activity::Poller) + .to_not receive(:new) + t = Thread.new {subject.stop} + subject.start + t.join + end + end + describe '#start' do - let(:workflow_poller_1) { instance_double(Temporal::Workflow::Poller, start: nil) } - let(:workflow_poller_2) { instance_double(Temporal::Workflow::Poller, start: nil) } - let(:activity_poller_1) { instance_double(Temporal::Activity::Poller, start: nil) } - let(:activity_poller_2) { instance_double(Temporal::Activity::Poller, start: nil) } + let(:workflow_poller_1) { instance_double(Temporal::Workflow::Poller, start: nil, stop_polling: nil, cancel_pending_requests: nil, wait: nil) } + let(:workflow_poller_2) { instance_double(Temporal::Workflow::Poller, start: nil, stop_polling: nil, cancel_pending_requests: nil, wait: nil) } + let(:activity_poller_1) { instance_double(Temporal::Activity::Poller, start: nil, stop_polling: nil, cancel_pending_requests: nil, wait: nil) } + let(:activity_poller_2) { instance_double(Temporal::Activity::Poller, start: nil, stop_polling: nil, cancel_pending_requests: nil, wait: nil) } it 'starts a poller for each namespace/task list combination' do - allow(subject).to receive(:shutting_down?).and_return(true) - allow(Temporal::Workflow::Poller) .to receive(:new) .with( @@ -272,7 +298,7 @@ class OtherTestWorkerActivity < Temporal::Activity subject.register_activity(TestWorkerActivity) subject.register_activity(TestWorkerActivity, task_queue: 'other-task-queue') - subject.start + start_and_stop(subject) expect(workflow_poller_1).to have_received(:start) expect(workflow_poller_2).to have_received(:start) @@ -281,7 +307,7 @@ class OtherTestWorkerActivity < Temporal::Activity end it 'can have an activity poller with a different thread pool size' do - activity_poller = instance_double(Temporal::Activity::Poller, start: nil) + activity_poller = instance_double(Temporal::Activity::Poller, start: nil, stop_polling: nil, cancel_pending_requests: nil, wait: nil) expect(Temporal::Activity::Poller) .to receive(:new) .with( @@ -294,28 +320,40 @@ class OtherTestWorkerActivity < Temporal::Activity ) .and_return(activity_poller) - workflow_poller = instance_double(Temporal::Workflow::Poller, start: nil) + workflow_poller = instance_double(Temporal::Workflow::Poller, start: nil, stop_polling: nil, cancel_pending_requests: nil, wait: nil) expect(Temporal::Workflow::Poller) .to receive(:new) .and_return(workflow_poller) worker = Temporal::Worker.new(activity_thread_pool_size: 10) - allow(worker).to receive(:shutting_down?).and_return(true) worker.register_workflow(TestWorkerWorkflow) worker.register_activity(TestWorkerActivity) - worker.start + start_and_stop(worker) expect(activity_poller).to have_received(:start) end + it 'is mutually exclusive with stop' do + subject.register_workflow(TestWorkerWorkflow) + subject.register_activity(TestWorkerActivity) + + allow(subject).to receive(:while_stopping_hook) do + # This callback is within a mutex, so this new thread shouldn't + # do anything until Worker.stop is complete. + Thread.new {subject.start} + sleep(THREAD_SYNC_DELAY) # give it a little time to do damage if it's going to + end + subject.stop + end + it 'can have a worklow poller with a binary checksum' do - activity_poller = instance_double(Temporal::Activity::Poller, start: nil) + activity_poller = instance_double(Temporal::Activity::Poller, start: nil, stop_polling: nil, cancel_pending_requests: nil, wait: nil) expect(Temporal::Activity::Poller) .to receive(:new) .and_return(activity_poller) - workflow_poller = instance_double(Temporal::Workflow::Poller, start: nil) + workflow_poller = instance_double(Temporal::Workflow::Poller, start: nil, stop_polling: nil, cancel_pending_requests: nil, wait: nil) binary_checksum = 'abc123' expect(Temporal::Workflow::Poller) .to receive(:new) @@ -333,17 +371,16 @@ class OtherTestWorkerActivity < Temporal::Activity .and_return(workflow_poller) worker = Temporal::Worker.new(binary_checksum: binary_checksum) - allow(worker).to receive(:shutting_down?).and_return(true) worker.register_workflow(TestWorkerWorkflow) worker.register_activity(TestWorkerActivity) - worker.start + start_and_stop(worker) expect(workflow_poller).to have_received(:start) end it 'can have an activity poller that sleeps after unsuccessful poll' do - activity_poller = instance_double(Temporal::Activity::Poller, start: nil) + activity_poller = instance_double(Temporal::Activity::Poller, start: nil, stop_polling: nil, cancel_pending_requests: nil, wait: nil) expect(Temporal::Activity::Poller) .to receive(:new) .with( @@ -357,17 +394,16 @@ class OtherTestWorkerActivity < Temporal::Activity .and_return(activity_poller) worker = Temporal::Worker.new(activity_poll_retry_seconds: 10) - allow(worker).to receive(:shutting_down?).and_return(true) worker.register_workflow(TestWorkerWorkflow) worker.register_activity(TestWorkerActivity) - worker.start + start_and_stop(worker) expect(activity_poller).to have_received(:start) end it 'can have a workflow poller sleeping after unsuccessful poll' do - workflow_poller = instance_double(Temporal::Workflow::Poller, start: nil) + workflow_poller = instance_double(Temporal::Workflow::Poller, start: nil, stop_polling: nil, cancel_pending_requests: nil, wait: nil) expect(Temporal::Workflow::Poller) .to receive(:new) .with( @@ -382,11 +418,10 @@ class OtherTestWorkerActivity < Temporal::Activity .and_return(workflow_poller) worker = Temporal::Worker.new(workflow_poll_retry_seconds: 10) - allow(worker).to receive(:shutting_down?).and_return(true) worker.register_workflow(TestWorkerWorkflow) worker.register_activity(TestWorkerActivity) - worker.start + start_and_stop(worker) expect(workflow_poller).to have_received(:start) end @@ -418,8 +453,6 @@ class OtherTestWorkerActivity < Temporal::Activity end it 'starts pollers with correct middleware' do - allow(subject).to receive(:shutting_down?).and_return(true) - allow(Temporal::Workflow::Poller) .to receive(:new) .with( @@ -451,7 +484,7 @@ class OtherTestWorkerActivity < Temporal::Activity subject.register_workflow(TestWorkerWorkflow) subject.register_activity(TestWorkerActivity) - subject.start + start_and_stop(subject) expect(workflow_poller_1).to have_received(:start) expect(activity_poller_1).to have_received(:start) @@ -459,12 +492,11 @@ class OtherTestWorkerActivity < Temporal::Activity end it 'sleeps while waiting for the shutdown' do - allow(subject).to receive(:shutting_down?).and_return(false, false, false, true) allow(subject).to receive(:sleep).and_return(nil) - subject.start + start_and_stop(subject) - expect(subject).to have_received(:sleep).with(1).exactly(3).times + expect(subject).to have_received(:sleep).with(1).once end describe 'signal handling' do @@ -530,17 +562,12 @@ class OtherTestWorkerActivity < Temporal::Activity subject.register_workflow(TestWorkerWorkflow) subject.register_activity(TestWorkerActivity) - - @thread = Thread.new { subject.start } - sleep THREAD_SYNC_DELAY # allow worker to start end it 'stops the pollers and cancels pending requests' do - subject.stop + thread = start_and_stop(subject) - sleep THREAD_SYNC_DELAY # wait for the worker to stop - - expect(@thread).not_to be_alive + expect(thread).not_to be_alive expect(workflow_poller).to have_received(:stop_polling) expect(workflow_poller).to have_received(:cancel_pending_requests) expect(activity_poller).to have_received(:stop_polling) @@ -548,11 +575,9 @@ class OtherTestWorkerActivity < Temporal::Activity end it 'waits for the pollers to stop' do - subject.stop - - sleep THREAD_SYNC_DELAY # wait for worker to stop + thread = start_and_stop(subject) - expect(@thread).not_to be_alive + expect(thread).not_to be_alive expect(workflow_poller).to have_received(:wait) expect(activity_poller).to have_received(:wait) end diff --git a/spec/unit/lib/temporal/workflow/poller_spec.rb b/spec/unit/lib/temporal/workflow/poller_spec.rb index 8c66a013..e8d5692b 100644 --- a/spec/unit/lib/temporal/workflow/poller_spec.rb +++ b/spec/unit/lib/temporal/workflow/poller_spec.rb @@ -15,6 +15,7 @@ let(:workflow_middleware) { [] } let(:empty_middleware_chain) { instance_double(Temporal::Middleware::Chain) } let(:binary_checksum) { 'v1.0.0' } + let(:busy_wait_delay) {0.01} subject do described_class.new( @@ -30,6 +31,29 @@ ) end + # poller will receive task times times, and nil thereafter. + # poller will be shut down after that + def poll(task, times: 1) + polled_times = 0 + allow(connection).to receive(:poll_workflow_task_queue) do + polled_times += 1 + if polled_times <= times + task + else + nil + end + end + + subject.start + + while polled_times < times + sleep(busy_wait_delay) + end + # stop poller before inspecting + subject.stop_polling; subject.wait + polled_times + end + before do allow(Temporal::Connection).to receive(:generate).and_return(connection) allow(Temporal::Middleware::Chain).to receive(:new).with(workflow_middleware).and_return(workflow_middleware_chain) @@ -40,29 +64,14 @@ end describe '#start' do - it 'polls for decision tasks' do - allow(subject).to receive(:shutting_down?).and_return(false, false, true) - allow(connection).to receive(:poll_workflow_task_queue).and_return(nil) - + it 'polls for workflow tasks' do subject.start - - # stop poller before inspecting - subject.stop_polling; subject.wait - - expect(connection) - .to have_received(:poll_workflow_task_queue) - .with(namespace: namespace, task_queue: task_queue, binary_checksum: binary_checksum) - .twice + times = poll(nil, times: 2) + expect(times).to be >=(2) end it 'reports time since last poll' do - allow(subject).to receive(:shutting_down?).and_return(false, false, true) - allow(connection).to receive(:poll_workflow_task_queue).and_return(nil) - - subject.start - - # stop poller before inspecting - subject.stop_polling; subject.wait + poll(nil) expect(Temporal.metrics) .to have_received(:timing) @@ -72,17 +81,11 @@ namespace: namespace, task_queue: task_queue ) - .twice + .at_least(2).times end it 'reports polling completed with received_task false' do - allow(subject).to receive(:shutting_down?).and_return(false, false, true) - allow(connection).to receive(:poll_workflow_task_queue).and_return(nil) - - subject.start - - # stop poller before inspecting - subject.stop_polling; subject.wait + poll(nil) expect(Temporal.metrics) .to have_received(:increment) @@ -92,7 +95,7 @@ namespace: namespace, task_queue: task_queue ) - .twice + .at_least(2).times end context 'when a workflow task is received' do @@ -102,16 +105,11 @@ let(:task) { Fabricate(:api_workflow_task) } before do - allow(subject).to receive(:shutting_down?).and_return(false, true) - allow(connection).to receive(:poll_workflow_task_queue).and_return(task) allow(Temporal::Workflow::TaskProcessor).to receive(:new).and_return(task_processor) end it 'uses TaskProcessor to process tasks' do - subject.start - - # stop poller before inspecting - subject.stop_polling; subject.wait + poll(task) expect(Temporal::Workflow::TaskProcessor) .to have_received(:new) @@ -120,10 +118,7 @@ end it 'reports polling completed with received_task true' do - subject.start - - # stop poller before inspecting - subject.stop_polling; subject.wait + poll(task) expect(Temporal.metrics) .to have_received(:increment) @@ -150,10 +145,7 @@ def call(_); end it 'initializes middleware chain and passes it down to TaskProcessor' do - subject.start - - # stop poller before inspecting - subject.stop_polling; subject.wait + poll(task) expect(Temporal::Middleware::Chain).to have_received(:new).with(middleware) expect(Temporal::Middleware::Chain).to have_received(:new).with(workflow_middleware) @@ -166,15 +158,24 @@ def call(_); end context 'when connection is unable to poll' do before do - allow(subject).to receive(:shutting_down?).and_return(false, true) - allow(connection).to receive(:poll_workflow_task_queue).and_raise(StandardError) allow(subject).to receive(:sleep).and_return(nil) end it 'logs' do allow(Temporal.logger).to receive(:error) + polled = false + allow(connection).to receive(:poll_workflow_task_queue) do + if !polled + polled = true + raise StandardError + end + end + subject.start + while !polled + sleep(busy_wait_delay) + end # stop poller before inspecting subject.stop_polling; subject.wait @@ -190,7 +191,18 @@ def call(_); end end it 'does not sleep' do + polled = false + allow(connection).to receive(:poll_workflow_task_queue) do + if !polled + polled = true + raise StandardError + end + end + subject.start + while !polled + sleep(busy_wait_delay) + end # stop poller before inspecting subject.stop_polling; subject.wait @@ -216,13 +228,22 @@ def call(_); end end before do - allow(subject).to receive(:shutting_down?).and_return(false, true) - allow(connection).to receive(:poll_workflow_task_queue).and_raise(StandardError) allow(subject).to receive(:sleep).and_return(nil) end it 'sleeps' do + polled = false + allow(connection).to receive(:poll_workflow_task_queue) do + if !polled + polled = true + raise StandardError + end + end + subject.start + while !polled + sleep(busy_wait_delay) + end # stop poller before inspecting subject.stop_polling; subject.wait From 41a8e923e52f62a6727e3ce6ffa2d68090b29439 Mon Sep 17 00:00:00 2001 From: Jeff Schoner Date: Wed, 10 May 2023 04:19:17 -0700 Subject: [PATCH 092/125] Heartbeat throttling (#234) * Scheduled thread pool * Heartbeat throttling * Shorten wait times to speed test * Comments indicating the origin of constants and defaults * heartbeat_scheduled -> heartbeat_check_scheduled * Don't schedule when delay is <= 0 * Add last_heartbeat_throttled flag --- examples/activities/long_running_activity.rb | 9 +- examples/workflows/long_workflow.rb | 2 +- lib/temporal/activity/context.rb | 105 ++++++++++++++- lib/temporal/activity/poller.rb | 25 +++- lib/temporal/activity/task_processor.rb | 11 +- lib/temporal/configuration.rb | 15 ++- lib/temporal/metadata.rb | 3 +- lib/temporal/metadata/activity.rb | 7 +- lib/temporal/scheduled_thread_pool.rb | 111 +++++++++++++++ .../testing/local_activity_context.rb | 2 +- .../testing/local_workflow_context.rb | 2 + .../activity_metadata_fabricator.rb | 1 + .../grpc/activity_task_fabricator.rb | 1 + .../record_activity_heartbeat_fabricator.rb | 3 + .../lib/temporal/activity/context_spec.rb | 81 ++++++++++- .../unit/lib/temporal/activity/poller_spec.rb | 8 +- .../temporal/activity/task_processor_spec.rb | 18 ++- .../lib/temporal/metadata/activity_spec.rb | 2 +- .../temporal/scheduled_thread_pool_spec.rb | 127 ++++++++++++++++++ .../lib/temporal/workflow/context_spec.rb | 2 +- 20 files changed, 503 insertions(+), 32 deletions(-) create mode 100644 lib/temporal/scheduled_thread_pool.rb create mode 100644 spec/fabricators/grpc/record_activity_heartbeat_fabricator.rb create mode 100644 spec/unit/lib/temporal/scheduled_thread_pool_spec.rb diff --git a/examples/activities/long_running_activity.rb b/examples/activities/long_running_activity.rb index 09673776..26cdd576 100644 --- a/examples/activities/long_running_activity.rb +++ b/examples/activities/long_running_activity.rb @@ -3,9 +3,14 @@ class Canceled < Temporal::ActivityException; end def execute(cycles, interval) cycles.times do - response = activity.heartbeat + # To detect if the activity has been canceled, you can check activity.cancel_requested or + # simply heartbeat in which case an ActivityCanceled error will be raised. Cancellation + # is only detected through heartbeating, but the setting of this bit can be delayed by + # heartbeat throttling which sends the heartbeat on a background thread. + activity.logger.info("activity.cancel_requested: #{activity.cancel_requested}") - if response.cancel_requested + activity.heartbeat + if activity.cancel_requested raise Canceled, 'cancel activity request received' end diff --git a/examples/workflows/long_workflow.rb b/examples/workflows/long_workflow.rb index c4b4682f..09138ae1 100644 --- a/examples/workflows/long_workflow.rb +++ b/examples/workflows/long_workflow.rb @@ -2,7 +2,7 @@ class LongWorkflow < Temporal::Workflow def execute(cycles = 10, interval = 1) - future = LongRunningActivity.execute(cycles, interval) + future = LongRunningActivity.execute(cycles, interval, options: { timeouts: { heartbeat: interval * 2 } }) workflow.on_signal do |signal, input| logger.warn "Signal received", { signal: signal, input: input } diff --git a/lib/temporal/activity/context.rb b/lib/temporal/activity/context.rb index 289024a8..dd330520 100644 --- a/lib/temporal/activity/context.rb +++ b/lib/temporal/activity/context.rb @@ -7,12 +7,21 @@ module Temporal class Activity class Context - def initialize(connection, metadata) + def initialize(connection, metadata, config, heartbeat_thread_pool) @connection = connection @metadata = metadata + @config = config + @heartbeat_thread_pool = heartbeat_thread_pool + @last_heartbeat_details = [] # an array to differentiate nil hearbeat from no heartbeat queued + @heartbeat_check_scheduled = nil + @heartbeat_mutex = Mutex.new @async = false + @cancel_requested = false + @last_heartbeat_throttled = false end + attr_reader :heartbeat_check_scheduled, :cancel_requested, :last_heartbeat_throttled + def async @async = true end @@ -32,7 +41,47 @@ def async_token def heartbeat(details = nil) logger.debug('Activity heartbeat', metadata.to_h) - connection.record_activity_task_heartbeat(namespace: metadata.namespace, task_token: task_token, details: details) + # Heartbeat throttling limits the number of calls made to Temporal server, reducing load on the server + # and improving activity performance. The first heartbeat in an activity will always be sent immediately. + # After that, a timer is scheduled on a background thread. While this check heartbeat thread is scheduled, + # heartbeats will not be directly sent to the server, but rather the value will be saved for later. When + # this timer fires and the thread resumes, it will send any heartbeats that came in while waiting, and + # begin the process over again. + # + # The interval is determined by the following criteria: + # - if a heartbeat timeout is set, 80% of it + # - or if there is no heartbeat timeout set, use the configuration for default_heartbeat_throttle_interval + # - any duration is capped by the max_heartbeat_throttle_interval configuration + # + # Example: + # Assume a heartbeat timeout of 10s + # Throttle interval will be 8s, below the 60s maximum interval cap + # Assume the following timeline: + # t = 0, heartbeat, sent, timer scheduled for 8s + # t = 1, heartbeat, saved + # t = 6, heartbeat, saved + # t = 8, timer wakes up, sends the saved heartbeat from t = 6, new timer scheduled for 16s + # ... no heartbeats + # t = 16, timer wakes up, no saved hearbeat to send, no new timer scheduled + # t = 20, heartbeat, sent, timer scheduled for 28s + # ... + + heartbeat_mutex.synchronize do + if heartbeat_check_scheduled.nil? + send_heartbeat(details) + @last_heartbeat_details = [] + @last_heartbeat_throttled = false + @heartbeat_check_scheduled = schedule_check_heartbeat(heartbeat_throttle_interval) + else + logger.debug('Throttling heartbeat for sending later', metadata.to_h) + @last_heartbeat_details = [details] + @last_heartbeat_throttled = true + end + end + + # Return back the context so that .cancel_requested works similarly to before when the + # GRPC response was returned back directly + self end def heartbeat_details @@ -64,11 +113,61 @@ def name private - attr_reader :connection, :metadata + attr_reader :connection, :metadata, :heartbeat_thread_pool, :config, :heartbeat_mutex, :last_heartbeat_details def task_token metadata.task_token end + + def heartbeat_throttle_interval + # This is a port of logic in the Go SDK + # https://github.com/temporalio/sdk-go/blob/eaa3802876de77500164f80f378559c51d6bb0e2/internal/internal_task_handlers.go#L1990 + interval = if metadata.heartbeat_timeout > 0 + metadata.heartbeat_timeout * 0.8 + else + config.timeouts[:default_heartbeat_throttle_interval] + end + + [interval, config.timeouts[:max_heartbeat_throttle_interval]].min + end + + def send_heartbeat(details) + begin + response = connection.record_activity_task_heartbeat( + namespace: metadata.namespace, + task_token: task_token, + details: details) + if response.cancel_requested + logger.info('Activity has been canceled', metadata.to_h) + @cancel_requested = true + end + rescue => error + Temporal::ErrorHandler.handle(error, config, metadata: metadata) + raise + end + end + + def schedule_check_heartbeat(delay) + return nil if delay <= 0 + + heartbeat_thread_pool.schedule([metadata.workflow_run_id, metadata.id, metadata.attempt], delay) do + details = heartbeat_mutex.synchronize do + @heartbeat_check_scheduled = nil + # Check to see if there is a saved heartbeat. If heartbeat was not called while this was waiting, + # this will be empty and there's no need to send anything or to scheduled another heartbeat + # check. + last_heartbeat_details + end + begin + unless details.empty? + heartbeat(details.first) + end + rescue + # Can swallow any errors here since this only runs on a background thread. Any error will be + # sent to the error handler above in send_heartbeat. + end + end + end end end end diff --git a/lib/temporal/activity/poller.rb b/lib/temporal/activity/poller.rb index f0e198d2..55271593 100644 --- a/lib/temporal/activity/poller.rb +++ b/lib/temporal/activity/poller.rb @@ -1,9 +1,10 @@ -require 'temporal/connection' -require 'temporal/thread_pool' -require 'temporal/middleware/chain' require 'temporal/activity/task_processor' +require 'temporal/connection' require 'temporal/error_handler' require 'temporal/metric_keys' +require 'temporal/middleware/chain' +require 'temporal/scheduled_thread_pool' +require 'temporal/thread_pool' module Temporal class Activity @@ -44,6 +45,7 @@ def wait thread.join thread_pool.shutdown + heartbeat_thread_pool.shutdown end private @@ -103,7 +105,11 @@ def poll_for_task def process(task) middleware_chain = Middleware::Chain.new(middleware) - TaskProcessor.new(task, namespace, activity_lookup, middleware_chain, config).process + TaskProcessor.new(task, namespace, activity_lookup, middleware_chain, config, heartbeat_thread_pool).process + end + + def poll_retry_seconds + @options[:poll_retry_seconds] end def thread_pool @@ -117,8 +123,15 @@ def thread_pool ) end - def poll_retry_seconds - @options[:poll_retry_seconds] + def heartbeat_thread_pool + @heartbeat_thread_pool ||= ScheduledThreadPool.new( + options[:thread_pool_size], + { + pool_name: 'heartbeat', + namespace: namespace, + task_queue: task_queue + } + ) end end end diff --git a/lib/temporal/activity/task_processor.rb b/lib/temporal/activity/task_processor.rb index c79b742f..34847b93 100644 --- a/lib/temporal/activity/task_processor.rb +++ b/lib/temporal/activity/task_processor.rb @@ -12,7 +12,7 @@ class Activity class TaskProcessor include Concerns::Payloads - def initialize(task, namespace, activity_lookup, middleware_chain, config) + def initialize(task, namespace, activity_lookup, middleware_chain, config, heartbeat_thread_pool) @task = task @namespace = namespace @metadata = Metadata.generate_activity_metadata(task, namespace) @@ -21,6 +21,7 @@ def initialize(task, namespace, activity_lookup, middleware_chain, config) @activity_class = activity_lookup.find(activity_name) @middleware_chain = middleware_chain @config = config + @heartbeat_thread_pool = heartbeat_thread_pool end def process @@ -29,7 +30,7 @@ def process Temporal.logger.debug("Processing Activity task", metadata.to_h) Temporal.metrics.timing(Temporal::MetricKeys::ACTIVITY_TASK_QUEUE_TIME, queue_time_ms, activity: activity_name, namespace: namespace, workflow: metadata.workflow_name) - context = Activity::Context.new(connection, metadata) + context = Activity::Context.new(connection, metadata, config, heartbeat_thread_pool) if !activity_class raise ActivityNotRegistered, 'Activity is not registered with this worker' @@ -46,6 +47,10 @@ def process respond_failed(error) ensure + unless context.heartbeat_check_scheduled.nil? + heartbeat_thread_pool.cancel(context.heartbeat_check_scheduled) + end + time_diff_ms = ((Time.now - start_time) * 1000).round Temporal.metrics.timing(Temporal::MetricKeys::ACTIVITY_TASK_LATENCY, time_diff_ms, activity: activity_name, namespace: namespace, workflow: metadata.workflow_name) Temporal.logger.debug("Activity task processed", metadata.to_h.merge(execution_time: time_diff_ms)) @@ -54,7 +59,7 @@ def process private attr_reader :task, :namespace, :task_token, :activity_name, :activity_class, - :middleware_chain, :metadata, :config + :middleware_chain, :metadata, :config, :heartbeat_thread_pool def connection @connection ||= Temporal::Connection.generate(config.for_connection) diff --git a/lib/temporal/configuration.rb b/lib/temporal/configuration.rb index 7ba12f28..df26315b 100644 --- a/lib/temporal/configuration.rb +++ b/lib/temporal/configuration.rb @@ -15,7 +15,9 @@ class Configuration Execution = Struct.new(:namespace, :task_queue, :timeouts, :headers, :search_attributes, keyword_init: true) attr_reader :timeouts, :error_handlers - attr_accessor :connection_type, :converter, :use_error_serialization_v2, :host, :port, :credentials, :identity, :logger, :metrics_adapter, :namespace, :task_queue, :headers, :search_attributes, :header_propagators, :payload_codec + attr_accessor :connection_type, :converter, :use_error_serialization_v2, :host, :port, :credentials, :identity, + :logger, :metrics_adapter, :namespace, :task_queue, :headers, :search_attributes, :header_propagators, + :payload_codec # See https://docs.temporal.io/blog/activity-timeouts/ for general docs. # We want an infinite execution timeout for cron schedules and other perpetual workflows. @@ -32,7 +34,16 @@ class Configuration # See # https://docs.temporal.io/blog/activity-timeouts/#schedule-to-start-timeout schedule_to_start: nil, start_to_close: 30, # Time spent processing an activity - heartbeat: nil # Max time between heartbeats (off by default) + heartbeat: nil, # Max time between heartbeats (off by default) + # If a heartbeat timeout is specified, 80% of that value will be used for throttling. If not specified, this + # value will be used. This default comes from the Go SDK. + # https://github.com/temporalio/sdk-go/blob/eaa3802876de77500164f80f378559c51d6bb0e2/internal/internal_task_handlers.go#L65 + default_heartbeat_throttle_interval: 30, + # Heartbeat throttling interval will always be capped by this value. This default comes from the Go SDK. + # https://github.com/temporalio/sdk-go/blob/eaa3802876de77500164f80f378559c51d6bb0e2/internal/internal_task_handlers.go#L66 + # + # To disable heartbeat throttling, set this timeout to 0. + max_heartbeat_throttle_interval: 60 }.freeze DEFAULT_HEADERS = {}.freeze diff --git a/lib/temporal/metadata.rb b/lib/temporal/metadata.rb index 38f003b2..7be46b31 100644 --- a/lib/temporal/metadata.rb +++ b/lib/temporal/metadata.rb @@ -23,7 +23,8 @@ def generate_activity_metadata(task, namespace) headers: from_payload_map(task.header&.fields || {}), heartbeat_details: from_details_payloads(task.heartbeat_details), scheduled_at: task.scheduled_time.to_time, - current_attempt_scheduled_at: task.current_attempt_scheduled_time.to_time + current_attempt_scheduled_at: task.current_attempt_scheduled_time.to_time, + heartbeat_timeout: task.heartbeat_timeout.seconds ) end diff --git a/lib/temporal/metadata/activity.rb b/lib/temporal/metadata/activity.rb index bdca3366..d7afea87 100644 --- a/lib/temporal/metadata/activity.rb +++ b/lib/temporal/metadata/activity.rb @@ -3,9 +3,9 @@ module Temporal module Metadata class Activity < Base - attr_reader :namespace, :id, :name, :task_token, :attempt, :workflow_run_id, :workflow_id, :workflow_name, :headers, :heartbeat_details, :scheduled_at, :current_attempt_scheduled_at + attr_reader :namespace, :id, :name, :task_token, :attempt, :workflow_run_id, :workflow_id, :workflow_name, :headers, :heartbeat_details, :scheduled_at, :current_attempt_scheduled_at, :heartbeat_timeout - def initialize(namespace:, id:, name:, task_token:, attempt:, workflow_run_id:, workflow_id:, workflow_name:, headers: {}, heartbeat_details:, scheduled_at:, current_attempt_scheduled_at:) + def initialize(namespace:, id:, name:, task_token:, attempt:, workflow_run_id:, workflow_id:, workflow_name:, headers: {}, heartbeat_details:, scheduled_at:, current_attempt_scheduled_at:, heartbeat_timeout:) @namespace = namespace @id = id @name = name @@ -18,6 +18,7 @@ def initialize(namespace:, id:, name:, task_token:, attempt:, workflow_run_id:, @heartbeat_details = heartbeat_details @scheduled_at = scheduled_at @current_attempt_scheduled_at = current_attempt_scheduled_at + @heartbeat_timeout = heartbeat_timeout freeze end @@ -36,7 +37,7 @@ def to_h 'activity_name' => name, 'attempt' => attempt, 'scheduled_at' => scheduled_at.to_s, - 'current_attempt_scheduled_at' => current_attempt_scheduled_at.to_s, + 'current_attempt_scheduled_at' => current_attempt_scheduled_at.to_s } end end diff --git a/lib/temporal/scheduled_thread_pool.rb b/lib/temporal/scheduled_thread_pool.rb new file mode 100644 index 00000000..99b70a10 --- /dev/null +++ b/lib/temporal/scheduled_thread_pool.rb @@ -0,0 +1,111 @@ +require 'temporal/metric_keys' + +# This class implements a thread pool for scheduling tasks with a delay. +# If threads are all occupied when a task is scheduled, it will be queued +# with the sleep delay adjusted based on the wait time. +module Temporal + class ScheduledThreadPool + attr_reader :size + + ScheduledItem = Struct.new(:id, :job, :fire_at, :canceled, keyword_init: true) + + def initialize(size, metrics_tags) + @size = size + @metrics_tags = metrics_tags + @queue = Queue.new + @mutex = Mutex.new + @available_threads = size + @occupied_threads = {} + @pool = Array.new(size) do |_i| + Thread.new { poll } + end + end + + def schedule(id, delay, &block) + item = ScheduledItem.new( + id: id, + job: block, + fire_at: Time.now + delay, + canceled: false) + @mutex.synchronize do + @available_threads -= 1 + @queue << item + end + + report_metrics + + item + end + + def cancel(item) + thread = @mutex.synchronize do + @occupied_threads[item.id] + end + + item.canceled = true + unless thread.nil? + thread.raise(CancelError.new) + end + + item + end + + def shutdown + size.times do + @mutex.synchronize do + @queue << EXIT_SYMBOL + end + end + + @pool.each(&:join) + end + + private + + class CancelError < StandardError; end + EXIT_SYMBOL = :exit + + def poll + loop do + item = @queue.pop + if item == EXIT_SYMBOL + return + end + + begin + Thread.handle_interrupt(CancelError => :immediate) do + @mutex.synchronize do + @occupied_threads[item.id] = Thread.current + end + + if !item.canceled + delay = item.fire_at - Time.now + if delay > 0 + sleep delay + end + end + end + + # Job call is outside cancel handle interrupt block because the job can't + # reliably be stopped once running. It's still in the begin/rescue block + # so that it won't be executed if the thread gets canceled. + if !item.canceled + item.job.call + end + rescue CancelError + end + + @mutex.synchronize do + @available_threads += 1 + @occupied_threads.delete(item.id) + end + + report_metrics + end + end + + def report_metrics + Temporal.metrics.gauge(Temporal::MetricKeys::THREAD_POOL_AVAILABLE_THREADS, @available_threads, @metrics_tags) + end + end +end diff --git a/lib/temporal/testing/local_activity_context.rb b/lib/temporal/testing/local_activity_context.rb index 4dec0479..b89fa44f 100644 --- a/lib/temporal/testing/local_activity_context.rb +++ b/lib/temporal/testing/local_activity_context.rb @@ -6,7 +6,7 @@ module Temporal module Testing class LocalActivityContext < Activity::Context def initialize(metadata) - super(nil, metadata) + super(nil, metadata, nil, nil) end def heartbeat(details = nil) diff --git a/lib/temporal/testing/local_workflow_context.rb b/lib/temporal/testing/local_workflow_context.rb index 9f03d9bf..0f30fe0f 100644 --- a/lib/temporal/testing/local_workflow_context.rb +++ b/lib/temporal/testing/local_workflow_context.rb @@ -63,6 +63,7 @@ def execute_activity(activity_class, *input, **args) heartbeat_details: nil, scheduled_at: Time.now, current_attempt_scheduled_at: Time.now, + heartbeat_timeout: 0 ) context = LocalActivityContext.new(metadata) @@ -113,6 +114,7 @@ def execute_local_activity(activity_class, *input, **args) heartbeat_details: nil, scheduled_at: Time.now, current_attempt_scheduled_at: Time.now, + heartbeat_timeout: 0 ) context = LocalActivityContext.new(metadata) diff --git a/spec/fabricators/activity_metadata_fabricator.rb b/spec/fabricators/activity_metadata_fabricator.rb index 5fd34e3a..34ee31a6 100644 --- a/spec/fabricators/activity_metadata_fabricator.rb +++ b/spec/fabricators/activity_metadata_fabricator.rb @@ -13,4 +13,5 @@ heartbeat_details nil scheduled_at { Time.now } current_attempt_scheduled_at { Time.now } + heartbeat_timeout 0 end diff --git a/spec/fabricators/grpc/activity_task_fabricator.rb b/spec/fabricators/grpc/activity_task_fabricator.rb index b0305f99..6d2a531d 100644 --- a/spec/fabricators/grpc/activity_task_fabricator.rb +++ b/spec/fabricators/grpc/activity_task_fabricator.rb @@ -19,4 +19,5 @@ end Temporalio::Api::Common::V1::Header.new(fields: fields) end + heartbeat_timeout { Google::Protobuf::Duration.new } end diff --git a/spec/fabricators/grpc/record_activity_heartbeat_fabricator.rb b/spec/fabricators/grpc/record_activity_heartbeat_fabricator.rb new file mode 100644 index 00000000..e1b6eacf --- /dev/null +++ b/spec/fabricators/grpc/record_activity_heartbeat_fabricator.rb @@ -0,0 +1,3 @@ +Fabricator(:api_record_activity_heartbeat_response, from: Temporalio::Api::WorkflowService::V1::RecordActivityTaskHeartbeatResponse) do + cancel_requested false +end diff --git a/spec/unit/lib/temporal/activity/context_spec.rb b/spec/unit/lib/temporal/activity/context_spec.rb index e9bee2a5..0ebe7020 100644 --- a/spec/unit/lib/temporal/activity/context_spec.rb +++ b/spec/unit/lib/temporal/activity/context_spec.rb @@ -1,16 +1,20 @@ require 'temporal/activity/context' require 'temporal/metadata/activity' +require 'temporal/scheduled_thread_pool' describe Temporal::Activity::Context do let(:client) { instance_double('Temporal::Client::GRPCClient') } let(:metadata_hash) { Fabricate(:activity_metadata).to_h } let(:metadata) { Temporal::Metadata::Activity.new(**metadata_hash) } + let(:config) { Temporal::Configuration.new } let(:task_token) { SecureRandom.uuid } + let(:heartbeat_thread_pool) { Temporal::ScheduledThreadPool.new(1, {}) } + let(:heartbeat_response) { Fabricate(:api_record_activity_heartbeat_response) } - subject { described_class.new(client, metadata) } + subject { described_class.new(client, metadata, config, heartbeat_thread_pool) } describe '#heartbeat' do - before { allow(client).to receive(:record_activity_task_heartbeat) } + before { allow(client).to receive(:record_activity_task_heartbeat).and_return(heartbeat_response) } it 'records heartbeat' do subject.heartbeat @@ -27,6 +31,77 @@ .to have_received(:record_activity_task_heartbeat) .with(namespace: metadata.namespace, task_token: metadata.task_token, details: { foo: :bar }) end + + context 'cancellation' do + let(:heartbeat_response) { Fabricate(:api_record_activity_heartbeat_response, cancel_requested: true) } + it 'sets when cancelled' do + subject.heartbeat + expect(subject.cancel_requested).to be(true) + end + end + + context 'throttling' do + context 'skips after the first heartbeat' do + let(:metadata_hash) { Fabricate(:activity_metadata, heartbeat_timeout: 30).to_h } + it 'discard duplicates after first when quickly completes' do + 10.times do |i| + subject.heartbeat(iteration: i) + end + + expect(client) + .to have_received(:record_activity_task_heartbeat) + .with(namespace: metadata.namespace, task_token: metadata.task_token, details: { iteration: 0 }) + .once + end + end + + context 'resumes' do + let(:metadata_hash) { Fabricate(:activity_metadata, heartbeat_timeout: 0.1).to_h } + it 'more heartbeats after time passes' do + subject.heartbeat(iteration: 1) + subject.heartbeat(iteration: 2) # skipped because 3 will overwrite + subject.heartbeat(iteration: 3) + sleep 0.1 + subject.heartbeat(iteration: 4) + + # Shutdown to drain remaining threads + heartbeat_thread_pool.shutdown + + expect(client) + .to have_received(:record_activity_task_heartbeat) + .ordered + .with(namespace: metadata.namespace, task_token: metadata.task_token, details: { iteration: 1 }) + .with(namespace: metadata.namespace, task_token: metadata.task_token, details: { iteration: 3 }) + .with(namespace: metadata.namespace, task_token: metadata.task_token, details: { iteration: 4 }) + end + end + + it 'no heartbeat check scheduled when max interval is zero' do + config.timeouts = { max_heartbeat_throttle_interval: 0 } + subject.heartbeat + + expect(client) + .to have_received(:record_activity_task_heartbeat) + .with(namespace: metadata.namespace, task_token: metadata.task_token, details: nil) + + expect(subject.heartbeat_check_scheduled).to be_nil + end + end + end + + describe '#last_heartbeat_throttled' do + before { allow(client).to receive(:record_activity_task_heartbeat).and_return(heartbeat_response) } + + let(:metadata_hash) { Fabricate(:activity_metadata, heartbeat_timeout: 10).to_h } + + it 'true when throttled, false when not' do + subject.heartbeat(iteration: 1) + expect(subject.last_heartbeat_throttled).to be(false) + subject.heartbeat(iteration: 2) + expect(subject.last_heartbeat_throttled).to be(true) + subject.heartbeat(iteration: 3) + expect(subject.last_heartbeat_throttled).to be(true) + end end describe '#heartbeat_details' do @@ -45,7 +120,7 @@ describe '#async?' do subject { context.async? } - let(:context) { described_class.new(client, metadata) } + let(:context) { described_class.new(client, metadata, nil, nil) } context 'when context is sync' do it { is_expected.to eq(false) } diff --git a/spec/unit/lib/temporal/activity/poller_spec.rb b/spec/unit/lib/temporal/activity/poller_spec.rb index 910d54d2..0476e950 100644 --- a/spec/unit/lib/temporal/activity/poller_spec.rb +++ b/spec/unit/lib/temporal/activity/poller_spec.rb @@ -11,6 +11,9 @@ let(:thread_pool) do instance_double(Temporal::ThreadPool, wait_for_available_threads: nil, shutdown: nil) end + let(:heartbeat_thread_pool) do + instance_double(Temporal::ScheduledThreadPool, shutdown: nil) + end let(:config) { Temporal::Configuration.new } let(:middleware_chain) { instance_double(Temporal::Middleware::Chain) } let(:middleware) { [] } @@ -21,6 +24,7 @@ before do allow(Temporal::Connection).to receive(:generate).and_return(connection) allow(Temporal::ThreadPool).to receive(:new).and_return(thread_pool) + allow(Temporal::ScheduledThreadPool).to receive(:new).and_return(heartbeat_thread_pool) allow(Temporal::Middleware::Chain).to receive(:new).and_return(middleware_chain) allow(Temporal.metrics).to receive(:timing) allow(Temporal.metrics).to receive(:increment) @@ -104,7 +108,7 @@ def poll(task, times: 1) expect(Temporal::Activity::TaskProcessor) .to have_received(:new) - .with(task, namespace, lookup, middleware_chain, config) + .with(task, namespace, lookup, middleware_chain, config, heartbeat_thread_pool) expect(task_processor).to have_received(:process) end @@ -139,7 +143,7 @@ def call(_); end expect(Temporal::Middleware::Chain).to have_received(:new).with(middleware) expect(Temporal::Activity::TaskProcessor) .to have_received(:new) - .with(task, namespace, lookup, middleware_chain, config) + .with(task, namespace, lookup, middleware_chain, config, heartbeat_thread_pool) end end end diff --git a/spec/unit/lib/temporal/activity/task_processor_spec.rb b/spec/unit/lib/temporal/activity/task_processor_spec.rb index 65560d63..afb1344a 100644 --- a/spec/unit/lib/temporal/activity/task_processor_spec.rb +++ b/spec/unit/lib/temporal/activity/task_processor_spec.rb @@ -2,9 +2,10 @@ require 'temporal/configuration' require 'temporal/metric_keys' require 'temporal/middleware/chain' +require 'temporal/scheduled_thread_pool' describe Temporal::Activity::TaskProcessor do - subject { described_class.new(task, namespace, lookup, middleware_chain, config) } + subject { described_class.new(task, namespace, lookup, middleware_chain, config, heartbeat_thread_pool) } let(:namespace) { 'test-namespace' } let(:lookup) { instance_double('Temporal::ExecutableLookup', find: nil) } @@ -21,10 +22,12 @@ let(:connection) { instance_double('Temporal::Connection::GRPC') } let(:middleware_chain) { Temporal::Middleware::Chain.new } let(:config) { Temporal::Configuration.new } + let(:heartbeat_thread_pool) { Temporal::ScheduledThreadPool.new(2, {}) } let(:input) { %w[arg1 arg2] } describe '#process' do - let(:context) { instance_double('Temporal::Activity::Context', async?: false) } + let(:heartbeat_check_scheduled) { nil } + let(:context) { instance_double('Temporal::Activity::Context', async?: false, heartbeat_check_scheduled: heartbeat_check_scheduled) } before do allow(Temporal::Connection) @@ -35,7 +38,7 @@ .to receive(:generate_activity_metadata) .with(task, namespace) .and_return(metadata) - allow(Temporal::Activity::Context).to receive(:new).with(connection, metadata).and_return(context) + allow(Temporal::Activity::Context).to receive(:new).with(connection, metadata, config, heartbeat_thread_pool).and_return(context) allow(connection).to receive(:respond_activity_task_completed) allow(connection).to receive(:respond_activity_task_failed) @@ -115,6 +118,15 @@ .with(namespace: namespace, task_token: task.task_token, result: 'result') end + context 'when there is an outstanding scheduled heartbeat' do + let(:heartbeat_check_scheduled) { Temporal::ScheduledThreadPool::ScheduledItem.new(id: :foo, canceled: false) } + it 'it gets canceled' do + subject.process + + expect(heartbeat_check_scheduled.canceled).to eq(true) + end + end + it 'ignores connection exception' do allow(connection) .to receive(:respond_activity_task_completed) diff --git a/spec/unit/lib/temporal/metadata/activity_spec.rb b/spec/unit/lib/temporal/metadata/activity_spec.rb index a07a67a8..db0f8e78 100644 --- a/spec/unit/lib/temporal/metadata/activity_spec.rb +++ b/spec/unit/lib/temporal/metadata/activity_spec.rb @@ -40,7 +40,7 @@ 'workflow_name' => subject.workflow_name, 'workflow_run_id' => subject.workflow_run_id, 'scheduled_at' => subject.scheduled_at.to_s, - 'current_attempt_scheduled_at' => subject.current_attempt_scheduled_at.to_s, + 'current_attempt_scheduled_at' => subject.current_attempt_scheduled_at.to_s }) end end diff --git a/spec/unit/lib/temporal/scheduled_thread_pool_spec.rb b/spec/unit/lib/temporal/scheduled_thread_pool_spec.rb new file mode 100644 index 00000000..74f73018 --- /dev/null +++ b/spec/unit/lib/temporal/scheduled_thread_pool_spec.rb @@ -0,0 +1,127 @@ +require 'temporal/scheduled_thread_pool' + +describe Temporal::ScheduledThreadPool do + before do + allow(Temporal.metrics).to receive(:gauge) + end + + let(:size) { 2 } + let(:tags) { { foo: 'bar', bat: 'baz' } } + let(:thread_pool) { described_class.new(size, tags) } + + describe '#schedule' do + it 'executes one task with zero delay on a thread and exits' do + times = 0 + + thread_pool.schedule(:foo, 0) do + times += 1 + end + + thread_pool.shutdown + + expect(times).to eq(1) + end + + it 'executes tasks with delays in time order' do + answers = Queue.new + + thread_pool.schedule(:second, 0.2) do + answers << :second + end + + thread_pool.schedule(:first, 0.1) do + answers << :first + end + + thread_pool.shutdown + + expect(answers.size).to eq(2) + expect(answers.pop).to eq(:first) + expect(answers.pop).to eq(:second) + end + end + + describe '#cancel' do + it 'cancels already waiting task' do + answers = Queue.new + handles = [] + + handles << thread_pool.schedule(:foo, 30) do + answers << :foo + end + + handles << thread_pool.schedule(:bar, 30) do + answers << :bar + end + + # Even though this has no wait, it will be blocked by the above + # two long running tasks until one is finished or cancels. + handles << thread_pool.schedule(:baz, 0) do + answers << :baz + end + + # Canceling one waiting item (foo) will let a blocked one (baz) through + thread_pool.cancel(handles[0]) + + # Canceling the other waiting item (bar) will prevent it from blocking + # on shutdown + thread_pool.cancel(handles[1]) + + thread_pool.shutdown + + expect(answers.size).to eq(1) + expect(answers.pop).to eq(:baz) + end + + it 'cancels blocked task' do + times = 0 + handles = [] + + handles << thread_pool.schedule(:foo, 30) do + times += 1 + end + + handles << thread_pool.schedule(:bar, 30) do + times += 1 + end + + # Even though this has no wait, it will be blocked by the above + # two long running tasks. This test ensures it can be canceled + # even while waiting to run. + handles << thread_pool.schedule(:baz, 0) do + times += 1 + end + + # Cancel this one before it can start running + thread_pool.cancel(handles[0]) + + # Cancel the others so that they don't block shutdown + thread_pool.cancel(handles[1]) + thread_pool.cancel(handles[2]) + + thread_pool.shutdown + + expect(times).to eq(0) + end + end + + describe '#new' do + it 'reports thread available metrics' do + thread_pool.schedule(:foo, 0) do + end + + thread_pool.shutdown + + # Thread behavior is not deterministic. Ensure the calls match without + # verifying exact gauge values. + expect(Temporal.metrics) + .to have_received(:gauge) + .with( + Temporal::MetricKeys::THREAD_POOL_AVAILABLE_THREADS, + instance_of(Integer), + tags + ) + .at_least(:once) + end + end +end diff --git a/spec/unit/lib/temporal/workflow/context_spec.rb b/spec/unit/lib/temporal/workflow/context_spec.rb index 6559e277..ab6c2303 100644 --- a/spec/unit/lib/temporal/workflow/context_spec.rb +++ b/spec/unit/lib/temporal/workflow/context_spec.rb @@ -82,7 +82,7 @@ def inject!(header) input: [], task_queue: 'default-task-queue', retry_policy: nil, - timeouts: {:execution => 315360000, :run => 315360000, :task => 10, :schedule_to_close => nil, :schedule_to_start => nil, :start_to_close => 30, :heartbeat => nil}, + timeouts: {execution: 315360000, run: 315360000, task: 10, schedule_to_close: nil, schedule_to_start: nil, start_to_close: 30, heartbeat: nil, default_heartbeat_throttle_interval: 30, max_heartbeat_throttle_interval: 60}, headers: { 'test' => 'asdf' } )) allow(dispatcher).to receive(:register_handler) From 8979bd8549ed4b3ab2a2cc1eb89d50f3e0bccd19 Mon Sep 17 00:00:00 2001 From: Jeff Schoner Date: Wed, 7 Jun 2023 07:25:37 -0700 Subject: [PATCH 093/125] Fix continue as new timeout propagation (#246) * Check that run timeout matches * Set run timeout --- lib/temporal/connection/serializer/continue_as_new.rb | 2 +- .../connection/serializer/continue_as_new_spec.rb | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/temporal/connection/serializer/continue_as_new.rb b/lib/temporal/connection/serializer/continue_as_new.rb index c2b484bb..6573c8ec 100644 --- a/lib/temporal/connection/serializer/continue_as_new.rb +++ b/lib/temporal/connection/serializer/continue_as_new.rb @@ -16,7 +16,7 @@ def to_proto workflow_type: Temporalio::Api::Common::V1::WorkflowType.new(name: object.workflow_type), task_queue: Temporalio::Api::TaskQueue::V1::TaskQueue.new(name: object.task_queue), input: to_payloads(object.input), - workflow_run_timeout: object.timeouts[:execution], + workflow_run_timeout: object.timeouts[:run], workflow_task_timeout: object.timeouts[:task], retry_policy: Temporal::Connection::Serializer::RetryPolicy.new(object.retry_policy).to_proto, header: serialize_headers(object.headers), diff --git a/spec/unit/lib/temporal/connection/serializer/continue_as_new_spec.rb b/spec/unit/lib/temporal/connection/serializer/continue_as_new_spec.rb index e2d44893..046b066c 100644 --- a/spec/unit/lib/temporal/connection/serializer/continue_as_new_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/continue_as_new_spec.rb @@ -4,11 +4,16 @@ describe Temporal::Connection::Serializer::ContinueAsNew do describe 'to_proto' do it 'produces a protobuf' do + timeouts = { + execution: 1000, + run: 100, + task: 10 + } command = Temporal::Workflow::Command::ContinueAsNew.new( workflow_type: 'my-workflow-type', task_queue: 'my-task-queue', input: ['one', 'two'], - timeouts: Temporal.configuration.timeouts, + timeouts: timeouts, headers: {'foo-header': 'bar'}, memo: {'foo-memo': 'baz'}, search_attributes: {'foo-search-attribute': 'qux'}, @@ -33,6 +38,9 @@ expect(attribs.header.fields['foo-header'].data).to eq('"bar"') expect(attribs.memo.fields['foo-memo'].data).to eq('"baz"') expect(attribs.search_attributes.indexed_fields['foo-search-attribute'].data).to eq('"qux"') + + expect(attribs.workflow_run_timeout.seconds).to eq(timeouts[:run]) + expect(attribs.workflow_task_timeout.seconds).to eq(timeouts[:task]) end end end From c60c0dd826136ec2e8d5e74dad031056fd677f4b Mon Sep 17 00:00:00 2001 From: Jeff Schoner Date: Wed, 7 Jun 2023 07:25:52 -0700 Subject: [PATCH 094/125] Fix and add spec for local activities (#247) * Add context spec for local activity invocation * Correct activity context for local activities --- lib/temporal/workflow/context.rb | 3 ++- .../lib/temporal/workflow/context_spec.rb | 22 ++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index 58f5dd17..58a730ac 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -1,5 +1,6 @@ require 'securerandom' +require 'temporal/activity/context' require 'temporal/execution_options' require 'temporal/errors' require 'temporal/thread_local_context' @@ -113,7 +114,7 @@ def execute_local_activity(activity_class, *input, **args) side_effect do # TODO: this probably requires a local context implementation - context = Activity::Context.new(nil, nil) + context = Activity::Context.new(nil, nil, nil, nil) activity_class.execute_in_context(context, input) end end diff --git a/spec/unit/lib/temporal/workflow/context_spec.rb b/spec/unit/lib/temporal/workflow/context_spec.rb index ab6c2303..6dddf3b2 100644 --- a/spec/unit/lib/temporal/workflow/context_spec.rb +++ b/spec/unit/lib/temporal/workflow/context_spec.rb @@ -9,7 +9,13 @@ require 'time' class MyTestWorkflow < Temporal::Workflow; end -class MyTestActivity < Temporal::Activity; end +class MyTestActivity < Temporal::Activity + RETURN_VALUE = 'this-is-a-return-value'.freeze + + def execute + RETURN_VALUE + end +end describe Temporal::Workflow::Context do let(:state_manager) { instance_double('Temporal::Workflow::StateManager') } @@ -91,6 +97,20 @@ def inject!(header) end end + describe '#execute_local_activity' do + it 'executes and schedules command' do + expect(state_manager).to receive(:next_side_effect) + expect(state_manager).to receive(:schedule).with( + Temporal::Workflow::Command::RecordMarker.new( + name: 'SIDE_EFFECT', + details: MyTestActivity::RETURN_VALUE + ) + ) + return_value = workflow_context.execute_local_activity(MyTestActivity) + expect(return_value).to eq(MyTestActivity::RETURN_VALUE) + end + end + describe '#execute_workflow' do it 'returns the correct futures when starting a child workflow' do allow(state_manager).to receive(:schedule) From 4bb72e612abb6adf035ad3bc32fb6840246d6c64 Mon Sep 17 00:00:00 2001 From: Raazia Hashim <67606070+hashimr1@users.noreply.github.com> Date: Wed, 14 Jun 2023 11:19:27 -0400 Subject: [PATCH 095/125] add filter to query_workflow_executions method arguments (#250) --- lib/temporal/client.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/temporal/client.rb b/lib/temporal/client.rb index 64b7c14b..7a72be0d 100644 --- a/lib/temporal/client.rb +++ b/lib/temporal/client.rb @@ -405,7 +405,9 @@ def list_closed_workflow_executions(namespace, from, to = Time.now, filter: {}, Temporal::Workflow::Executions.new(connection: connection, status: :closed, request_options: { namespace: namespace, from: from, to: to, next_page_token: next_page_token, max_page_size: max_page_size}.merge(filter)) end - def query_workflow_executions(namespace, query, next_page_token: nil, max_page_size: nil) + def query_workflow_executions(namespace, query, filter: {}, next_page_token: nil, max_page_size: nil) + validate_filter(filter, :status, :workflow, :workflow_id) + Temporal::Workflow::Executions.new(connection: connection, status: :all, request_options: { namespace: namespace, query: query, next_page_token: next_page_token, max_page_size: max_page_size }.merge(filter)) end From 71eaf801d43b754f21df1d3ef1c01258ba795fc1 Mon Sep 17 00:00:00 2001 From: jazev-stripe <128553781+jazev-stripe@users.noreply.github.com> Date: Fri, 14 Jul 2023 06:42:14 -0700 Subject: [PATCH 096/125] Add missing arguments to Temporal.reset_workflow and add integration tests for Resets (#256) * add request_id and reset_reapply_type arguments, add integration tests for reset_workflow * reduce number of resets to 2 * move request_id generation towards outside of package * replace unspecified reset_reapply_type with actual default (signal) * fix client specs after previous commit * fix search attribute tests being change detectors --- .../initial_search_attributes_spec.rb | 4 +- .../spec/integration/reset_workflow_spec.rb | 163 ++++++++++++++++++ .../upsert_search_attributes_spec.rb | 4 +- lib/temporal/client.rb | 20 ++- lib/temporal/connection/grpc.rb | 18 +- lib/temporal/reset_reapply_type.rb | 6 + spec/unit/lib/temporal/client_spec.rb | 48 +++++- 7 files changed, 251 insertions(+), 12 deletions(-) create mode 100644 examples/spec/integration/reset_workflow_spec.rb create mode 100644 lib/temporal/reset_reapply_type.rb diff --git a/examples/spec/integration/initial_search_attributes_spec.rb b/examples/spec/integration/initial_search_attributes_spec.rb index fae51adf..7ed26eec 100644 --- a/examples/spec/integration/initial_search_attributes_spec.rb +++ b/examples/spec/integration/initial_search_attributes_spec.rb @@ -58,6 +58,8 @@ workflow_id, nil ) - expect(execution_info.search_attributes).to eq(expected_attributes) + # Temporal might add new built-in search attributes, so just assert that + # the expected attributes are a subset of the actual attributes: + expect(execution_info.search_attributes).to be >= expected_attributes end end diff --git a/examples/spec/integration/reset_workflow_spec.rb b/examples/spec/integration/reset_workflow_spec.rb new file mode 100644 index 00000000..57f41d82 --- /dev/null +++ b/examples/spec/integration/reset_workflow_spec.rb @@ -0,0 +1,163 @@ +require 'workflows/hello_world_workflow' +require 'workflows/query_workflow' +require 'temporal/reset_reapply_type' + +describe 'Temporal.reset_workflow' do + it 'can reset a closed workflow to the beginning' do + workflow_id = SecureRandom.uuid + original_run_id = Temporal.start_workflow( + HelloWorldWorkflow, + 'Test', + options: { workflow_id: workflow_id } + ) + + original_result = Temporal.await_workflow_result( + HelloWorldWorkflow, + workflow_id: workflow_id, + run_id: original_run_id + ) + expect(original_result).to eq('Hello World, Test') + + new_run_id = Temporal.reset_workflow( + Temporal.configuration.namespace, + workflow_id, + original_run_id, + strategy: Temporal::ResetStrategy::FIRST_WORKFLOW_TASK + ) + + new_result = Temporal.await_workflow_result( + HelloWorldWorkflow, + workflow_id: workflow_id, + run_id: new_run_id, + ) + expect(new_result).to eq('Hello World, Test') + end + + def reset_hello_world_workflow_twice(workflow_id, original_run_id, request_id:) + 2.times.map do + new_run_id = Temporal.reset_workflow( + Temporal.configuration.namespace, + workflow_id, + original_run_id, + strategy: Temporal::ResetStrategy::FIRST_WORKFLOW_TASK, + request_id: request_id + ) + + new_result = Temporal.await_workflow_result( + HelloWorldWorkflow, + workflow_id: workflow_id, + run_id: new_run_id, + ) + expect(new_result).to eq('Hello World, Test') + + new_run_id + end + end + + it 'can repeatedly reset the same closed workflow to the beginning' do + workflow_id = SecureRandom.uuid + original_run_id = Temporal.start_workflow( + HelloWorldWorkflow, + 'Test', + options: { workflow_id: workflow_id } + ) + + original_result = Temporal.await_workflow_result( + HelloWorldWorkflow, + workflow_id: workflow_id, + run_id: original_run_id, + ) + expect(original_result).to eq('Hello World, Test') + + new_run_ids = reset_hello_world_workflow_twice( + workflow_id, + original_run_id, + # This causes the request_id to be generated with a random value: + request_id: nil + ) + + # Each Reset request should have resulted in a unique workflow execution + expect(new_run_ids.uniq.size).to eq(new_run_ids.size) + end + + it 'can deduplicate reset requests' do + workflow_id = SecureRandom.uuid + original_run_id = Temporal.start_workflow( + HelloWorldWorkflow, + 'Test', + options: { workflow_id: workflow_id } + ) + + original_result = Temporal.await_workflow_result( + HelloWorldWorkflow, + workflow_id: workflow_id, + run_id: original_run_id, + ) + expect(original_result).to eq('Hello World, Test') + + reset_request_id = SecureRandom.uuid + new_run_ids = reset_hello_world_workflow_twice( + workflow_id, + original_run_id, + request_id: reset_request_id + ) + + # Each Reset request except the first should have been deduplicated + expect(new_run_ids.uniq.size).to eq(1) + end + + def start_query_workflow_and_signal_three_times + workflow_id = SecureRandom.uuid + run_id = Temporal.start_workflow( + QueryWorkflow, + options: { workflow_id: workflow_id } + ) + + expect(Temporal.query_workflow(QueryWorkflow, 'signal_count', workflow_id, run_id)) + .to eq 0 + + Temporal.signal_workflow(QueryWorkflow, 'make_progress', workflow_id, run_id) + Temporal.signal_workflow(QueryWorkflow, 'make_progress', workflow_id, run_id) + Temporal.signal_workflow(QueryWorkflow, 'make_progress', workflow_id, run_id) + + expect(Temporal.query_workflow(QueryWorkflow, 'signal_count', workflow_id, run_id)) + .to eq 3 + + { workflow_id: workflow_id, run_id: run_id } + end + + it 'can reapply signals when resetting a workflow' do + workflow_id, original_run_id = start_query_workflow_and_signal_three_times.values_at(:workflow_id, :run_id) + + new_run_id = Temporal.reset_workflow( + Temporal.configuration.namespace, + workflow_id, + original_run_id, + strategy: Temporal::ResetStrategy::FIRST_WORKFLOW_TASK, + reset_reapply_type: Temporal::ResetReapplyType::SIGNAL + ) + + expect(Temporal.query_workflow(QueryWorkflow, 'signal_count', workflow_id, new_run_id)) + .to eq 3 + + Temporal.terminate_workflow(workflow_id, run_id: new_run_id) + end + + it 'can skip reapplying signals when resetting a workflow' do + workflow_id, original_run_id = start_query_workflow_and_signal_three_times.values_at(:workflow_id, :run_id) + + new_run_id = Temporal.reset_workflow( + Temporal.configuration.namespace, + workflow_id, + original_run_id, + strategy: Temporal::ResetStrategy::FIRST_WORKFLOW_TASK, + reset_reapply_type: Temporal::ResetReapplyType::NONE + ) + + expect(Temporal.query_workflow(QueryWorkflow, 'signal_count', workflow_id, new_run_id)) + .to eq 0 + + Temporal.terminate_workflow(workflow_id, run_id: new_run_id) + end +end + \ No newline at end of file diff --git a/examples/spec/integration/upsert_search_attributes_spec.rb b/examples/spec/integration/upsert_search_attributes_spec.rb index 0757da3d..99c20f9f 100644 --- a/examples/spec/integration/upsert_search_attributes_spec.rb +++ b/examples/spec/integration/upsert_search_attributes_spec.rb @@ -44,6 +44,8 @@ workflow_id, nil ) - expect(execution_info.search_attributes).to eq(expected_attributes) + # Temporal might add new built-in search attributes, so just assert that + # the expected attributes are a subset of the actual attributes: + expect(execution_info.search_attributes).to be >= expected_attributes end end diff --git a/lib/temporal/client.rb b/lib/temporal/client.rb index 7a72be0d..af6ae2bf 100644 --- a/lib/temporal/client.rb +++ b/lib/temporal/client.rb @@ -271,7 +271,7 @@ def await_workflow_result(workflow, workflow_id:, run_id: nil, timeout: nil, nam # Reset a workflow # # @note More on resetting a workflow here — - # https://docs.temporal.io/docs/system-tools/tctl/#restart-reset-workflow + # https://docs.temporal.io/tctl-v1/workflow#reset # # @param namespace [String] # @param workflow_id [String] @@ -281,9 +281,13 @@ def await_workflow_result(workflow, workflow_id:, run_id: nil, timeout: nil, nam # @param workflow_task_id [Integer, nil] A specific event ID to reset to. The event has to # be of a type WorkflowTaskCompleted, WorkflowTaskFailed or WorkflowTaskTimedOut # @param reason [String] a reset reason to be recorded in workflow's history for reference + # @param request_id [String, nil] an idempotency key for the Reset request or `nil` to use + # an auto-generated, unique value + # @param reset_reapply_type [Symbol] one of the Temporal::ResetReapplyType values. Defaults + # to SIGNAL. # # @return [String] run_id of the new workflow execution - def reset_workflow(namespace, workflow_id, run_id, strategy: nil, workflow_task_id: nil, reason: 'manual reset') + def reset_workflow(namespace, workflow_id, run_id, strategy: nil, workflow_task_id: nil, reason: 'manual reset', request_id: nil, reset_reapply_type: Temporal::ResetReapplyType::SIGNAL) # Pick default strategy for backwards-compatibility strategy ||= :last_workflow_task unless workflow_task_id @@ -294,12 +298,22 @@ def reset_workflow(namespace, workflow_id, run_id, strategy: nil, workflow_task_ workflow_task_id ||= find_workflow_task(namespace, workflow_id, run_id, strategy)&.id raise Error, 'Could not find an event to reset to' unless workflow_task_id + if request_id.nil? + # Generate a request ID if one is not provided. + # This is consistent with the Go SDK: + # https://github.com/temporalio/sdk-go/blob/e1d76b7c798828302980d483f0981128c97a20c2/internal/internal_workflow_client.go#L952-L972 + + request_id = SecureRandom.uuid + end + response = connection.reset_workflow_execution( namespace: namespace, workflow_id: workflow_id, run_id: run_id, reason: reason, - workflow_task_event_id: workflow_task_id + workflow_task_event_id: workflow_task_id, + request_id: request_id, + reset_reapply_type: reset_reapply_type ) response.run_id diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index 169ba390..092faf96 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -44,6 +44,11 @@ class GRPC [Temporalio::Api::Enums::V1::IndexedValueType.lookup(int_value), symbol] end.to_h.freeze + SYMBOL_TO_RESET_REAPPLY_TYPE = { + signal: Temporalio::Api::Enums::V1::ResetReapplyType::RESET_REAPPLY_TYPE_SIGNAL, + none: Temporalio::Api::Enums::V1::ResetReapplyType::RESET_REAPPLY_TYPE_NONE, + } + DEFAULT_OPTIONS = { max_page_size: 100 }.freeze @@ -409,7 +414,7 @@ def signal_with_start_workflow_execution( client.signal_with_start_workflow_execution(request) end - def reset_workflow_execution(namespace:, workflow_id:, run_id:, reason:, workflow_task_event_id:) + def reset_workflow_execution(namespace:, workflow_id:, run_id:, reason:, workflow_task_event_id:, request_id:, reset_reapply_type: Temporal::ResetReapplyType::SIGNAL) request = Temporalio::Api::WorkflowService::V1::ResetWorkflowExecutionRequest.new( namespace: namespace, workflow_execution: Temporalio::Api::Common::V1::WorkflowExecution.new( @@ -417,8 +422,17 @@ def reset_workflow_execution(namespace:, workflow_id:, run_id:, reason:, workflo run_id: run_id, ), reason: reason, - workflow_task_finish_event_id: workflow_task_event_id + workflow_task_finish_event_id: workflow_task_event_id, + request_id: request_id ) + + if reset_reapply_type + reapply_type = SYMBOL_TO_RESET_REAPPLY_TYPE[reset_reapply_type] + raise Client::ArgumentError, 'Unknown reset_reapply_type specified' unless reapply_type + + request.reset_reapply_type = reapply_type + end + client.reset_workflow_execution(request) end diff --git a/lib/temporal/reset_reapply_type.rb b/lib/temporal/reset_reapply_type.rb new file mode 100644 index 00000000..29a489d4 --- /dev/null +++ b/lib/temporal/reset_reapply_type.rb @@ -0,0 +1,6 @@ +module Temporal + module ResetReapplyType + SIGNAL = :signal + NONE = :none + end +end diff --git a/spec/unit/lib/temporal/client_spec.rb b/spec/unit/lib/temporal/client_spec.rb index 66ec3bf4..315f6c8d 100644 --- a/spec/unit/lib/temporal/client_spec.rb +++ b/spec/unit/lib/temporal/client_spec.rb @@ -4,6 +4,7 @@ require 'temporal/workflow' require 'temporal/workflow/history' require 'temporal/connection/grpc' +require 'temporal/reset_reapply_type' describe Temporal::Client do subject { described_class.new(config) } @@ -610,7 +611,32 @@ class NamespacedWorkflow < Temporal::Workflow workflow_id: '123', run_id: '1234', reason: 'Test reset', - workflow_task_event_id: workflow_task_id + workflow_task_event_id: workflow_task_id, + # The request ID will be a random UUID: + request_id: anything, + reset_reapply_type: :signal + ) + end + + it 'passes through request_id and reset_reapply_type' do + subject.reset_workflow( + 'default-test-namespace', + '123', + '1234', + workflow_task_id: workflow_task_id, + reason: 'Test reset', + request_id: 'foo', + reset_reapply_type: Temporal::ResetReapplyType::SIGNAL + ) + + expect(connection).to have_received(:reset_workflow_execution).with( + namespace: 'default-test-namespace', + workflow_id: '123', + run_id: '1234', + reason: 'Test reset', + workflow_task_event_id: workflow_task_id, + request_id: 'foo', + reset_reapply_type: :signal ) end @@ -635,7 +661,10 @@ class NamespacedWorkflow < Temporal::Workflow workflow_id: workflow_id, run_id: run_id, reason: 'manual reset', - workflow_task_event_id: 16 + workflow_task_event_id: 16, + # The request ID will be a random UUID: + request_id: instance_of(String), + reset_reapply_type: :signal ) end end @@ -664,7 +693,10 @@ class NamespacedWorkflow < Temporal::Workflow workflow_id: workflow_id, run_id: run_id, reason: 'manual reset', - workflow_task_event_id: 16 + workflow_task_event_id: 16, + # The request ID will be a random UUID: + request_id: instance_of(String), + reset_reapply_type: :signal ) end end @@ -678,7 +710,10 @@ class NamespacedWorkflow < Temporal::Workflow workflow_id: workflow_id, run_id: run_id, reason: 'manual reset', - workflow_task_event_id: 4 + workflow_task_event_id: 4, + # The request ID will be a random UUID: + request_id: instance_of(String), + reset_reapply_type: :signal ) end end @@ -693,7 +728,10 @@ class NamespacedWorkflow < Temporal::Workflow workflow_id: workflow_id, run_id: run_id, reason: 'manual reset', - workflow_task_event_id: 10 + workflow_task_event_id: 10, + # The request ID will be a random UUID: + request_id: instance_of(String), + reset_reapply_type: :signal ) end end From 80f00631a0254508e23defac0d50a98af62301ed Mon Sep 17 00:00:00 2001 From: cj-cb <106132555+cj-cb@users.noreply.github.com> Date: Mon, 17 Jul 2023 09:08:50 -0700 Subject: [PATCH 097/125] Update version to 0.0.3 (#257) Co-authored-by: cj-cb --- lib/temporal/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/temporal/version.rb b/lib/temporal/version.rb index 22474736..dde4f73c 100644 --- a/lib/temporal/version.rb +++ b/lib/temporal/version.rb @@ -1,3 +1,3 @@ module Temporal - VERSION = '0.0.2'.freeze + VERSION = '0.0.3'.freeze end From 4a379c182c0f2da23cee5817a003a0b1b5791008 Mon Sep 17 00:00:00 2001 From: Jeff Schoner Date: Mon, 11 Sep 2023 06:59:00 -0700 Subject: [PATCH 098/125] Signals first ordering (#261) * Correct uspert -> upsert * Signals first ordering w/ config, flags * Tests and fabricators * rubyfmt substantially modified files * Integration test for signal fix * Check for supported server version for signals first ordering * Add safe rollout instructions to change log * Clean up styling * Factor out flags used function in state manager * Refactor capabilities for lazy loading * Refactor how HANDLE_SIGNALS_FIRST SDK flag is managed * Require set --- CHANGELOG.md | 24 +- examples/bin/worker | 1 + examples/spec/integration/signal_spec.rb | 40 +++ examples/workflows/signal_workflow.rb | 12 + lib/temporal/capabilities.rb | 30 ++ lib/temporal/configuration.rb | 17 +- lib/temporal/connection/grpc.rb | 90 +++--- lib/temporal/errors.rb | 6 + lib/temporal/version.rb | 2 +- lib/temporal/worker.rb | 19 +- lib/temporal/workflow/executor.rb | 14 +- lib/temporal/workflow/history/window.rb | 14 +- lib/temporal/workflow/poller.rb | 25 +- lib/temporal/workflow/sdk_flags.rb | 12 + lib/temporal/workflow/state_manager.rb | 128 ++++++-- lib/temporal/workflow/task_processor.rb | 57 ++-- .../grpc/get_system_info_fabricator.rb | 10 + .../grpc/history_event_fabricator.rb | 32 +- spec/unit/lib/temporal/grpc_spec.rb | 5 +- spec/unit/lib/temporal/worker_spec.rb | 23 +- .../lib/temporal/workflow/executor_spec.rb | 93 ++++-- .../lib/temporal/workflow/history_spec.rb | 7 - .../temporal/workflow/state_manager_spec.rb | 296 +++++++++++++++++- .../temporal/workflow/task_processor_spec.rb | 32 +- 24 files changed, 788 insertions(+), 201 deletions(-) create mode 100644 examples/spec/integration/signal_spec.rb create mode 100644 examples/workflows/signal_workflow.rb create mode 100644 lib/temporal/capabilities.rb create mode 100644 lib/temporal/workflow/sdk_flags.rb create mode 100644 spec/fabricators/grpc/get_system_info_fabricator.rb delete mode 100644 spec/unit/lib/temporal/workflow/history_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index e604aca7..47d336b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,24 @@ # Changelog -## 0.0.1 -- First release +## 0.1.0 + +This introduces signal first ordering. See https://github.com/coinbase/temporal-ruby/issues/258 for +details on why this is necessary for correct handling of signals. + +**IMPORTANT: ** This feature requires Temporal server 1.20.0 or newer. If you are running an older +version of the server, you must either upgrade to at least this version, or you can set the +`.legacy_signals` configuration option to true until you can upgrade. + +If you do not have existing workflows with signals running or are standing up a worker service +for the first time, you can ignore all the below instructions. + +If you have any workflows with signals running during a deployment and run more than one worker +process, you must follow these rollout steps to avoid non-determinism errors: +1. Set `.legacy_signals` in `Temporal::Configuration` to true +2. Deploy your worker +3. Remove the `.legacy_signals` setting or set it to `false` +4. Deploy your worker + +These steps ensure any workflow that executes in signals first mode will continue to be executed +in this order on replay. If you don't follow these steps, you may see failed workflow tasks, which +in some cases could result in unrecoverable history corruption. \ No newline at end of file diff --git a/examples/bin/worker b/examples/bin/worker index cead588d..16633673 100755 --- a/examples/bin/worker +++ b/examples/bin/worker @@ -64,6 +64,7 @@ worker.register_workflow(SendSignalToExternalWorkflow) worker.register_workflow(SerialHelloWorldWorkflow) worker.register_workflow(SideEffectWorkflow) worker.register_workflow(SignalWithStartWorkflow) +worker.register_workflow(SignalWorkflow) worker.register_workflow(SimpleTimerWorkflow) worker.register_workflow(SlowChildWorkflow) worker.register_workflow(StartChildWorkflowWorkflow) diff --git a/examples/spec/integration/signal_spec.rb b/examples/spec/integration/signal_spec.rb new file mode 100644 index 00000000..4789219a --- /dev/null +++ b/examples/spec/integration/signal_spec.rb @@ -0,0 +1,40 @@ +require 'securerandom' +require 'workflows/signal_workflow' + +describe 'signal' do + it 'all signals process' do + workflow_id = SecureRandom.uuid + expected_score = 7 + run_id = Temporal.start_workflow( + SignalWorkflow, + 1, # seconds + options: { + workflow_id: workflow_id, + signal_name: 'score', + signal_input: expected_score, + timeouts: { execution: 10 } + } + ) + + loop do + value = SecureRandom.rand(10) + + begin + Temporal.signal_workflow(SignalWorkflow, 'score', workflow_id, run_id, value) + rescue StandardError + # Keep going until there's an error such as the workflow finishing + break + end + expected_score += value + sleep 0.01 + end + + result = Temporal.await_workflow_result( + SignalWorkflow, + workflow_id: workflow_id, + run_id: run_id + ) + + expect(result).to eq(expected_score) + end +end diff --git a/examples/workflows/signal_workflow.rb b/examples/workflows/signal_workflow.rb new file mode 100644 index 00000000..d665533d --- /dev/null +++ b/examples/workflows/signal_workflow.rb @@ -0,0 +1,12 @@ +class SignalWorkflow < Temporal::Workflow + def execute(sleep_for) + score = 0 + workflow.on_signal('score') do |signal_value| + score += signal_value + end + + workflow.sleep(sleep_for) + + score + end +end diff --git a/lib/temporal/capabilities.rb b/lib/temporal/capabilities.rb new file mode 100644 index 00000000..644aac31 --- /dev/null +++ b/lib/temporal/capabilities.rb @@ -0,0 +1,30 @@ +require 'temporal/errors' + +module Temporal + class Capabilities + def initialize(config) + @config = config + @sdk_metadata = nil + end + + def sdk_metadata + set_capabilities if @sdk_metadata.nil? + + @sdk_metadata + end + + private + + def set_capabilities + connection = Temporal::Connection.generate(@config.for_connection) + system_info = connection.get_system_info + + @sdk_metadata = system_info&.capabilities&.sdk_metadata || false + + Temporal.logger.debug( + "Connected to Temporal server running version #{system_info.server_version}. " \ + "SDK Metadata supported: #{@sdk_metadata}" + ) + end + end +end diff --git a/lib/temporal/configuration.rb b/lib/temporal/configuration.rb index df26315b..b414d76e 100644 --- a/lib/temporal/configuration.rb +++ b/lib/temporal/configuration.rb @@ -1,3 +1,4 @@ +require 'temporal/capabilities' require 'temporal/logger' require 'temporal/metrics_adapters/null' require 'temporal/middleware/header_propagator_chain' @@ -14,10 +15,10 @@ class Configuration Connection = Struct.new(:type, :host, :port, :credentials, :identity, keyword_init: true) Execution = Struct.new(:namespace, :task_queue, :timeouts, :headers, :search_attributes, keyword_init: true) - attr_reader :timeouts, :error_handlers + attr_reader :timeouts, :error_handlers, :capabilities attr_accessor :connection_type, :converter, :use_error_serialization_v2, :host, :port, :credentials, :identity, :logger, :metrics_adapter, :namespace, :task_queue, :headers, :search_attributes, :header_propagators, - :payload_codec + :payload_codec, :legacy_signals # See https://docs.temporal.io/blog/activity-timeouts/ for general docs. # We want an infinite execution timeout for cron schedules and other perpetual workflows. @@ -57,7 +58,7 @@ class Configuration Temporal::Connection::Converter::Payload::JSON.new ] ).freeze - + # The Payload Codec is an optional step that happens between the wire and the Payload Converter: # Temporal Server <--> Wire <--> Payload Codec <--> Payload Converter <--> User code # which can be useful for transformations such as compression and encryption @@ -82,6 +83,15 @@ def initialize @identity = nil @search_attributes = {} @header_propagators = [] + @capabilities = Capabilities.new(self) + + # Signals previously were incorrectly replayed in order within a workflow task window, rather + # than at the beginning. Correcting this changes the determinism of any workflow with signals. + # This flag exists to force this legacy behavior to gradually roll out the new ordering. + # Because this feature depends on the SDK Metadata capability which only became available + # in Temporal server 1.20, it is ignored when connected to older versions and effectively + # treated as true. + @legacy_signals = false end def on_error(&block) @@ -122,6 +132,7 @@ def default_execution_options def add_header_propagator(propagator_class, *args) raise 'header propagator must implement `def inject!(headers)`' unless propagator_class.method_defined? :inject! + @header_propagators << Middleware::Entry.new(propagator_class, args) end diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index 092faf96..3c206160 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -21,7 +21,7 @@ class GRPC HISTORY_EVENT_FILTER = { all: Temporalio::Api::Enums::V1::HistoryEventFilterType::HISTORY_EVENT_FILTER_TYPE_ALL_EVENT, - close: Temporalio::Api::Enums::V1::HistoryEventFilterType::HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT, + close: Temporalio::Api::Enums::V1::HistoryEventFilterType::HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT }.freeze QUERY_REJECT_CONDITION = { @@ -37,7 +37,7 @@ class GRPC double: Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_DOUBLE, bool: Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_BOOL, datetime: Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_DATETIME, - keyword_list: Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_KEYWORD_LIST, + keyword_list: Temporalio::Api::Enums::V1::IndexedValueType::INDEXED_VALUE_TYPE_KEYWORD_LIST }.freeze INDEXED_VALUE_TYPE_TO_SYMBOL = SYMBOL_TO_INDEXED_VALUE_TYPE.map do |symbol, int_value| @@ -46,7 +46,7 @@ class GRPC SYMBOL_TO_RESET_REAPPLY_TYPE = { signal: Temporalio::Api::Enums::V1::ResetReapplyType::RESET_REAPPLY_TYPE_SIGNAL, - none: Temporalio::Api::Enums::V1::ResetReapplyType::RESET_REAPPLY_TYPE_NONE, + none: Temporalio::Api::Enums::V1::ResetReapplyType::RESET_REAPPLY_TYPE_NONE } DEFAULT_OPTIONS = { @@ -73,7 +73,7 @@ def register_namespace(name:, description: nil, is_global: false, retention_peri workflow_execution_retention_period: Google::Protobuf::Duration.new( seconds: (retention_period * 24 * 60 * 60).to_i ), - data: data, + data: data ) client.register_namespace(request) rescue ::GRPC::AlreadyExists => e @@ -85,8 +85,9 @@ def describe_namespace(name:) client.describe_namespace(request) end - def list_namespaces(page_size:, next_page_token: "") - request = Temporalio::Api::WorkflowService::V1::ListNamespacesRequest.new(page_size: page_size, next_page_token: next_page_token) + def list_namespaces(page_size:, next_page_token: '') + request = Temporalio::Api::WorkflowService::V1::ListNamespacesRequest.new(page_size: page_size, + next_page_token: next_page_token) client.list_namespaces(request) end @@ -110,10 +111,10 @@ def start_workflow_execution( workflow_id:, workflow_name:, task_queue:, - input: nil, execution_timeout:, run_timeout:, task_timeout:, + input: nil, workflow_id_reuse_policy: nil, headers: nil, cron_schedule: nil, @@ -145,7 +146,7 @@ def start_workflow_execution( ), search_attributes: Temporalio::Api::Common::V1::SearchAttributes.new( indexed_fields: to_payload_map_without_codec(search_attributes || {}) - ), + ) ) client.start_workflow_execution(request) @@ -169,11 +170,10 @@ def get_workflow_execution_history( if wait_for_new_event if timeout.nil? # This is an internal error. Wrappers should enforce this. - raise "You must specify a timeout when wait_for_new_event = true." + raise 'You must specify a timeout when wait_for_new_event = true.' elsif timeout > SERVER_MAX_GET_WORKFLOW_EXECUTION_HISTORY_POLL - raise ClientError.new( - "You may not specify a timeout of more than #{SERVER_MAX_GET_WORKFLOW_EXECUTION_HISTORY_POLL} seconds, got: #{timeout}." - ) + raise ClientError, + "You may not specify a timeout of more than #{SERVER_MAX_GET_WORKFLOW_EXECUTION_HISTORY_POLL} seconds, got: #{timeout}." end end request = Temporalio::Api::WorkflowService::V1::GetWorkflowExecutionHistoryRequest.new( @@ -202,6 +202,7 @@ def poll_workflow_task_queue(namespace:, task_queue:, binary_checksum:) poll_mutex.synchronize do return unless can_poll? + @poll_request = client.poll_workflow_task_queue(request, return_op: true) end @@ -215,20 +216,26 @@ def respond_query_task_completed(namespace:, task_token:, query_result:) namespace: namespace, completed_type: query_result_proto.result_type, query_result: query_result_proto.answer, - error_message: query_result_proto.error_message, + error_message: query_result_proto.error_message ) client.respond_query_task_completed(request) end - def respond_workflow_task_completed(namespace:, task_token:, commands:, binary_checksum:, query_results: {}) + def respond_workflow_task_completed(namespace:, task_token:, commands:, binary_checksum:, new_sdk_flags_used:, query_results: {}) request = Temporalio::Api::WorkflowService::V1::RespondWorkflowTaskCompletedRequest.new( namespace: namespace, identity: identity, task_token: task_token, commands: Array(commands).map { |(_, command)| Serializer.serialize(command) }, query_results: query_results.transform_values { |value| Serializer.serialize(value) }, - binary_checksum: binary_checksum + binary_checksum: binary_checksum, + sdk_metadata: if new_sdk_flags_used.any? + Temporalio::Api::Sdk::V1::WorkflowTaskCompletedMetadata.new( + lang_used_flags: new_sdk_flags_used.to_a + ) + # else nil + end ) client.respond_workflow_task_completed(request) @@ -257,6 +264,7 @@ def poll_activity_task_queue(namespace:, task_queue:) poll_mutex.synchronize do return unless can_poll? + @poll_request = client.poll_activity_task_queue(request, return_op: true) end @@ -282,7 +290,7 @@ def respond_activity_task_completed(namespace:, task_token:, result:) namespace: namespace, identity: identity, task_token: task_token, - result: to_result_payloads(result), + result: to_result_payloads(result) ) client.respond_activity_task_completed(request) end @@ -359,27 +367,22 @@ def signal_with_start_workflow_execution( workflow_id:, workflow_name:, task_queue:, - input: nil, - execution_timeout:, - run_timeout:, - task_timeout:, + execution_timeout:, run_timeout:, task_timeout:, signal_name:, signal_input:, input: nil, workflow_id_reuse_policy: nil, headers: nil, cron_schedule: nil, - signal_name:, - signal_input:, memo: nil, search_attributes: nil ) proto_header_fields = if headers.nil? - to_payload_map({}) - elsif headers.class == Hash - to_payload_map(headers) - else - # Preserve backward compatability for headers specified using proto objects - warn '[DEPRECATION] Specify headers using a hash rather than protobuf objects' - headers - end + to_payload_map({}) + elsif headers.instance_of?(Hash) + to_payload_map(headers) + else + # Preserve backward compatability for headers specified using proto objects + warn '[DEPRECATION] Specify headers using a hash rather than protobuf objects' + headers + end request = Temporalio::Api::WorkflowService::V1::SignalWithStartWorkflowExecutionRequest.new( identity: identity, @@ -398,7 +401,7 @@ def signal_with_start_workflow_execution( workflow_task_timeout: task_timeout, request_id: SecureRandom.uuid, header: Temporalio::Api::Common::V1::Header.new( - fields: proto_header_fields, + fields: proto_header_fields ), cron_schedule: cron_schedule, signal_name: signal_name, @@ -408,7 +411,7 @@ def signal_with_start_workflow_execution( ), search_attributes: Temporalio::Api::Common::V1::SearchAttributes.new( indexed_fields: to_payload_map_without_codec(search_attributes || {}) - ), + ) ) client.signal_with_start_workflow_execution(request) @@ -419,7 +422,7 @@ def reset_workflow_execution(namespace:, workflow_id:, run_id:, reason:, workflo namespace: namespace, workflow_execution: Temporalio::Api::Common::V1::WorkflowExecution.new( workflow_id: workflow_id, - run_id: run_id, + run_id: run_id ), reason: reason, workflow_task_finish_event_id: workflow_task_event_id, @@ -448,7 +451,7 @@ def terminate_workflow_execution( namespace: namespace, workflow_execution: Temporalio::Api::Common::V1::WorkflowExecution.new( workflow_id: workflow_id, - run_id: run_id, + run_id: run_id ), reason: reason, details: to_details_payloads(details) @@ -512,9 +515,8 @@ def add_custom_search_attributes(attributes, namespace) attributes.each_value do |symbol_type| next if SYMBOL_TO_INDEXED_VALUE_TYPE.include?(symbol_type) - raise Temporal::InvalidSearchAttributeTypeFailure.new( - "Cannot add search attributes (#{attributes}): unknown search attribute type :#{symbol_type}, supported types: #{SYMBOL_TO_INDEXED_VALUE_TYPE.keys}" - ) + raise Temporal::InvalidSearchAttributeTypeFailure, + "Cannot add search attributes (#{attributes}): unknown search attribute type :#{symbol_type}, supported types: #{SYMBOL_TO_INDEXED_VALUE_TYPE.keys}" end request = Temporalio::Api::OperatorService::V1::AddSearchAttributesRequest.new( @@ -524,12 +526,12 @@ def add_custom_search_attributes(attributes, namespace) begin operator_client.add_search_attributes(request) rescue ::GRPC::AlreadyExists => e - raise Temporal::SearchAttributeAlreadyExistsFailure.new(e) + raise Temporal::SearchAttributeAlreadyExistsFailure, e rescue ::GRPC::Internal => e # The internal workflow that adds search attributes can fail for a variety of reasons such # as recreating a removed attribute with a new type. Wrap these all up into a fall through # exception. - raise Temporal::SearchAttributeFailure.new(e) + raise Temporal::SearchAttributeFailure, e end end @@ -549,7 +551,7 @@ def remove_custom_search_attributes(attribute_names, namespace) begin operator_client.remove_search_attributes(request) rescue ::GRPC::NotFound => e - raise Temporal::NotFoundFailure.new(e) + raise Temporal::NotFoundFailure, e end end @@ -622,6 +624,10 @@ def cancel_polling_request end end + def get_system_info + client.get_system_info(Temporalio::Api::WorkflowService::V1::GetSystemInfoRequest.new) + end + private attr_reader :url, :identity, :credentials, :options, :poll_mutex, :poll_request @@ -631,7 +637,7 @@ def client url, credentials, timeout: CONNECTION_TIMEOUT_SECONDS, - interceptors: [ ClientNameVersionInterceptor.new() ] + interceptors: [ClientNameVersionInterceptor.new] ) end @@ -640,7 +646,7 @@ def operator_client url, credentials, timeout: CONNECTION_TIMEOUT_SECONDS, - interceptors: [ ClientNameVersionInterceptor.new() ] + interceptors: [ClientNameVersionInterceptor.new] ) end diff --git a/lib/temporal/errors.rb b/lib/temporal/errors.rb index 7aa11405..a13ada62 100644 --- a/lib/temporal/errors.rb +++ b/lib/temporal/errors.rb @@ -9,6 +9,9 @@ class InternalError < Error; end # a non-deterministic workflow implementation or the gem's bug class NonDeterministicWorkflowError < InternalError; end + # Indicates a workflow task was encountered that used an unknown SDK flag + class UnknownSDKFlagError < InternalError; end + # Superclass for misconfiguration/misuse on the client (user) side class ClientError < Error; end @@ -49,8 +52,10 @@ class WorkflowCanceled < WorkflowError; end # Errors where the workflow run didn't complete but not an error for the whole workflow. class WorkflowRunError < Error; end + class WorkflowRunContinuedAsNew < WorkflowRunError attr_reader :new_run_id + def initialize(new_run_id:) super @new_run_id = new_run_id @@ -72,6 +77,7 @@ def initialize(message, run_id = nil) @run_id = run_id end end + class NamespaceNotActiveFailure < ApiError; end class ClientVersionNotSupportedFailure < ApiError; end class FeatureVersionNotSupportedFailure < ApiError; end diff --git a/lib/temporal/version.rb b/lib/temporal/version.rb index dde4f73c..87781382 100644 --- a/lib/temporal/version.rb +++ b/lib/temporal/version.rb @@ -1,3 +1,3 @@ module Temporal - VERSION = '0.0.3'.freeze + VERSION = '0.1.0'.freeze end diff --git a/lib/temporal/worker.rb b/lib/temporal/worker.rb index 2a232458..5d84df6e 100644 --- a/lib/temporal/worker.rb +++ b/lib/temporal/worker.rb @@ -1,3 +1,4 @@ +require 'temporal/errors' require 'temporal/workflow/poller' require 'temporal/activity/poller' require 'temporal/execution_options' @@ -65,9 +66,9 @@ def register_dynamic_workflow(workflow_class, options = {}) @workflows[namespace_and_task_queue].add_dynamic(execution_options.name, workflow_class) rescue Temporal::ExecutableLookup::SecondDynamicExecutableError => e raise Temporal::SecondDynamicWorkflowError, - "Temporal::Worker#register_dynamic_workflow: cannot register #{execution_options.name} "\ - "dynamically; #{e.previous_executable_name} was already registered dynamically for task queue "\ - "'#{execution_options.task_queue}', and there can be only one." + "Temporal::Worker#register_dynamic_workflow: cannot register #{execution_options.name} "\ + "dynamically; #{e.previous_executable_name} was already registered dynamically for task queue "\ + "'#{execution_options.task_queue}', and there can be only one." end end @@ -86,9 +87,9 @@ def register_dynamic_activity(activity_class, options = {}) @activities[namespace_and_task_queue].add_dynamic(execution_options.name, activity_class) rescue Temporal::ExecutableLookup::SecondDynamicExecutableError => e raise Temporal::SecondDynamicActivityError, - "Temporal::Worker#register_dynamic_activity: cannot register #{execution_options.name} "\ - "dynamically; #{e.previous_executable_name} was already registered dynamically for task queue "\ - "'#{execution_options.task_queue}', and there can be only one." + "Temporal::Worker#register_dynamic_activity: cannot register #{execution_options.name} "\ + "dynamically; #{e.previous_executable_name} was already registered dynamically for task queue "\ + "'#{execution_options.task_queue}', and there can be only one." end end @@ -123,7 +124,7 @@ def start on_started_hook # keep the main thread alive - sleep 1 while !shutting_down? + sleep 1 until shutting_down? end def stop @@ -158,7 +159,8 @@ def while_stopping_hook; end def on_stopped_hook; end def workflow_poller_for(namespace, task_queue, lookup) - Workflow::Poller.new(namespace, task_queue, lookup.freeze, config, workflow_task_middleware, workflow_middleware, workflow_poller_options) + Workflow::Poller.new(namespace, task_queue, lookup.freeze, config, workflow_task_middleware, workflow_middleware, + workflow_poller_options) end def activity_poller_for(namespace, task_queue, lookup) @@ -176,6 +178,5 @@ def trap_signals Signal.trap(signal) { stop } end end - end end diff --git a/lib/temporal/workflow/executor.rb b/lib/temporal/workflow/executor.rb index b11aa328..762ae250 100644 --- a/lib/temporal/workflow/executor.rb +++ b/lib/temporal/workflow/executor.rb @@ -11,16 +11,19 @@ module Temporal class Workflow class Executor + RunResult = Struct.new(:commands, :new_sdk_flags_used, keyword_init: true) + # @param workflow_class [Class] # @param history [Workflow::History] # @param task_metadata [Metadata::WorkflowTask] # @param config [Configuration] # @param track_stack_trace [Boolean] + # @return [RunResult] def initialize(workflow_class, history, task_metadata, config, track_stack_trace, middleware_chain) @workflow_class = workflow_class @dispatcher = Dispatcher.new @query_registry = QueryRegistry.new - @state_manager = StateManager.new(dispatcher) + @state_manager = StateManager.new(dispatcher, config) @history = history @task_metadata = task_metadata @config = config @@ -39,7 +42,7 @@ def run state_manager.apply(window) end - return state_manager.commands + RunResult.new(commands: state_manager.commands, new_sdk_flags_used: state_manager.new_sdk_flags_used) end # Process queries using the pre-registered query handlers @@ -63,13 +66,14 @@ def process_query(query) result = query_registry.handle(query.query_type, query.query_args) QueryResult.answer(result) - rescue StandardError => error - QueryResult.failure(error) + rescue StandardError => e + QueryResult.failure(e) end def execute_workflow(input, workflow_started_event) metadata = Metadata.generate_workflow_metadata(workflow_started_event, task_metadata) - context = Workflow::Context.new(state_manager, dispatcher, workflow_class, metadata, config, query_registry, track_stack_trace) + context = Workflow::Context.new(state_manager, dispatcher, workflow_class, metadata, config, query_registry, + track_stack_trace) Fiber.new do middleware_chain.invoke(metadata) do diff --git a/lib/temporal/workflow/history/window.rb b/lib/temporal/workflow/history/window.rb index 944c8d25..83112feb 100644 --- a/lib/temporal/workflow/history/window.rb +++ b/lib/temporal/workflow/history/window.rb @@ -1,15 +1,18 @@ +require 'set' +require 'temporal/workflow/sdk_flags' + module Temporal class Workflow class History class Window - attr_reader :local_time, :last_event_id, :events, :markers + attr_reader :local_time, :last_event_id, :events, :sdk_flags def initialize @local_time = nil @last_event_id = nil @events = [] - @markers = [] @replay = false + @sdk_flags = Set.new end def replay? @@ -18,8 +21,6 @@ def replay? def add(event) case event.type - when 'MARKER_RECORDED' - markers << event when 'WORKFLOW_TASK_STARTED' @last_event_id = event.id + 1 # one for completed @local_time = event.timestamp @@ -28,6 +29,11 @@ def add(event) @local_time = nil when 'WORKFLOW_TASK_COMPLETED' @replay = true + used_flags = Set.new(event.attributes&.sdk_metadata&.lang_used_flags) + unknown_flags = used_flags.difference(SDKFlags::ALL) + raise Temporal::UnknownSDKFlagError, "Unknown SDK flags: #{unknown_flags.join(',')}" if unknown_flags.any? + + used_flags.each { |flag| sdk_flags.add(flag) } when 'WORKFLOW_TASK_SCHEDULED' # no-op else diff --git a/lib/temporal/workflow/poller.rb b/lib/temporal/workflow/poller.rb index 07162ce1..0ec5aaba 100644 --- a/lib/temporal/workflow/poller.rb +++ b/lib/temporal/workflow/poller.rb @@ -41,8 +41,8 @@ def cancel_pending_requests end def wait - if !shutting_down? - raise "Workflow poller waiting for shutdown completion without being in shutting_down state!" + unless shutting_down? + raise 'Workflow poller waiting for shutdown completion without being in shutting_down state!' end thread.join @@ -51,7 +51,8 @@ def wait private - attr_reader :namespace, :task_queue, :connection, :workflow_lookup, :config, :middleware, :workflow_middleware, :options, :thread + attr_reader :namespace, :task_queue, :connection, :workflow_lookup, :config, :middleware, :workflow_middleware, + :options, :thread def connection @connection ||= Temporal::Connection.generate(config.for_connection) @@ -71,8 +72,9 @@ def poll_loop return if shutting_down? time_diff_ms = ((Time.now - last_poll_time) * 1000).round - Temporal.metrics.timing(Temporal::MetricKeys::WORKFLOW_POLLER_TIME_SINCE_LAST_POLL, time_diff_ms, metrics_tags) - Temporal.logger.debug("Polling workflow task queue", { namespace: namespace, task_queue: task_queue }) + Temporal.metrics.timing(Temporal::MetricKeys::WORKFLOW_POLLER_TIME_SINCE_LAST_POLL, time_diff_ms, + metrics_tags) + Temporal.logger.debug('Polling workflow task queue', { namespace: namespace, task_queue: task_queue }) task = poll_for_task last_poll_time = Time.now @@ -89,13 +91,15 @@ def poll_loop end def poll_for_task - connection.poll_workflow_task_queue(namespace: namespace, task_queue: task_queue, binary_checksum: binary_checksum) + connection.poll_workflow_task_queue(namespace: namespace, task_queue: task_queue, + binary_checksum: binary_checksum) rescue ::GRPC::Cancelled # We're shutting down and we've already reported that in the logs nil - rescue StandardError => error - Temporal.logger.error("Unable to poll Workflow task queue", { namespace: namespace, task_queue: task_queue, error: error.inspect }) - Temporal::ErrorHandler.handle(error, config) + rescue StandardError => e + Temporal.logger.error('Unable to poll Workflow task queue', + { namespace: namespace, task_queue: task_queue, error: e.inspect }) + Temporal::ErrorHandler.handle(e, config) sleep(poll_retry_seconds) @@ -106,7 +110,8 @@ def process(task) middleware_chain = Middleware::Chain.new(middleware) workflow_middleware_chain = Middleware::Chain.new(workflow_middleware) - TaskProcessor.new(task, namespace, workflow_lookup, middleware_chain, workflow_middleware_chain, config, binary_checksum).process + TaskProcessor.new(task, namespace, workflow_lookup, middleware_chain, workflow_middleware_chain, config, + binary_checksum).process end def thread_pool diff --git a/lib/temporal/workflow/sdk_flags.rb b/lib/temporal/workflow/sdk_flags.rb new file mode 100644 index 00000000..6b24fe05 --- /dev/null +++ b/lib/temporal/workflow/sdk_flags.rb @@ -0,0 +1,12 @@ +require 'set' + +module Temporal + class Workflow + module SDKFlags + HANDLE_SIGNALS_FIRST = 1 + + # Make sure to include all known flags here + ALL = Set.new([HANDLE_SIGNALS_FIRST]) + end + end +end diff --git a/lib/temporal/workflow/state_manager.rb b/lib/temporal/workflow/state_manager.rb index 6fbf8983..62302ac2 100644 --- a/lib/temporal/workflow/state_manager.rb +++ b/lib/temporal/workflow/state_manager.rb @@ -5,6 +5,7 @@ require 'temporal/workflow/history/event_target' require 'temporal/concerns/payloads' require 'temporal/workflow/errors' +require 'temporal/workflow/sdk_flags' require 'temporal/workflow/signal' module Temporal @@ -18,9 +19,9 @@ class StateManager class UnsupportedEvent < Temporal::InternalError; end class UnsupportedMarkerType < Temporal::InternalError; end - attr_reader :commands, :local_time, :search_attributes + attr_reader :commands, :local_time, :search_attributes, :new_sdk_flags_used - def initialize(dispatcher) + def initialize(dispatcher, config) @dispatcher = dispatcher @commands = [] @marker_ids = Set.new @@ -31,6 +32,13 @@ def initialize(dispatcher) @local_time = nil @replay = false @search_attributes = {} + @config = config + + # Current flags in use, built up from workflow task completed history entries + @sdk_flags = Set.new + + # New flags used when not replaying + @new_sdk_flags_used = Set.new end def replay? @@ -41,9 +49,7 @@ def schedule(command) # Fast-forward event IDs to skip all the markers (version markers can # be removed, so we can't rely on them being scheduled during a replay) command_id = next_event_id - while marker_ids.include?(command_id) do - command_id = next_event_id - end + command_id = next_event_id while marker_ids.include?(command_id) cancelation_id = case command @@ -66,7 +72,7 @@ def schedule(command) validate_append_command(command) commands << [command_id, command] - return [event_target_from(command_id, command), cancelation_id] + [event_target_from(command_id, command), cancelation_id] end def release?(release_name) @@ -83,20 +89,76 @@ def apply(history_window) @replay = history_window.replay? @local_time = history_window.local_time @last_event_id = history_window.last_event_id + history_window.sdk_flags.each { |flag| sdk_flags.add(flag) } - # handle markers first since their data is needed for processing events - history_window.markers.each do |event| + order_events(history_window.events).each do |event| apply_event(event) end + end - history_window.events.each do |event| - apply_event(event) + def self.event_order(event, signals_first) + if event.type == 'MARKER_RECORDED' + # markers always come first + 0 + elsif event.type == 'WORKFLOW_EXECUTION_STARTED' + # This always comes next if present + 1 + elsif signals_first && signal_event?(event) + # signals come next if we are in signals first mode + 2 + else + # then everything else + 3 end end + def self.signal_event?(event) + event.type == 'WORKFLOW_EXECUTION_SIGNALED' + end + private - attr_reader :dispatcher, :command_tracker, :marker_ids, :side_effects, :releases + attr_reader :dispatcher, :command_tracker, :marker_ids, :side_effects, :releases, :sdk_flags + + def use_signals_first(raw_events) + if sdk_flags.include?(SDKFlags::HANDLE_SIGNALS_FIRST) + # If signals were handled first when this task or a previous one in this run were first + # played, we must continue to do so in order to ensure determinism regardless of what + # the configuration value is set to. Even the capabilities can be ignored because the + # server must have returned SDK metadata for this to be true. + true + elsif raw_events.any? { |event| StateManager.signal_event?(event) } && + # If this is being played for the first time, use the configuration flag to choose + (!replay? && !@config.legacy_signals) && + # In order to preserve determinism, the server must support SDK metadata to order signals + # first. This is checked last because it will result in a Temporal server call the first + # time it's called in the worker process. + @config.capabilities.sdk_metadata + report_flag_used(SDKFlags::HANDLE_SIGNALS_FIRST) + true + else + false + end + end + + def order_events(raw_events) + signals_first = use_signals_first(raw_events) + + raw_events.sort_by.with_index do |event, index| + # sort_by is not stable, so include index to preserve order + [StateManager.event_order(event, signals_first), index] + end + end + + def report_flag_used(flag) + # Only add the flag if it's not already present and we are not replaying + if !replay? && + !sdk_flags.include?(flag) && + !new_sdk_flags_used.include?(flag) + new_sdk_flags_used << flag + sdk_flags << flag + end + end def next_event_id @last_event_id += 1 @@ -104,22 +166,21 @@ def next_event_id def validate_append_command(command) return if commands.last.nil? + _, previous_command = commands.last case previous_command when Command::CompleteWorkflow, Command::FailWorkflow, Command::ContinueAsNew context_string = case previous_command - when Command::CompleteWorkflow - "The workflow completed" - when Command::FailWorkflow - "The workflow failed" - when Command::ContinueAsNew - "The workflow continued as new" - end - raise Temporal::WorkflowAlreadyCompletingError.new( - "You cannot do anything in a Workflow after it completes. #{context_string}, "\ + when Command::CompleteWorkflow + 'The workflow completed' + when Command::FailWorkflow + 'The workflow failed' + when Command::ContinueAsNew + 'The workflow continued as new' + end + raise Temporal::WorkflowAlreadyCompletingError, "You cannot do anything in a Workflow after it completes. #{context_string}, "\ "but then it sent a new command: #{command.class}. This can happen, for example, if you've "\ - "not waited for all of your Activity futures before finishing the Workflow." - ) + 'not waited for all of your Activity futures before finishing the Workflow.' end end @@ -138,7 +199,7 @@ def apply_event(event) History::EventTarget.workflow, 'started', from_payloads(event.attributes.input), - event, + event ) when 'WORKFLOW_EXECUTION_COMPLETED' @@ -178,7 +239,8 @@ def apply_event(event) when 'ACTIVITY_TASK_FAILED' state_machine.fail - dispatch(history_target, 'failed', Temporal::Workflow::Errors.generate_error(event.attributes.failure, ActivityException)) + dispatch(history_target, 'failed', + Temporal::Workflow::Errors.generate_error(event.attributes.failure, ActivityException)) when 'ACTIVITY_TASK_TIMED_OUT' state_machine.time_out @@ -195,7 +257,8 @@ def apply_event(event) when 'ACTIVITY_TASK_CANCELED' state_machine.cancel - dispatch(history_target, 'failed', Temporal::ActivityCanceled.new(from_details_payloads(event.attributes.details))) + dispatch(history_target, 'failed', + Temporal::ActivityCanceled.new(from_details_payloads(event.attributes.details))) when 'TIMER_STARTED' state_machine.start @@ -237,7 +300,8 @@ def apply_event(event) when 'WORKFLOW_EXECUTION_SIGNALED' # relies on Signal#== for matching in Dispatcher signal_target = Signal.new(event.attributes.signal_name) - dispatch(signal_target, 'signaled', event.attributes.signal_name, from_signal_payloads(event.attributes.input)) + dispatch(signal_target, 'signaled', event.attributes.signal_name, + from_signal_payloads(event.attributes.input)) when 'WORKFLOW_EXECUTION_TERMINATED' # todo @@ -274,7 +338,8 @@ def apply_event(event) when 'CHILD_WORKFLOW_EXECUTION_TIMED_OUT' state_machine.time_out - dispatch(history_target, 'failed', ChildWorkflowTimeoutError.new('The child workflow timed out before succeeding')) + dispatch(history_target, 'failed', + ChildWorkflowTimeoutError.new('The child workflow timed out before succeeding')) when 'CHILD_WORKFLOW_EXECUTION_TERMINATED' state_machine.terminated @@ -347,16 +412,16 @@ def discard_command(history_target) # Pop the first command from the list, it is expected to match replay_command_id, replay_command = commands.shift - if !replay_command_id + unless replay_command_id raise NonDeterministicWorkflowError, - "A command in the history of previous executions, #{history_target}, was not scheduled upon replay. " + NONDETERMINISM_ERROR_SUGGESTION + "A command in the history of previous executions, #{history_target}, was not scheduled upon replay. " + NONDETERMINISM_ERROR_SUGGESTION end replay_target = event_target_from(replay_command_id, replay_command) if history_target != replay_target raise NonDeterministicWorkflowError, - "Unexpected command. The replaying code is issuing: #{replay_target}, "\ - "but the history of previous executions recorded: #{history_target}. " + NONDETERMINISM_ERROR_SUGGESTION + "Unexpected command. The replaying code is issuing: #{replay_target}, "\ + "but the history of previous executions recorded: #{history_target}. " + NONDETERMINISM_ERROR_SUGGESTION end end @@ -382,7 +447,6 @@ def track_release(release_name) schedule(Command::RecordMarker.new(name: RELEASE_MARKER, details: release_name)) end end - end end end diff --git a/lib/temporal/workflow/task_processor.rb b/lib/temporal/workflow/task_processor.rb index dbd14d05..9b79b454 100644 --- a/lib/temporal/workflow/task_processor.rb +++ b/lib/temporal/workflow/task_processor.rb @@ -40,12 +40,11 @@ def initialize(task, namespace, workflow_lookup, middleware_chain, workflow_midd def process start_time = Time.now - Temporal.logger.debug("Processing Workflow task", metadata.to_h) - Temporal.metrics.timing(Temporal::MetricKeys::WORKFLOW_TASK_QUEUE_TIME, queue_time_ms, workflow: workflow_name, namespace: namespace) + Temporal.logger.debug('Processing Workflow task', metadata.to_h) + Temporal.metrics.timing(Temporal::MetricKeys::WORKFLOW_TASK_QUEUE_TIME, queue_time_ms, workflow: workflow_name, + namespace: namespace) - if !workflow_class - raise Temporal::WorkflowNotRegistered, 'Workflow is not registered with this worker' - end + raise Temporal::WorkflowNotRegistered, 'Workflow is not registered with this worker' unless workflow_class history = fetch_full_history queries = parse_queries @@ -54,9 +53,10 @@ def process track_stack_trace = queries.values.map(&:query_type).include?(StackTraceTracker::STACK_TRACE_QUERY_NAME) # TODO: For sticky workflows we need to cache the Executor instance - executor = Workflow::Executor.new(workflow_class, history, metadata, config, track_stack_trace, workflow_middleware_chain) + executor = Workflow::Executor.new(workflow_class, history, metadata, config, track_stack_trace, + workflow_middleware_chain) - commands = middleware_chain.invoke(metadata) do + run_result = middleware_chain.invoke(metadata) do executor.run end @@ -65,22 +65,23 @@ def process if legacy_query_task? complete_query(query_results[LEGACY_QUERY_KEY]) else - complete_task(commands, query_results) + complete_task(run_result, query_results) end - rescue StandardError => error - Temporal::ErrorHandler.handle(error, config, metadata: metadata) + rescue StandardError => e + Temporal::ErrorHandler.handle(e, config, metadata: metadata) - fail_task(error) + fail_task(e) ensure time_diff_ms = ((Time.now - start_time) * 1000).round - Temporal.metrics.timing(Temporal::MetricKeys::WORKFLOW_TASK_LATENCY, time_diff_ms, workflow: workflow_name, namespace: namespace) - Temporal.logger.debug("Workflow task processed", metadata.to_h.merge(execution_time: time_diff_ms)) + Temporal.metrics.timing(Temporal::MetricKeys::WORKFLOW_TASK_LATENCY, time_diff_ms, workflow: workflow_name, + namespace: namespace) + Temporal.logger.debug('Workflow task processed', metadata.to_h.merge(execution_time: time_diff_ms)) end private attr_reader :task, :namespace, :task_token, :workflow_name, :workflow_class, - :middleware_chain, :workflow_middleware_chain, :metadata, :config, :binary_checksum + :middleware_chain, :workflow_middleware_chain, :metadata, :config, :binary_checksum def connection @connection ||= Temporal::Connection.generate(config.for_connection) @@ -95,7 +96,7 @@ def queue_time_ms def fetch_full_history events = task.history.events.to_a next_page_token = task.next_page_token - while !next_page_token.empty? do + until next_page_token.empty? response = connection.get_workflow_execution_history( namespace: namespace, workflow_id: task.workflow_execution.workflow_id, @@ -125,34 +126,36 @@ def parse_queries end end - def complete_task(commands, query_results) - Temporal.logger.info("Workflow task completed", metadata.to_h) + def complete_task(run_result, query_results) + Temporal.logger.info('Workflow task completed', metadata.to_h) connection.respond_workflow_task_completed( namespace: namespace, task_token: task_token, - commands: commands, + commands: run_result.commands, binary_checksum: binary_checksum, - query_results: query_results + query_results: query_results, + new_sdk_flags_used: run_result.new_sdk_flags_used ) end def complete_query(result) - Temporal.logger.info("Workflow Query task completed", metadata.to_h) + Temporal.logger.info('Workflow Query task completed', metadata.to_h) connection.respond_query_task_completed( namespace: namespace, task_token: task_token, query_result: result ) - rescue StandardError => error - Temporal.logger.error("Unable to complete a query", metadata.to_h.merge(error: error.inspect)) + rescue StandardError => e + Temporal.logger.error('Unable to complete a query', metadata.to_h.merge(error: e.inspect)) - Temporal::ErrorHandler.handle(error, config, metadata: metadata) + Temporal::ErrorHandler.handle(e, config, metadata: metadata) end def fail_task(error) - Temporal.metrics.increment(Temporal::MetricKeys::WORKFLOW_TASK_EXECUTION_FAILED, workflow: workflow_name, namespace: namespace) + Temporal.metrics.increment(Temporal::MetricKeys::WORKFLOW_TASK_EXECUTION_FAILED, workflow: workflow_name, + namespace: namespace) Temporal.logger.error('Workflow task failed', metadata.to_h.merge(error: error.inspect)) Temporal.logger.debug(error.backtrace.join("\n")) @@ -168,10 +171,10 @@ def fail_task(error) exception: error, binary_checksum: binary_checksum ) - rescue StandardError => error - Temporal.logger.error("Unable to fail Workflow task", metadata.to_h.merge(error: error.inspect)) + rescue StandardError => e + Temporal.logger.error('Unable to fail Workflow task', metadata.to_h.merge(error: e.inspect)) - Temporal::ErrorHandler.handle(error, config, metadata: metadata) + Temporal::ErrorHandler.handle(e, config, metadata: metadata) end end end diff --git a/spec/fabricators/grpc/get_system_info_fabricator.rb b/spec/fabricators/grpc/get_system_info_fabricator.rb new file mode 100644 index 00000000..5b35cb33 --- /dev/null +++ b/spec/fabricators/grpc/get_system_info_fabricator.rb @@ -0,0 +1,10 @@ +Fabricator(:api_get_system_info, from: Temporalio::Api::WorkflowService::V1::GetSystemInfoResponse) do + transient :sdk_metadata_capability + + server_version 'test-7.8.9' + capabilities do |attrs| + Temporalio::Api::WorkflowService::V1::GetSystemInfoResponse::Capabilities.new( + sdk_metadata: attrs.fetch(:sdk_metadata, true) + ) + end +end diff --git a/spec/fabricators/grpc/history_event_fabricator.rb b/spec/fabricators/grpc/history_event_fabricator.rb index 6f043b4d..f2102671 100644 --- a/spec/fabricators/grpc/history_event_fabricator.rb +++ b/spec/fabricators/grpc/history_event_fabricator.rb @@ -52,7 +52,7 @@ class TestSerializer Fabricator(:api_workflow_task_scheduled_event, from: :api_history_event) do event_type { Temporalio::Api::Enums::V1::EventType::EVENT_TYPE_WORKFLOW_TASK_SCHEDULED } - workflow_task_scheduled_event_attributes do |attrs| + workflow_task_scheduled_event_attributes do |_attrs| Temporalio::Api::History::V1::WorkflowTaskScheduledEventAttributes.new( task_queue: Fabricate(:api_task_queue), start_to_close_timeout: 15, @@ -73,6 +73,7 @@ class TestSerializer end Fabricator(:api_workflow_task_completed_event, from: :api_history_event) do + transient :sdk_flags event_type { Temporalio::Api::Enums::V1::EventType::EVENT_TYPE_WORKFLOW_TASK_COMPLETED } workflow_task_completed_event_attributes do |attrs| Temporalio::Api::History::V1::WorkflowTaskCompletedEventAttributes.new( @@ -80,6 +81,9 @@ class TestSerializer started_event_id: attrs[:event_id] - 1, identity: 'test-worker@test-host', binary_checksum: 'v1.0.0', + sdk_metadata: Temporalio::Api::Sdk::V1::WorkflowTaskCompletedMetadata.new( + lang_used_flags: attrs[:sdk_flags] || [] + ) ) end end @@ -123,7 +127,7 @@ class TestSerializer event_type { Temporalio::Api::Enums::V1::EventType::EVENT_TYPE_ACTIVITY_TASK_FAILED } activity_task_failed_event_attributes do |attrs| Temporalio::Api::History::V1::ActivityTaskFailedEventAttributes.new( - failure: Temporalio::Api::Failure::V1::Failure.new(message: "Activity failed"), + failure: Temporalio::Api::Failure::V1::Failure.new(message: 'Activity failed'), scheduled_event_id: attrs[:event_id] - 2, started_event_id: attrs[:event_id] - 1, identity: 'test-worker@test-host' @@ -148,7 +152,7 @@ class TestSerializer activity_task_cancel_requested_event_attributes do |attrs| Temporalio::Api::History::V1::ActivityTaskCancelRequestedEventAttributes.new( scheduled_event_id: attrs[:event_id] - 1, - workflow_task_completed_event_id: attrs[:event_id] - 2, + workflow_task_completed_event_id: attrs[:event_id] - 2 ) end end @@ -199,3 +203,25 @@ class TestSerializer ) end end + +Fabricator(:api_marker_recorded_event, from: :api_history_event) do + event_type { Temporalio::Api::Enums::V1::EventType::EVENT_TYPE_MARKER_RECORDED } + marker_recorded_event_attributes do |attrs| + Temporalio::Api::History::V1::MarkerRecordedEventAttributes.new( + workflow_task_completed_event_id: attrs[:event_id] - 1, + marker_name: 'SIDE_EFFECT', + details: to_payload_map({}) + ) + end +end + +Fabricator(:api_workflow_execution_signaled_event, from: :api_history_event) do + event_type { Temporalio::Api::Enums::V1::EventType::EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED } + workflow_execution_signaled_event_attributes do + Temporalio::Api::History::V1::WorkflowExecutionSignaledEventAttributes.new( + signal_name: 'a_signal', + input: nil, + identity: 'test-worker@test-host' + ) + end +end diff --git a/spec/unit/lib/temporal/grpc_spec.rb b/spec/unit/lib/temporal/grpc_spec.rb index ce11e251..ee3c1fcb 100644 --- a/spec/unit/lib/temporal/grpc_spec.rb +++ b/spec/unit/lib/temporal/grpc_spec.rb @@ -588,7 +588,8 @@ class TestDeserializer task_token: task_token, commands: [], query_results: query_results, - binary_checksum: binary_checksum + binary_checksum: binary_checksum, + new_sdk_flags_used: [1] ) expect(grpc_stub).to have_received(:respond_workflow_task_completed) do |request| @@ -612,6 +613,8 @@ class TestDeserializer Temporalio::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_FAILED) ) expect(request.query_results['2'].error_message).to eq('Test query failure') + + expect(request.sdk_metadata.lang_used_flags).to eq([1]) end end end diff --git a/spec/unit/lib/temporal/worker_spec.rb b/spec/unit/lib/temporal/worker_spec.rb index f8a74b21..685e07a0 100644 --- a/spec/unit/lib/temporal/worker_spec.rb +++ b/spec/unit/lib/temporal/worker_spec.rb @@ -6,6 +6,19 @@ describe Temporal::Worker do subject { described_class.new(config) } let(:config) { Temporal::Configuration.new } + let(:connection) { instance_double('Temporal::Connection::GRPC') } + let(:sdk_metadata_enabled) { true } + before do + allow(Temporal::Connection).to receive(:generate).and_return(connection) + allow(connection).to receive(:get_system_info).and_return( + Temporalio::Api::WorkflowService::V1::GetSystemInfoResponse.new( + server_version: 'test', + capabilities: Temporalio::Api::WorkflowService::V1::GetSystemInfoResponse::Capabilities.new( + sdk_metadata: sdk_metadata_enabled + ) + ) + ) + end class TestWorkerWorkflow < Temporal::Workflow namespace 'default-namespace' @@ -211,7 +224,11 @@ def start_and_stop(worker) stopped = true } - thread = Thread.new {worker.start} + thread = Thread.new do + Thread.current.abort_on_exception = true + worker.start + end + while !stopped sleep(THREAD_SYNC_DELAY) end @@ -341,7 +358,7 @@ def start_and_stop(worker) allow(subject).to receive(:while_stopping_hook) do # This callback is within a mutex, so this new thread shouldn't # do anything until Worker.stop is complete. - Thread.new {subject.start} + Thread.new { subject.start } sleep(THREAD_SYNC_DELAY) # give it a little time to do damage if it's going to end subject.stop @@ -394,7 +411,6 @@ def start_and_stop(worker) .and_return(activity_poller) worker = Temporal::Worker.new(activity_poll_retry_seconds: 10) - worker.register_workflow(TestWorkerWorkflow) worker.register_activity(TestWorkerActivity) start_and_stop(worker) @@ -419,7 +435,6 @@ def start_and_stop(worker) worker = Temporal::Worker.new(workflow_poll_retry_seconds: 10) worker.register_workflow(TestWorkerWorkflow) - worker.register_activity(TestWorkerActivity) start_and_stop(worker) diff --git a/spec/unit/lib/temporal/workflow/executor_spec.rb b/spec/unit/lib/temporal/workflow/executor_spec.rb index 47f10b86..ff15e719 100644 --- a/spec/unit/lib/temporal/workflow/executor_spec.rb +++ b/spec/unit/lib/temporal/workflow/executor_spec.rb @@ -8,20 +8,25 @@ describe Temporal::Workflow::Executor do subject { described_class.new(workflow, history, workflow_metadata, config, false, middleware_chain) } + let(:connection) { instance_double('Temporal::Connection::GRPC') } let(:workflow_started_event) { Fabricate(:api_workflow_execution_started_event, event_id: 1) } let(:history) do Temporal::Workflow::History.new([ - workflow_started_event, - Fabricate(:api_workflow_task_scheduled_event, event_id: 2), - Fabricate(:api_workflow_task_started_event, event_id: 3), - Fabricate(:api_workflow_task_completed_event, event_id: 4) - ]) + workflow_started_event, + Fabricate(:api_workflow_task_scheduled_event, event_id: 2), + Fabricate(:api_workflow_task_started_event, event_id: 3), + Fabricate(:api_workflow_task_completed_event, event_id: 4) + ]) end let(:workflow) { TestWorkflow } let(:workflow_metadata) { Fabricate(:workflow_metadata) } let(:config) { Temporal::Configuration.new } let(:middleware_chain) { Temporal::Middleware::Chain.new } + before do + allow(Temporal::Connection).to receive(:generate).and_return(connection) + end + class TestWorkflow < Temporal::Workflow def execute 'test' @@ -37,32 +42,66 @@ def execute expect(workflow) .to have_received(:execute_in_context) - .with( - an_instance_of(Temporal::Workflow::Context), - nil - ) + .with( + an_instance_of(Temporal::Workflow::Context), + nil + ) end it 'returns a complete workflow decision' do decisions = subject.run - expect(decisions.length).to eq(1) + expect(decisions.commands.length).to eq(1) + expect(decisions.new_sdk_flags_used).to be_empty - decision_id, decision = decisions.first + decision_id, decision = decisions.commands.first expect(decision_id).to eq(history.events.length + 1) expect(decision).to be_an_instance_of(Temporal::Workflow::Command::CompleteWorkflow) expect(decision.result).to eq('test') end + context 'history with signal' do + let(:history) do + Temporal::Workflow::History.new([ + workflow_started_event, + Fabricate(:api_workflow_execution_signaled_event, event_id: 2), + Fabricate(:api_workflow_task_scheduled_event, event_id: 3), + Fabricate(:api_workflow_task_started_event, event_id: 4) + ]) + end + let(:system_info) { Fabricate(:api_get_system_info) } + + context 'signals first config enabled' do + it 'set signals first sdk flag' do + allow(connection).to receive(:get_system_info).and_return(system_info) + + decisions = subject.run + + expect(decisions.commands.length).to eq(1) + expect(decisions.new_sdk_flags_used).to eq(Set.new([Temporal::Workflow::SDKFlags::HANDLE_SIGNALS_FIRST])) + end + end + + context 'signals first config disabled' do + let(:config) { Temporal::Configuration.new.tap { |c| c.legacy_signals = true } } + it 'no sdk flag' do + decisions = subject.run + + expect(decisions.commands.length).to eq(1) + expect(decisions.new_sdk_flags_used).to be_empty + end + end + end + it 'generates workflow metadata' do allow(Temporal::Metadata::Workflow).to receive(:new) payload = Temporalio::Api::Common::V1::Payload.new( metadata: { 'encoding' => 'json/plain' }, data: '"bar"'.b ) - header = + header = Google::Protobuf::Map.new(:string, :message, Temporalio::Api::Common::V1::Payload, { 'Foo' => payload }) - workflow_started_event.workflow_execution_started_event_attributes.header = + workflow_started_event.workflow_execution_started_event_attributes.header = Fabricate(:api_header, fields: header) subject.run @@ -70,19 +109,19 @@ def execute event_attributes = workflow_started_event.workflow_execution_started_event_attributes expect(Temporal::Metadata::Workflow) .to have_received(:new) - .with( - namespace: workflow_metadata.namespace, - id: workflow_metadata.workflow_id, - name: event_attributes.workflow_type.name, - run_id: event_attributes.original_execution_run_id, - parent_id: nil, - parent_run_id: nil, - attempt: event_attributes.attempt, - task_queue: event_attributes.task_queue.name, - headers: {'Foo' => 'bar'}, - run_started_at: workflow_started_event.event_time.to_time, - memo: {}, - ) + .with( + namespace: workflow_metadata.namespace, + id: workflow_metadata.workflow_id, + name: event_attributes.workflow_type.name, + run_id: event_attributes.original_execution_run_id, + parent_id: nil, + parent_run_id: nil, + attempt: event_attributes.attempt, + task_queue: event_attributes.task_queue.name, + headers: { 'Foo' => 'bar' }, + run_started_at: workflow_started_event.event_time.to_time, + memo: {} + ) end end @@ -94,7 +133,7 @@ def execute { '1' => Temporal::Workflow::TaskProcessor::Query.new(Fabricate(:api_workflow_query, query_type: 'success')), '2' => Temporal::Workflow::TaskProcessor::Query.new(Fabricate(:api_workflow_query, query_type: 'failure')), - '3' => Temporal::Workflow::TaskProcessor::Query.new(Fabricate(:api_workflow_query, query_type: 'unknown')), + '3' => Temporal::Workflow::TaskProcessor::Query.new(Fabricate(:api_workflow_query, query_type: 'unknown')) } end diff --git a/spec/unit/lib/temporal/workflow/history_spec.rb b/spec/unit/lib/temporal/workflow/history_spec.rb deleted file mode 100644 index 8a058fc1..00000000 --- a/spec/unit/lib/temporal/workflow/history_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'temporal/workflow/history' - -describe Temporal::Workflow::History do - describe '#next_window' do - - end -end diff --git a/spec/unit/lib/temporal/workflow/state_manager_spec.rb b/spec/unit/lib/temporal/workflow/state_manager_spec.rb index ce193bb1..99685cd9 100644 --- a/spec/unit/lib/temporal/workflow/state_manager_spec.rb +++ b/spec/unit/lib/temporal/workflow/state_manager_spec.rb @@ -2,11 +2,11 @@ require 'temporal/workflow/dispatcher' require 'temporal/workflow/history/event' require 'temporal/workflow/history/window' +require 'temporal/workflow/signal' require 'temporal/workflow/state_manager' require 'temporal/errors' describe Temporal::Workflow::StateManager do - describe '#schedule' do class MyWorkflow < Temporal::Workflow; end @@ -14,21 +14,21 @@ class MyWorkflow < Temporal::Workflow; end [ Temporal::Workflow::Command::ContinueAsNew.new( workflow_type: MyWorkflow, - task_queue: 'dummy', + task_queue: 'dummy' ), Temporal::Workflow::Command::FailWorkflow.new( - exception: StandardError.new('dummy'), + exception: StandardError.new('dummy') ), Temporal::Workflow::Command::CompleteWorkflow.new( - result: 5, - ), + result: 5 + ) ].each do |terminal_command| it "fails to validate if #{terminal_command.class} is not the last command scheduled" do - state_manager = described_class.new(Temporal::Workflow::Dispatcher.new) + state_manager = described_class.new(Temporal::Workflow::Dispatcher.new, Temporal::Configuration.new) next_command = Temporal::Workflow::Command::RecordMarker.new( name: Temporal::Workflow::StateManager::RELEASE_MARKER, - details: 'dummy', + details: 'dummy' ) state_manager.schedule(terminal_command) @@ -39,6 +39,269 @@ class MyWorkflow < Temporal::Workflow; end end end + describe '#apply' do + let(:dispatcher) { Temporal::Workflow::Dispatcher.new } + let(:state_manager) do + Temporal::Workflow::StateManager.new(dispatcher, config) + end + let(:config) { Temporal::Configuration.new } + let(:connection) { instance_double('Temporal::Connection::GRPC') } + let(:system_info) { Fabricate(:api_get_system_info) } + + before do + allow(Temporal::Connection).to receive(:generate).and_return(connection) + end + + context 'workflow execution started' do + let(:history) do + Temporal::Workflow::History.new([Fabricate(:api_workflow_execution_started_event, event_id: 1)]) + end + + it 'dispatcher invoked for start' do + expect(dispatcher).to receive(:dispatch).with( + Temporal::Workflow::History::EventTarget.workflow, 'started', instance_of(Array) + ).once + state_manager.apply(history.next_window) + end + end + + context 'workflow execution started with signal' do + let(:signal_entry) { Fabricate(:api_workflow_execution_signaled_event, event_id: 2) } + let(:history) do + Temporal::Workflow::History.new( + [ + Fabricate(:api_workflow_execution_started_event, event_id: 1), + signal_entry + ] + ) + end + + it 'dispatcher invoked for start' do + allow(connection).to receive(:get_system_info).and_return(system_info) + + # While markers do come before the workflow execution started event, signals do not + expect(dispatcher).to receive(:dispatch).with( + Temporal::Workflow::History::EventTarget.workflow, 'started', instance_of(Array) + ).once.ordered + expect(dispatcher).to receive(:dispatch).with( + Temporal::Workflow::Signal.new(signal_entry.workflow_execution_signaled_event_attributes.signal_name), + 'signaled', + [ + signal_entry.workflow_execution_signaled_event_attributes.signal_name, + signal_entry.workflow_execution_signaled_event_attributes.input + ] + ).once.ordered + + state_manager.apply(history.next_window) + end + end + + context 'with a marker' do + let(:activity_entry) { Fabricate(:api_activity_task_scheduled_event, event_id: 5) } + let(:marker_entry) { Fabricate(:api_marker_recorded_event, event_id: 8) } + let(:history) do + Temporal::Workflow::History.new( + [ + Fabricate(:api_workflow_execution_started_event, event_id: 1), + Fabricate(:api_workflow_task_scheduled_event, event_id: 2), + Fabricate(:api_workflow_task_started_event, event_id: 3), + Fabricate(:api_workflow_task_completed_event, event_id: 4), + activity_entry, + Fabricate(:api_activity_task_started_event, event_id: 6), + Fabricate(:api_activity_task_completed_event, event_id: 7), + marker_entry, + Fabricate(:api_workflow_task_scheduled_event, event_id: 9), + Fabricate(:api_workflow_task_started_event, event_id: 10), + Fabricate(:api_workflow_task_completed_event, event_id: 11) + ] + ) + end + + it 'marker handled first' do + activity_target = nil + dispatcher.register_handler(Temporal::Workflow::History::EventTarget.workflow, 'started') do + activity_target, = state_manager.schedule( + Temporal::Workflow::Command::ScheduleActivity.new( + activity_id: activity_entry.event_id, + activity_type: activity_entry.activity_task_scheduled_event_attributes.activity_type, + input: nil, + task_queue: activity_entry.activity_task_scheduled_event_attributes.task_queue, + retry_policy: nil, + timeouts: nil, + headers: nil + ) + ) + end + + # First task: starts workflow execution, schedules an activity + state_manager.apply(history.next_window) + + expect(activity_target).not_to be_nil + + activity_completed = false + dispatcher.register_handler(activity_target, 'completed') do + activity_completed = true + state_manager.schedule( + Temporal::Workflow::Command::RecordMarker.new( + name: marker_entry.marker_recorded_event_attributes.marker_name, + details: to_payload_map({}) + ) + ) + + # Activity completed event comes before marker recorded event in history, but + # when activity completion is handled, the marker has already been handled. + expect(state_manager.send(:marker_ids).count).to eq(1) + end + + # Second task: Handles activity completion, records marker + state_manager.apply(history.next_window) + + expect(activity_completed).to eq(true) + end + end + + def test_order(signal_first) + activity_target = nil + signaled = false + + dispatcher.register_handler(Temporal::Workflow::History::EventTarget.workflow, 'started') do + activity_target, = state_manager.schedule( + Temporal::Workflow::Command::ScheduleActivity.new( + activity_id: activity_entry.event_id, + activity_type: activity_entry.activity_task_scheduled_event_attributes.activity_type, + input: nil, + task_queue: activity_entry.activity_task_scheduled_event_attributes.task_queue, + retry_policy: nil, + timeouts: nil, + headers: nil + ) + ) + end + + dispatcher.register_handler( + Temporal::Workflow::Signal.new( + signal_entry.workflow_execution_signaled_event_attributes.signal_name + ), + 'signaled' + ) do + signaled = true + end + + # First task: starts workflow execution, schedules an activity + state_manager.apply(history.next_window) + + expect(activity_target).not_to be_nil + expect(signaled).to eq(false) + + activity_completed = false + dispatcher.register_handler(activity_target, 'completed') do + activity_completed = true + + expect(signaled).to eq(signal_first) + end + + # Second task: Handles activity completion, signal + state_manager.apply(history.next_window) + + expect(activity_completed).to eq(true) + expect(signaled).to eq(true) + end + + context 'replaying with a signal' do + let(:activity_entry) { Fabricate(:api_activity_task_scheduled_event, event_id: 5) } + let(:signal_entry) { Fabricate(:api_workflow_execution_signaled_event, event_id: 8) } + let(:signal_handling_task) { Fabricate(:api_workflow_task_completed_event, event_id: 11) } + let(:history) do + Temporal::Workflow::History.new( + [ + Fabricate(:api_workflow_execution_started_event, event_id: 1), + Fabricate(:api_workflow_task_scheduled_event, event_id: 2), + Fabricate(:api_workflow_task_started_event, event_id: 3), + Fabricate(:api_workflow_task_completed_event, event_id: 4), + activity_entry, + Fabricate(:api_activity_task_started_event, event_id: 6), + Fabricate(:api_activity_task_completed_event, event_id: 7), + signal_entry, + Fabricate(:api_workflow_task_scheduled_event, event_id: 9), + Fabricate(:api_workflow_task_started_event, event_id: 10), + signal_handling_task + ] + ) + end + + context 'no SDK flag' do + it 'signal inline' do + test_order(false) + end + end + + context 'with SDK flag' do + let(:signal_handling_task) do + Fabricate( + :api_workflow_task_completed_event, + event_id: 11, + sdk_flags: [Temporal::Workflow::SDKFlags::HANDLE_SIGNALS_FIRST] + ) + end + it 'signal first' do + allow(connection).to receive(:get_system_info).and_return(system_info) + + test_order(true) + end + + context 'even with legacy config enabled' do + let(:config) { Temporal::Configuration.new.tap { |c| c.legacy_signals = true } } + it 'signal first' do + allow(connection).to receive(:get_system_info).and_return(system_info) + + test_order(true) + end + end + end + end + + context 'not replaying with a signal' do + let(:activity_entry) { Fabricate(:api_activity_task_scheduled_event, event_id: 5) } + let(:signal_entry) { Fabricate(:api_workflow_execution_signaled_event, event_id: 8) } + let(:history) do + Temporal::Workflow::History.new( + [ + Fabricate(:api_workflow_execution_started_event, event_id: 1), + Fabricate(:api_workflow_task_scheduled_event, event_id: 2), + Fabricate(:api_workflow_task_started_event, event_id: 3), + Fabricate(:api_workflow_task_completed_event, event_id: 4), + activity_entry, + Fabricate(:api_activity_task_started_event, event_id: 6), + Fabricate(:api_activity_task_completed_event, event_id: 7), + signal_entry, + Fabricate(:api_workflow_task_scheduled_event, event_id: 9) + ] + ) + end + + context 'signals first config disabled' do + let(:config) { Temporal::Configuration.new.tap { |c| c.legacy_signals = true } } + it 'signal inline' do + test_order(false) + + expect(state_manager.new_sdk_flags_used).to be_empty + end + end + + context 'signals first with default config' do + let(:config) { Temporal::Configuration.new } + + it 'signal first' do + allow(connection).to receive(:get_system_info).and_return(system_info) + + test_order(true) + + expect(state_manager.new_sdk_flags_used).to eq(Set.new([Temporal::Workflow::SDKFlags::HANDLE_SIGNALS_FIRST])) + end + end + end + end + describe '#search_attributes' do let(:initial_search_attributes) do { @@ -62,7 +325,7 @@ class MyWorkflow < Temporal::Workflow; end let(:upsert_search_attribute_event_1) do Fabricate(:api_upsert_search_attributes_event, search_attributes: upserted_attributes_1) end - let(:usperted_attributes_2) do + let(:upserted_attributes_2) do { 'CustomAttribute3' => 'bar', 'CustomAttribute4' => 10 @@ -70,15 +333,15 @@ class MyWorkflow < Temporal::Workflow; end end let(:upsert_search_attribute_event_2) do Fabricate(:api_upsert_search_attributes_event, - event_id: 4, - search_attributes: usperted_attributes_2) + event_id: 4, + search_attributes: upserted_attributes_2) end let(:upsert_empty_search_attributes_event) do Fabricate(:api_upsert_search_attributes_event, search_attributes: {}) end it 'initial merges with upserted' do - state_manager = described_class.new(Temporal::Workflow::Dispatcher.new) + state_manager = described_class.new(Temporal::Workflow::Dispatcher.new, Temporal::Configuration.new) window = Temporal::Workflow::History::Window.new window.add(Temporal::Workflow::History::Event.new(start_workflow_execution_event)) @@ -100,13 +363,13 @@ class MyWorkflow < Temporal::Workflow; end { 'CustomAttribute1' => 42, # from initial (not overridden) 'CustomAttribute2' => 8, # only from upsert - 'CustomAttribute3' => 'foo', # overridden by upsert + 'CustomAttribute3' => 'foo' # overridden by upsert } ) end it 'initial and upsert treated as empty hash' do - state_manager = described_class.new(Temporal::Workflow::Dispatcher.new) + state_manager = described_class.new(Temporal::Workflow::Dispatcher.new, Temporal::Configuration.new) window = Temporal::Workflow::History::Window.new window.add(Temporal::Workflow::History::Event.new(start_workflow_execution_event_no_search_attributes)) @@ -121,9 +384,8 @@ class MyWorkflow < Temporal::Workflow; end expect(state_manager.search_attributes).to eq({}) end - it 'multiple upserts merge' do - state_manager = described_class.new(Temporal::Workflow::Dispatcher.new) + state_manager = described_class.new(Temporal::Workflow::Dispatcher.new, Temporal::Configuration.new) window_1 = Temporal::Workflow::History::Window.new window_1.add(Temporal::Workflow::History::Event.new(workflow_task_started_event)) @@ -138,7 +400,7 @@ class MyWorkflow < Temporal::Workflow; end window_2 = Temporal::Workflow::History::Window.new window_2.add(Temporal::Workflow::History::Event.new(upsert_search_attribute_event_2)) - command_2 = Temporal::Workflow::Command::UpsertSearchAttributes.new(search_attributes: usperted_attributes_2) + command_2 = Temporal::Workflow::Command::UpsertSearchAttributes.new(search_attributes: upserted_attributes_2) state_manager.schedule(command_2) state_manager.apply(window_2) @@ -146,7 +408,7 @@ class MyWorkflow < Temporal::Workflow; end { 'CustomAttribute2' => 8, 'CustomAttribute3' => 'bar', - 'CustomAttribute4' => 10, + 'CustomAttribute4' => 10 } ) end diff --git a/spec/unit/lib/temporal/workflow/task_processor_spec.rb b/spec/unit/lib/temporal/workflow/task_processor_spec.rb index e20c1721..33d5506f 100644 --- a/spec/unit/lib/temporal/workflow/task_processor_spec.rb +++ b/spec/unit/lib/temporal/workflow/task_processor_spec.rb @@ -4,13 +4,17 @@ require 'temporal/workflow/task_processor' describe Temporal::Workflow::TaskProcessor do - subject { described_class.new(task, namespace, lookup, middleware_chain, workflow_middleware_chain, config, binary_checksum) } + subject do + described_class.new(task, namespace, lookup, middleware_chain, workflow_middleware_chain, config, binary_checksum) + end let(:namespace) { 'test-namespace' } let(:lookup) { instance_double('Temporal::ExecutableLookup', find: nil) } let(:query) { nil } let(:queries) { nil } - let(:task) { Fabricate(:api_workflow_task, { workflow_type: api_workflow_type, query: query, queries: queries }.compact) } + let(:task) do + Fabricate(:api_workflow_task, { workflow_type: api_workflow_type, query: query, queries: queries }.compact) + end let(:api_workflow_type) { Fabricate(:api_workflow_type, name: workflow_name) } let(:workflow_name) { 'TestWorkflow' } let(:connection) { instance_double('Temporal::Connection::GRPC') } @@ -79,16 +83,20 @@ let(:workflow_class) { double('Temporal::Workflow', execute_in_context: nil) } let(:executor) { double('Temporal::Workflow::Executor') } let(:commands) { double('commands') } + let(:new_sdk_flags_used) { double('new_sdk_flags_used') } + let(:run_result) do + Temporal::Workflow::Executor::RunResult.new(commands: commands, new_sdk_flags_used: new_sdk_flags_used) + end before do allow(lookup).to receive(:find).with(workflow_name).and_return(workflow_class) allow(Temporal::Workflow::Executor).to receive(:new).and_return(executor) - allow(executor).to receive(:run) { workflow_class.execute_in_context(context, input); commands } + allow(executor).to receive(:run) { workflow_class.execute_in_context(context, input) }.and_return(run_result) allow(executor).to receive(:process_queries) end context 'when workflow task completes' do - # Note: This is a bit of a pointless test because I short circuit this with stubs. + # NOTE: This is a bit of a pointless test because I short circuit this with stubs. # The code does not drop down into the state machine and so forth. it 'runs the specified task' do subject.process @@ -130,7 +138,8 @@ task_token: task.task_token, commands: commands, binary_checksum: binary_checksum, - query_results: { query_id => query_result } + query_results: { query_id => query_result }, + new_sdk_flags_used: new_sdk_flags_used ) end end @@ -167,7 +176,14 @@ expect(connection).to_not have_received(:respond_query_task_completed) expect(connection) .to have_received(:respond_workflow_task_completed) - .with(namespace: namespace, task_token: task.task_token, commands: commands, query_results: nil, binary_checksum: binary_checksum) + .with( + namespace: namespace, + task_token: task.task_token, + commands: commands, + query_results: nil, + binary_checksum: binary_checksum, + new_sdk_flags_used: new_sdk_flags_used + ) end it 'ignores connection exception' do @@ -369,7 +385,9 @@ context 'when a page has no events' do let(:page_two) { 'page-2' } let(:page_three) { 'page-3' } - let(:first_history_response) { Fabricate(:workflow_execution_history, events: [event], _next_page_token: page_two) } + let(:first_history_response) do + Fabricate(:workflow_execution_history, events: [event], _next_page_token: page_two) + end let(:empty_history_response) do Fabricate(:workflow_execution_history, events: [], _next_page_token: page_three) From d75dfee99c6555950a482860a02b04b150b1de49 Mon Sep 17 00:00:00 2001 From: Jeff Schoner Date: Mon, 11 Sep 2023 12:18:42 -0700 Subject: [PATCH 099/125] Fix continue as new timeout propagation (#265) * Check that run timeout matches * Set run timeout From 054094285cfc9ccc59184cd70b70288530074049 Mon Sep 17 00:00:00 2001 From: stuppy Date: Wed, 11 Oct 2023 08:45:36 -0500 Subject: [PATCH 100/125] ProtoJSON safely encode JSON to ASCII-8BIT (String#b) (#264) --- lib/temporal/connection/converter/payload/proto_json.rb | 2 +- .../connection/converter/payload/proto_json_spec.rb | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/temporal/connection/converter/payload/proto_json.rb b/lib/temporal/connection/converter/payload/proto_json.rb index 6d952a60..cdbc36f9 100644 --- a/lib/temporal/connection/converter/payload/proto_json.rb +++ b/lib/temporal/connection/converter/payload/proto_json.rb @@ -25,7 +25,7 @@ def to_payload(data) 'encoding' => ENCODING, 'messageType' => data.class.descriptor.name, }, - data: data.to_json, + data: data.to_json.b, ) end end diff --git a/spec/unit/lib/temporal/connection/converter/payload/proto_json_spec.rb b/spec/unit/lib/temporal/connection/converter/payload/proto_json_spec.rb index 21111d5f..589c921f 100644 --- a/spec/unit/lib/temporal/connection/converter/payload/proto_json_spec.rb +++ b/spec/unit/lib/temporal/connection/converter/payload/proto_json_spec.rb @@ -15,6 +15,13 @@ expect(subject.from_payload(subject.to_payload(input))).to eq(input) end + + it 'encodes special characters' do + input = Temporalio::Api::Common::V1::Payload.new( + metadata: { 'it’ll work!' => 'bytebytebyte' }, + ) + expect(subject.from_payload(subject.to_payload(input))).to eq(input) + end end it 'skips if not proto message' do From 263e975961ce0da1613af93775377b182ebbe5db Mon Sep 17 00:00:00 2001 From: Jeff Schoner Date: Thu, 19 Oct 2023 11:41:56 -0700 Subject: [PATCH 101/125] History size and suggest continue as new (#269) * Track history size * Add example integration test for continuing as new --- examples/bin/worker | 1 + .../spec/integration/continue_as_new_spec.rb | 43 +++++++++++++++++++ .../workflows/continue_as_new_workflow.rb | 19 ++++++++ lib/temporal/workflow/context.rb | 6 +++ lib/temporal/workflow/history/size.rb | 11 +++++ lib/temporal/workflow/history/window.rb | 6 ++- lib/temporal/workflow/state_manager.rb | 11 +++++ .../grpc/history_event_fabricator.rb | 5 ++- .../temporal/workflow/state_manager_spec.rb | 34 +++++++++++++++ 9 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 examples/workflows/continue_as_new_workflow.rb create mode 100644 lib/temporal/workflow/history/size.rb diff --git a/examples/bin/worker b/examples/bin/worker index 16633673..be6f2b97 100755 --- a/examples/bin/worker +++ b/examples/bin/worker @@ -41,6 +41,7 @@ worker.register_workflow(CancellingTimerWorkflow) worker.register_workflow(CheckWorkflow) worker.register_workflow(ChildWorkflowTimeoutWorkflow) worker.register_workflow(ChildWorkflowTerminatedWorkflow) +worker.register_workflow(ContinueAsNewWorkflow) worker.register_workflow(FailingActivitiesWorkflow) worker.register_workflow(FailingWorkflow) worker.register_workflow(HandlingStructuredErrorWorkflow) diff --git a/examples/spec/integration/continue_as_new_spec.rb b/examples/spec/integration/continue_as_new_spec.rb index 9efa0e2c..75e9f4e0 100644 --- a/examples/spec/integration/continue_as_new_spec.rb +++ b/examples/spec/integration/continue_as_new_spec.rb @@ -1,3 +1,4 @@ +require 'workflows/continue_as_new_workflow' require 'workflows/loop_workflow' describe LoopWorkflow do @@ -47,4 +48,46 @@ expect(final_result[:memo]).to eq(memo) expect(final_result[:headers]).to eq(headers) end + + it 'uses history bytes size to continue as new' do + workflow_id = SecureRandom.uuid + # 7 activity invocations produce about 10,000 bytes of history. This should + # result in one continue as new with 7 activities in the first and 3 in the + # second run. + run_id = Temporal.start_workflow( + ContinueAsNewWorkflow, + 10, # hello count + 10_000, # max bytes limit + options: { + workflow_id: workflow_id, + timeouts: { + execution: 60, + run: 20 + } + }, + ) + + # First run will throw because it continued as new + next_run_id = nil + expect do + Temporal.await_workflow_result( + ContinueAsNewWorkflow, + workflow_id: workflow_id, + run_id: run_id, + ) + end.to raise_error(Temporal::WorkflowRunContinuedAsNew) do |error| + next_run_id = error.new_run_id + end + + expect(next_run_id).to_not eq(nil) + + # Second run will not throw because it returns rather than continues as new. + final_result = Temporal.await_workflow_result( + ContinueAsNewWorkflow, + workflow_id: workflow_id, + run_id: next_run_id, + ) + + expect(final_result[:runs]).to eq(2) + end end diff --git a/examples/workflows/continue_as_new_workflow.rb b/examples/workflows/continue_as_new_workflow.rb new file mode 100644 index 00000000..bf97b079 --- /dev/null +++ b/examples/workflows/continue_as_new_workflow.rb @@ -0,0 +1,19 @@ +require 'activities/hello_world_activity' + +# Demonstrates how to use history_size to determine when to continue as new +class ContinueAsNewWorkflow < Temporal::Workflow + def execute(hello_count, bytes_max, run = 1) + while hello_count.positive? && workflow.history_size.bytes < bytes_max + HelloWorldActivity.execute!("Alice Long#{'long' * 100}name") + hello_count -= 1 + end + + workflow.logger.info("Workflow history size: #{workflow.history_size}, remaining hellos: #{hello_count}") + + return workflow.continue_as_new(hello_count, bytes_max, run + 1) if hello_count.positive? + + { + runs: run + } + end +end diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index 58a730ac..8ecf849b 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -67,6 +67,12 @@ def has_release?(release_name) state_manager.release?(release_name.to_s) end + # Returns information about the workflow run's history up to this point. This can be used to + # determine when to continue as new. + def history_size + state_manager.history_size + end + def execute_activity(activity_class, *input, **args) options = args.delete(:options) || {} input << args unless args.empty? diff --git a/lib/temporal/workflow/history/size.rb b/lib/temporal/workflow/history/size.rb new file mode 100644 index 00000000..502cb0b4 --- /dev/null +++ b/lib/temporal/workflow/history/size.rb @@ -0,0 +1,11 @@ +module Temporal + class Workflow + class History + Size = Struct.new( + :bytes, # integer, total number of bytes used + :events, # integer, total number of history events used + :suggest_continue_as_new, # boolean, true if server history length limits are being approached + keyword_init: true) + end + end +end diff --git a/lib/temporal/workflow/history/window.rb b/lib/temporal/workflow/history/window.rb index 83112feb..657b129b 100644 --- a/lib/temporal/workflow/history/window.rb +++ b/lib/temporal/workflow/history/window.rb @@ -5,7 +5,7 @@ module Temporal class Workflow class History class Window - attr_reader :local_time, :last_event_id, :events, :sdk_flags + attr_reader :local_time, :last_event_id, :events, :sdk_flags, :history_size_bytes, :suggest_continue_as_new def initialize @local_time = nil @@ -13,6 +13,8 @@ def initialize @events = [] @replay = false @sdk_flags = Set.new + @history_size_bytes = 0 + @suggest_continue_as_new = false end def replay? @@ -24,6 +26,8 @@ def add(event) when 'WORKFLOW_TASK_STARTED' @last_event_id = event.id + 1 # one for completed @local_time = event.timestamp + @history_size_bytes = event.attributes.history_size_bytes + @suggest_continue_as_new = event.attributes.suggest_continue_as_new when 'WORKFLOW_TASK_FAILED', 'WORKFLOW_TASK_TIMED_OUT' @last_event_id = nil @local_time = nil diff --git a/lib/temporal/workflow/state_manager.rb b/lib/temporal/workflow/state_manager.rb index 62302ac2..dbf1d1e6 100644 --- a/lib/temporal/workflow/state_manager.rb +++ b/lib/temporal/workflow/state_manager.rb @@ -3,6 +3,7 @@ require 'temporal/workflow/command' require 'temporal/workflow/command_state_machine' require 'temporal/workflow/history/event_target' +require 'temporal/workflow/history/size' require 'temporal/concerns/payloads' require 'temporal/workflow/errors' require 'temporal/workflow/sdk_flags' @@ -90,6 +91,8 @@ def apply(history_window) @local_time = history_window.local_time @last_event_id = history_window.last_event_id history_window.sdk_flags.each { |flag| sdk_flags.add(flag) } + @history_size_bytes = history_window.history_size_bytes + @suggest_continue_as_new = history_window.suggest_continue_as_new order_events(history_window.events).each do |event| apply_event(event) @@ -116,6 +119,14 @@ def self.signal_event?(event) event.type == 'WORKFLOW_EXECUTION_SIGNALED' end + def history_size + History::Size.new( + events: @last_event_id, + bytes: @history_size_bytes, + suggest_continue_as_new: @suggest_continue_as_new + ).freeze + end + private attr_reader :dispatcher, :command_tracker, :marker_ids, :side_effects, :releases, :sdk_flags diff --git a/spec/fabricators/grpc/history_event_fabricator.rb b/spec/fabricators/grpc/history_event_fabricator.rb index f2102671..4562d7ef 100644 --- a/spec/fabricators/grpc/history_event_fabricator.rb +++ b/spec/fabricators/grpc/history_event_fabricator.rb @@ -62,12 +62,15 @@ class TestSerializer end Fabricator(:api_workflow_task_started_event, from: :api_history_event) do + transient :history_size_bytes, :suggest_continue_as_new event_type { Temporalio::Api::Enums::V1::EventType::EVENT_TYPE_WORKFLOW_TASK_STARTED } workflow_task_started_event_attributes do |attrs| Temporalio::Api::History::V1::WorkflowTaskStartedEventAttributes.new( scheduled_event_id: attrs[:event_id] - 1, identity: 'test-worker@test-host', - request_id: SecureRandom.uuid + request_id: SecureRandom.uuid, + history_size_bytes: attrs[:history_size_bytes], + suggest_continue_as_new: attrs[:suggest_continue_as_new] ) end end diff --git a/spec/unit/lib/temporal/workflow/state_manager_spec.rb b/spec/unit/lib/temporal/workflow/state_manager_spec.rb index 99685cd9..9de891fa 100644 --- a/spec/unit/lib/temporal/workflow/state_manager_spec.rb +++ b/spec/unit/lib/temporal/workflow/state_manager_spec.rb @@ -302,6 +302,40 @@ def test_order(signal_first) end end + describe '#history_size' do + let(:config) { Temporal::Configuration.new } + let(:history_size_bytes) { 27 } + let(:suggest_continue_as_new) { true } + let(:start_workflow_execution_event) { Fabricate(:api_workflow_execution_started_event) } + let(:workflow_task_scheduled_event) { Fabricate(:api_workflow_task_scheduled_event, event_id: 2) } + let(:workflow_task_started_event) do + Fabricate( + :api_workflow_task_started_event, + event_id: 3, + history_size_bytes: history_size_bytes, + suggest_continue_as_new: suggest_continue_as_new) + end + + it 'has correct event count' do + state_manager = described_class.new(Temporal::Workflow::Dispatcher.new, config) + + window = Temporal::Workflow::History::Window.new + window.add(Temporal::Workflow::History::Event.new(start_workflow_execution_event)) + window.add(Temporal::Workflow::History::Event.new(workflow_task_scheduled_event)) + window.add(Temporal::Workflow::History::Event.new(workflow_task_started_event)) + + state_manager.apply(window) + + expect(state_manager.history_size).to eq( + Temporal::Workflow::History::Size.new( + events: 4, # comes from event id of started + 1 + bytes: history_size_bytes, + suggest_continue_as_new: suggest_continue_as_new + ) + ) + end + end + describe '#search_attributes' do let(:initial_search_attributes) do { From 08fe1e9698308c7cec43474b937e22e886237af5 Mon Sep 17 00:00:00 2001 From: Jeff Schoner Date: Fri, 20 Oct 2023 06:17:18 -0700 Subject: [PATCH 102/125] Save signals on first workflow task (#268) * Save signals on first workflow task * Save signals on first workflow task * Config option for preserving no signals in the first task * Update version and CHANGELOG * Remove redundant sdk flags --- CHANGELOG.md | 16 +- .../workflows/signal_with_start_workflow.rb | 4 +- lib/temporal/configuration.rb | 6 +- lib/temporal/version.rb | 2 +- lib/temporal/workflow/context.rb | 16 ++ lib/temporal/workflow/sdk_flags.rb | 4 +- lib/temporal/workflow/state_manager.rb | 60 ++++++-- .../lib/temporal/workflow/executor_spec.rb | 5 +- .../temporal/workflow/state_manager_spec.rb | 141 +++++++++++++++++- 9 files changed, 229 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47d336b8..3536f399 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.1.1 +Allows signals to be processed within the first workflow task. + +**IMPORTANT:** This change is backward compatible, but workflows started +on this version cannot run on earlier versions. If you roll back, you will +see workflow task failures mentioning an unknown SDK flag. This will prevent +those workflows from making progress until your code is rolled forward +again. If you'd like to roll this out more gradually, you can, +1. Set the `no_signals_in_first_task` configuration option to `true` +2. Deploy your worker +3. Wait until you are certain you won't need to roll back +4. Remove the configuration option, which will default it to `false` +5. Deploy your worker + ## 0.1.0 This introduces signal first ordering. See https://github.com/coinbase/temporal-ruby/issues/258 for @@ -21,4 +35,4 @@ process, you must follow these rollout steps to avoid non-determinism errors: These steps ensure any workflow that executes in signals first mode will continue to be executed in this order on replay. If you don't follow these steps, you may see failed workflow tasks, which -in some cases could result in unrecoverable history corruption. \ No newline at end of file +in some cases could result in unrecoverable history corruption. diff --git a/examples/workflows/signal_with_start_workflow.rb b/examples/workflows/signal_with_start_workflow.rb index f8693ce1..ab94a5d1 100644 --- a/examples/workflows/signal_with_start_workflow.rb +++ b/examples/workflows/signal_with_start_workflow.rb @@ -13,7 +13,9 @@ def execute(expected_signal) end end - # Do something to get descheduled so the signal handler has a chance to run + # Wait for the activity in signal callbacks to complete. The workflow will + # not automatically wait for any blocking calls made in callbacks to complete + # before returning. workflow.wait_until { received != initial_value } received end diff --git a/lib/temporal/configuration.rb b/lib/temporal/configuration.rb index b414d76e..9deb4226 100644 --- a/lib/temporal/configuration.rb +++ b/lib/temporal/configuration.rb @@ -18,7 +18,7 @@ class Configuration attr_reader :timeouts, :error_handlers, :capabilities attr_accessor :connection_type, :converter, :use_error_serialization_v2, :host, :port, :credentials, :identity, :logger, :metrics_adapter, :namespace, :task_queue, :headers, :search_attributes, :header_propagators, - :payload_codec, :legacy_signals + :payload_codec, :legacy_signals, :no_signals_in_first_task # See https://docs.temporal.io/blog/activity-timeouts/ for general docs. # We want an infinite execution timeout for cron schedules and other perpetual workflows. @@ -92,6 +92,10 @@ def initialize # in Temporal server 1.20, it is ignored when connected to older versions and effectively # treated as true. @legacy_signals = false + + # This is a legacy behavior that is incorrect, but which existing workflow code may rely on. Only + # set to true until you can fix your workflow code. + @no_signals_in_first_task = false end def on_error(&block) diff --git a/lib/temporal/version.rb b/lib/temporal/version.rb index 87781382..eb368292 100644 --- a/lib/temporal/version.rb +++ b/lib/temporal/version.rb @@ -1,3 +1,3 @@ module Temporal - VERSION = '0.1.0'.freeze + VERSION = '0.1.1'.freeze end diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index 8ecf849b..2d69bd2d 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -349,16 +349,32 @@ def now # # @param signal_name [String, Symbol, nil] an optional signal name; converted to a String def on_signal(signal_name = nil, &block) + first_task_signals = if state_manager.sdk_flags.include?(SDKFlags::SAVE_FIRST_TASK_SIGNALS) + state_manager.first_task_signals + else + [] + end + if signal_name target = Signal.new(signal_name) dispatcher.register_handler(target, 'signaled') do |_, input| # do not pass signal name when triggering a named handler call_in_fiber(block, input) end + + first_task_signals.each do |name, input| + if name == signal_name + call_in_fiber(block, input) + end + end else dispatcher.register_handler(Dispatcher::WILDCARD, 'signaled') do |signal, input| call_in_fiber(block, signal, input) end + + first_task_signals.each do |name, input| + call_in_fiber(block, name, input) + end end return diff --git a/lib/temporal/workflow/sdk_flags.rb b/lib/temporal/workflow/sdk_flags.rb index 6b24fe05..7849bf08 100644 --- a/lib/temporal/workflow/sdk_flags.rb +++ b/lib/temporal/workflow/sdk_flags.rb @@ -4,9 +4,11 @@ module Temporal class Workflow module SDKFlags HANDLE_SIGNALS_FIRST = 1 + # The presence of SAVE_FIRST_TASK_SIGNALS implies HANDLE_SIGNALS_FIRST + SAVE_FIRST_TASK_SIGNALS = 2 # Make sure to include all known flags here - ALL = Set.new([HANDLE_SIGNALS_FIRST]) + ALL = Set.new([HANDLE_SIGNALS_FIRST, SAVE_FIRST_TASK_SIGNALS]) end end end diff --git a/lib/temporal/workflow/state_manager.rb b/lib/temporal/workflow/state_manager.rb index dbf1d1e6..e3809662 100644 --- a/lib/temporal/workflow/state_manager.rb +++ b/lib/temporal/workflow/state_manager.rb @@ -20,7 +20,7 @@ class StateManager class UnsupportedEvent < Temporal::InternalError; end class UnsupportedMarkerType < Temporal::InternalError; end - attr_reader :commands, :local_time, :search_attributes, :new_sdk_flags_used + attr_reader :commands, :local_time, :search_attributes, :new_sdk_flags_used, :sdk_flags, :first_task_signals def initialize(dispatcher, config) @dispatcher = dispatcher @@ -40,6 +40,17 @@ def initialize(dispatcher, config) # New flags used when not replaying @new_sdk_flags_used = Set.new + + # Because signals must be processed first and a signal handler cannot be registered + # until workflow code runs, this dispatcher handler will save these signals for + # when a callback is first registered. + @first_task_signals = [] + @first_task_signal_handler = dispatcher.register_handler( + Dispatcher::WILDCARD, + 'signaled' + ) do |name, input| + @first_task_signals << [name, input] + end end def replay? @@ -97,14 +108,19 @@ def apply(history_window) order_events(history_window.events).each do |event| apply_event(event) end + + return unless @first_task_signal_handler + + @first_task_signal_handler.unregister + @first_task_signals = [] + @first_task_signal_handler = nil end - def self.event_order(event, signals_first) + def self.event_order(event, signals_first, execution_started_before_signals) if event.type == 'MARKER_RECORDED' # markers always come first 0 - elsif event.type == 'WORKFLOW_EXECUTION_STARTED' - # This always comes next if present + elsif !execution_started_before_signals && workflow_execution_started_event?(event) 1 elsif signals_first && signal_event?(event) # signals come next if we are in signals first mode @@ -119,6 +135,10 @@ def self.signal_event?(event) event.type == 'WORKFLOW_EXECUTION_SIGNALED' end + def self.workflow_execution_started_event?(event) + event.type == 'WORKFLOW_EXECUTION_STARTED' + end + def history_size History::Size.new( events: @last_event_id, @@ -129,10 +149,11 @@ def history_size private - attr_reader :dispatcher, :command_tracker, :marker_ids, :side_effects, :releases, :sdk_flags + attr_reader :dispatcher, :command_tracker, :marker_ids, :side_effects, :releases, :config def use_signals_first(raw_events) - if sdk_flags.include?(SDKFlags::HANDLE_SIGNALS_FIRST) + # The presence of SAVE_FIRST_TASK_SIGNALS implies HANDLE_SIGNALS_FIRST + if sdk_flags.include?(SDKFlags::HANDLE_SIGNALS_FIRST) || sdk_flags.include?(SDKFlags::SAVE_FIRST_TASK_SIGNALS) # If signals were handled first when this task or a previous one in this run were first # played, we must continue to do so in order to ensure determinism regardless of what # the configuration value is set to. Even the capabilities can be ignored because the @@ -140,12 +161,20 @@ def use_signals_first(raw_events) true elsif raw_events.any? { |event| StateManager.signal_event?(event) } && # If this is being played for the first time, use the configuration flag to choose - (!replay? && !@config.legacy_signals) && + !replay? && !config.legacy_signals && # In order to preserve determinism, the server must support SDK metadata to order signals # first. This is checked last because it will result in a Temporal server call the first # time it's called in the worker process. - @config.capabilities.sdk_metadata - report_flag_used(SDKFlags::HANDLE_SIGNALS_FIRST) + config.capabilities.sdk_metadata + + if raw_events.any? do |event| + StateManager.workflow_execution_started_event?(event) + end && !config.no_signals_in_first_task + report_flag_used(SDKFlags::SAVE_FIRST_TASK_SIGNALS) + else + report_flag_used(SDKFlags::HANDLE_SIGNALS_FIRST) + end + true else false @@ -154,10 +183,11 @@ def use_signals_first(raw_events) def order_events(raw_events) signals_first = use_signals_first(raw_events) + execution_started_before_signals = sdk_flags.include?(SDKFlags::SAVE_FIRST_TASK_SIGNALS) raw_events.sort_by.with_index do |event, index| # sort_by is not stable, so include index to preserve order - [StateManager.event_order(event, signals_first), index] + [StateManager.event_order(event, signals_first, execution_started_before_signals), index] end end @@ -429,11 +459,11 @@ def discard_command(history_target) end replay_target = event_target_from(replay_command_id, replay_command) - if history_target != replay_target - raise NonDeterministicWorkflowError, - "Unexpected command. The replaying code is issuing: #{replay_target}, "\ - "but the history of previous executions recorded: #{history_target}. " + NONDETERMINISM_ERROR_SUGGESTION - end + return unless history_target != replay_target + + raise NonDeterministicWorkflowError, + "Unexpected command. The replaying code is issuing: #{replay_target}, "\ + "but the history of previous executions recorded: #{history_target}. " + NONDETERMINISM_ERROR_SUGGESTION end def handle_marker(id, type, details) diff --git a/spec/unit/lib/temporal/workflow/executor_spec.rb b/spec/unit/lib/temporal/workflow/executor_spec.rb index ff15e719..ee567a8b 100644 --- a/spec/unit/lib/temporal/workflow/executor_spec.rb +++ b/spec/unit/lib/temporal/workflow/executor_spec.rb @@ -78,7 +78,10 @@ def execute decisions = subject.run expect(decisions.commands.length).to eq(1) - expect(decisions.new_sdk_flags_used).to eq(Set.new([Temporal::Workflow::SDKFlags::HANDLE_SIGNALS_FIRST])) + expect(decisions.new_sdk_flags_used).to eq( + Set.new([ + Temporal::Workflow::SDKFlags::SAVE_FIRST_TASK_SIGNALS + ])) end end diff --git a/spec/unit/lib/temporal/workflow/state_manager_spec.rb b/spec/unit/lib/temporal/workflow/state_manager_spec.rb index 9de891fa..50aa74d3 100644 --- a/spec/unit/lib/temporal/workflow/state_manager_spec.rb +++ b/spec/unit/lib/temporal/workflow/state_manager_spec.rb @@ -79,10 +79,6 @@ class MyWorkflow < Temporal::Workflow; end it 'dispatcher invoked for start' do allow(connection).to receive(:get_system_info).and_return(system_info) - # While markers do come before the workflow execution started event, signals do not - expect(dispatcher).to receive(:dispatch).with( - Temporal::Workflow::History::EventTarget.workflow, 'started', instance_of(Array) - ).once.ordered expect(dispatcher).to receive(:dispatch).with( Temporal::Workflow::Signal.new(signal_entry.workflow_execution_signaled_event_attributes.signal_name), 'signaled', @@ -91,11 +87,100 @@ class MyWorkflow < Temporal::Workflow; end signal_entry.workflow_execution_signaled_event_attributes.input ] ).once.ordered + expect(dispatcher).to receive(:dispatch).with( + Temporal::Workflow::History::EventTarget.workflow, 'started', instance_of(Array) + ).once.ordered state_manager.apply(history.next_window) end end + context 'workflow execution started with signal, replaying without flag' do + let(:signal_entry) { Fabricate(:api_workflow_execution_signaled_event, event_id: 2) } + let(:history) do + Temporal::Workflow::History.new( + [ + Fabricate(:api_workflow_execution_started_event, event_id: 1), + signal_entry, + Fabricate(:api_workflow_task_scheduled_event, event_id: 3), + Fabricate(:api_workflow_task_started_event, event_id: 4), + Fabricate( + :api_workflow_task_completed_event, + event_id: 5, + sdk_flags: sdk_flags + ) + ] + ) + end + + context 'replaying without HANDLE_SIGNALS_FIRST sdk flag' do + let(:sdk_flags) { [] } + it 'dispatcher invokes start before signal' do + allow(connection).to receive(:get_system_info).and_return(system_info) + + expect(dispatcher).to receive(:dispatch).with( + Temporal::Workflow::History::EventTarget.workflow, 'started', instance_of(Array) + ).once.ordered + expect(dispatcher).to receive(:dispatch).with( + Temporal::Workflow::Signal.new(signal_entry.workflow_execution_signaled_event_attributes.signal_name), + 'signaled', + [ + signal_entry.workflow_execution_signaled_event_attributes.signal_name, + signal_entry.workflow_execution_signaled_event_attributes.input + ] + ).once.ordered + + state_manager.apply(history.next_window) + end + end + + context 'replaying without SAVE_FIRST_TASK_SIGNALS sdk flag' do + let(:sdk_flags) { [Temporal::Workflow::SDKFlags::HANDLE_SIGNALS_FIRST] } + it 'dispatcher invokes start before signal' do + allow(connection).to receive(:get_system_info).and_return(system_info) + + expect(dispatcher).to receive(:dispatch).with( + Temporal::Workflow::History::EventTarget.workflow, 'started', instance_of(Array) + ).once.ordered + expect(dispatcher).to receive(:dispatch).with( + Temporal::Workflow::Signal.new(signal_entry.workflow_execution_signaled_event_attributes.signal_name), + 'signaled', + [ + signal_entry.workflow_execution_signaled_event_attributes.signal_name, + signal_entry.workflow_execution_signaled_event_attributes.input + ] + ).once.ordered + + state_manager.apply(history.next_window) + end + end + + context 'replaying with SAVE_FIRST_TASK_SIGNALS sdk flag' do + let(:sdk_flags) do [ + Temporal::Workflow::SDKFlags::HANDLE_SIGNALS_FIRST, + Temporal::Workflow::SDKFlags::SAVE_FIRST_TASK_SIGNALS + ] + end + it 'dispatcher invokes signal before start' do + allow(connection).to receive(:get_system_info).and_return(system_info) + + expect(dispatcher).to receive(:dispatch).with( + Temporal::Workflow::Signal.new(signal_entry.workflow_execution_signaled_event_attributes.signal_name), + 'signaled', + [ + signal_entry.workflow_execution_signaled_event_attributes.signal_name, + signal_entry.workflow_execution_signaled_event_attributes.input + ] + ).once.ordered + expect(dispatcher).to receive(:dispatch).with( + Temporal::Workflow::History::EventTarget.workflow, 'started', instance_of(Array) + ).once.ordered + + state_manager.apply(history.next_window) + end + end + end + context 'with a marker' do let(:activity_entry) { Fabricate(:api_activity_task_scheduled_event, event_id: 5) } let(:marker_entry) { Fabricate(:api_marker_recorded_event, event_id: 8) } @@ -300,6 +385,54 @@ def test_order(signal_first) end end end + + context 'not replaying with a signal in the first workflow task' do + let(:signal_entry) { Fabricate(:api_workflow_execution_signaled_event, event_id: 2) } + let(:history) do + Temporal::Workflow::History.new( + [ + Fabricate(:api_workflow_execution_started_event, event_id: 1), + signal_entry, + Fabricate(:api_workflow_task_scheduled_event, event_id: 3) + ] + ) + end + + def test_order_one_task(*expected_sdk_flags) + allow(connection).to receive(:get_system_info).and_return(system_info) + signaled = false + + dispatcher.register_handler( + Temporal::Workflow::Signal.new( + signal_entry.workflow_execution_signaled_event_attributes.signal_name + ), + 'signaled' + ) do + signaled = true + end + + state_manager.apply(history.next_window) + expect(state_manager.new_sdk_flags_used).to eq(Set.new(expected_sdk_flags)) + expect(signaled).to eq(true) + end + + context 'default config' do + let(:config) { Temporal::Configuration.new } + + it 'signal first' do + test_order_one_task( + Temporal::Workflow::SDKFlags::SAVE_FIRST_TASK_SIGNALS + ) + end + end + + context 'signals in first task disabled' do + let(:config) { Temporal::Configuration.new.tap { |c| c.no_signals_in_first_task = true } } + it 'signal inline' do + test_order_one_task(Temporal::Workflow::SDKFlags::HANDLE_SIGNALS_FIRST) + end + end + end end describe '#history_size' do From 6a11a81a25498a2f3bd30819f68c979f774a7884 Mon Sep 17 00:00:00 2001 From: Jeff Schoner Date: Mon, 30 Oct 2023 07:08:13 -0700 Subject: [PATCH 103/125] Improve thread pool error handling (#273) * Guard nil context * Set abort_on_exception for thread pool threads * Logging and handling of errors in thread top level * Set abort_on_exception in poller threads * New thread pool tests for error cases * Clean up/fix related tests --------- Co-authored-by: Jeff Schoner --- lib/temporal/activity/poller.rb | 5 +++ lib/temporal/activity/task_processor.rb | 2 +- lib/temporal/scheduled_thread_pool.rb | 16 +++++++-- lib/temporal/thread_pool.rb | 16 +++++++-- lib/temporal/workflow/poller.rb | 4 +++ .../lib/temporal/activity/context_spec.rb | 29 +++++++-------- .../temporal/activity/task_processor_spec.rb | 14 +++++--- .../temporal/scheduled_thread_pool_spec.rb | 35 ++++++++++++++++++- spec/unit/lib/temporal/thread_pool_spec.rb | 34 +++++++++++++++++- 9 files changed, 130 insertions(+), 25 deletions(-) diff --git a/lib/temporal/activity/poller.rb b/lib/temporal/activity/poller.rb index 55271593..40259f16 100644 --- a/lib/temporal/activity/poller.rb +++ b/lib/temporal/activity/poller.rb @@ -61,6 +61,9 @@ def shutting_down? end def poll_loop + # Prevent the poller thread from silently dying + Thread.current.abort_on_exception = true + last_poll_time = Time.now metrics_tags = { namespace: namespace, task_queue: task_queue }.freeze @@ -115,6 +118,7 @@ def poll_retry_seconds def thread_pool @thread_pool ||= ThreadPool.new( options[:thread_pool_size], + @config, { pool_name: 'activity_task_poller', namespace: namespace, @@ -126,6 +130,7 @@ def thread_pool def heartbeat_thread_pool @heartbeat_thread_pool ||= ScheduledThreadPool.new( options[:thread_pool_size], + @config, { pool_name: 'heartbeat', namespace: namespace, diff --git a/lib/temporal/activity/task_processor.rb b/lib/temporal/activity/task_processor.rb index 34847b93..51ae5408 100644 --- a/lib/temporal/activity/task_processor.rb +++ b/lib/temporal/activity/task_processor.rb @@ -47,7 +47,7 @@ def process respond_failed(error) ensure - unless context.heartbeat_check_scheduled.nil? + unless context&.heartbeat_check_scheduled.nil? heartbeat_thread_pool.cancel(context.heartbeat_check_scheduled) end diff --git a/lib/temporal/scheduled_thread_pool.rb b/lib/temporal/scheduled_thread_pool.rb index 99b70a10..5e9025af 100644 --- a/lib/temporal/scheduled_thread_pool.rb +++ b/lib/temporal/scheduled_thread_pool.rb @@ -9,11 +9,12 @@ class ScheduledThreadPool ScheduledItem = Struct.new(:id, :job, :fire_at, :canceled, keyword_init: true) - def initialize(size, metrics_tags) + def initialize(size, config, metrics_tags) @size = size @metrics_tags = metrics_tags @queue = Queue.new @mutex = Mutex.new + @config = config @available_threads = size @occupied_threads = {} @pool = Array.new(size) do |_i| @@ -66,6 +67,8 @@ class CancelError < StandardError; end EXIT_SYMBOL = :exit def poll + Thread.current.abort_on_exception = true + loop do item = @queue.pop if item == EXIT_SYMBOL @@ -90,7 +93,16 @@ def poll # reliably be stopped once running. It's still in the begin/rescue block # so that it won't be executed if the thread gets canceled. if !item.canceled - item.job.call + begin + item.job.call + rescue StandardError => e + Temporal.logger.error('Error reached top of thread pool thread', { error: e.inspect }) + Temporal::ErrorHandler.handle(e, @config) + rescue Exception => ex + Temporal.logger.error('Exception reached top of thread pool thread', { error: ex.inspect }) + Temporal::ErrorHandler.handle(ex, @config) + raise + end end rescue CancelError end diff --git a/lib/temporal/thread_pool.rb b/lib/temporal/thread_pool.rb index 7ff2a2fe..3febbf82 100644 --- a/lib/temporal/thread_pool.rb +++ b/lib/temporal/thread_pool.rb @@ -11,11 +11,12 @@ module Temporal class ThreadPool attr_reader :size - def initialize(size, metrics_tags) + def initialize(size, config, metrics_tags) @size = size @metrics_tags = metrics_tags @queue = Queue.new @mutex = Mutex.new + @config = config @availability = ConditionVariable.new @available_threads = size @pool = Array.new(size) do |_i| @@ -55,10 +56,21 @@ def shutdown EXIT_SYMBOL = :exit def poll + Thread.current.abort_on_exception = true + catch(EXIT_SYMBOL) do loop do job = @queue.pop - job.call + begin + job.call + rescue StandardError => e + Temporal.logger.error('Error reached top of thread pool thread', { error: e.inspect }) + Temporal::ErrorHandler.handle(e, @config) + rescue Exception => ex + Temporal.logger.error('Exception reached top of thread pool thread', { error: ex.inspect }) + Temporal::ErrorHandler.handle(ex, @config) + raise + end @mutex.synchronize do @available_threads += 1 @availability.signal diff --git a/lib/temporal/workflow/poller.rb b/lib/temporal/workflow/poller.rb index 0ec5aaba..89fed958 100644 --- a/lib/temporal/workflow/poller.rb +++ b/lib/temporal/workflow/poller.rb @@ -63,6 +63,9 @@ def shutting_down? end def poll_loop + # Prevent the poller thread from silently dying + Thread.current.abort_on_exception = true + last_poll_time = Time.now metrics_tags = { namespace: namespace, task_queue: task_queue }.freeze @@ -117,6 +120,7 @@ def process(task) def thread_pool @thread_pool ||= ThreadPool.new( options[:thread_pool_size], + @config, { pool_name: 'workflow_task_poller', namespace: namespace, diff --git a/spec/unit/lib/temporal/activity/context_spec.rb b/spec/unit/lib/temporal/activity/context_spec.rb index 0ebe7020..e9bc274b 100644 --- a/spec/unit/lib/temporal/activity/context_spec.rb +++ b/spec/unit/lib/temporal/activity/context_spec.rb @@ -3,23 +3,23 @@ require 'temporal/scheduled_thread_pool' describe Temporal::Activity::Context do - let(:client) { instance_double('Temporal::Client::GRPCClient') } + let(:connection) { instance_double('Temporal::Connection::GRPC') } let(:metadata_hash) { Fabricate(:activity_metadata).to_h } let(:metadata) { Temporal::Metadata::Activity.new(**metadata_hash) } let(:config) { Temporal::Configuration.new } let(:task_token) { SecureRandom.uuid } - let(:heartbeat_thread_pool) { Temporal::ScheduledThreadPool.new(1, {}) } + let(:heartbeat_thread_pool) { Temporal::ScheduledThreadPool.new(1, config, {}) } let(:heartbeat_response) { Fabricate(:api_record_activity_heartbeat_response) } - subject { described_class.new(client, metadata, config, heartbeat_thread_pool) } + subject { described_class.new(connection, metadata, config, heartbeat_thread_pool) } describe '#heartbeat' do - before { allow(client).to receive(:record_activity_task_heartbeat).and_return(heartbeat_response) } + before { allow(connection).to receive(:record_activity_task_heartbeat).and_return(heartbeat_response) } it 'records heartbeat' do subject.heartbeat - expect(client) + expect(connection) .to have_received(:record_activity_task_heartbeat) .with(namespace: metadata.namespace, task_token: metadata.task_token, details: nil) end @@ -27,7 +27,7 @@ it 'records heartbeat with details' do subject.heartbeat(foo: :bar) - expect(client) + expect(connection) .to have_received(:record_activity_task_heartbeat) .with(namespace: metadata.namespace, task_token: metadata.task_token, details: { foo: :bar }) end @@ -48,7 +48,7 @@ subject.heartbeat(iteration: i) end - expect(client) + expect(connection) .to have_received(:record_activity_task_heartbeat) .with(namespace: metadata.namespace, task_token: metadata.task_token, details: { iteration: 0 }) .once @@ -67,7 +67,7 @@ # Shutdown to drain remaining threads heartbeat_thread_pool.shutdown - expect(client) + expect(connection) .to have_received(:record_activity_task_heartbeat) .ordered .with(namespace: metadata.namespace, task_token: metadata.task_token, details: { iteration: 1 }) @@ -80,7 +80,7 @@ config.timeouts = { max_heartbeat_throttle_interval: 0 } subject.heartbeat - expect(client) + expect(connection) .to have_received(:record_activity_task_heartbeat) .with(namespace: metadata.namespace, task_token: metadata.task_token, details: nil) @@ -90,17 +90,18 @@ end describe '#last_heartbeat_throttled' do - before { allow(client).to receive(:record_activity_task_heartbeat).and_return(heartbeat_response) } + before { allow(connection).to receive(:record_activity_task_heartbeat).and_return(heartbeat_response) } - let(:metadata_hash) { Fabricate(:activity_metadata, heartbeat_timeout: 10).to_h } + let(:metadata_hash) { Fabricate(:activity_metadata, heartbeat_timeout: 3).to_h } it 'true when throttled, false when not' do subject.heartbeat(iteration: 1) expect(subject.last_heartbeat_throttled).to be(false) subject.heartbeat(iteration: 2) expect(subject.last_heartbeat_throttled).to be(true) - subject.heartbeat(iteration: 3) - expect(subject.last_heartbeat_throttled).to be(true) + + # Shutdown to drain remaining threads + heartbeat_thread_pool.shutdown end end @@ -120,7 +121,7 @@ describe '#async?' do subject { context.async? } - let(:context) { described_class.new(client, metadata, nil, nil) } + let(:context) { described_class.new(connection, metadata, nil, nil) } context 'when context is sync' do it { is_expected.to eq(false) } diff --git a/spec/unit/lib/temporal/activity/task_processor_spec.rb b/spec/unit/lib/temporal/activity/task_processor_spec.rb index afb1344a..41ea952f 100644 --- a/spec/unit/lib/temporal/activity/task_processor_spec.rb +++ b/spec/unit/lib/temporal/activity/task_processor_spec.rb @@ -22,12 +22,15 @@ let(:connection) { instance_double('Temporal::Connection::GRPC') } let(:middleware_chain) { Temporal::Middleware::Chain.new } let(:config) { Temporal::Configuration.new } - let(:heartbeat_thread_pool) { Temporal::ScheduledThreadPool.new(2, {}) } + let(:heartbeat_thread_pool) { Temporal::ScheduledThreadPool.new(2, config, {}) } let(:input) { %w[arg1 arg2] } describe '#process' do let(:heartbeat_check_scheduled) { nil } - let(:context) { instance_double('Temporal::Activity::Context', async?: false, heartbeat_check_scheduled: heartbeat_check_scheduled) } + let(:context) do + instance_double('Temporal::Activity::Context', async?: false, + heartbeat_check_scheduled: heartbeat_check_scheduled) + end before do allow(Temporal::Connection) @@ -38,7 +41,8 @@ .to receive(:generate_activity_metadata) .with(task, namespace) .and_return(metadata) - allow(Temporal::Activity::Context).to receive(:new).with(connection, metadata, config, heartbeat_thread_pool).and_return(context) + allow(Temporal::Activity::Context).to receive(:new).with(connection, metadata, config, + heartbeat_thread_pool).and_return(context) allow(connection).to receive(:respond_activity_task_completed) allow(connection).to receive(:respond_activity_task_failed) @@ -119,7 +123,9 @@ end context 'when there is an outstanding scheduled heartbeat' do - let(:heartbeat_check_scheduled) { Temporal::ScheduledThreadPool::ScheduledItem.new(id: :foo, canceled: false) } + let(:heartbeat_check_scheduled) do + Temporal::ScheduledThreadPool::ScheduledItem.new(id: :foo, canceled: false) + end it 'it gets canceled' do subject.process diff --git a/spec/unit/lib/temporal/scheduled_thread_pool_spec.rb b/spec/unit/lib/temporal/scheduled_thread_pool_spec.rb index 74f73018..56fab272 100644 --- a/spec/unit/lib/temporal/scheduled_thread_pool_spec.rb +++ b/spec/unit/lib/temporal/scheduled_thread_pool_spec.rb @@ -5,9 +5,10 @@ allow(Temporal.metrics).to receive(:gauge) end + let(:config) { Temporal::Configuration.new } let(:size) { 2 } let(:tags) { { foo: 'bar', bat: 'baz' } } - let(:thread_pool) { described_class.new(size, tags) } + let(:thread_pool) { described_class.new(size, config, tags) } describe '#schedule' do it 'executes one task with zero delay on a thread and exits' do @@ -39,6 +40,38 @@ expect(answers.pop).to eq(:first) expect(answers.pop).to eq(:second) end + + it 'error does not exit' do + times = 0 + + thread_pool.schedule(:foo, 0) do + times += 1 + raise 'foo' + end + + thread_pool.shutdown + + expect(times).to eq(1) + end + + it 'exception does exit' do + Thread.report_on_exception = false + times = 0 + + thread_pool.schedule(:foo, 0) do + times += 1 + raise Exception, 'crash' + end + + begin + thread_pool.shutdown + raise 'should not be reached' + rescue Exception => e + 'ok' + end + + expect(times).to eq(1) + end end describe '#cancel' do diff --git a/spec/unit/lib/temporal/thread_pool_spec.rb b/spec/unit/lib/temporal/thread_pool_spec.rb index 20659367..5de5b03a 100644 --- a/spec/unit/lib/temporal/thread_pool_spec.rb +++ b/spec/unit/lib/temporal/thread_pool_spec.rb @@ -5,9 +5,10 @@ allow(Temporal.metrics).to receive(:gauge) end + let(:config) { Temporal::Configuration.new } let(:size) { 2 } let(:tags) { { foo: 'bar', bat: 'baz' } } - let(:thread_pool) { described_class.new(size, tags) } + let(:thread_pool) { described_class.new(size, config, tags) } describe '#new' do it 'executes one task on a thread and exits' do @@ -22,6 +23,37 @@ expect(times).to eq(1) end + it 'handles error without exiting' do + times = 0 + + thread_pool.schedule do + times += 1 + raise 'failure' + end + + thread_pool.shutdown + + expect(times).to eq(1) + end + + it 'handles exception with exiting' do + Thread.report_on_exception = false + times = 0 + + thread_pool.schedule do + times += 1 + raise Exception, 'crash' + end + + begin + thread_pool.shutdown + rescue Exception => e + 'ok' + end + + expect(times).to eq(1) + end + it 'reports thread available metrics' do thread_pool.schedule do end From cce6f02f4ceff61ba5cfe4f39aae6cefc729d474 Mon Sep 17 00:00:00 2001 From: harsh-stripe <109252877+harsh-stripe@users.noreply.github.com> Date: Thu, 9 Nov 2023 11:03:03 -0800 Subject: [PATCH 104/125] Expose count_workflow_executions on the temporal client (#272) * Expose count_workflow_executions on the temporal client * Return a wrapped type for count_workflows response * Add integration tests * Remove debug log lines * Update lib/temporal/client.rb Fix a typo with the new return type. Co-authored-by: jazev-stripe <128553781+jazev-stripe@users.noreply.github.com> * Return the count value of count_workflow_executions instead of wrapping it * Remove the example integration spec for count_workflows. ES isn't setup in github actions and while this test is nice to have, it isn't critical to have an integration spec for it because it relies on timing due to the async visibility store --------- Co-authored-by: jazev-stripe <128553781+jazev-stripe@users.noreply.github.com> --- lib/temporal.rb | 1 + lib/temporal/client.rb | 11 +++++++++++ spec/unit/lib/temporal/client_spec.rb | 24 ++++++++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/lib/temporal.rb b/lib/temporal.rb index ec1ca412..617c63b3 100644 --- a/lib/temporal.rb +++ b/lib/temporal.rb @@ -30,6 +30,7 @@ module Temporal :list_open_workflow_executions, :list_closed_workflow_executions, :query_workflow_executions, + :count_workflow_executions, :add_custom_search_attributes, :list_custom_search_attributes, :remove_custom_search_attributes, diff --git a/lib/temporal/client.rb b/lib/temporal/client.rb index af6ae2bf..02473432 100644 --- a/lib/temporal/client.rb +++ b/lib/temporal/client.rb @@ -425,6 +425,17 @@ def query_workflow_executions(namespace, query, filter: {}, next_page_token: nil Temporal::Workflow::Executions.new(connection: connection, status: :all, request_options: { namespace: namespace, query: query, next_page_token: next_page_token, max_page_size: max_page_size }.merge(filter)) end + # Count the number of workflows matching the provided query + # + # @param namespace [String] + # @param query [String] + # + # @return [Integer] an integer count of workflows matching the query + def count_workflow_executions(namespace, query: nil) + response = connection.count_workflow_executions(namespace: namespace, query: query) + response.count + end + # @param attributes [Hash[String, Symbol]] name to symbol for type, see INDEXED_VALUE_TYPE above # @param namespace String, required for SQL enhanced visibility, ignored for elastic search def add_custom_search_attributes(attributes, namespace: nil) diff --git a/spec/unit/lib/temporal/client_spec.rb b/spec/unit/lib/temporal/client_spec.rb index 315f6c8d..1dd4995d 100644 --- a/spec/unit/lib/temporal/client_spec.rb +++ b/spec/unit/lib/temporal/client_spec.rb @@ -1102,4 +1102,28 @@ class NamespacedWorkflow < Temporal::Workflow end end end + + describe '#count_workflow_executions' do + let(:response) do + Temporalio::Api::WorkflowService::V1::CountWorkflowExecutionsResponse.new( + count: 5 + ) + end + + before do + allow(connection) + .to receive(:count_workflow_executions) + .and_return(response) + end + + it 'returns the count' do + resp = subject.count_workflow_executions(namespace, query: 'ExecutionStatus="Running"') + + expect(connection) + .to have_received(:count_workflow_executions) + .with(namespace: namespace, query: 'ExecutionStatus="Running"') + + expect(resp).to eq(5) + end + end end From 628960b6793f2f5e5efd196bf12b3550eb2a2665 Mon Sep 17 00:00:00 2001 From: dhruv-stripe <131680800+dhruv-stripe@users.noreply.github.com> Date: Thu, 4 Jan 2024 16:15:32 -0500 Subject: [PATCH 105/125] Add Schedule api support (#277) * Add schedule model classes These are meant to be the interface that users will have to use - I tried to copy as much useful documentation from the temporal grpc api as I could * Add serializers to convert model objects to proto * Add create, describe and list schedules * Delete and update schedule * Trigger and pause schedule * Add methods and Schedule module to Temporal obj * Remove unrelated spec Co-authored-by: Jeff Schoner --------- Co-authored-by: Jeff Schoner --- .../spec/integration/create_schedule_spec.rb | 87 ++++++++++ .../spec/integration/delete_schedule_spec.rb | 50 ++++++ .../spec/integration/list_schedules_spec.rb | 109 ++++++++++++ .../spec/integration/pause_schedule_spec.rb | 44 +++++ .../spec/integration/trigger_schedule_spec.rb | 49 ++++++ .../spec/integration/update_schedule_spec.rb | 103 +++++++++++ lib/temporal.rb | 11 +- lib/temporal/client.rb | 92 ++++++++++ lib/temporal/connection/grpc.rb | 162 ++++++++++++++++++ .../connection/serializer/backfill.rb | 26 +++ .../connection/serializer/schedule.rb | 22 +++ .../connection/serializer/schedule_action.rb | 43 +++++ .../serializer/schedule_overlap_policy.rb | 26 +++ .../serializer/schedule_policies.rb | 20 +++ .../connection/serializer/schedule_spec.rb | 45 +++++ .../connection/serializer/schedule_state.rb | 20 +++ lib/temporal/schedule.rb | 16 ++ lib/temporal/schedule/backfill.rb | 42 +++++ lib/temporal/schedule/calendar.rb | 48 ++++++ .../schedule/describe_schedule_response.rb | 11 ++ lib/temporal/schedule/interval.rb | 24 +++ .../schedule/list_schedules_response.rb | 11 ++ lib/temporal/schedule/schedule.rb | 14 ++ lib/temporal/schedule/schedule_list_entry.rb | 12 ++ lib/temporal/schedule/schedule_policies.rb | 48 ++++++ lib/temporal/schedule/schedule_spec.rb | 93 ++++++++++ lib/temporal/schedule/schedule_state.rb | 18 ++ .../schedule/start_workflow_action.rb | 58 +++++++ .../connection/serializer/backfill_spec.rb | 32 ++++ .../serializer/schedule_action_spec.rb | 50 ++++++ .../serializer/schedule_policies_spec.rb | 31 ++++ .../serializer/schedule_spec_spec.rb | 57 ++++++ .../serializer/schedule_state_spec.rb | 25 +++ 33 files changed, 1498 insertions(+), 1 deletion(-) create mode 100644 examples/spec/integration/create_schedule_spec.rb create mode 100644 examples/spec/integration/delete_schedule_spec.rb create mode 100644 examples/spec/integration/list_schedules_spec.rb create mode 100644 examples/spec/integration/pause_schedule_spec.rb create mode 100644 examples/spec/integration/trigger_schedule_spec.rb create mode 100644 examples/spec/integration/update_schedule_spec.rb create mode 100644 lib/temporal/connection/serializer/backfill.rb create mode 100644 lib/temporal/connection/serializer/schedule.rb create mode 100644 lib/temporal/connection/serializer/schedule_action.rb create mode 100644 lib/temporal/connection/serializer/schedule_overlap_policy.rb create mode 100644 lib/temporal/connection/serializer/schedule_policies.rb create mode 100644 lib/temporal/connection/serializer/schedule_spec.rb create mode 100644 lib/temporal/connection/serializer/schedule_state.rb create mode 100644 lib/temporal/schedule.rb create mode 100644 lib/temporal/schedule/backfill.rb create mode 100644 lib/temporal/schedule/calendar.rb create mode 100644 lib/temporal/schedule/describe_schedule_response.rb create mode 100644 lib/temporal/schedule/interval.rb create mode 100644 lib/temporal/schedule/list_schedules_response.rb create mode 100644 lib/temporal/schedule/schedule.rb create mode 100644 lib/temporal/schedule/schedule_list_entry.rb create mode 100644 lib/temporal/schedule/schedule_policies.rb create mode 100644 lib/temporal/schedule/schedule_spec.rb create mode 100644 lib/temporal/schedule/schedule_state.rb create mode 100644 lib/temporal/schedule/start_workflow_action.rb create mode 100644 spec/unit/lib/temporal/connection/serializer/backfill_spec.rb create mode 100644 spec/unit/lib/temporal/connection/serializer/schedule_action_spec.rb create mode 100644 spec/unit/lib/temporal/connection/serializer/schedule_policies_spec.rb create mode 100644 spec/unit/lib/temporal/connection/serializer/schedule_spec_spec.rb create mode 100644 spec/unit/lib/temporal/connection/serializer/schedule_state_spec.rb diff --git a/examples/spec/integration/create_schedule_spec.rb b/examples/spec/integration/create_schedule_spec.rb new file mode 100644 index 00000000..02f3e0b4 --- /dev/null +++ b/examples/spec/integration/create_schedule_spec.rb @@ -0,0 +1,87 @@ +require "temporal/errors" +require "temporal/schedule/backfill" +require "temporal/schedule/calendar" +require "temporal/schedule/interval" +require "temporal/schedule/schedule" +require "temporal/schedule/schedule_spec" +require "temporal/schedule/schedule_policies" +require "temporal/schedule/schedule_state" +require "temporal/schedule/start_workflow_action" + +describe "Temporal.create_schedule", :integration do + let(:example_schedule) do + workflow_id = SecureRandom.uuid + Temporal::Schedule::Schedule.new( + spec: Temporal::Schedule::ScheduleSpec.new( + calendars: [Temporal::Schedule::Calendar.new(day_of_week: "*", hour: "18", minute: "30")], + intervals: [Temporal::Schedule::Interval.new(every: 6000, offset: 300)], + cron_expressions: ["@hourly"], + jitter: 30, + # Set an end time so that the test schedule doesn't run forever + end_time: Time.now + 600 + ), + action: Temporal::Schedule::StartWorkflowAction.new( + "HelloWorldWorkflow", + "Test", + options: { + workflow_id: workflow_id, + task_queue: Temporal.configuration.task_queue + } + ), + policies: Temporal::Schedule::SchedulePolicies.new( + overlap_policy: :buffer_one + ), + state: Temporal::Schedule::ScheduleState.new( + notes: "Created by integration test" + ) + ) + end + + it "can create schedules" do + namespace = integration_spec_namespace + + schedule_id = SecureRandom.uuid + + create_response = Temporal.create_schedule( + namespace, + schedule_id, + example_schedule, + memo: {"schedule_memo" => "schedule memo value"}, + trigger_immediately: true, + backfill: Temporal::Schedule::Backfill.new(start_time: (Date.today - 90).to_time, end_time: Time.now) + ) + expect(create_response).to(be_an_instance_of(Temporalio::Api::WorkflowService::V1::CreateScheduleResponse)) + + describe_response = Temporal.describe_schedule(namespace, schedule_id) + + expect(describe_response.memo).to(eq({"schedule_memo" => "schedule memo value"})) + expect(describe_response.schedule.spec.jitter.seconds).to(eq(30)) + expect(describe_response.schedule.policies.overlap_policy).to(eq(:SCHEDULE_OVERLAP_POLICY_BUFFER_ONE)) + expect(describe_response.schedule.action.start_workflow.workflow_type.name).to(eq("HelloWorldWorkflow")) + expect(describe_response.schedule.state.notes).to(eq("Created by integration test")) + end + + it "can create schedules with a minimal set of fields" do + namespace = integration_spec_namespace + schedule_id = SecureRandom.uuid + + schedule = Temporal::Schedule::Schedule.new( + spec: Temporal::Schedule::ScheduleSpec.new( + cron_expressions: ["@hourly"], + # Set an end time so that the test schedule doesn't run forever + end_time: Time.now + 600 + ), + action: Temporal::Schedule::StartWorkflowAction.new( + "HelloWorldWorkflow", + "Test", + options: {task_queue: Temporal.configuration.task_queue} + ) + ) + + Temporal.create_schedule(namespace, schedule_id, schedule) + + describe_response = Temporal.describe_schedule(namespace, schedule_id) + expect(describe_response.schedule.action.start_workflow.workflow_type.name).to(eq("HelloWorldWorkflow")) + expect(describe_response.schedule.policies.overlap_policy).to(eq(:SCHEDULE_OVERLAP_POLICY_SKIP)) + end +end diff --git a/examples/spec/integration/delete_schedule_spec.rb b/examples/spec/integration/delete_schedule_spec.rb new file mode 100644 index 00000000..b12d7220 --- /dev/null +++ b/examples/spec/integration/delete_schedule_spec.rb @@ -0,0 +1,50 @@ +require "temporal/errors" +require "temporal/schedule/schedule" +require "temporal/schedule/schedule_spec" +require "temporal/schedule/start_workflow_action" + +describe "Temporal.delete_schedule", :integration do + let(:example_schedule) do + Temporal::Schedule::Schedule.new( + spec: Temporal::Schedule::ScheduleSpec.new( + cron_expressions: ["@hourly"], + # Set an end time so that the test schedule doesn't run forever + end_time: Time.now + 600 + ), + action: Temporal::Schedule::StartWorkflowAction.new( + "HelloWorldWorkflow", + "Test", + options: { + task_queue: Temporal.configuration.task_queue + } + ) + ) + end + + it "can delete schedules" do + namespace = integration_spec_namespace + + schedule_id = SecureRandom.uuid + + Temporal.create_schedule(namespace, schedule_id, example_schedule) + describe_response = Temporal.describe_schedule(namespace, schedule_id) + expect(describe_response.schedule.action.start_workflow.workflow_type.name).to(eq("HelloWorldWorkflow")) + + Temporal.delete_schedule(namespace, schedule_id) + + # Now that the schedule is delted it should raise a not found error + expect do + Temporal.describe_schedule(namespace, schedule_id) + end + .to(raise_error(Temporal::NotFoundFailure)) + end + + it "raises a NotFoundFailure if a schedule doesn't exist" do + namespace = integration_spec_namespace + + expect do + Temporal.delete_schedule(namespace, "some-invalid-schedule-id") + end + .to(raise_error(Temporal::NotFoundFailure)) + end +end diff --git a/examples/spec/integration/list_schedules_spec.rb b/examples/spec/integration/list_schedules_spec.rb new file mode 100644 index 00000000..cfdc97b6 --- /dev/null +++ b/examples/spec/integration/list_schedules_spec.rb @@ -0,0 +1,109 @@ +require "timeout" +require "temporal/errors" +require "temporal/schedule/backfill" +require "temporal/schedule/calendar" +require "temporal/schedule/interval" +require "temporal/schedule/schedule" +require "temporal/schedule/schedule_spec" +require "temporal/schedule/schedule_policies" +require "temporal/schedule/schedule_state" +require "temporal/schedule/start_workflow_action" + +describe "Temporal.list_schedules", :integration do + let(:example_schedule) do + workflow_id = SecureRandom.uuid + Temporal::Schedule::Schedule.new( + spec: Temporal::Schedule::ScheduleSpec.new( + cron_expressions: ["@hourly"], + # Set an end time so that the test schedule doesn't run forever + end_time: Time.now + 600 + ), + action: Temporal::Schedule::StartWorkflowAction.new( + "HelloWorldWorkflow", + "Test", + options: { + task_queue: Temporal.configuration.task_queue + } + ) + ) + end + + def cleanup + namespace = integration_spec_namespace + loop do + resp = Temporal.list_schedules(namespace, maximum_page_size: 1000) + resp.schedules.each do |schedule| + begin + Temporal.delete_schedule(namespace, schedule.schedule_id) + rescue Temporal::NotFoundFailure + # This sometimes throws if a schedule has already been 'completed' (end time is reached) + end + end + break if resp.next_page_token == "" + end + end + + before do + cleanup + end + + + it "can list schedules with pagination" do + namespace = integration_spec_namespace + + 10.times do + schedule_id = SecureRandom.uuid + Temporal.create_schedule(namespace, schedule_id, example_schedule) + end + + # list_schedules is eventually consistent. Wait until at least 10 schedules are returned + Timeout.timeout(10) do + loop do + result = Temporal.list_schedules(namespace, maximum_page_size: 100) + + break if result && result.schedules.count >= 10 + + sleep(0.5) + end + end + + page_one = Temporal.list_schedules(namespace, maximum_page_size: 2) + expect(page_one.schedules.count).to(eq(2)) + page_two = Temporal.list_schedules(namespace, next_page_token: page_one.next_page_token, maximum_page_size: 8) + expect(page_two.schedules.count).to(eq(8)) + + # ensure that we got dfifereent schedules in each page + page_two_schedule_ids = page_two.schedules.map(&:schedule_id) + page_one.schedules.each do |schedule| + expect(page_two_schedule_ids).not_to(include(schedule.schedule_id)) + end + end + + it "roundtrip encodes/decodes memo with payload" do + namespace = integration_spec_namespace + schedule_id = "schedule_with_encoded_memo_payload-#{SecureRandom.uuid}}" + Temporal.create_schedule( + namespace, + schedule_id, + example_schedule, + memo: {"schedule_memo" => "schedule memo value"} + ) + + resp = nil + matching_schedule = nil + + # list_schedules is eventually consistent. Wait until our created schedule is returned + Timeout.timeout(10) do + loop do + resp = Temporal.list_schedules(namespace, maximum_page_size: 1000) + + matching_schedule = resp.schedules.find { |s| s.schedule_id == schedule_id } + break unless matching_schedule.nil? + + sleep(0.1) + end + end + + expect(matching_schedule.memo).to(eq({"schedule_memo" => "schedule memo value"})) + end +end diff --git a/examples/spec/integration/pause_schedule_spec.rb b/examples/spec/integration/pause_schedule_spec.rb new file mode 100644 index 00000000..12a750f4 --- /dev/null +++ b/examples/spec/integration/pause_schedule_spec.rb @@ -0,0 +1,44 @@ +require "temporal/schedule/schedule" +require "temporal/schedule/calendar" +require "temporal/schedule/schedule_spec" +require "temporal/schedule/schedule_policies" +require "temporal/schedule/schedule_state" +require "temporal/schedule/start_workflow_action" + +describe "Temporal.pause_schedule", :integration do + let(:example_schedule) do + Temporal::Schedule::Schedule.new( + spec: Temporal::Schedule::ScheduleSpec.new( + cron_expressions: ["@hourly"], + # Set an end time so that the test schedule doesn't run forever + end_time: Time.now + 600 + ), + action: Temporal::Schedule::StartWorkflowAction.new( + "HelloWorldWorkflow", + "Test", + options: { + task_queue: Temporal.configuration.task_queue + } + ) + ) + end + + it "can pause and unpause a schedule" do + namespace = integration_spec_namespace + schedule_id = SecureRandom.uuid + + Temporal.create_schedule(namespace, schedule_id, example_schedule) + describe_response = Temporal.describe_schedule(namespace, schedule_id) + expect(describe_response.schedule.state.paused).to(eq(false)) + + Temporal.pause_schedule(namespace, schedule_id) + + describe_response = Temporal.describe_schedule(namespace, schedule_id) + expect(describe_response.schedule.state.paused).to(eq(true)) + + Temporal.unpause_schedule(namespace, schedule_id) + + describe_response = Temporal.describe_schedule(namespace, schedule_id) + expect(describe_response.schedule.state.paused).to(eq(false)) + end +end diff --git a/examples/spec/integration/trigger_schedule_spec.rb b/examples/spec/integration/trigger_schedule_spec.rb new file mode 100644 index 00000000..ffa3e5c8 --- /dev/null +++ b/examples/spec/integration/trigger_schedule_spec.rb @@ -0,0 +1,49 @@ +require "timeout" +require "temporal/schedule/schedule" +require "temporal/schedule/calendar" +require "temporal/schedule/schedule_spec" +require "temporal/schedule/schedule_policies" +require "temporal/schedule/schedule_state" +require "temporal/schedule/start_workflow_action" + +describe "Temporal.trigger_schedule", :integration do + let(:example_schedule) do + Temporal::Schedule::Schedule.new( + spec: Temporal::Schedule::ScheduleSpec.new( + # Set this to a date in the future to avoid triggering the schedule immediately + calendars: [Temporal::Schedule::Calendar.new(year: "2055", month: "12", day_of_month: "25")] + ), + action: Temporal::Schedule::StartWorkflowAction.new( + "HelloWorldWorkflow", + "Test", + options: { + task_queue: Temporal.configuration.task_queue + } + ) + ) + end + + it "can trigger a schedule to run immediately" do + namespace = integration_spec_namespace + schedule_id = SecureRandom.uuid + + Temporal.create_schedule(namespace, schedule_id, example_schedule) + describe_response = Temporal.describe_schedule(namespace, schedule_id) + expect(describe_response.info.recent_actions.size).to(eq(0)) + + # Trigger the schedule and wait to see that it actually ran + Temporal.trigger_schedule(namespace, schedule_id, overlap_policy: :buffer_one) + + Timeout.timeout(10) do + loop do + describe_response = Temporal.describe_schedule(namespace, schedule_id) + + break if describe_response.info && describe_response.info.recent_actions.size >= 1 + + sleep(0.5) + end + end + + expect(describe_response.info.recent_actions.size).to(eq(1)) + end +end diff --git a/examples/spec/integration/update_schedule_spec.rb b/examples/spec/integration/update_schedule_spec.rb new file mode 100644 index 00000000..4aa358c6 --- /dev/null +++ b/examples/spec/integration/update_schedule_spec.rb @@ -0,0 +1,103 @@ +require "temporal/errors" +require "temporal/schedule/schedule" +require "temporal/schedule/schedule_spec" +require "temporal/schedule/schedule_policies" +require "temporal/schedule/schedule_state" +require "temporal/schedule/start_workflow_action" + +describe "Temporal.update_schedule", :integration do + let(:example_schedule) do + Temporal::Schedule::Schedule.new( + spec: Temporal::Schedule::ScheduleSpec.new( + cron_expressions: ["@hourly"], + jitter: 30, + # Set an end time so that the test schedule doesn't run forever + end_time: Time.now + 600 + ), + action: Temporal::Schedule::StartWorkflowAction.new( + "HelloWorldWorkflow", + "Test", + options: { + task_queue: Temporal.configuration.task_queue + } + ), + policies: Temporal::Schedule::SchedulePolicies.new( + overlap_policy: :buffer_one + ), + state: Temporal::Schedule::ScheduleState.new( + notes: "Created by integration test" + ) + ) + end + + let(:updated_schedule) do + Temporal::Schedule::Schedule.new( + spec: Temporal::Schedule::ScheduleSpec.new( + cron_expressions: ["@hourly"], + jitter: 500, + # Set an end time so that the test schedule doesn't run forever + end_time: Time.now + 600 + ), + action: Temporal::Schedule::StartWorkflowAction.new( + "HelloWorldWorkflow", + "UpdatedInput", + options: { + task_queue: Temporal.configuration.task_queue + } + ), + policies: Temporal::Schedule::SchedulePolicies.new( + overlap_policy: :buffer_all + ), + state: Temporal::Schedule::ScheduleState.new( + notes: "Updated by integration test" + ) + ) + end + + it "can update schedules" do + namespace = integration_spec_namespace + schedule_id = SecureRandom.uuid + + Temporal.create_schedule(namespace, schedule_id, example_schedule) + + describe_response = Temporal.describe_schedule(namespace, schedule_id) + expect(describe_response.schedule.spec.jitter.seconds).to(eq(30)) + expect(describe_response.schedule.policies.overlap_policy).to(eq(:SCHEDULE_OVERLAP_POLICY_BUFFER_ONE)) + expect(describe_response.schedule.action.start_workflow.workflow_type.name).to(eq("HelloWorldWorkflow")) + expect(describe_response.schedule.state.notes).to(eq("Created by integration test")) + + Temporal.update_schedule(namespace, schedule_id, updated_schedule) + updated_describe = Temporal.describe_schedule(namespace, schedule_id) + expect(updated_describe.schedule.spec.jitter.seconds).to(eq(500)) + expect(updated_describe.schedule.policies.overlap_policy).to(eq(:SCHEDULE_OVERLAP_POLICY_BUFFER_ALL)) + expect(updated_describe.schedule.state.notes).to(eq("Updated by integration test")) + end + + it "does not update if conflict token doesnt match" do + namespace = integration_spec_namespace + schedule_id = SecureRandom.uuid + + initial_response = Temporal.create_schedule(namespace, schedule_id, example_schedule) + + # Update the schedule but pass the incorrect token + Temporal.update_schedule(namespace, schedule_id, updated_schedule, conflict_token: "invalid token") + + # The schedule should not have been updated (we don't get an error message from the server in this case) + describe_response = Temporal.describe_schedule(namespace, schedule_id) + expect(describe_response.schedule.spec.jitter.seconds).to(eq(30)) + + # If we pass the right conflict token the update should be applied + Temporal.update_schedule(namespace, schedule_id, updated_schedule, conflict_token: initial_response.conflict_token) + updated_describe = Temporal.describe_schedule(namespace, schedule_id) + expect(updated_describe.schedule.spec.jitter.seconds).to(eq(500)) + end + + it "raises a NotFoundFailure if a schedule doesn't exist" do + namespace = integration_spec_namespace + + expect do + Temporal.update_schedule(namespace, "some-invalid-schedule-id", updated_schedule) + end + .to(raise_error(Temporal::NotFoundFailure)) + end +end diff --git a/lib/temporal.rb b/lib/temporal.rb index 617c63b3..4ba588a3 100644 --- a/lib/temporal.rb +++ b/lib/temporal.rb @@ -8,6 +8,7 @@ require 'temporal/metrics' require 'temporal/json' require 'temporal/errors' +require 'temporal/schedule' require 'temporal/workflow/errors' module Temporal @@ -34,7 +35,15 @@ module Temporal :add_custom_search_attributes, :list_custom_search_attributes, :remove_custom_search_attributes, - :connection + :connection, + :list_schedules, + :describe_schedule, + :create_schedule, + :delete_schedule, + :update_schedule, + :trigger_schedule, + :pause_schedule, + :unpause_schedule class << self def configure(&block) diff --git a/lib/temporal/client.rb b/lib/temporal/client.rb index 02473432..f1e6d3a7 100644 --- a/lib/temporal/client.rb +++ b/lib/temporal/client.rb @@ -454,6 +454,98 @@ def remove_custom_search_attributes(*attribute_names, namespace: nil) connection.remove_custom_search_attributes(attribute_names, namespace || config.default_execution_options.namespace) end + # List all schedules in a namespace + # + # @param namespace [String] namespace to list schedules in + # @param maximum_page_size [Integer] number of namespace results to return per page. + # @param next_page_token [String] a optional pagination token returned by a previous list_namespaces call + def list_schedules(namespace, maximum_page_size:, next_page_token: '') + connection.list_schedules(namespace: namespace, maximum_page_size: maximum_page_size, next_page_token: next_page_token) + end + + # Describe a schedule in a namespace + # + # @param namespace [String] namespace to list schedules in + # @param schedule_id [String] schedule id + def describe_schedule(namespace, schedule_id) + connection.describe_schedule(namespace: namespace, schedule_id: schedule_id) + end + + # Create a new schedule + # + # + # @param namespace [String] namespace to create schedule in + # @param schedule_id [String] schedule id + # @param schedule [Temporal::Schedule::Schedule] schedule to create + # @param trigger_immediately [Boolean] If set, trigger one action to run immediately + # @param backfill [Temporal::Schedule::Backfill] If set, run through the backfill schedule and trigger actions. + # @param memo [Hash] optional key-value memo map to attach to the schedule + # @param search attributes [Hash] optional key-value search attributes to attach to the schedule + def create_schedule( + namespace, + schedule_id, + schedule, + trigger_immediately: false, + backfill: nil, + memo: nil, + search_attributes: nil + ) + connection.create_schedule( + namespace: namespace, + schedule_id: schedule_id, + schedule: schedule, + trigger_immediately: trigger_immediately, + backfill: backfill, + memo: memo, + search_attributes: search_attributes + ) + end + + # Delete a schedule in a namespace + # + # @param namespace [String] namespace to list schedules in + # @param schedule_id [String] schedule id + def delete_schedule(namespace, schedule_id) + connection.delete_schedule(namespace: namespace, schedule_id: schedule_id) + end + + # Update a schedule in a namespace + # + # @param namespace [String] namespace to list schedules in + # @param schedule_id [String] schedule id + # @param schedule [Temporal::Schedule::Schedule] schedule to update. All fields in the schedule will be replaced completely by this updated schedule. + # @param conflict_token [String] a token that was returned by a previous describe_schedule call. If provided and does not match the current schedule's token, the update will fail. + def update_schedule(namespace, schedule_id, schedule, conflict_token: nil) + connection.update_schedule(namespace: namespace, schedule_id: schedule_id, schedule: schedule, conflict_token: conflict_token) + end + + # Trigger one action of a schedule to run immediately + # + # @param namespace [String] namespace + # @param schedule_id [String] schedule id + # @param overlap_policy [Symbol] Should be one of :skip, :buffer_one, :buffer_all, :cancel_other, :terminate_other, :allow_all + def trigger_schedule(namespace, schedule_id, overlap_policy: nil) + connection.trigger_schedule(namespace: namespace, schedule_id: schedule_id, overlap_policy: overlap_policy) + end + + # Pause a schedule so actions will not run + # + # @param namespace [String] namespace + # @param schedule_id [String] schedule id + # @param note [String] an optional note to explain why the schedule was paused + def pause_schedule(namespace, schedule_id, note: nil) + connection.pause_schedule(namespace: namespace, schedule_id: schedule_id, should_pause: true, note: note) + end + + # Unpause a schedule so actions will run + # + # @param namespace [String] namespace + # @param schedule_id [String] schedule id + # @param note [String] an optional note to explain why the schedule was unpaused + def unpause_schedule(namespace, schedule_id, note: nil) + connection.pause_schedule(namespace: namespace, schedule_id: schedule_id, should_pause: false, note: note) + end + def connection @connection ||= Temporal::Connection.generate(config.for_connection) end diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index 3c206160..c66c2865 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -11,6 +11,8 @@ require 'temporal/connection/interceptors/client_name_version_interceptor' require 'temporal/connection/serializer' require 'temporal/connection/serializer/failure' +require 'temporal/connection/serializer/backfill' +require 'temporal/connection/serializer/schedule' require 'temporal/connection/serializer/workflow_id_reuse_policy' require 'temporal/concerns/payloads' @@ -628,6 +630,166 @@ def get_system_info client.get_system_info(Temporalio::Api::WorkflowService::V1::GetSystemInfoRequest.new) end + def list_schedules(namespace:, maximum_page_size:, next_page_token:) + request = Temporalio::Api::WorkflowService::V1::ListSchedulesRequest.new( + namespace: namespace, + maximum_page_size: maximum_page_size, + next_page_token: next_page_token + ) + resp = client.list_schedules(request) + + Temporal::Schedule::ListSchedulesResponse.new( + schedules: resp.schedules.map do |schedule| + Temporal::Schedule::ScheduleListEntry.new( + schedule_id: schedule.schedule_id, + memo: from_payload_map(schedule.memo&.fields || {}), + search_attributes: from_payload_map_without_codec(schedule.search_attributes&.indexed_fields || {}), + info: schedule.info + ) + end, + next_page_token: resp.next_page_token, + ) + end + + def describe_schedule(namespace:, schedule_id:) + request = Temporalio::Api::WorkflowService::V1::DescribeScheduleRequest.new( + namespace: namespace, + schedule_id: schedule_id + ) + + resp = nil + begin + resp = client.describe_schedule(request) + rescue ::GRPC::NotFound => e + raise Temporal::NotFoundFailure, e + end + + Temporal::Schedule::DescribeScheduleResponse.new( + schedule: resp.schedule, + info: resp.info, + memo: from_payload_map(resp.memo&.fields || {}), + search_attributes: from_payload_map_without_codec(resp.search_attributes&.indexed_fields || {}), + conflict_token: resp.conflict_token + ) + end + + def create_schedule( + namespace:, + schedule_id:, + schedule:, + trigger_immediately: nil, + backfill: nil, + memo: nil, + search_attributes: nil + ) + initial_patch = nil + if trigger_immediately || backfill + initial_patch = Temporalio::Api::Schedule::V1::SchedulePatch.new + if trigger_immediately + initial_patch.trigger_immediately = Temporalio::Api::Schedule::V1::TriggerImmediatelyRequest.new( + overlap_policy: Temporal::Connection::Serializer::ScheduleOverlapPolicy.new( + schedule.policies&.overlap_policy + ).to_proto + ) + end + + if backfill + initial_patch.backfill_request += [Temporal::Connection::Serializer::Backfill.new(backfill).to_proto] + end + end + + request = Temporalio::Api::WorkflowService::V1::CreateScheduleRequest.new( + namespace: namespace, + schedule_id: schedule_id, + schedule: Temporal::Connection::Serializer::Schedule.new(schedule).to_proto, + identity: identity, + request_id: SecureRandom.uuid, + memo: Temporalio::Api::Common::V1::Memo.new( + fields: to_payload_map(memo || {}) + ), + search_attributes: Temporalio::Api::Common::V1::SearchAttributes.new( + indexed_fields: to_payload_map_without_codec(search_attributes || {}) + ) + ) + client.create_schedule(request) + end + + def delete_schedule(namespace:, schedule_id:) + request = Temporalio::Api::WorkflowService::V1::DeleteScheduleRequest.new( + namespace: namespace, + schedule_id: schedule_id, + identity: identity + ) + + begin + client.delete_schedule(request) + rescue ::GRPC::NotFound => e + raise Temporal::NotFoundFailure, e + end + end + + def update_schedule(namespace:, schedule_id:, schedule:, conflict_token: nil) + request = Temporalio::Api::WorkflowService::V1::UpdateScheduleRequest.new( + namespace: namespace, + schedule_id: schedule_id, + schedule: Temporal::Connection::Serializer::Schedule.new(schedule).to_proto, + conflict_token: conflict_token, + identity: identity, + request_id: SecureRandom.uuid + ) + + begin + client.update_schedule(request) + rescue ::GRPC::NotFound => e + raise Temporal::NotFoundFailure, e + end + end + + def trigger_schedule(namespace:, schedule_id:, overlap_policy: nil) + request = Temporalio::Api::WorkflowService::V1::PatchScheduleRequest.new( + namespace: namespace, + schedule_id: schedule_id, + patch: Temporalio::Api::Schedule::V1::SchedulePatch.new( + trigger_immediately: Temporalio::Api::Schedule::V1::TriggerImmediatelyRequest.new( + overlap_policy: Temporal::Connection::Serializer::ScheduleOverlapPolicy.new( + overlap_policy + ).to_proto + ), + ), + identity: identity, + request_id: SecureRandom.uuid + ) + + begin + client.patch_schedule(request) + rescue ::GRPC::NotFound => e + raise Temporal::NotFoundFailure, e + end + end + + def pause_schedule(namespace:, schedule_id:, should_pause:, note: nil) + patch = Temporalio::Api::Schedule::V1::SchedulePatch.new + if should_pause + patch.pause = note || 'Paused by temporal-ruby' + else + patch.unpause = note || 'Unpaused by temporal-ruby' + end + + request = Temporalio::Api::WorkflowService::V1::PatchScheduleRequest.new( + namespace: namespace, + schedule_id: schedule_id, + patch: patch, + identity: identity, + request_id: SecureRandom.uuid + ) + + begin + client.patch_schedule(request) + rescue ::GRPC::NotFound => e + raise Temporal::NotFoundFailure, e + end + end + private attr_reader :url, :identity, :credentials, :options, :poll_mutex, :poll_request diff --git a/lib/temporal/connection/serializer/backfill.rb b/lib/temporal/connection/serializer/backfill.rb new file mode 100644 index 00000000..04998cfb --- /dev/null +++ b/lib/temporal/connection/serializer/backfill.rb @@ -0,0 +1,26 @@ +require "temporal/connection/serializer/base" +require "temporal/connection/serializer/schedule_overlap_policy" + +module Temporal + module Connection + module Serializer + class Backfill < Base + def to_proto + return unless object + + Temporalio::Api::Schedule::V1::BackfillRequest.new( + start_time: serialize_time(object.start_time), + end_time: serialize_time(object.end_time), + overlap_policy: Temporal::Connection::Serializer::ScheduleOverlapPolicy.new(object.overlap_policy).to_proto + ) + end + + def serialize_time(input_time) + return unless input_time + + Google::Protobuf::Timestamp.new.from_time(input_time) + end + end + end + end +end diff --git a/lib/temporal/connection/serializer/schedule.rb b/lib/temporal/connection/serializer/schedule.rb new file mode 100644 index 00000000..32c206fd --- /dev/null +++ b/lib/temporal/connection/serializer/schedule.rb @@ -0,0 +1,22 @@ +require "temporal/connection/serializer/base" +require "temporal/connection/serializer/schedule_spec" +require "temporal/connection/serializer/schedule_action" +require "temporal/connection/serializer/schedule_policies" +require "temporal/connection/serializer/schedule_state" + +module Temporal + module Connection + module Serializer + class Schedule < Base + def to_proto + Temporalio::Api::Schedule::V1::Schedule.new( + spec: Temporal::Connection::Serializer::ScheduleSpec.new(object.spec).to_proto, + action: Temporal::Connection::Serializer::ScheduleAction.new(object.action).to_proto, + policies: Temporal::Connection::Serializer::SchedulePolicies.new(object.policies).to_proto, + state: Temporal::Connection::Serializer::ScheduleState.new(object.state).to_proto + ) + end + end + end + end +end diff --git a/lib/temporal/connection/serializer/schedule_action.rb b/lib/temporal/connection/serializer/schedule_action.rb new file mode 100644 index 00000000..ab4ce4c0 --- /dev/null +++ b/lib/temporal/connection/serializer/schedule_action.rb @@ -0,0 +1,43 @@ +require "temporal/connection/serializer/base" +require "temporal/concerns/payloads" + +module Temporal + module Connection + module Serializer + class ScheduleAction < Base + include Concerns::Payloads + + def to_proto + unless object.is_a?(Temporal::Schedule::StartWorkflowAction) + raise ArgumentError, "Unknown action type #{object.class}" + end + + Temporalio::Api::Schedule::V1::ScheduleAction.new( + start_workflow: Temporalio::Api::Workflow::V1::NewWorkflowExecutionInfo.new( + workflow_id: object.workflow_id, + workflow_type: Temporalio::Api::Common::V1::WorkflowType.new( + name: object.name + ), + task_queue: Temporalio::Api::TaskQueue::V1::TaskQueue.new( + name: object.task_queue + ), + input: to_payloads(object.input), + workflow_execution_timeout: object.execution_timeout, + workflow_run_timeout: object.run_timeout, + workflow_task_timeout: object.task_timeout, + header: Temporalio::Api::Common::V1::Header.new( + fields: to_payload_map(object.headers || {}) + ), + memo: Temporalio::Api::Common::V1::Memo.new( + fields: to_payload_map(object.memo || {}) + ), + search_attributes: Temporalio::Api::Common::V1::SearchAttributes.new( + indexed_fields: to_payload_map_without_codec(object.search_attributes || {}) + ) + ) + ) + end + end + end + end +end diff --git a/lib/temporal/connection/serializer/schedule_overlap_policy.rb b/lib/temporal/connection/serializer/schedule_overlap_policy.rb new file mode 100644 index 00000000..a866c8ee --- /dev/null +++ b/lib/temporal/connection/serializer/schedule_overlap_policy.rb @@ -0,0 +1,26 @@ +require "temporal/connection/serializer/base" + +module Temporal + module Connection + module Serializer + class ScheduleOverlapPolicy < Base + SCHEDULE_OVERLAP_POLICY = { + skip: Temporalio::Api::Enums::V1::ScheduleOverlapPolicy::SCHEDULE_OVERLAP_POLICY_SKIP, + buffer_one: Temporalio::Api::Enums::V1::ScheduleOverlapPolicy::SCHEDULE_OVERLAP_POLICY_BUFFER_ONE, + buffer_all: Temporalio::Api::Enums::V1::ScheduleOverlapPolicy::SCHEDULE_OVERLAP_POLICY_BUFFER_ALL, + cancel_other: Temporalio::Api::Enums::V1::ScheduleOverlapPolicy::SCHEDULE_OVERLAP_POLICY_CANCEL_OTHER, + terminate_other: Temporalio::Api::Enums::V1::ScheduleOverlapPolicy::SCHEDULE_OVERLAP_POLICY_TERMINATE_OTHER, + allow_all: Temporalio::Api::Enums::V1::ScheduleOverlapPolicy::SCHEDULE_OVERLAP_POLICY_ALLOW_ALL + }.freeze + + def to_proto + return unless object + + SCHEDULE_OVERLAP_POLICY.fetch(object) do + raise ArgumentError, "Unknown schedule overlap policy specified: #{object}" + end + end + end + end + end +end diff --git a/lib/temporal/connection/serializer/schedule_policies.rb b/lib/temporal/connection/serializer/schedule_policies.rb new file mode 100644 index 00000000..4a92c226 --- /dev/null +++ b/lib/temporal/connection/serializer/schedule_policies.rb @@ -0,0 +1,20 @@ +require "temporal/connection/serializer/base" +require "temporal/connection/serializer/schedule_overlap_policy" + +module Temporal + module Connection + module Serializer + class SchedulePolicies < Base + def to_proto + return unless object + + Temporalio::Api::Schedule::V1::SchedulePolicies.new( + overlap_policy: Temporal::Connection::Serializer::ScheduleOverlapPolicy.new(object.overlap_policy).to_proto, + catchup_window: object.catchup_window, + pause_on_failure: object.pause_on_failure + ) + end + end + end + end +end diff --git a/lib/temporal/connection/serializer/schedule_spec.rb b/lib/temporal/connection/serializer/schedule_spec.rb new file mode 100644 index 00000000..7fb07b48 --- /dev/null +++ b/lib/temporal/connection/serializer/schedule_spec.rb @@ -0,0 +1,45 @@ +require "temporal/connection/serializer/base" + +module Temporal + module Connection + module Serializer + class ScheduleSpec < Base + def to_proto + return unless object + + Temporalio::Api::Schedule::V1::ScheduleSpec.new( + cron_string: object.cron_expressions, + interval: object.intervals.map do |interval| + Temporalio::Api::Schedule::V1::IntervalSpec.new( + interval: interval.every, + phase: interval.offset + ) + end, + calendar: object.calendars.map do |calendar| + Temporalio::Api::Schedule::V1::CalendarSpec.new( + second: calendar.second, + minute: calendar.minute, + hour: calendar.hour, + day_of_month: calendar.day_of_month, + month: calendar.month, + year: calendar.year, + day_of_week: calendar.day_of_week, + comment: calendar.comment + ) + end, + jitter: object.jitter, + timezone_name: object.timezone_name, + start_time: serialize_time(object.start_time), + end_time: serialize_time(object.end_time) + ) + end + + def serialize_time(input_time) + return unless input_time + + Google::Protobuf::Timestamp.new.from_time(input_time) + end + end + end + end +end diff --git a/lib/temporal/connection/serializer/schedule_state.rb b/lib/temporal/connection/serializer/schedule_state.rb new file mode 100644 index 00000000..9e243de5 --- /dev/null +++ b/lib/temporal/connection/serializer/schedule_state.rb @@ -0,0 +1,20 @@ +require "temporal/connection/serializer/base" + +module Temporal + module Connection + module Serializer + class ScheduleState < Base + def to_proto + return unless object + + Temporalio::Api::Schedule::V1::ScheduleState.new( + notes: object.notes, + paused: object.paused, + limited_actions: object.limited_actions, + remaining_actions: object.remaining_actions + ) + end + end + end + end +end diff --git a/lib/temporal/schedule.rb b/lib/temporal/schedule.rb new file mode 100644 index 00000000..8fa84d2d --- /dev/null +++ b/lib/temporal/schedule.rb @@ -0,0 +1,16 @@ +require "temporal/schedule/backfill" +require "temporal/schedule/calendar" +require "temporal/schedule/describe_schedule_response" +require "temporal/schedule/interval" +require "temporal/schedule/list_schedules_response" +require "temporal/schedule/schedule" +require "temporal/schedule/schedule_list_entry" +require "temporal/schedule/schedule_policies" +require "temporal/schedule/schedule_spec" +require "temporal/schedule/schedule_state" +require "temporal/schedule/start_workflow_action" + +module Temporal + module Schedule + end +end diff --git a/lib/temporal/schedule/backfill.rb b/lib/temporal/schedule/backfill.rb new file mode 100644 index 00000000..b107d3f6 --- /dev/null +++ b/lib/temporal/schedule/backfill.rb @@ -0,0 +1,42 @@ +module Temporal + module Schedule + class Backfill + # Controls what happens when a workflow would be started + # by a schedule, and is already running. + # + # If provided, must be one of: + # - :skip (default): means don't start anything. When the workflow + # completes, the next scheduled event after that time will be considered. + # - :buffer_one: means start the workflow again soon as the + # current one completes, but only buffer one start in this way. If + # another start is supposed to happen when the workflow is running, + # and one is already buffered, then only the first one will be + # started after the running workflow finishes. + # - :buffer_all : means buffer up any number of starts to all happen + # sequentially, immediately after the running workflow completes. + # - :cancel_other: means that if there is another workflow running, cancel + # it, and start the new one after the old one completes cancellation. + # - :terminate_other: means that if there is another workflow running, + # terminate it and start the new one immediately. + # - :allow_all: means start any number of concurrent workflows. + # Note that with this policy, last completion result and last failure + # will not be available since workflows are not sequential. + attr_reader :overlap_policy + + # The time to start the backfill + attr_reader :start_time + + # The time to end the backfill + attr_reader :end_time + + # @param start_time [Time] The time to start the backfill + # @param end_time [Time] The time to end the backfill + # @param overlap_policy [Time] Should be one of :skip, :buffer_one, :buffer_all, :cancel_other, :terminate_other, :allow_all + def initialize(start_time: nil, end_time: nil, overlap_policy: nil) + @start_time = start_time + @end_time = end_time + @overlap_policy = overlap_policy + end + end + end +end diff --git a/lib/temporal/schedule/calendar.rb b/lib/temporal/schedule/calendar.rb new file mode 100644 index 00000000..26d24d49 --- /dev/null +++ b/lib/temporal/schedule/calendar.rb @@ -0,0 +1,48 @@ +module Temporal + module Schedule + + # Calendar describes an event specification relative to the calendar, + # similar to a traditional cron specification, but with labeled fields. Each + # field can be one of: + # *: matches always + # x: matches when the field equals x + # x/y : matches when the field equals x+n*y where n is an integer + # x-z: matches when the field is between x and z inclusive + # w,x,y,...: matches when the field is one of the listed values + # + # Each x, y, z, ... is either a decimal integer, or a month or day of week name + # or abbreviation (in the appropriate fields). + # + # A timestamp matches if all fields match. + # + # Note that fields have different default values, for convenience. + # + # Note that the special case that some cron implementations have for treating + # day_of_month and day_of_week as "or" instead of "and" when both are set is + # not implemented. + # + # day_of_week can accept 0 or 7 as Sunday + class Calendar + attr_reader :second, :minute, :hour, :day_of_month, :month, :year, :day_of_week, :comment + + # @param second [String] Expression to match seconds. Default: 0 + # @param minute [String] Expression to match minutes. Default: 0 + # @param hour [String] Expression to match hours. Default: 0 + # @param day_of_month [String] Expression to match days of the month. Default: * + # @param month [String] Expression to match months. Default: * + # @param year [String] Expression to match years. Default: * + # @param day_of_week [String] Expression to match days of the week. Default: * + # @param comment [String] Free form comment describing the intent of this calendar. + def initialize(second: nil, minute: nil, hour: nil, day_of_month: nil, month: nil, year: nil, day_of_week: nil, comment: nil) + @second = second + @minute = minute + @hour = hour + @day_of_month = day_of_month + @month = month + @day_of_week = day_of_week + @year = year + @comment = comment + end + end + end +end diff --git a/lib/temporal/schedule/describe_schedule_response.rb b/lib/temporal/schedule/describe_schedule_response.rb new file mode 100644 index 00000000..d0d3c627 --- /dev/null +++ b/lib/temporal/schedule/describe_schedule_response.rb @@ -0,0 +1,11 @@ +module Temporal + module Schedule + class DescribeScheduleResponse < Struct.new(:schedule, :info, :memo, :search_attributes, :conflict_token, keyword_init: true) + # Override the constructor to make these objects immutable + def initialize(*args) + super(*args) + self.freeze + end + end + end +end diff --git a/lib/temporal/schedule/interval.rb b/lib/temporal/schedule/interval.rb new file mode 100644 index 00000000..0d3650c9 --- /dev/null +++ b/lib/temporal/schedule/interval.rb @@ -0,0 +1,24 @@ +module Temporal + module Schedule + # Interval matches times that can be expressed as: + # Epoch + (n * every) + offset + # where n is all integers ≥ 0. + + # For example, an `every` of 1 hour with `offset` of zero would match + # every hour, on the hour. The same `every` but an `offset` + # of 19 minutes would match every `xx:19:00`. An `every` of 28 days with + # `offset` zero would match `2022-02-17T00:00:00Z` (among other times). + # The same `every` with `offset` of 3 days, 5 hours, and 23 minutes + # would match `2022-02-20T05:23:00Z` instead. + class Interval + attr_reader :every, :offset + + # @param every [Integer] the number of seconds between each interval + # @param offset [Integer] the number of seconds to provide as offset + def initialize(every:, offset: nil) + @every = every + @offset = offset + end + end + end +end diff --git a/lib/temporal/schedule/list_schedules_response.rb b/lib/temporal/schedule/list_schedules_response.rb new file mode 100644 index 00000000..acf90b74 --- /dev/null +++ b/lib/temporal/schedule/list_schedules_response.rb @@ -0,0 +1,11 @@ +module Temporal + module Schedule + class ListSchedulesResponse < Struct.new(:schedules, :next_page_token, keyword_init: true) + # Override the constructor to make these objects immutable + def initialize(*args) + super(*args) + self.freeze + end + end + end +end diff --git a/lib/temporal/schedule/schedule.rb b/lib/temporal/schedule/schedule.rb new file mode 100644 index 00000000..91fcf7d1 --- /dev/null +++ b/lib/temporal/schedule/schedule.rb @@ -0,0 +1,14 @@ +module Temporal + module Schedule + class Schedule + attr_reader :spec, :action, :policies, :state + + def initialize(spec:, action:, policies: nil, state: nil) + @spec = spec + @action = action + @policies = policies + @state = state + end + end + end +end diff --git a/lib/temporal/schedule/schedule_list_entry.rb b/lib/temporal/schedule/schedule_list_entry.rb new file mode 100644 index 00000000..338d966e --- /dev/null +++ b/lib/temporal/schedule/schedule_list_entry.rb @@ -0,0 +1,12 @@ +module Temporal + module Schedule + # ScheduleListEntry is returned by ListSchedules. + class ScheduleListEntry < Struct.new(:schedule_id, :memo, :search_attributes, :info, keyword_init: true) + # Override the constructor to make these objects immutable + def initialize(*args) + super(*args) + self.freeze + end + end + end +end diff --git a/lib/temporal/schedule/schedule_policies.rb b/lib/temporal/schedule/schedule_policies.rb new file mode 100644 index 00000000..f8aeea21 --- /dev/null +++ b/lib/temporal/schedule/schedule_policies.rb @@ -0,0 +1,48 @@ +module Temporal + module Schedule + class SchedulePolicies + # Controls what happens when a workflow would be started + # by a schedule, and is already running. + # + # If provided, must be one of: + # - :skip (default): means don't start anything. When the workflow + # completes, the next scheduled event after that time will be considered. + # - :buffer_one: means start the workflow again soon as the + # current one completes, but only buffer one start in this way. If + # another start is supposed to happen when the workflow is running, + # and one is already buffered, then only the first one will be + # started after the running workflow finishes. + # - :buffer_all : means buffer up any number of starts to all happen + # sequentially, immediately after the running workflow completes. + # - :cancel_other: means that if there is another workflow running, cancel + # it, and start the new one after the old one completes cancellation. + # - :terminate_other: means that if there is another workflow running, + # terminate it and start the new one immediately. + # - :allow_all: means start any number of concurrent workflows. + # Note that with this policy, last completion result and last failure + # will not be available since workflows are not sequential. + attr_reader :overlap_policy + + # Policy for catchups: + # If the Temporal server misses an action due to one or more components + # being down, and comes back up, the action will be run if the scheduled + # time is within this window from the current time. + # This value defaults to 60 seconds, and can't be less than 10 seconds. + attr_reader :catchup_window + + # If true, and a workflow run fails or times out, turn on "paused". + # This applies after retry policies: the full chain of retries must fail to + # trigger a pause here. + attr_reader :pause_on_failure + + # @param overlap_policy [Symbol] Should be one of :skip, :buffer_one, :buffer_all, :cancel_other, :terminate_other, :allow_all + # @param catchup_window [Integer] The number of seconds to catchup if the Temporal server misses an action + # @param pause_on_failure [Boolean] Whether to pause the schedule if the action fails + def initialize(overlap_policy: nil, catchup_window: nil, pause_on_failure: nil) + @overlap_policy = overlap_policy + @catchup_window = catchup_window + @pause_on_failure = pause_on_failure + end + end + end +end diff --git a/lib/temporal/schedule/schedule_spec.rb b/lib/temporal/schedule/schedule_spec.rb new file mode 100644 index 00000000..1034d298 --- /dev/null +++ b/lib/temporal/schedule/schedule_spec.rb @@ -0,0 +1,93 @@ +module Temporal + module Schedule + # ScheduleSpec is a complete description of a set of absolute timestamps + # (possibly infinite) that an action should occur at. The meaning of a + # ScheduleSpec depends only on its contents and never changes, except that the + # definition of a time zone can change over time (most commonly, when daylight + # saving time policy changes for an area). To create a totally self-contained + # ScheduleSpec, use UTC or include timezone_data + + # For input, you can provide zero or more of: calendars, intervals or + # cron_expressions and all of them will be used (the schedule will take + # action at the union of all of their times, minus the ones that match + # exclude_structured_calendar). + class ScheduleSpec + # Calendar-based specifications of times. + # + # @return [Array] + attr_reader :calendars + + # Interval-based specifications of times. + # + # @return [Array] + attr_reader :intervals + + # [Cron expressions](https://crontab.guru/). This is provided for easy + # migration from legacy Cron Workflows. For new use cases, we recommend + # using calendars or intervals for readability and maintainability. + # + # + # The string can have 5, 6, or 7 fields, separated by spaces. + # + # - 5 fields: minute, hour, day_of_month, month, day_of_week + # - 6 fields: minute, hour, day_of_month, month, day_of_week, year + # - 7 fields: second, minute, hour, day_of_month, month, day_of_week, year + # + # Notes: + # + # - If year is not given, it defaults to *. + # - If second is not given, it defaults to 0. + # - Shorthands `@yearly`, `@monthly`, `@weekly`, `@daily`, and `@hourly` are also + # accepted instead of the 5-7 time fields. + # - `@every interval[/]` is accepted and gets compiled into an + # IntervalSpec instead. `` and `` should be a decimal integer + # with a unit suffix s, m, h, or d. + # - Optionally, the string can be preceded by `CRON_TZ=` or + # `TZ=`, which will get copied to {@link timezone}. + # (In which case the {@link timezone} field should be left empty.) + # - Optionally, "#" followed by a comment can appear at the end of the string. + # - Note that the special case that some cron implementations have for + # treating day_of_month and day_of_week as "or" instead of "and" when both + # are set is not implemented. + # + # @return [Array] + attr_reader :cron_expressions + + # If set, any timestamps before start_time will be skipped. + attr_reader :start_time + + # If set, any timestamps after end_time will be skipped. + attr_reader :end_time + + # If set, the schedule will be randomly offset by up to this many seconds. + attr_reader :jitter + + # Time zone to interpret all calendar-based specs in. + # + # If unset, defaults to UTC. We recommend using UTC for your application if + # at all possible, to avoid various surprising properties of time zones. + # + # Time zones may be provided by name, corresponding to names in the IANA + # time zone database (see https://www.iana.org/time-zones). The definition + # will be loaded by the Temporal server from the environment it runs in. + attr_reader :timezone_name + + # @param cron_expressions [Array] + # @param intervals [Array] + # @param calendars [Array] + # @param start_time [Time] If set, any timestamps before start_time will be skipped. + # @param end_time [Time] If set, any timestamps after end_time will be skipped. + # @param jitter [Integer] If set, the schedule will be randomly offset by up to this many seconds. + # @param timezone_name [String] If set, the schedule will be interpreted in this time zone. + def initialize(cron_expressions: nil, intervals: nil, calendars: nil, start_time: nil, end_time: nil, jitter: nil, timezone_name: nil) + @cron_expressions = cron_expressions || [] + @intervals = intervals || [] + @calendars = calendars || [] + @start_time = start_time + @end_time = end_time + @jitter = jitter + @timezone_name = timezone_name + end + end + end +end diff --git a/lib/temporal/schedule/schedule_state.rb b/lib/temporal/schedule/schedule_state.rb new file mode 100644 index 00000000..4debb82c --- /dev/null +++ b/lib/temporal/schedule/schedule_state.rb @@ -0,0 +1,18 @@ +module Temporal + module Schedule + class ScheduleState + attr_reader :notes, :paused, :limited_actions, :remaining_actions + + # @param notes [String] Human-readable notes about the schedule. + # @param paused [Boolean] If true, do not take any actions based on the schedule spec. + # @param limited_actions [Boolean] If true, decrement remaining_actions when an action is taken. + # @param remaining_actions [Integer] The number of actions remaining to be taken. + def initialize(notes: nil, paused: nil, limited_actions: nil, remaining_actions: nil) + @notes = notes + @paused = paused + @limited_actions = limited_actions + @remaining_actions = remaining_actions + end + end + end +end diff --git a/lib/temporal/schedule/start_workflow_action.rb b/lib/temporal/schedule/start_workflow_action.rb new file mode 100644 index 00000000..19348fcd --- /dev/null +++ b/lib/temporal/schedule/start_workflow_action.rb @@ -0,0 +1,58 @@ +require "forwardable" + +module Temporal + module Schedule + class StartWorkflowAction + extend Forwardable + + #target + def_delegators( + :@execution_options, + :name, + :task_queue, + :headers, + :memo + ) + + attr_reader :workflow_id, :input + + # @param workflow [Temporal::Workflow, String] workflow class or name. When a workflow class + # is passed, its config (namespace, task_queue, timeouts, etc) will be used + # @param input [any] arguments to be passed to workflow's #execute method + # @param args [Hash] keyword arguments to be passed to workflow's #execute method + # @param options [Hash, nil] optional overrides + # @option options [String] :workflow_id + # @option options [String] :name workflow name + # @option options [String] :namespace + # @option options [String] :task_queue + # @option options [Hash] :retry_policy check Temporal::RetryPolicy for available options + # @option options [Hash] :timeouts check Temporal::Configuration::DEFAULT_TIMEOUTS + # @option options [Hash] :headers + # @option options [Hash] :search_attributes + # + # @return [String] workflow's run ID + def initialize(workflow, *input, options: {}) + @workflow_id = options[:workflow_id] || SecureRandom.uuid + @input = input + + @execution_options = ExecutionOptions.new(workflow, options) + end + + def execution_timeout + @execution_options.timeouts[:execution] + end + + def run_timeout + @execution_options.timeouts[:run] || @execution_options.timeouts[:execution] + end + + def task_timeout + @execution_options.timeouts[:task] + end + + def search_attributes + Workflow::Context::Helpers.process_search_attributes(@execution_options.search_attributes) + end + end + end +end diff --git a/spec/unit/lib/temporal/connection/serializer/backfill_spec.rb b/spec/unit/lib/temporal/connection/serializer/backfill_spec.rb new file mode 100644 index 00000000..e7d980f3 --- /dev/null +++ b/spec/unit/lib/temporal/connection/serializer/backfill_spec.rb @@ -0,0 +1,32 @@ +require "temporal/connection/errors" +require "temporal/schedule/backfill" +require "temporal/connection/serializer/backfill" + +describe Temporal::Connection::Serializer::Backfill do + let(:example_backfill) do + Temporal::Schedule::Backfill.new( + start_time: Time.new(2000, 1, 1, 0, 0, 0), + end_time: Time.new(2031, 1, 1, 0, 0, 0), + overlap_policy: :buffer_all + ) + end + + describe "to_proto" do + it "raises an error if an invalid overlap_policy is specified" do + invalid = Temporal::Schedule::Backfill.new(overlap_policy: :foobar) + expect do + described_class.new(invalid).to_proto + end + .to(raise_error(Temporal::Connection::ArgumentError, "Unknown schedule overlap policy specified: foobar")) + end + + it "produces well-formed protobuf" do + result = described_class.new(example_backfill).to_proto + + expect(result).to(be_a(Temporalio::Api::Schedule::V1::BackfillRequest)) + expect(result.overlap_policy).to(eq(:SCHEDULE_OVERLAP_POLICY_BUFFER_ALL)) + expect(result.start_time.to_time).to(eq(example_backfill.start_time)) + expect(result.end_time.to_time).to(eq(example_backfill.end_time)) + end + end +end diff --git a/spec/unit/lib/temporal/connection/serializer/schedule_action_spec.rb b/spec/unit/lib/temporal/connection/serializer/schedule_action_spec.rb new file mode 100644 index 00000000..275bb8e0 --- /dev/null +++ b/spec/unit/lib/temporal/connection/serializer/schedule_action_spec.rb @@ -0,0 +1,50 @@ +require "temporal/connection/errors" +require "temporal/schedule/start_workflow_action" +require "temporal/connection/serializer/schedule_action" + +describe Temporal::Connection::Serializer::ScheduleAction do + let(:timeouts) { {run: 100, task: 10} } + + let(:example_action) do + Temporal::Schedule::StartWorkflowAction.new( + "HelloWorldWorkflow", + "one", + "two", + options: { + workflow_id: "foobar", + task_queue: "my-task-queue", + timeouts: timeouts, + memo: {:"foo-memo" => "baz"}, + search_attributes: {:"foo-search-attribute" => "qux"}, + headers: {:"foo-header" => "bar"} + } + ) + end + + describe "to_proto" do + it "raises an error if an invalid action is specified" do + expect do + described_class.new(123).to_proto + end + .to(raise_error(Temporal::Connection::ArgumentError)) do |e| + expect(e.message).to(eq("Unknown action type Integer")) + end + end + + it "produces well-formed protobuf" do + result = described_class.new(example_action).to_proto + + expect(result).to(be_a(Temporalio::Api::Schedule::V1::ScheduleAction)) + + action = result.start_workflow + expect(action).to(be_a(Temporalio::Api::Workflow::V1::NewWorkflowExecutionInfo)) + expect(action.task_queue.name).to(eq("my-task-queue")) + expect(action.input.payloads.map(&:data)).to(eq(["\"one\"", "\"two\""])) + expect(action.header.fields["foo-header"].data).to(eq("\"bar\"")) + expect(action.memo.fields["foo-memo"].data).to(eq("\"baz\"")) + expect(action.search_attributes.indexed_fields["foo-search-attribute"].data).to(eq("\"qux\"")) + expect(action.workflow_run_timeout.seconds).to(eq(timeouts[:run])) + expect(action.workflow_task_timeout.seconds).to(eq(timeouts[:task])) + end + end +end diff --git a/spec/unit/lib/temporal/connection/serializer/schedule_policies_spec.rb b/spec/unit/lib/temporal/connection/serializer/schedule_policies_spec.rb new file mode 100644 index 00000000..cf64ed98 --- /dev/null +++ b/spec/unit/lib/temporal/connection/serializer/schedule_policies_spec.rb @@ -0,0 +1,31 @@ +require "temporal/schedule/schedule_policies" +require "temporal/connection/serializer/schedule_policies" + +describe Temporal::Connection::Serializer::SchedulePolicies do + let(:example_policies) do + Temporal::Schedule::SchedulePolicies.new( + overlap_policy: :buffer_one, + catchup_window: 600, + pause_on_failure: true + ) + end + + describe "to_proto" do + it "produces well-formed protobuf" do + result = described_class.new(example_policies).to_proto + + expect(result).to(be_a(Temporalio::Api::Schedule::V1::SchedulePolicies)) + expect(result.overlap_policy).to(eq(:SCHEDULE_OVERLAP_POLICY_BUFFER_ONE)) + expect(result.catchup_window.seconds).to(eq(600)) + expect(result.pause_on_failure).to(eq(true)) + end + + it "should raise if an unknown overlap policy is specified" do + invalid_policies = Temporal::Schedule::SchedulePolicies.new(overlap_policy: :foobar) + expect do + described_class.new(invalid_policies).to_proto + end + .to(raise_error(Temporal::Connection::ArgumentError, "Unknown schedule overlap policy specified: foobar")) + end + end +end diff --git a/spec/unit/lib/temporal/connection/serializer/schedule_spec_spec.rb b/spec/unit/lib/temporal/connection/serializer/schedule_spec_spec.rb new file mode 100644 index 00000000..c0aa636f --- /dev/null +++ b/spec/unit/lib/temporal/connection/serializer/schedule_spec_spec.rb @@ -0,0 +1,57 @@ +require "temporal/schedule/schedule_spec" +require "temporal/schedule/interval" +require "temporal/schedule/calendar" +require "temporal/connection/serializer/schedule_spec" + +describe Temporal::Connection::Serializer::ScheduleSpec do + let(:example_spec) do + Temporal::Schedule::ScheduleSpec.new( + cron_expressions: ["@hourly"], + intervals: [ + Temporal::Schedule::Interval.new(every: 50, offset: 30), + Temporal::Schedule::Interval.new(every: 60) + ], + calendars: [ + Temporal::Schedule::Calendar.new( + hour: "7", + minute: "0,3,15", + day_of_week: "MONDAY", + month: "1-6", + comment: "some comment explaining intent" + ), + Temporal::Schedule::Calendar.new( + minute: "8", + hour: "*" + ) + ], + start_time: Time.new(2000, 1, 1, 0, 0, 0), + end_time: Time.new(2031, 1, 1, 0, 0, 0), + jitter: 500, + timezone_name: "America/New_York" + ) + end + + describe "to_proto" do + it "produces well-formed protobuf" do + result = described_class.new(example_spec).to_proto + + expect(result).to(be_a(Temporalio::Api::Schedule::V1::ScheduleSpec)) + expect(result.cron_string).to(eq(["@hourly"])) + expect(result.interval[0].interval.seconds).to(eq(50)) + expect(result.interval[0].phase.seconds).to(eq(30)) + expect(result.interval[1].interval.seconds).to(eq(60)) + expect(result.interval[1].phase).to(be_nil) + expect(result.calendar[0].hour).to(eq("7")) + expect(result.calendar[0].minute).to(eq("0,3,15")) + expect(result.calendar[0].day_of_week).to(eq("MONDAY")) + expect(result.calendar[0].month).to(eq("1-6")) + expect(result.calendar[0].comment).to(eq("some comment explaining intent")) + expect(result.calendar[1].hour).to(eq("*")) + expect(result.calendar[1].minute).to(eq("8")) + expect(result.start_time.to_time).to(eq(example_spec.start_time)) + expect(result.end_time.to_time).to(eq(example_spec.end_time)) + expect(result.jitter.seconds).to(eq(500)) + expect(result.timezone_name).to(eq("America/New_York")) + end + end +end diff --git a/spec/unit/lib/temporal/connection/serializer/schedule_state_spec.rb b/spec/unit/lib/temporal/connection/serializer/schedule_state_spec.rb new file mode 100644 index 00000000..16c47732 --- /dev/null +++ b/spec/unit/lib/temporal/connection/serializer/schedule_state_spec.rb @@ -0,0 +1,25 @@ +require "temporal/schedule/schedule_state" +require "temporal/connection/serializer/schedule_state" + +describe Temporal::Connection::Serializer::ScheduleState do + let(:example_state) do + Temporal::Schedule::ScheduleState.new( + notes: "some notes", + paused: true, + limited_actions: true, + remaining_actions: 500 + ) + end + + describe "to_proto" do + it "produces well-formed protobuf" do + result = described_class.new(example_state).to_proto + + expect(result).to(be_a(Temporalio::Api::Schedule::V1::ScheduleState)) + expect(result.notes).to(eq("some notes")) + expect(result.paused).to(eq(true)) + expect(result.limited_actions).to(eq(true)) + expect(result.remaining_actions).to(eq(500)) + end + end +end From cfcbdd34f0ff3c37ca22c1c828a923b9e0fc6b95 Mon Sep 17 00:00:00 2001 From: Jeff Schoner Date: Thu, 4 Jan 2024 13:18:28 -0800 Subject: [PATCH 106/125] Remove cancelation commands when underlying futures are closed (#275) * Remove cancelation commands when underlying futures are closed * Fix spec for timer command preservation * Remove potentially flaky example spec --- .../workflow/command_state_machine.rb | 8 ++ lib/temporal/workflow/executor.rb | 2 +- lib/temporal/workflow/state_manager.rb | 22 ++++- .../temporal/workflow/state_manager_spec.rb | 83 +++++++++++++++++++ 4 files changed, 112 insertions(+), 3 deletions(-) diff --git a/lib/temporal/workflow/command_state_machine.rb b/lib/temporal/workflow/command_state_machine.rb index 74adcf16..69bb2528 100644 --- a/lib/temporal/workflow/command_state_machine.rb +++ b/lib/temporal/workflow/command_state_machine.rb @@ -48,6 +48,14 @@ def fail def time_out @state = TIMED_OUT_STATE end + + def closed? + @state == COMPLETED_STATE || + @state == CANCELED_STATE || + @state == FAILED_STATE || + @state == TIMED_OUT_STATE || + @state == TERMINATED_STATE + end end end end diff --git a/lib/temporal/workflow/executor.rb b/lib/temporal/workflow/executor.rb index 762ae250..5c8eaf9e 100644 --- a/lib/temporal/workflow/executor.rb +++ b/lib/temporal/workflow/executor.rb @@ -42,7 +42,7 @@ def run state_manager.apply(window) end - RunResult.new(commands: state_manager.commands, new_sdk_flags_used: state_manager.new_sdk_flags_used) + RunResult.new(commands: state_manager.final_commands, new_sdk_flags_used: state_manager.new_sdk_flags_used) end # Process queries using the pre-registered query handlers diff --git a/lib/temporal/workflow/state_manager.rb b/lib/temporal/workflow/state_manager.rb index e3809662..2cf82159 100644 --- a/lib/temporal/workflow/state_manager.rb +++ b/lib/temporal/workflow/state_manager.rb @@ -20,7 +20,7 @@ class StateManager class UnsupportedEvent < Temporal::InternalError; end class UnsupportedMarkerType < Temporal::InternalError; end - attr_reader :commands, :local_time, :search_attributes, :new_sdk_flags_used, :sdk_flags, :first_task_signals + attr_reader :local_time, :search_attributes, :new_sdk_flags_used, :sdk_flags, :first_task_signals def initialize(dispatcher, config) @dispatcher = dispatcher @@ -87,6 +87,24 @@ def schedule(command) [event_target_from(command_id, command), cancelation_id] end + def final_commands + # Filter out any activity or timer cancellation commands if the underlying activity or + # timer has completed. This can occur when an activity or timer completes while a + # workflow task is being processed that would otherwise cancel this time or activity. + commands.filter do |command_pair| + case command_pair.last + when Command::CancelTimer + state_machine = command_tracker[command_pair.last.timer_id] + !state_machine.closed? + when Command::RequestActivityCancellation + state_machine = command_tracker[command_pair.last.activity_id] + !state_machine.closed? + else + true + end + end + end + def release?(release_name) track_release(release_name) unless releases.key?(release_name) @@ -149,7 +167,7 @@ def history_size private - attr_reader :dispatcher, :command_tracker, :marker_ids, :side_effects, :releases, :config + attr_reader :commands, :dispatcher, :command_tracker, :marker_ids, :side_effects, :releases, :config def use_signals_first(raw_events) # The presence of SAVE_FIRST_TASK_SIGNALS implies HANDLE_SIGNALS_FIRST diff --git a/spec/unit/lib/temporal/workflow/state_manager_spec.rb b/spec/unit/lib/temporal/workflow/state_manager_spec.rb index 50aa74d3..bad27d22 100644 --- a/spec/unit/lib/temporal/workflow/state_manager_spec.rb +++ b/spec/unit/lib/temporal/workflow/state_manager_spec.rb @@ -469,6 +469,89 @@ def test_order_one_task(*expected_sdk_flags) end end + describe "#final_commands" do + let(:dispatcher) { Temporal::Workflow::Dispatcher.new } + let(:state_manager) do + Temporal::Workflow::StateManager.new(dispatcher, config) + end + + let(:config) { Temporal::Configuration.new } + + it "preserves canceled activity or timer commands when not completed" do + schedule_activity_command = Temporal::Workflow::Command::ScheduleActivity.new + state_manager.schedule(schedule_activity_command) + + start_timer_command = Temporal::Workflow::Command::StartTimer.new + state_manager.schedule(start_timer_command) + + cancel_activity_command = Temporal::Workflow::Command::RequestActivityCancellation.new( + activity_id: schedule_activity_command.activity_id + ) + state_manager.schedule(cancel_activity_command) + + cancel_timer_command = Temporal::Workflow::Command::CancelTimer.new( + timer_id: start_timer_command.timer_id + ) + state_manager.schedule(cancel_timer_command) + + expect(state_manager.final_commands).to( + eq( + [ + [1, schedule_activity_command], + [2, start_timer_command], + [3, cancel_activity_command], + [4, cancel_timer_command] + ] + ) + ) + end + + it "drop cancel activity command when completed" do + schedule_activity_command = Temporal::Workflow::Command::ScheduleActivity.new + state_manager.schedule(schedule_activity_command) + + cancel_command = Temporal::Workflow::Command::RequestActivityCancellation.new( + activity_id: schedule_activity_command.activity_id + ) + state_manager.schedule(cancel_command) + + # Fake completing the activity + window = Temporal::Workflow::History::Window.new + # The fake assumes an activity event completed two events ago, so fix the event id to +2 + window.add( + Temporal::Workflow::History::Event.new( + Fabricate(:api_activity_task_completed_event, event_id: schedule_activity_command.activity_id + 2) + ) + ) + state_manager.apply(window) + + expect(state_manager.final_commands).to(eq([[1, schedule_activity_command]])) + end + + it "drop cancel timer command when completed" do + start_timer_command = Temporal::Workflow::Command::StartTimer.new + state_manager.schedule(start_timer_command) + + cancel_command = Temporal::Workflow::Command::CancelTimer.new( + timer_id: start_timer_command.timer_id + ) + state_manager.schedule(cancel_command) + + # Fake completing the timer + window = Temporal::Workflow::History::Window.new + # The fake assumes an activity event completed four events ago, so fix the event id to +4 + window.add( + Temporal::Workflow::History::Event.new( + Fabricate(:api_timer_fired_event, event_id: start_timer_command.timer_id + 4) + ) + ) + state_manager.apply(window) + + expect(state_manager.final_commands).to(eq([[1, start_timer_command]])) + end + end + + describe '#search_attributes' do let(:initial_search_attributes) do { From 052641c68de51968d86bf502aa02169e8197c4bf Mon Sep 17 00:00:00 2001 From: Nishchay Date: Thu, 4 Jan 2024 13:25:11 -0800 Subject: [PATCH 107/125] Fix task queue type to match enum (#252) --- lib/temporal/connection/grpc.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index c66c2865..06be6a6d 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -613,7 +613,7 @@ def describe_task_queue(namespace:, task_queue:) task_queue: Temporalio::Api::TaskQueue::V1::TaskQueue.new( name: task_queue ), - task_queue_type: Temporalio::Api::Enums::V1::TaskQueueType::Workflow, + task_queue_type: Temporalio::Api::Enums::V1::TaskQueueType::TASK_QUEUE_TYPE_WORKFLOW, include_task_queue_status: true ) client.describe_task_queue(request) From b6c7a76182a438044cb39466486f00576ec3d30c Mon Sep 17 00:00:00 2001 From: Progyan Bhattacharya <14367736+0xTheProDev@users.noreply.github.com> Date: Fri, 5 Jan 2024 21:23:21 +0530 Subject: [PATCH 108/125] fix: Use Standard Interface for Metrics Tags (#228) Convert keyword argument into hashes in order to fulfill Temporal::Metrics API contract obligations. Fixes: #90 Signed-off-by: Progyan Bhattacharya --- lib/temporal/activity/poller.rb | 2 +- lib/temporal/activity/task_processor.rb | 18 +++++++-- lib/temporal/workflow/poller.rb | 4 +- lib/temporal/workflow/task_processor.rb | 24 +++++++----- .../unit/lib/temporal/activity/poller_spec.rb | 4 +- .../temporal/activity/task_processor_spec.rb | 21 ++++++---- .../unit/lib/temporal/workflow/poller_spec.rb | 4 +- .../temporal/workflow/task_processor_spec.rb | 39 ++++++++++++------- 8 files changed, 76 insertions(+), 40 deletions(-) diff --git a/lib/temporal/activity/poller.rb b/lib/temporal/activity/poller.rb index 40259f16..29c0977d 100644 --- a/lib/temporal/activity/poller.rb +++ b/lib/temporal/activity/poller.rb @@ -108,7 +108,7 @@ def poll_for_task def process(task) middleware_chain = Middleware::Chain.new(middleware) - TaskProcessor.new(task, namespace, activity_lookup, middleware_chain, config, heartbeat_thread_pool).process + TaskProcessor.new(task, task_queue, namespace, activity_lookup, middleware_chain, config, heartbeat_thread_pool).process end def poll_retry_seconds diff --git a/lib/temporal/activity/task_processor.rb b/lib/temporal/activity/task_processor.rb index 51ae5408..35f4bbc6 100644 --- a/lib/temporal/activity/task_processor.rb +++ b/lib/temporal/activity/task_processor.rb @@ -12,8 +12,9 @@ class Activity class TaskProcessor include Concerns::Payloads - def initialize(task, namespace, activity_lookup, middleware_chain, config, heartbeat_thread_pool) + def initialize(task, task_queue, namespace, activity_lookup, middleware_chain, config, heartbeat_thread_pool) @task = task + @task_queue = task_queue @namespace = namespace @metadata = Metadata.generate_activity_metadata(task, namespace) @task_token = task.task_token @@ -28,7 +29,7 @@ def process start_time = Time.now Temporal.logger.debug("Processing Activity task", metadata.to_h) - Temporal.metrics.timing(Temporal::MetricKeys::ACTIVITY_TASK_QUEUE_TIME, queue_time_ms, activity: activity_name, namespace: namespace, workflow: metadata.workflow_name) + Temporal.metrics.timing(Temporal::MetricKeys::ACTIVITY_TASK_QUEUE_TIME, queue_time_ms, metric_tags) context = Activity::Context.new(connection, metadata, config, heartbeat_thread_pool) @@ -52,13 +53,22 @@ def process end time_diff_ms = ((Time.now - start_time) * 1000).round - Temporal.metrics.timing(Temporal::MetricKeys::ACTIVITY_TASK_LATENCY, time_diff_ms, activity: activity_name, namespace: namespace, workflow: metadata.workflow_name) + Temporal.metrics.timing(Temporal::MetricKeys::ACTIVITY_TASK_LATENCY, time_diff_ms, metric_tags) Temporal.logger.debug("Activity task processed", metadata.to_h.merge(execution_time: time_diff_ms)) end + def metric_tags + { + activity: activity_name, + namespace: namespace, + task_queue: task_queue, + workflow: metadata.workflow_name + } + end + private - attr_reader :task, :namespace, :task_token, :activity_name, :activity_class, + attr_reader :task, :task_queue, :namespace, :task_token, :activity_name, :activity_class, :middleware_chain, :metadata, :config, :heartbeat_thread_pool def connection diff --git a/lib/temporal/workflow/poller.rb b/lib/temporal/workflow/poller.rb index 89fed958..198f4502 100644 --- a/lib/temporal/workflow/poller.rb +++ b/lib/temporal/workflow/poller.rb @@ -113,8 +113,8 @@ def process(task) middleware_chain = Middleware::Chain.new(middleware) workflow_middleware_chain = Middleware::Chain.new(workflow_middleware) - TaskProcessor.new(task, namespace, workflow_lookup, middleware_chain, workflow_middleware_chain, config, - binary_checksum).process + TaskProcessor.new(task, task_queue, namespace, workflow_lookup, middleware_chain, workflow_middleware_chain, + config, binary_checksum).process end def thread_pool diff --git a/lib/temporal/workflow/task_processor.rb b/lib/temporal/workflow/task_processor.rb index 9b79b454..f415aed5 100644 --- a/lib/temporal/workflow/task_processor.rb +++ b/lib/temporal/workflow/task_processor.rb @@ -24,8 +24,9 @@ def query_args MAX_FAILED_ATTEMPTS = 1 LEGACY_QUERY_KEY = :legacy_query - def initialize(task, namespace, workflow_lookup, middleware_chain, workflow_middleware_chain, config, binary_checksum) + def initialize(task, task_queue, namespace, workflow_lookup, middleware_chain, workflow_middleware_chain, config, binary_checksum) @task = task + @task_queue = task_queue @namespace = namespace @metadata = Metadata.generate_workflow_task_metadata(task, namespace) @task_token = task.task_token @@ -40,9 +41,8 @@ def initialize(task, namespace, workflow_lookup, middleware_chain, workflow_midd def process start_time = Time.now - Temporal.logger.debug('Processing Workflow task', metadata.to_h) - Temporal.metrics.timing(Temporal::MetricKeys::WORKFLOW_TASK_QUEUE_TIME, queue_time_ms, workflow: workflow_name, - namespace: namespace) + Temporal.logger.debug("Processing Workflow task", metadata.to_h) + Temporal.metrics.timing(Temporal::MetricKeys::WORKFLOW_TASK_QUEUE_TIME, queue_time_ms, metric_tags) raise Temporal::WorkflowNotRegistered, 'Workflow is not registered with this worker' unless workflow_class @@ -73,14 +73,21 @@ def process fail_task(e) ensure time_diff_ms = ((Time.now - start_time) * 1000).round - Temporal.metrics.timing(Temporal::MetricKeys::WORKFLOW_TASK_LATENCY, time_diff_ms, workflow: workflow_name, - namespace: namespace) + Temporal.metrics.timing(Temporal::MetricKeys::WORKFLOW_TASK_LATENCY, time_diff_ms, metric_tags) Temporal.logger.debug('Workflow task processed', metadata.to_h.merge(execution_time: time_diff_ms)) end + def metric_tags + { + workflow: workflow_name, + namespace: namespace, + task_queue: task_queue + } + end + private - attr_reader :task, :namespace, :task_token, :workflow_name, :workflow_class, + attr_reader :task, :task_queue, :namespace, :task_token, :workflow_name, :workflow_class, :middleware_chain, :workflow_middleware_chain, :metadata, :config, :binary_checksum def connection @@ -154,8 +161,7 @@ def complete_query(result) end def fail_task(error) - Temporal.metrics.increment(Temporal::MetricKeys::WORKFLOW_TASK_EXECUTION_FAILED, workflow: workflow_name, - namespace: namespace) + Temporal.metrics.increment(Temporal::MetricKeys::WORKFLOW_TASK_EXECUTION_FAILED, metric_tags) Temporal.logger.error('Workflow task failed', metadata.to_h.merge(error: error.inspect)) Temporal.logger.debug(error.backtrace.join("\n")) diff --git a/spec/unit/lib/temporal/activity/poller_spec.rb b/spec/unit/lib/temporal/activity/poller_spec.rb index 0476e950..76d8396a 100644 --- a/spec/unit/lib/temporal/activity/poller_spec.rb +++ b/spec/unit/lib/temporal/activity/poller_spec.rb @@ -108,7 +108,7 @@ def poll(task, times: 1) expect(Temporal::Activity::TaskProcessor) .to have_received(:new) - .with(task, namespace, lookup, middleware_chain, config, heartbeat_thread_pool) + .with(task, task_queue, namespace, lookup, middleware_chain, config, heartbeat_thread_pool) expect(task_processor).to have_received(:process) end @@ -143,7 +143,7 @@ def call(_); end expect(Temporal::Middleware::Chain).to have_received(:new).with(middleware) expect(Temporal::Activity::TaskProcessor) .to have_received(:new) - .with(task, namespace, lookup, middleware_chain, config, heartbeat_thread_pool) + .with(task, task_queue, namespace, lookup, middleware_chain, config, heartbeat_thread_pool) end end end diff --git a/spec/unit/lib/temporal/activity/task_processor_spec.rb b/spec/unit/lib/temporal/activity/task_processor_spec.rb index 41ea952f..e4ccdb2a 100644 --- a/spec/unit/lib/temporal/activity/task_processor_spec.rb +++ b/spec/unit/lib/temporal/activity/task_processor_spec.rb @@ -5,9 +5,10 @@ require 'temporal/scheduled_thread_pool' describe Temporal::Activity::TaskProcessor do - subject { described_class.new(task, namespace, lookup, middleware_chain, config, heartbeat_thread_pool) } + subject { described_class.new(task, task_queue, namespace, lookup, middleware_chain, config, heartbeat_thread_pool) } let(:namespace) { 'test-namespace' } + let(:task_queue) { 'test-queue' } let(:lookup) { instance_double('Temporal::ExecutableLookup', find: nil) } let(:task) do Fabricate( @@ -149,9 +150,11 @@ .with( Temporal::MetricKeys::ACTIVITY_TASK_QUEUE_TIME, an_instance_of(Integer), - activity: activity_name, - namespace: namespace, - workflow: workflow_name + hash_including({ + activity: activity_name, + namespace: namespace, + workflow: workflow_name + }) ) end @@ -165,6 +168,7 @@ an_instance_of(Integer), activity: activity_name, namespace: namespace, + task_queue: task_queue, workflow: workflow_name ) end @@ -240,9 +244,11 @@ .with( Temporal::MetricKeys::ACTIVITY_TASK_QUEUE_TIME, an_instance_of(Integer), - activity: activity_name, - namespace: namespace, - workflow: workflow_name + hash_including({ + activity: activity_name, + namespace: namespace, + workflow: workflow_name + }) ) end @@ -256,6 +262,7 @@ an_instance_of(Integer), activity: activity_name, namespace: namespace, + task_queue: task_queue, workflow: workflow_name ) end diff --git a/spec/unit/lib/temporal/workflow/poller_spec.rb b/spec/unit/lib/temporal/workflow/poller_spec.rb index e8d5692b..020e2e91 100644 --- a/spec/unit/lib/temporal/workflow/poller_spec.rb +++ b/spec/unit/lib/temporal/workflow/poller_spec.rb @@ -113,7 +113,7 @@ def poll(task, times: 1) expect(Temporal::Workflow::TaskProcessor) .to have_received(:new) - .with(task, namespace, lookup, empty_middleware_chain, empty_middleware_chain, config, binary_checksum) + .with(task, task_queue, namespace, lookup, empty_middleware_chain, empty_middleware_chain, config, binary_checksum) expect(task_processor).to have_received(:process) end @@ -151,7 +151,7 @@ def call(_); end expect(Temporal::Middleware::Chain).to have_received(:new).with(workflow_middleware) expect(Temporal::Workflow::TaskProcessor) .to have_received(:new) - .with(task, namespace, lookup, middleware_chain, workflow_middleware_chain, config, binary_checksum) + .with(task, task_queue, namespace, lookup, middleware_chain, workflow_middleware_chain, config, binary_checksum) end end end diff --git a/spec/unit/lib/temporal/workflow/task_processor_spec.rb b/spec/unit/lib/temporal/workflow/task_processor_spec.rb index 33d5506f..6ad3c12c 100644 --- a/spec/unit/lib/temporal/workflow/task_processor_spec.rb +++ b/spec/unit/lib/temporal/workflow/task_processor_spec.rb @@ -5,10 +5,11 @@ describe Temporal::Workflow::TaskProcessor do subject do - described_class.new(task, namespace, lookup, middleware_chain, workflow_middleware_chain, config, binary_checksum) + described_class.new(task, task_queue, namespace, lookup, middleware_chain, workflow_middleware_chain, config, binary_checksum) end let(:namespace) { 'test-namespace' } + let(:task_queue) { 'test-queue' } let(:lookup) { instance_double('Temporal::ExecutableLookup', find: nil) } let(:query) { nil } let(:queries) { nil } @@ -73,8 +74,10 @@ .to have_received(:increment) .with( Temporal::MetricKeys::WORKFLOW_TASK_EXECUTION_FAILED, - workflow: workflow_name, - namespace: namespace + hash_including({ + workflow: workflow_name, + namespace: namespace + }) ) end end @@ -203,8 +206,10 @@ .with( Temporal::MetricKeys::WORKFLOW_TASK_QUEUE_TIME, an_instance_of(Integer), - workflow: workflow_name, - namespace: namespace + hash_including({ + workflow: workflow_name, + namespace: namespace + }) ) end @@ -216,8 +221,10 @@ .with( Temporal::MetricKeys::WORKFLOW_TASK_LATENCY, an_instance_of(Integer), - workflow: workflow_name, - namespace: namespace + hash_including({ + workflow: workflow_name, + namespace: namespace + }) ) end end @@ -251,8 +258,10 @@ .to have_received(:increment) .with( Temporal::MetricKeys::WORKFLOW_TASK_EXECUTION_FAILED, - workflow: workflow_name, - namespace: namespace + hash_including({ + workflow: workflow_name, + namespace: namespace + }) ) end end @@ -312,8 +321,10 @@ .with( Temporal::MetricKeys::WORKFLOW_TASK_QUEUE_TIME, an_instance_of(Integer), - workflow: workflow_name, - namespace: namespace + hash_including({ + workflow: workflow_name, + namespace: namespace + }) ) end @@ -325,8 +336,10 @@ .with( Temporal::MetricKeys::WORKFLOW_TASK_LATENCY, an_instance_of(Integer), - workflow: workflow_name, - namespace: namespace + hash_including({ + workflow: workflow_name, + namespace: namespace + }) ) end end From 95d62d239448cb062994006be335e00c2d028db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Dold=C3=A1n?= Date: Fri, 5 Jan 2024 13:50:25 -0300 Subject: [PATCH 109/125] Add keyword arguments support to Activity classes (#255) --- lib/temporal/activity.rb | 5 +- lib/temporal/callable.rb | 19 +++++++ lib/temporal/workflow.rb | 5 +- spec/unit/lib/temporal/activity_spec.rb | 49 +++++++++++++++-- spec/unit/lib/temporal/workflow_spec.rb | 72 +++++++++++++++++++++++++ 5 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 lib/temporal/callable.rb diff --git a/lib/temporal/activity.rb b/lib/temporal/activity.rb index a3a726af..d5524a0a 100644 --- a/lib/temporal/activity.rb +++ b/lib/temporal/activity.rb @@ -1,4 +1,5 @@ require 'temporal/activity/workflow_convenience_methods' +require 'temporal/callable' require 'temporal/concerns/executable' require 'temporal/errors' @@ -9,7 +10,9 @@ class Activity def self.execute_in_context(context, input) activity = new(context) - activity.execute(*input) + callable = Temporal::Callable.new(method: activity.method(:execute)) + + callable.call(input) end def initialize(context) diff --git a/lib/temporal/callable.rb b/lib/temporal/callable.rb new file mode 100644 index 00000000..3bec64fd --- /dev/null +++ b/lib/temporal/callable.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Temporal + class Callable + def initialize(method:) + @method = method + end + + def call(input) + if input.is_a?(Array) && input.last.instance_of?(Hash) + *args, kwargs = input + + @method.call(*args, **kwargs) + else + @method.call(*input) + end + end + end +end diff --git a/lib/temporal/workflow.rb b/lib/temporal/workflow.rb index 3b5dcfe6..c135a19c 100644 --- a/lib/temporal/workflow.rb +++ b/lib/temporal/workflow.rb @@ -1,3 +1,4 @@ +require 'temporal/callable' require 'temporal/concerns/executable' require 'temporal/workflow/convenience_methods' require 'temporal/thread_local_context' @@ -13,7 +14,9 @@ def self.execute_in_context(context, input) Temporal::ThreadLocalContext.set(context) workflow = new(context) - result = workflow.execute(*input) + callable = Temporal::Callable.new(method: workflow.method(:execute)) + + result = callable.call(input) context.complete(result) unless context.completed? rescue StandardError, ScriptError => error diff --git a/spec/unit/lib/temporal/activity_spec.rb b/spec/unit/lib/temporal/activity_spec.rb index 47a0e8b0..10b32677 100644 --- a/spec/unit/lib/temporal/activity_spec.rb +++ b/spec/unit/lib/temporal/activity_spec.rb @@ -4,15 +4,28 @@ describe Temporal::Activity do it_behaves_like 'an executable' + class ArgsActivity < Temporal::Activity + def execute(a) + 'args result' + end + end + + class KwargsActivity < Temporal::Activity + def execute(a, b:, c:) + 'kwargs result' + end + end + subject { described_class.new(context) } let(:context) { instance_double('Temporal::Activity::Context') } describe '.execute_in_context' do + subject { ArgsActivity.new(context) } + let(:input) { ['test'] } before do allow(described_class).to receive(:new).and_return(subject) - allow(subject).to receive(:execute).and_return('result') end it 'passes the context' do @@ -22,13 +35,41 @@ end it 'calls #execute' do - described_class.execute_in_context(context, input) + expect(subject).to receive(:execute).with(*input) - expect(subject).to have_received(:execute).with(*input) + described_class.execute_in_context(context, input) end it 'returns #execute result' do - expect(described_class.execute_in_context(context, input)).to eq('result') + expect(described_class.execute_in_context(context, input)).to eq('args result') + end + + context 'when using keyword arguments' do + subject { KwargsActivity.new(context) } + + let(:input) { ['test', { b: 'b', c: 'c' }] } + + it 'passes the context' do + described_class.execute_in_context(context, input) + + expect(described_class).to have_received(:new).with(context) + end + + it 'calls #execute' do + expect(subject).to receive(:execute).with('test', b: 'b', c: 'c') + + described_class.execute_in_context(context, input) + end + + it 'does not raise an ArgumentError' do + expect { + described_class.execute_in_context(context, input) + }.not_to raise_error(ArgumentError, 'wrong number of arguments (given 2, expected 1; required keywords: b, c)') + end + + it 'returns #execute result' do + expect(described_class.execute_in_context(context, input)).to eq('kwargs result') + end end end diff --git a/spec/unit/lib/temporal/workflow_spec.rb b/spec/unit/lib/temporal/workflow_spec.rb index b8f6af5f..41014aae 100644 --- a/spec/unit/lib/temporal/workflow_spec.rb +++ b/spec/unit/lib/temporal/workflow_spec.rb @@ -1,6 +1,78 @@ require 'temporal/workflow' +require 'temporal/workflow/context' require 'shared_examples/an_executable' describe Temporal::Workflow do it_behaves_like 'an executable' + + class ArgsWorkflow < Temporal::Workflow + def execute(a) + 'args result' + end + end + + class KwargsWorkflow < Temporal::Workflow + def execute(a, b:, c:) + 'kwargs result' + end + end + + subject { described_class.new(ctx) } + let(:ctx) { instance_double('Temporal::Workflow::Context') } + + before do + allow(ctx).to receive(:completed?).and_return(true) + end + + describe '.execute_in_context' do + subject { ArgsWorkflow.new(ctx) } + + let(:input) { ['test'] } + + before do + allow(described_class).to receive(:new).and_return(subject) + end + + it 'passes the context' do + described_class.execute_in_context(ctx, input) + + expect(described_class).to have_received(:new).with(ctx) + end + + it 'calls #execute' do + expect(subject).to receive(:execute).with(*input) + + described_class.execute_in_context(ctx, input) + end + + context 'when using keyword arguments' do + subject { KwargsWorkflow.new(ctx) } + + let(:input) { ['test', { b: 'b', c: 'c' }] } + + it 'passes the context' do + described_class.execute_in_context(ctx, input) + + expect(described_class).to have_received(:new).with(ctx) + end + + it 'calls #execute' do + expect(subject).to receive(:execute).with('test', b: 'b', c: 'c') + + described_class.execute_in_context(ctx, input) + end + + it 'does not raise an ArgumentError' do + expect { + described_class.execute_in_context(ctx, input) + }.not_to raise_error(ArgumentError, 'wrong number of arguments (given 2, expected 1; required keywords: b, c)') + end + end + end + + describe '#execute' do + it 'is not implemented on a superclass' do + expect { subject.execute }.to raise_error(NotImplementedError) + end + end end From 3e0dae708ec0e3eab8c44b57b64f5cd1881848e6 Mon Sep 17 00:00:00 2001 From: Jeff Schoner Date: Tue, 16 Jan 2024 11:07:56 -0800 Subject: [PATCH 110/125] Fix warnings (#282) * Add base64 to gemspec for Ruby 3.4.0 * Fix not to raise expectations to stop warning --- spec/unit/lib/temporal/activity_spec.rb | 2 +- spec/unit/lib/temporal/workflow_spec.rb | 2 +- temporal.gemspec | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/unit/lib/temporal/activity_spec.rb b/spec/unit/lib/temporal/activity_spec.rb index 10b32677..f7dc5662 100644 --- a/spec/unit/lib/temporal/activity_spec.rb +++ b/spec/unit/lib/temporal/activity_spec.rb @@ -64,7 +64,7 @@ def execute(a, b:, c:) it 'does not raise an ArgumentError' do expect { described_class.execute_in_context(context, input) - }.not_to raise_error(ArgumentError, 'wrong number of arguments (given 2, expected 1; required keywords: b, c)') + }.not_to raise_error end it 'returns #execute result' do diff --git a/spec/unit/lib/temporal/workflow_spec.rb b/spec/unit/lib/temporal/workflow_spec.rb index 41014aae..fb8cc32a 100644 --- a/spec/unit/lib/temporal/workflow_spec.rb +++ b/spec/unit/lib/temporal/workflow_spec.rb @@ -65,7 +65,7 @@ def execute(a, b:, c:) it 'does not raise an ArgumentError' do expect { described_class.execute_in_context(ctx, input) - }.not_to raise_error(ArgumentError, 'wrong number of arguments (given 2, expected 1; required keywords: b, c)') + }.not_to raise_error end end end diff --git a/temporal.gemspec b/temporal.gemspec index 8c51adef..3cc624ab 100644 --- a/temporal.gemspec +++ b/temporal.gemspec @@ -14,6 +14,7 @@ Gem::Specification.new do |spec| spec.require_paths = ['lib'] spec.files = Dir["{lib,rbi}/**/*.*"] + %w(temporal.gemspec Gemfile LICENSE README.md) + spec.add_dependency 'base64' spec.add_dependency 'grpc' spec.add_dependency 'oj' From 65dfdb0822d3e71342e4dd34629565ee962c4512 Mon Sep 17 00:00:00 2001 From: Hugh Evans <2580+hughevans@users.noreply.github.com> Date: Tue, 6 Feb 2024 08:07:42 +1100 Subject: [PATCH 111/125] Add option for gRPC client connection retries (#270) * Allow passing channel args to GRPC connection * Add config.connection_options hash * Add option for client grpc connection retries * Allow passing custom gRPC retry policy --- lib/temporal/configuration.rb | 8 ++- lib/temporal/connection.rb | 3 +- lib/temporal/connection/grpc.rb | 39 +++++++++++- spec/unit/lib/temporal/grpc_spec.rb | 97 +++++++++++++++++++++++++++++ 4 files changed, 141 insertions(+), 6 deletions(-) diff --git a/lib/temporal/configuration.rb b/lib/temporal/configuration.rb index 9deb4226..a2c895bc 100644 --- a/lib/temporal/configuration.rb +++ b/lib/temporal/configuration.rb @@ -12,13 +12,13 @@ module Temporal class Configuration - Connection = Struct.new(:type, :host, :port, :credentials, :identity, keyword_init: true) + Connection = Struct.new(:type, :host, :port, :credentials, :identity, :connection_options, keyword_init: true) Execution = Struct.new(:namespace, :task_queue, :timeouts, :headers, :search_attributes, keyword_init: true) attr_reader :timeouts, :error_handlers, :capabilities attr_accessor :connection_type, :converter, :use_error_serialization_v2, :host, :port, :credentials, :identity, :logger, :metrics_adapter, :namespace, :task_queue, :headers, :search_attributes, :header_propagators, - :payload_codec, :legacy_signals, :no_signals_in_first_task + :payload_codec, :legacy_signals, :no_signals_in_first_task, :connection_options # See https://docs.temporal.io/blog/activity-timeouts/ for general docs. # We want an infinite execution timeout for cron schedules and other perpetual workflows. @@ -84,6 +84,7 @@ def initialize @search_attributes = {} @header_propagators = [] @capabilities = Capabilities.new(self) + @connection_options = {} # Signals previously were incorrectly replayed in order within a workflow task window, rather # than at the beginning. Correcting this changes the determinism of any workflow with signals. @@ -120,7 +121,8 @@ def for_connection host: host, port: port, credentials: credentials, - identity: identity || default_identity + identity: identity || default_identity, + connection_options: connection_options ).freeze end diff --git a/lib/temporal/connection.rb b/lib/temporal/connection.rb index b70bcbed..a36fe091 100644 --- a/lib/temporal/connection.rb +++ b/lib/temporal/connection.rb @@ -12,8 +12,9 @@ def self.generate(configuration) port = configuration.port credentials = configuration.credentials identity = configuration.identity + options = configuration.connection_options - connection_class.new(host, port, identity, credentials) + connection_class.new(host, port, identity, credentials, options) end end end diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index 06be6a6d..1bec78f2 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -2,6 +2,7 @@ require 'time' require 'google/protobuf/well_known_types' require 'securerandom' +require 'json' require 'gen/temporal/api/filter/v1/message_pb' require 'gen/temporal/api/workflowservice/v1/service_services_pb' require 'gen/temporal/api/operatorservice/v1/service_services_pb' @@ -795,11 +796,45 @@ def pause_schedule(namespace:, schedule_id:, should_pause:, note: nil) attr_reader :url, :identity, :credentials, :options, :poll_mutex, :poll_request def client - @client ||= Temporalio::Api::WorkflowService::V1::WorkflowService::Stub.new( + return @client if @client + + channel_args = {} + + if options[:keepalive_time_ms] + channel_args["grpc.keepalive_time_ms"] = options[:keepalive_time_ms] + end + + if options[:retry_connection] || options[:retry_policy] + channel_args["grpc.enable_retries"] = 1 + + retry_policy = options[:retry_policy] || { + retryableStatusCodes: ["UNAVAILABLE"], + maxAttempts: 3, + initialBackoff: "0.1s", + backoffMultiplier: 2.0, + maxBackoff: "0.3s" + } + + channel_args["grpc.service_config"] = ::JSON.generate( + methodConfig: [ + { + name: [ + { + service: "temporal.api.workflowservice.v1.WorkflowService", + } + ], + retryPolicy: retry_policy + } + ] + ) + end + + @client = Temporalio::Api::WorkflowService::V1::WorkflowService::Stub.new( url, credentials, timeout: CONNECTION_TIMEOUT_SECONDS, - interceptors: [ClientNameVersionInterceptor.new] + interceptors: [ClientNameVersionInterceptor.new], + channel_args: channel_args ) end diff --git a/spec/unit/lib/temporal/grpc_spec.rb b/spec/unit/lib/temporal/grpc_spec.rb index ee3c1fcb..0799c5d0 100644 --- a/spec/unit/lib/temporal/grpc_spec.rb +++ b/spec/unit/lib/temporal/grpc_spec.rb @@ -830,4 +830,101 @@ class TestDeserializer end end end + + describe "passing in options" do + before do + allow(subject).to receive(:client).and_call_original + end + + context "when keepalive_time_ms is passed" do + subject { Temporal::Connection::GRPC.new(nil, nil, identity, :this_channel_is_insecure, keepalive_time_ms: 30_000) } + + it "passes the option to the channel args" do + expect(Temporalio::Api::WorkflowService::V1::WorkflowService::Stub).to receive(:new).with( + ":", + :this_channel_is_insecure, + timeout: 60, + interceptors: [instance_of(Temporal::Connection::ClientNameVersionInterceptor)], + channel_args: { + "grpc.keepalive_time_ms" => 30_000 + } + ) + subject.send(:client) + end + end + + context "when passing retry_connection" do + subject { Temporal::Connection::GRPC.new(nil, nil, identity, :this_channel_is_insecure, retry_connection: true) } + + it "passes the option to the channel args" do + expect(Temporalio::Api::WorkflowService::V1::WorkflowService::Stub).to receive(:new).with( + ":", + :this_channel_is_insecure, + timeout: 60, + interceptors: [instance_of(Temporal::Connection::ClientNameVersionInterceptor)], + channel_args: { + "grpc.enable_retries" => 1, + "grpc.service_config" => { + methodConfig: [ + { + name: [ + { + service: "temporal.api.workflowservice.v1.WorkflowService", + } + ], + retryPolicy: { + retryableStatusCodes: ["UNAVAILABLE"], + maxAttempts: 3, + initialBackoff: "0.1s", + backoffMultiplier: 2.0, + maxBackoff: "0.3s" + } + } + ] + }.to_json + } + ) + subject.send(:client) + end + end + + context "when passing a custom retry policy" do + subject { Temporal::Connection::GRPC.new(nil, nil, identity, :this_channel_is_insecure, retry_policy: retry_policy) } + + let(:retry_policy) do + { + retryableStatusCodes: ["UNAVAILABLE", "INTERNAL"], + maxAttempts: 1, + initialBackoff: "0.2s", + backoffMultiplier: 1.0, + maxBackoff: "0.5s" + } + end + + it "passes the policy to the channel args" do + expect(Temporalio::Api::WorkflowService::V1::WorkflowService::Stub).to receive(:new).with( + ":", + :this_channel_is_insecure, + timeout: 60, + interceptors: [instance_of(Temporal::Connection::ClientNameVersionInterceptor)], + channel_args: { + "grpc.enable_retries" => 1, + "grpc.service_config" => { + methodConfig: [ + { + name: [ + { + service: "temporal.api.workflowservice.v1.WorkflowService", + } + ], + retryPolicy: retry_policy + } + ] + }.to_json + } + ) + subject.send(:client) + end + end + end end From c4fb094638c85289c16b4611c3432f759cfb13a8 Mon Sep 17 00:00:00 2001 From: DeRauk Gibble Date: Tue, 5 Mar 2024 16:27:45 -0500 Subject: [PATCH 112/125] Update README with middleware documentation (#288) * Update README * Add heading --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.md b/README.md index 04a1533b..ff47a2d5 100644 --- a/README.md +++ b/README.md @@ -400,6 +400,36 @@ arguments are identical to the `Temporal.start_workflow` API. set it to allow as many invocations as you need. You can also set it to `nil`, which will use a default value of 10 years.* +## Middleware +Middleware sits between the execution of your workflows/activities and the Temporal SDK, allowing you to insert custom code before or after the execution. + +### Activity Middleware Stack +Middleware added to the activity middleware stack will be executed around each activity method. This is useful when you want to perform a certain task before and/or after each activity execution, such as logging, error handling, or measuring execution time. + +### Workflow Middleware Stack +There are actually two types of workflow middleware in Temporal Ruby SDK: + +*Workflow Middleware*: This middleware is executed around each entire workflow. This is similar to activity middleware, but for workflows. + +*Workflow Task Middleware*: This middleware is executed around each workflow task, of which there will be many for each workflow. + +### Example +To add a middleware, you need to define a class that responds to the call method. Within the call method, you should call yield to allow the next middleware in the stack (or the workflow/activity method itself if there are no more middlewares) to execute. Here's an example: + +``` +class MyMiddleware + def call(metadata) + puts "Before execution" + yield + puts "After execution" + result + end +end +``` + +You can add this middleware to the stack like so `worker.add_activity_middleware(MyMiddleware)` + +Please note that the order of middleware in the stack matters. The middleware that is added last will be the first one to execute. In the example above, MyMiddleware will execute before any other middleware in the stack. ## Breaking Changes From b20abf69ed7b5818ba02454f74f6a1efe7a772b7 Mon Sep 17 00:00:00 2001 From: DeRauk Gibble Date: Sat, 6 Apr 2024 14:54:49 -0700 Subject: [PATCH 113/125] Pin examples auto-setup image to v1.22.0 (#298) * Testing build with readme change * Sleep longer * Pin Temporal to version 1.22 * Remove test changes * Nit --- examples/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/docker-compose.yml b/examples/docker-compose.yml index 4bff724c..03d87744 100644 --- a/examples/docker-compose.yml +++ b/examples/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.5' services: temporal: - image: temporalio/auto-setup:latest + image: temporalio/auto-setup:1.22.0 ports: - "7233:7233" environment: From f0751020055f922a1e376f25d9dc0d237a311a78 Mon Sep 17 00:00:00 2001 From: Salvatore Testa Date: Sat, 6 Apr 2024 15:49:36 -0700 Subject: [PATCH 114/125] Move off `Dry::Struct::Value` before its removed from `dry-struct` (#293) * Bump `dry-types`/`dry-struct` example versions The current versions in the examples are pretty old. * Use `Dry::Struct` instead of `Dry::Struct::Value` The gem is warning that `Dry::Struct::Value` is finally going to be removed. ``` [dry-struct] Dry::Struct::Value is deprecated and will be removed in the next major version /Users/sal/Development/temporal-ruby/lib/temporal/concerns/typed.rb:35:in `generate_struct' ``` --- examples/Gemfile | 4 ++-- lib/temporal/concerns/typed.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/Gemfile b/examples/Gemfile index 9c543b77..38523465 100644 --- a/examples/Gemfile +++ b/examples/Gemfile @@ -2,7 +2,7 @@ source 'https://rubygems.org' gem 'temporal-ruby', path: '../' -gem 'dry-types', '>= 1.2.0' -gem 'dry-struct', '~> 1.1.1' +gem 'dry-types', '>= 1.7.2' +gem 'dry-struct', '~> 1.6.0' gem 'rspec', group: :test diff --git a/lib/temporal/concerns/typed.rb b/lib/temporal/concerns/typed.rb index 2a05f144..0b8c6702 100644 --- a/lib/temporal/concerns/typed.rb +++ b/lib/temporal/concerns/typed.rb @@ -32,7 +32,7 @@ def input(klass = nil, &block) private def generate_struct - Class.new(Dry::Struct::Value) { transform_keys(&:to_sym) } + Class.new(Dry::Struct) { transform_keys(&:to_sym) } end end end From 34a7e4d8559f1c6da517f9f3e30168c4c10bd098 Mon Sep 17 00:00:00 2001 From: David Hughes Date: Thu, 9 May 2024 14:49:54 -0700 Subject: [PATCH 115/125] Plumb through :use_error_serialization_v2 from Configuration -> GRPC (#296) --- lib/temporal/configuration.rb | 2 +- lib/temporal/connection/grpc.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/temporal/configuration.rb b/lib/temporal/configuration.rb index a2c895bc..014187c1 100644 --- a/lib/temporal/configuration.rb +++ b/lib/temporal/configuration.rb @@ -122,7 +122,7 @@ def for_connection port: port, credentials: credentials, identity: identity || default_identity, - connection_options: connection_options + connection_options: connection_options.merge(use_error_serialization_v2: @use_error_serialization_v2) ).freeze end diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index 1bec78f2..fa246e9f 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -311,7 +311,7 @@ def respond_activity_task_completed_by_id(namespace:, activity_id:, workflow_id: end def respond_activity_task_failed(namespace:, task_token:, exception:) - serialize_whole_error = Temporal.configuration.use_error_serialization_v2 + serialize_whole_error = options.fetch(:use_error_serialization_v2, Temporal.configuration.use_error_serialization_v2) request = Temporalio::Api::WorkflowService::V1::RespondActivityTaskFailedRequest.new( namespace: namespace, identity: identity, From 3fbc675fbe24bce236fba2376910fd5dc9a9ff5f Mon Sep 17 00:00:00 2001 From: Tao Guo Date: Fri, 10 May 2024 07:50:28 +1000 Subject: [PATCH 116/125] Mark continue_as_new as not implemented in testing context (#299) Raise NotImplementedError instead of NoMethodError for clarify. --- lib/temporal/testing/local_workflow_context.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/temporal/testing/local_workflow_context.rb b/lib/temporal/testing/local_workflow_context.rb index 0f30fe0f..7d3321ae 100644 --- a/lib/temporal/testing/local_workflow_context.rb +++ b/lib/temporal/testing/local_workflow_context.rb @@ -180,6 +180,10 @@ def fail(exception) raise exception end + def continue_as_new(*input, **args) + raise NotImplementedError, 'not yet available for testing' + end + def wait_for_all(*futures) futures.each(&:wait) From 5d12aa30e3f3169c5950c7cfb287bca796ce1a4e Mon Sep 17 00:00:00 2001 From: Jeff Schoner Date: Mon, 24 Jun 2024 08:04:57 -0700 Subject: [PATCH 117/125] Replay testing (#300) * Specialize workflow event targets * Methods for downloading histories * Replay tester * Basic replay tester unit tests * Add example replay test with history file * Add workflow stack trace to replay error * Dynamically load replay state in workflow logger * Log when replaying in replay tests * Use binpb extension for protobuf biniaries * Better comments about logging during replay * Simplify file -> bytes read calls * Fix comment typos * More ergonomic replaying callback * Improve ReplayTesterError, rubyfmt spec * Remove extra commands check * Don't default to logging in replay tests * Check history starts correctly * Remove correct_event_types * Refactor to more composable API * Use real namespace from configuration * rubyfmt * Fix test name typo --- examples/bin/update_replay_test_histories | 51 +++ .../replay/histories/signal_with_start.binpb | Bin 0 -> 1959 bytes .../replay/histories/signal_with_start.json | 361 ++++++++++++++++++ .../spec/replay/signal_with_start_spec.rb | 21 + .../workflows/signal_with_start_workflow.rb | 10 +- lib/temporal.rb | 5 +- lib/temporal/client.rb | 42 +- lib/temporal/configuration.rb | 4 +- lib/temporal/testing/replay_tester.rb | 73 ++++ lib/temporal/workflow/context.rb | 6 +- lib/temporal/workflow/executor.rb | 2 +- lib/temporal/workflow/history.rb | 3 + lib/temporal/workflow/history/event_target.rb | 19 +- .../workflow/history/serialization.rb | 61 +++ lib/temporal/workflow/replay_aware_logger.rb | 8 +- lib/temporal/workflow/state_manager.rb | 23 +- .../testing/replay_histories/do_nothing.json | 103 +++++ .../temporal/testing/replay_tester_spec.rb | 142 +++++++ .../temporal/workflow/state_manager_spec.rb | 14 +- 19 files changed, 915 insertions(+), 33 deletions(-) create mode 100755 examples/bin/update_replay_test_histories create mode 100644 examples/spec/replay/histories/signal_with_start.binpb create mode 100644 examples/spec/replay/histories/signal_with_start.json create mode 100644 examples/spec/replay/signal_with_start_spec.rb create mode 100644 lib/temporal/testing/replay_tester.rb create mode 100644 lib/temporal/workflow/history/serialization.rb create mode 100644 spec/unit/lib/temporal/testing/replay_histories/do_nothing.json create mode 100644 spec/unit/lib/temporal/testing/replay_tester_spec.rb diff --git a/examples/bin/update_replay_test_histories b/examples/bin/update_replay_test_histories new file mode 100755 index 00000000..bc0f807a --- /dev/null +++ b/examples/bin/update_replay_test_histories @@ -0,0 +1,51 @@ +#!/usr/bin/env ruby + +# This script regenerates the workflow history files used in the example replay tests +# under examples/spec/replay/histories. It starts the necessary workflow, sends some +# signals, awaits workflow completion, then collects the history into JSON and protobuf +# binary file formats. +# +# To use this, start your Temporal server and bin/worker first. This script can then +# be run without any arguments. It will overwrite existing history files in the tree. +# +# NOTE: By default, collected history files contain the host names of the machines +# where the worker and this script are run because the default identity is pid@hostname. +# If you'd like, you can override this by setting an identity in the configuration in +# init.rb. + +require_relative "../init" +require_relative "../workflows/signal_with_start_workflow" + +workflow_id = SecureRandom.uuid +run_id = Temporal.start_workflow( + SignalWithStartWorkflow, + "hit", + options: { + workflow_id: workflow_id, + timeouts: { + execution: 30 + }, + signal_name: "miss", + signal_input: 1 + } +) +Temporal.logger.info("Started workflow", {workflow_id: workflow_id, run_id: run_id}) +sleep(1) +Temporal.signal_workflow(SignalWithStartWorkflow, "miss", workflow_id, run_id, 2) +sleep(1) +Temporal.signal_workflow(SignalWithStartWorkflow, "hit", workflow_id, run_id, 3) +Temporal.await_workflow_result(SignalWithStartWorkflow, workflow_id: workflow_id, run_id: run_id) + +# Save in JSON, exactly like would be downloaded from Temporal UI +history_json = Temporal.get_workflow_history_json(workflow_id: workflow_id, run_id: run_id) +filename = File.expand_path("../spec/replay/histories/signal_with_start.json", File.dirname(__FILE__)) +File.open(filename, "w") do |f| + f.write(history_json) +end + +# Save in protobuf binary format +history_binary = Temporal.get_workflow_history_protobuf(workflow_id: workflow_id, run_id: run_id) +filename = File.expand_path("../spec/replay/histories/signal_with_start.protobin", File.dirname(__FILE__)) +File.open(filename, "wb") do |f| + f.write(history_binary) +end diff --git a/examples/spec/replay/histories/signal_with_start.binpb b/examples/spec/replay/histories/signal_with_start.binpb new file mode 100644 index 0000000000000000000000000000000000000000..7d7bf89c999e7ea675892378a8415d3ed39853ea GIT binary patch literal 1959 zcmbuAPiP!f9LM+V>}H>DlV*34>bhdwE;$TsADuVz=3f+PON$Z@N}^4vD9pU~CRtr~ z!|vjrp3+zq3|Q$QXtX5-lWL4ss~+q@N(Bq{Vgf=?=)r>rp%etsgNSc7Fa+HVg)Hpt zH=o~rKl}Ur1}!8ZPe)ga%+FC%_{|<&CJa78#76CFS=*EGFkJh6P4D~c*}3J zChCpn!)pDdy;+o*6gAQCt2tooK@Xt25fn96=at%|ik@rMYX_#Qex;_S%TtwBc_ayk z4$JRo5~9X1vxUorXJaaei>dB$>FO~cOb>L%G2;QjGzdBN9FyVDr=}30&BMUbZl%$) z@U9qs>h@osMIPrPk4Ir92Y2idJq3~EpKdGscJ-sBNZ~teg%2V{?%4LTXm&T4jj?BM zMcB0V?luN{lQU^dsgbA#UjxGQe(#oPti0BILwgOjTOB?(mrk@~2pR_pT0Rrw*- zs=Qcfy;9z~kb4O_fU+&oY~d;4bJ0*sUH8AI9MzQ#jP`Y{$(O~;(}J}GFE!hjcI%}a z8cQ7S3-U;w=zL=HbFi3%T;7BJILD2ju3u9MS#3$?m<5Mq!p<989Cpmm^F(N3d1~e9 zHHeRW?s!=4shng=mRGw`-E2a$8-g?QVbR zzp-+=3jO8IzYOhz`MBfSx%X0X0x!yrop0d@sK&)Bi#L%Rz!h06Phq}ma^IlBCxLv+ zST4ns_yW6(nOJvSJ#c-G+MH;g_n~LtK+OLAlOG1<{Cz6fa)a|Zb?5Wv!NC}R@tgI* SLQ=aV@gsZCwyzO0H}EgVb`d!M literal 0 HcmV?d00001 diff --git a/examples/spec/replay/histories/signal_with_start.json b/examples/spec/replay/histories/signal_with_start.json new file mode 100644 index 00000000..fe301a04 --- /dev/null +++ b/examples/spec/replay/histories/signal_with_start.json @@ -0,0 +1,361 @@ +{ + "events": [ + { + "eventId": "1", + "eventTime": "2024-05-28T02:46:26.852786129Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", + "taskId": "31457280", + "workflowExecutionStartedEventAttributes": { + "workflowType": { + "name": "SignalWithStartWorkflow" + }, + "taskQueue": { + "name": "general", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "ImhpdCI=" + } + ] + }, + "workflowExecutionTimeout": "30s", + "workflowRunTimeout": "30s", + "workflowTaskTimeout": "10s", + "originalExecutionRunId": "c6e8de96-4e18-409d-8e60-38d58f2f11b9", + "identity": "4514@DESKTOP-JRJDVRG\n", + "firstExecutionRunId": "c6e8de96-4e18-409d-8e60-38d58f2f11b9", + "attempt": 1, + "workflowExecutionExpirationTime": "2024-05-28T02:46:56.853Z", + "firstWorkflowTaskBackoff": "0s", + "memo": { + }, + "searchAttributes": { + }, + "header": { + } + } + }, + { + "eventId": "2", + "eventTime": "2024-05-28T02:46:26.852896774Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED", + "taskId": "31457281", + "workflowExecutionSignaledEventAttributes": { + "signalName": "miss", + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "MQ==" + } + ] + }, + "identity": "4514@DESKTOP-JRJDVRG\n", + "header": { + } + } + }, + { + "eventId": "3", + "eventTime": "2024-05-28T02:46:26.852900524Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "31457282", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "general", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "4", + "eventTime": "2024-05-28T02:46:26.873042948Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "31457287", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "3", + "identity": "4417@DESKTOP-JRJDVRG\n", + "requestId": "0074c78e-013b-4845-86d5-f83f1f6feb61", + "historySizeBytes": "421" + } + }, + { + "eventId": "5", + "eventTime": "2024-05-28T02:46:26.896346434Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "31457291", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "3", + "startedEventId": "4", + "identity": "4417@DESKTOP-JRJDVRG\n", + "binaryChecksum": "07d96d88e3691440609a4f5de039969b14a4e6f8", + "sdkMetadata": { + "langUsedFlags": [ + 2 + ] + } + } + }, + { + "eventId": "6", + "eventTime": "2024-05-28T02:46:27.869664722Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED", + "taskId": "31457294", + "workflowExecutionSignaledEventAttributes": { + "signalName": "miss", + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "Mg==" + } + ] + }, + "identity": "4514@DESKTOP-JRJDVRG\n" + } + }, + { + "eventId": "7", + "eventTime": "2024-05-28T02:46:27.869669568Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "31457295", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "general", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "8", + "eventTime": "2024-05-28T02:46:27.881436143Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "31457298", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "7", + "identity": "4417@DESKTOP-JRJDVRG\n", + "requestId": "b1c0b0cd-cdb1-4bfd-973c-fa43eef6dfb5", + "historySizeBytes": "749" + } + }, + { + "eventId": "9", + "eventTime": "2024-05-28T02:46:27.907949953Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "31457302", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "7", + "startedEventId": "8", + "identity": "4417@DESKTOP-JRJDVRG\n", + "binaryChecksum": "07d96d88e3691440609a4f5de039969b14a4e6f8" + } + }, + { + "eventId": "10", + "eventTime": "2024-05-28T02:46:28.883578435Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED", + "taskId": "31457304", + "workflowExecutionSignaledEventAttributes": { + "signalName": "hit", + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "Mw==" + } + ] + }, + "identity": "4514@DESKTOP-JRJDVRG\n" + } + }, + { + "eventId": "11", + "eventTime": "2024-05-28T02:46:28.883586706Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "31457305", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "general", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "12", + "eventTime": "2024-05-28T02:46:28.899268187Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "31457308", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "11", + "identity": "4417@DESKTOP-JRJDVRG\n", + "requestId": "4840d372-5d7f-46f0-af41-85c9fcac752d", + "historySizeBytes": "1071" + } + }, + { + "eventId": "13", + "eventTime": "2024-05-28T02:46:28.925343005Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "31457312", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "11", + "startedEventId": "12", + "identity": "4417@DESKTOP-JRJDVRG\n", + "binaryChecksum": "07d96d88e3691440609a4f5de039969b14a4e6f8" + } + }, + { + "eventId": "14", + "eventTime": "2024-05-28T02:46:28.925386163Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", + "taskId": "31457313", + "activityTaskScheduledEventAttributes": { + "activityId": "14", + "activityType": { + "name": "HelloWorldActivity" + }, + "taskQueue": { + "name": "general", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "header": { + "fields": { + "test-header": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "InRlc3Qi" + } + } + }, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "ImV4cGVjdGVkIHNpZ25hbCI=" + } + ] + }, + "scheduleToCloseTimeout": "30s", + "scheduleToStartTimeout": "30s", + "startToCloseTimeout": "30s", + "heartbeatTimeout": "0s", + "workflowTaskCompletedEventId": "13", + "retryPolicy": { + "initialInterval": "1s", + "backoffCoefficient": 2, + "maximumInterval": "100s" + } + } + }, + { + "eventId": "15", + "eventTime": "2024-05-28T02:46:28.944893259Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", + "taskId": "31457317", + "activityTaskStartedEventAttributes": { + "scheduledEventId": "14", + "identity": "4417@DESKTOP-JRJDVRG\n", + "requestId": "73f99ef3-e606-421a-ad79-a4e43e41ceba", + "attempt": 1 + } + }, + { + "eventId": "16", + "eventTime": "2024-05-28T02:46:29.008828231Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", + "taskId": "31457318", + "activityTaskCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IkhlbGxvIFdvcmxkLCBleHBlY3RlZCBzaWduYWwi" + } + ] + }, + "scheduledEventId": "14", + "startedEventId": "15", + "identity": "4417@DESKTOP-JRJDVRG\n" + } + }, + { + "eventId": "17", + "eventTime": "2024-05-28T02:46:29.008834769Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "31457319", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "general", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "18", + "eventTime": "2024-05-28T02:46:29.022515754Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "31457322", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "17", + "identity": "4417@DESKTOP-JRJDVRG\n", + "requestId": "a24ea1bd-8584-41ae-8cc3-0880b8a946d1", + "historySizeBytes": "1713" + } + }, + { + "eventId": "19", + "eventTime": "2024-05-28T02:46:29.043259634Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "31457326", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "17", + "startedEventId": "18", + "identity": "4417@DESKTOP-JRJDVRG\n", + "binaryChecksum": "07d96d88e3691440609a4f5de039969b14a4e6f8" + } + }, + { + "eventId": "20", + "eventTime": "2024-05-28T02:46:29.043294503Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", + "taskId": "31457327", + "workflowExecutionCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "Mw==" + } + ] + }, + "workflowTaskCompletedEventId": "19" + } + } + ] +} \ No newline at end of file diff --git a/examples/spec/replay/signal_with_start_spec.rb b/examples/spec/replay/signal_with_start_spec.rb new file mode 100644 index 00000000..13c1cb0d --- /dev/null +++ b/examples/spec/replay/signal_with_start_spec.rb @@ -0,0 +1,21 @@ +require "workflows/signal_with_start_workflow" +require "temporal/testing/replay_tester" +require "temporal/workflow/history/serialization" + +describe "signal with start" do + let(:replay_tester) { Temporal::Testing::ReplayTester.new } + + it "two misses, one hit, replay, json" do + replay_tester.replay_history( + SignalWithStartWorkflow, + Temporal::Workflow::History::Serialization.from_json_file("spec/replay/histories/signal_with_start.json") + ) + end + + it "two misses, one hit, replay, binary" do + replay_tester.replay_history( + SignalWithStartWorkflow, + Temporal::Workflow::History::Serialization.from_protobuf_file("spec/replay/histories/signal_with_start.binpb") + ) + end +end diff --git a/examples/workflows/signal_with_start_workflow.rb b/examples/workflows/signal_with_start_workflow.rb index ab94a5d1..cfee2bed 100644 --- a/examples/workflows/signal_with_start_workflow.rb +++ b/examples/workflows/signal_with_start_workflow.rb @@ -1,21 +1,25 @@ -require 'activities/hello_world_activity' +require "activities/hello_world_activity" class SignalWithStartWorkflow < Temporal::Workflow def execute(expected_signal) - initial_value = 'no signal received' + initial_value = "no signal received" received = initial_value workflow.on_signal do |signal, input| if signal == expected_signal - HelloWorldActivity.execute!('expected signal') + workflow.logger.info("Accepting expected signal #{signal}: #{input}") + HelloWorldActivity.execute!("expected signal") received = input + else + workflow.logger.info("Ignoring unexpected signal #{signal}: #{input}") end end # Wait for the activity in signal callbacks to complete. The workflow will # not automatically wait for any blocking calls made in callbacks to complete # before returning. + workflow.logger.info("Waiting for expected signal #{expected_signal}") workflow.wait_until { received != initial_value } received end diff --git a/lib/temporal.rb b/lib/temporal.rb index 4ba588a3..078defe0 100644 --- a/lib/temporal.rb +++ b/lib/temporal.rb @@ -43,7 +43,10 @@ module Temporal :update_schedule, :trigger_schedule, :pause_schedule, - :unpause_schedule + :unpause_schedule, + :get_workflow_history, + :get_workflow_history_json, + :get_workflow_history_protobuf class << self def configure(&block) diff --git a/lib/temporal/client.rb b/lib/temporal/client.rb index f1e6d3a7..e738e2b6 100644 --- a/lib/temporal/client.rb +++ b/lib/temporal/client.rb @@ -1,3 +1,4 @@ +require 'json' require 'temporal/execution_options' require 'temporal/connection' require 'temporal/activity' @@ -5,6 +6,7 @@ require 'temporal/workflow' require 'temporal/workflow/context_helpers' require 'temporal/workflow/history' +require 'temporal/workflow/history/serialization' require 'temporal/workflow/execution_info' require 'temporal/workflow/executions' require 'temporal/workflow/status' @@ -397,9 +399,9 @@ def fail_activity(async_token, exception) # @param run_id [String] # # @return [Temporal::Workflow::History] workflow's execution history - def get_workflow_history(namespace:, workflow_id:, run_id:) + def get_workflow_history(namespace: nil, workflow_id:, run_id:) history_response = connection.get_workflow_execution_history( - namespace: namespace, + namespace: namespace || config.default_execution_options.namespace, workflow_id: workflow_id, run_id: run_id ) @@ -407,6 +409,42 @@ def get_workflow_history(namespace:, workflow_id:, run_id:) Workflow::History.new(history_response.history.events) end + # Fetch workflow's execution history as JSON. This output can be used for replay testing. + # + # @param namespace [String] + # @param workflow_id [String] + # @param run_id [String] optional + # @param pretty_print [Boolean] optional + # + # @return a JSON string representation of the history + def get_workflow_history_json(namespace: nil, workflow_id:, run_id: nil, pretty_print: true) + history_response = connection.get_workflow_execution_history( + namespace: namespace || config.default_execution_options.namespace, + workflow_id: workflow_id, + run_id: run_id + ) + Temporal::Workflow::History::Serialization.to_json(history_response.history) + end + + # Fetch workflow's execution history as protobuf binary. This output can be used for replay testing. + # + # @param namespace [String] + # @param workflow_id [String] + # @param run_id [String] optional + # + # @return a binary string representation of the history + def get_workflow_history_protobuf(namespace: nil, workflow_id:, run_id: nil) + history_response = connection.get_workflow_execution_history( + namespace: namespace || config.default_execution_options.namespace, + workflow_id: workflow_id, + run_id: run_id + ) + + # Protobuf for Ruby unfortunately does not support textproto. Plain binary provides + # a less debuggable, but compact option. + Temporal::Workflow::History::Serialization.to_protobuf(history_response.history) + end + def list_open_workflow_executions(namespace, from, to = Time.now, filter: {}, next_page_token: nil, max_page_size: nil) validate_filter(filter, :workflow, :workflow_id) diff --git a/lib/temporal/configuration.rb b/lib/temporal/configuration.rb index 014187c1..136c73ce 100644 --- a/lib/temporal/configuration.rb +++ b/lib/temporal/configuration.rb @@ -18,7 +18,7 @@ class Configuration attr_reader :timeouts, :error_handlers, :capabilities attr_accessor :connection_type, :converter, :use_error_serialization_v2, :host, :port, :credentials, :identity, :logger, :metrics_adapter, :namespace, :task_queue, :headers, :search_attributes, :header_propagators, - :payload_codec, :legacy_signals, :no_signals_in_first_task, :connection_options + :payload_codec, :legacy_signals, :no_signals_in_first_task, :connection_options, :log_on_workflow_replay # See https://docs.temporal.io/blog/activity-timeouts/ for general docs. # We want an infinite execution timeout for cron schedules and other perpetual workflows. @@ -85,6 +85,8 @@ def initialize @header_propagators = [] @capabilities = Capabilities.new(self) @connection_options = {} + # Setting this to true can be useful when debugging workflow code or running replay tests + @log_on_workflow_replay = false # Signals previously were incorrectly replayed in order within a workflow task window, rather # than at the beginning. Correcting this changes the determinism of any workflow with signals. diff --git a/lib/temporal/testing/replay_tester.rb b/lib/temporal/testing/replay_tester.rb new file mode 100644 index 00000000..6a98c86e --- /dev/null +++ b/lib/temporal/testing/replay_tester.rb @@ -0,0 +1,73 @@ +require "gen/temporal/api/history/v1/message_pb" +require "json" +require "temporal/errors" +require "temporal/metadata/workflow_task" +require "temporal/middleware/chain" +require "temporal/workflow/executor" +require "temporal/workflow/stack_trace_tracker" + +module Temporal + module Testing + class ReplayError < StandardError + end + + class ReplayTester + def initialize(config: Temporal.configuration) + @config = config + end + + attr_reader :config + + # Runs a replay test by using the specific Temporal::Workflow::History object. Instances of these objects + # can be obtained using various from_ methods in Temporal::Workflow::History::Serialization. + # + # If the replay test succeeds, the method will return silently. If the replay tests fails, an error will be raised. + def replay_history(workflow_class, history) + # This code roughly resembles the workflow TaskProcessor but with history being fed in rather + # than being pulled via a workflow task, no query support, no metrics, and other + # simplifications. Fake metadata needs to be provided. + start_workflow_event = history.find_event_by_id(1) + if start_workflow_event.nil? || start_workflow_event.type != "WORKFLOW_EXECUTION_STARTED" + raise ReplayError, "History does not start with workflow_execution_started event" + end + + metadata = Temporal::Metadata::WorkflowTask.new( + namespace: config.namespace, + id: 1, + task_token: "", + attempt: 1, + workflow_run_id: "run_id", + workflow_id: "workflow_id", + # Protobuf deserialization will ensure this tree is present + workflow_name: start_workflow_event.attributes.workflow_type.name + ) + + executor = Workflow::Executor.new( + workflow_class, + history, + metadata, + config, + true, + Middleware::Chain.new([]) + ) + + begin + executor.run + rescue StandardError + query = Struct.new(:query_type, :query_args).new( + Temporal::Workflow::StackTraceTracker::STACK_TRACE_QUERY_NAME, + nil + ) + query_result = executor.process_queries( + {"stack_trace" => query} + ) + replay_error = ReplayError.new("Workflow code failed to replay successfully against history") + # Override the stack trace to the point in the workflow code where the failure occured, not the + # point in the StateManager where non-determinism is detected + replay_error.set_backtrace("Fiber backtraces: #{query_result["stack_trace"].result}") + raise replay_error + end + end + end + end +end diff --git a/lib/temporal/workflow/context.rb b/lib/temporal/workflow/context.rb index 2d69bd2d..07b917a2 100644 --- a/lib/temporal/workflow/context.rb +++ b/lib/temporal/workflow/context.rb @@ -47,8 +47,10 @@ def completed? end def logger - @logger ||= ReplayAwareLogger.new(Temporal.logger) - @logger.replay = state_manager.replay? + @logger ||= ReplayAwareLogger.new( + @config.logger, + replaying: -> { state_manager.replay? && !@config.log_on_workflow_replay } + ) @logger end diff --git a/lib/temporal/workflow/executor.rb b/lib/temporal/workflow/executor.rb index 5c8eaf9e..6233feb0 100644 --- a/lib/temporal/workflow/executor.rb +++ b/lib/temporal/workflow/executor.rb @@ -33,7 +33,7 @@ def initialize(workflow_class, history, task_metadata, config, track_stack_trace def run dispatcher.register_handler( - History::EventTarget.workflow, + History::EventTarget.start_workflow, 'started', &method(:execute_workflow) ) diff --git a/lib/temporal/workflow/history.rb b/lib/temporal/workflow/history.rb index e11ea9b2..07bbe96d 100644 --- a/lib/temporal/workflow/history.rb +++ b/lib/temporal/workflow/history.rb @@ -51,6 +51,9 @@ def next_window CANCEL_TIMER_FAILED TIMER_CANCELED WORKFLOW_EXECUTION_CANCEL_REQUESTED + WORKFLOW_EXECUTION_COMPLETED + WORKFLOW_EXECUTION_CONTINUED_AS_NEW + WORKFLOW_EXECUTION_FAILED START_CHILD_WORKFLOW_EXECUTION_INITIATED SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_INITIATED REQUEST_CANCEL_ACTIVITY_TASK_FAILED diff --git a/lib/temporal/workflow/history/event_target.rb b/lib/temporal/workflow/history/event_target.rb index d054947f..881a7823 100644 --- a/lib/temporal/workflow/history/event_target.rb +++ b/lib/temporal/workflow/history/event_target.rb @@ -14,8 +14,13 @@ class UnexpectedEventType < InternalError; end MARKER_TYPE = :marker EXTERNAL_WORKFLOW_TYPE = :external_workflow CANCEL_EXTERNAL_WORKFLOW_REQUEST_TYPE = :cancel_external_workflow_request - WORKFLOW_TYPE = :workflow CANCEL_WORKFLOW_REQUEST_TYPE = :cancel_workflow_request + WORKFLOW_TYPE = :workflow + COMPLETE_WORKFLOW_TYPE = :complete_workflow + CONTINUE_AS_NEW_WORKFLOW_TYPE = :continue_as_new_workflow + FAIL_WORKFLOW_TYPE = :fail_workflow + SIGNAL_WORKFLOW_TYPE = :signal_workflow + START_WORKFLOW_TYPE = :start_workflow UPSERT_SEARCH_ATTRIBUTES_REQUEST_TYPE = :upsert_search_attributes_request # NOTE: The order is important, first prefix match wins (will be a longer match) @@ -35,13 +40,21 @@ class UnexpectedEventType < InternalError; end 'REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION' => CANCEL_EXTERNAL_WORKFLOW_REQUEST_TYPE, 'UPSERT_WORKFLOW_SEARCH_ATTRIBUTES' => UPSERT_SEARCH_ATTRIBUTES_REQUEST_TYPE, 'WORKFLOW_EXECUTION_CANCEL' => CANCEL_WORKFLOW_REQUEST_TYPE, + 'WORKFLOW_EXECUTION_COMPLETED' => COMPLETE_WORKFLOW_TYPE, + 'WORKFLOW_EXECUTION_CONTINUED_AS_NEW' => CONTINUE_AS_NEW_WORKFLOW_TYPE, + 'WORKFLOW_EXECUTION_FAILED' => FAIL_WORKFLOW_TYPE, + 'WORKFLOW_EXECUTION_SIGNALED' => SIGNAL_WORKFLOW_TYPE, + 'WORKFLOW_EXECUTION_STARTED' => START_WORKFLOW_TYPE, + # This is a fall-through type for various event types that workflow code cannot + # react to, either because they're externally triggered (workflow termination, + # timeout) or use an unsupported feature (workflow cancellation, updates). 'WORKFLOW_EXECUTION' => WORKFLOW_TYPE, }.freeze attr_reader :id, :type - def self.workflow - @workflow ||= new(1, WORKFLOW_TYPE) + def self.start_workflow + @workflow ||= new(1, START_WORKFLOW_TYPE) end def self.from_event(event) diff --git a/lib/temporal/workflow/history/serialization.rb b/lib/temporal/workflow/history/serialization.rb new file mode 100644 index 00000000..2219dddd --- /dev/null +++ b/lib/temporal/workflow/history/serialization.rb @@ -0,0 +1,61 @@ +module Temporal + class Workflow + class History + # Functions for deserializing workflow histories from JSON and protobuf. These are useful + # in writing replay tests + # + # `from_` methods return Temporal::Workflow::History instances.` + # `to_` methods take Temporalio::Api::History::V1::History instances + # + # This asymmetry stems from our own internal history representation being a projection + # of the "full" history. + class Serialization + # Parse History from a JSON string + def self.from_json(json) + raw_history = Temporalio::Api::History::V1::History.decode_json(json, ignore_unknown_fields: true) + Workflow::History.new(raw_history.events) + end + + # Convert a raw history to JSON. This method is typically only used by methods on Workflow::Client + def self.to_json(raw_history, pretty_print: true) + json = raw_history.to_json + if pretty_print + # pretty print JSON to make it more debuggable + ::JSON.pretty_generate(::JSON.load(json)) + else + json + end + end + + def self.from_json_file(path) + self.from_json(File.read(path)) + end + + def self.to_json_file(raw_history, path, pretty_print: true) + json = self.to_json(raw_history, pretty_print: pretty_print) + File.write(path, json) + end + + def self.from_protobuf(protobuf) + raw_history = Temporalio::Api::History::V1::History.decode(protobuf) + Workflow::History.new(raw_history.events) + end + + def self.to_protobuf(raw_history) + raw_history.to_proto + end + + def self.from_protobuf_file(path) + self.from_protobuf(File.open(path, "rb", &:read)) + end + + def self.to_protobuf_file(raw_history, path) + protobuf = self.to_protobuf(raw_history) + File.open(path, "wb") do |f| + f.write(protobuf) + end + end + end + end + end +end diff --git a/lib/temporal/workflow/replay_aware_logger.rb b/lib/temporal/workflow/replay_aware_logger.rb index a56494b4..65dafc59 100644 --- a/lib/temporal/workflow/replay_aware_logger.rb +++ b/lib/temporal/workflow/replay_aware_logger.rb @@ -3,11 +3,9 @@ class Workflow class ReplayAwareLogger SEVERITIES = %i[debug info warn error fatal unknown].freeze - attr_writer :replay - - def initialize(main_logger, replay = true) + def initialize(main_logger, replaying:) @main_logger = main_logger - @replay = replay + @replaying = replaying end SEVERITIES.each do |severity| @@ -29,7 +27,7 @@ def log(severity, message, data = {}) attr_reader :main_logger def replay? - @replay + @replaying.call end end end diff --git a/lib/temporal/workflow/state_manager.rb b/lib/temporal/workflow/state_manager.rb index 2cf82159..f8c5cc64 100644 --- a/lib/temporal/workflow/state_manager.rb +++ b/lib/temporal/workflow/state_manager.rb @@ -255,17 +255,19 @@ def apply_event(event) state_machine.start dispatch( - History::EventTarget.workflow, + History::EventTarget.start_workflow, 'started', from_payloads(event.attributes.input), event ) when 'WORKFLOW_EXECUTION_COMPLETED' - # todo + # should only be triggered in query execution and replay testing + discard_command(history_target) when 'WORKFLOW_EXECUTION_FAILED' - # todo + # should only be triggered in query execution and replay testing + discard_command(history_target) when 'WORKFLOW_EXECUTION_TIMED_OUT' # todo @@ -366,7 +368,8 @@ def apply_event(event) # todo when 'WORKFLOW_EXECUTION_CONTINUED_AS_NEW' - # todo + # should only be triggered in query execution and replay testing + discard_command(history_target) when 'START_CHILD_WORKFLOW_EXECUTION_INITIATED' state_machine.schedule @@ -446,8 +449,12 @@ def event_target_from(command_id, command) History::EventTarget::TIMER_TYPE when Command::CancelTimer History::EventTarget::CANCEL_TIMER_REQUEST_TYPE - when Command::CompleteWorkflow, Command::FailWorkflow - History::EventTarget::WORKFLOW_TYPE + when Command::CompleteWorkflow + History::EventTarget::COMPLETE_WORKFLOW_TYPE + when Command::ContinueAsNew + History::EventTarget::CONTINUE_AS_NEW_WORKFLOW_TYPE + when Command::FailWorkflow + History::EventTarget::FAIL_WORKFLOW_TYPE when Command::StartChildWorkflow History::EventTarget::CHILD_WORKFLOW_TYPE when Command::UpsertSearchAttributes @@ -465,7 +472,7 @@ def dispatch(history_target, name, *attributes) NONDETERMINISM_ERROR_SUGGESTION = 'Likely, either you have made a version-unsafe change to your workflow or have non-deterministic '\ - 'behavior in your workflow. See https://docs.temporal.io/docs/java/versioning/#introduction-to-versioning.'.freeze + 'behavior in your workflow. See https://docs.temporal.io/docs/java/versioning/#introduction-to-versioning.'.freeze def discard_command(history_target) # Pop the first command from the list, it is expected to match @@ -480,7 +487,7 @@ def discard_command(history_target) return unless history_target != replay_target raise NonDeterministicWorkflowError, - "Unexpected command. The replaying code is issuing: #{replay_target}, "\ + "Unexpected command. The replaying code is issuing: #{replay_target}, "\ "but the history of previous executions recorded: #{history_target}. " + NONDETERMINISM_ERROR_SUGGESTION end diff --git a/spec/unit/lib/temporal/testing/replay_histories/do_nothing.json b/spec/unit/lib/temporal/testing/replay_histories/do_nothing.json new file mode 100644 index 00000000..45a0ef20 --- /dev/null +++ b/spec/unit/lib/temporal/testing/replay_histories/do_nothing.json @@ -0,0 +1,103 @@ +{ + "events":[ + { + "eventId":"1", + "eventTime":"2024-05-27T18:53:53.483530640Z", + "eventType":"EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", + "taskId":"27263213", + "workflowExecutionStartedEventAttributes":{ + "workflowType":{ + "name":"TestReplayWorkflow" + }, + "taskQueue":{ + "name":"general", + "kind":"TASK_QUEUE_KIND_NORMAL" + }, + "input":{ + "payloads":[ + { + "metadata":{ + "encoding":"anNvbi9wbGFpbg==" + }, + "data":"eyI6cmVzdWx0Ijoic3VjY2VzcyJ9Cg==" + } + ] + }, + "workflowExecutionTimeout":"30s", + "workflowRunTimeout":"30s", + "workflowTaskTimeout":"10s", + "originalExecutionRunId":"b3711f7b-2693-4c1b-ab67-24e73f80bdcf", + "identity":"123@test", + "firstExecutionRunId":"b3711f7b-2693-4c1b-ab67-24e73f80bdcf", + "attempt":1, + "workflowExecutionExpirationTime":"2024-05-27T18:54:23.483Z", + "firstWorkflowTaskBackoff":"0s", + "memo":{}, + "searchAttributes":{}, + "header":{} + } + }, + { + "eventId":"2", + "eventTime":"2024-05-27T18:53:53.483621296Z", + "eventType":"EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId":"27263215", + "workflowTaskScheduledEventAttributes":{ + "taskQueue":{ + "name":"general", + "kind":"TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout":"10s", + "attempt":1 + } + }, + { + "eventId":"3", + "eventTime":"2024-05-27T18:53:53.504351823Z", + "eventType":"EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId":"27263220", + "workflowTaskStartedEventAttributes":{ + "scheduledEventId":"2", + "identity":"123@test", + "requestId":"195003c8-4c89-486b-8ae8-85cb209dc8b9", + "historySizeBytes":"395" + } + }, + { + "eventId":"4", + "eventTime":"2024-05-27T18:53:53.620416193Z", + "eventType":"EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId":"27263224", + "workflowTaskCompletedEventAttributes":{ + "scheduledEventId":"2", + "startedEventId":"3", + "identity":"123@test", + "binaryChecksum":"d1feac6b4ac2fb57a304ddf1419efd6e06088e41", + "sdkMetadata":{ + "langUsedFlags":[ + 2 + ] + } + } + }, + { + "eventId":"5", + "eventTime":"2024-05-27T18:53:55.790974964Z", + "eventType":"EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", + "taskId":"27263260", + "workflowExecutionCompletedEventAttributes":{ + "result":{ + "payloads":[ + { + "metadata":{ + "encoding":"anNvbi9wbGFpbg==" + }, + "data":"ImRvbmUiCg==" + } + ] + }, + "workflowTaskCompletedEventId":"4" + } + } + ] +} \ No newline at end of file diff --git a/spec/unit/lib/temporal/testing/replay_tester_spec.rb b/spec/unit/lib/temporal/testing/replay_tester_spec.rb new file mode 100644 index 00000000..861a5813 --- /dev/null +++ b/spec/unit/lib/temporal/testing/replay_tester_spec.rb @@ -0,0 +1,142 @@ +require "base64" +require "json" +require "temporal/testing/replay_tester" +require "temporal/workflow" +require "temporal/workflow/history" + +describe Temporal::Testing::ReplayTester do + class TestReplayActivity < Temporal::Activity + def execute + raise "should never run" + end + end + + class TestReplayWorkflow < Temporal::Workflow + def execute(run_activity: false, run_sleep: false, result: "success") + TestReplayActivity.execute! if run_activity + + workflow.sleep(1) if run_sleep + + case result + when "success" + "done" + when "continue_as_new" + workflow.continue_as_new + nil + when "await" + # wait forever + workflow.wait_until { false } + when "fail" + raise "failed" + end + end + end + + let(:replay_tester) { Temporal::Testing::ReplayTester.new } + let(:do_nothing_json) do + File.read( + "spec/unit/lib/temporal/testing/replay_histories/do_nothing.json" + ) + end + + let(:do_nothing) do + Temporal::Workflow::History::Serialization.from_json(do_nothing_json) + end + + it "replay do nothing successful" do + replay_tester.replay_history( + TestReplayWorkflow, + do_nothing + ) + end + + def remove_first_history_event(history) + history.events.shift + history + end + + it "replay missing start workflow execution event" do + replay_tester.replay_history( + TestReplayWorkflow, + remove_first_history_event(do_nothing) + ) + raise "Expected error to raise" + rescue Temporal::Testing::ReplayError => e + expect(e.message).to(eq("History does not start with workflow_execution_started event")) + end + + def set_workflow_args_in_history(json_args) + obj = JSON.load(do_nothing_json) + obj["events"][0]["workflowExecutionStartedEventAttributes"]["input"]["payloads"][0]["data"] = Base64.strict_encode64( + json_args + ) + new_json = JSON.generate(obj) + Temporal::Workflow::History::Serialization.from_json(new_json) + end + + it "replay extra activity" do + # The linked history will cause an error because it will cause an activity run even though + # there isn't one in the history. + + replay_tester.replay_history( + TestReplayWorkflow, + set_workflow_args_in_history("{\":run_activity\":true}") + ) + raise "Expected error to raise" + rescue Temporal::Testing::ReplayError => e + expect(e.message).to(eq("Workflow code failed to replay successfully against history")) + # Ensure backtrace was overwritten + expect(e.backtrace.first).to(start_with("Fiber backtraces:")) + expect(e.cause).to(be_a(Temporal::NonDeterministicWorkflowError)) + expect(e.cause.message).to( + eq( + "Unexpected command. The replaying code is issuing: activity (5), but the history of previous executions " \ + "recorded: complete_workflow (5). Likely, either you have made a version-unsafe change to your workflow or " \ + "have non-deterministic behavior in your workflow. See https://docs.temporal.io/docs/java/versioning/#introduction-to-versioning." + ) + ) + end + + it "replay continues as new when history completed" do + # The linked history will cause an error because it will cause the workflow to continue + # as new on replay when in the history, it completed successfully. + + replay_tester.replay_history( + TestReplayWorkflow, + set_workflow_args_in_history("{\":result\":\"continue_as_new\"}") + ) + raise "Expected error to raise" + rescue Temporal::Testing::ReplayError => e + expect(e.message).to(eq("Workflow code failed to replay successfully against history")) + expect(e.cause).to(be_a(Temporal::NonDeterministicWorkflowError)) + expect(e.cause.message).to( + eq( + "Unexpected command. The replaying code is issuing: continue_as_new_workflow (5), but the history of " \ + "previous executions recorded: complete_workflow (5). Likely, either you have made a version-unsafe " \ + "change to your workflow or have non-deterministic behavior in your workflow. " \ + "See https://docs.temporal.io/docs/java/versioning/#introduction-to-versioning." + ) + ) + end + + it "replay keeps going when history succeeded" do + # The linked history will cause an error because it will cause the workflow to keep running + # when in the history, it completed successfully. + + replay_tester.replay_history( + TestReplayWorkflow, + set_workflow_args_in_history("{\":result\":\"await\"}") + ) + raise "Expected error to raise" + rescue Temporal::Testing::ReplayError => e + expect(e.message).to(eq("Workflow code failed to replay successfully against history")) + expect(e.cause).to(be_a(Temporal::NonDeterministicWorkflowError)) + expect(e.cause.message).to( + eq( + "A command in the history of previous executions, complete_workflow (5), was not scheduled upon replay. " \ + "Likely, either you have made a version-unsafe change to your workflow or have non-deterministic behavior " \ + "in your workflow. See https://docs.temporal.io/docs/java/versioning/#introduction-to-versioning." + ) + ) + end +end diff --git a/spec/unit/lib/temporal/workflow/state_manager_spec.rb b/spec/unit/lib/temporal/workflow/state_manager_spec.rb index bad27d22..a1caaa2c 100644 --- a/spec/unit/lib/temporal/workflow/state_manager_spec.rb +++ b/spec/unit/lib/temporal/workflow/state_manager_spec.rb @@ -59,7 +59,7 @@ class MyWorkflow < Temporal::Workflow; end it 'dispatcher invoked for start' do expect(dispatcher).to receive(:dispatch).with( - Temporal::Workflow::History::EventTarget.workflow, 'started', instance_of(Array) + Temporal::Workflow::History::EventTarget.start_workflow, 'started', instance_of(Array) ).once state_manager.apply(history.next_window) end @@ -88,7 +88,7 @@ class MyWorkflow < Temporal::Workflow; end ] ).once.ordered expect(dispatcher).to receive(:dispatch).with( - Temporal::Workflow::History::EventTarget.workflow, 'started', instance_of(Array) + Temporal::Workflow::History::EventTarget.start_workflow, 'started', instance_of(Array) ).once.ordered state_manager.apply(history.next_window) @@ -119,7 +119,7 @@ class MyWorkflow < Temporal::Workflow; end allow(connection).to receive(:get_system_info).and_return(system_info) expect(dispatcher).to receive(:dispatch).with( - Temporal::Workflow::History::EventTarget.workflow, 'started', instance_of(Array) + Temporal::Workflow::History::EventTarget.start_workflow, 'started', instance_of(Array) ).once.ordered expect(dispatcher).to receive(:dispatch).with( Temporal::Workflow::Signal.new(signal_entry.workflow_execution_signaled_event_attributes.signal_name), @@ -140,7 +140,7 @@ class MyWorkflow < Temporal::Workflow; end allow(connection).to receive(:get_system_info).and_return(system_info) expect(dispatcher).to receive(:dispatch).with( - Temporal::Workflow::History::EventTarget.workflow, 'started', instance_of(Array) + Temporal::Workflow::History::EventTarget.start_workflow, 'started', instance_of(Array) ).once.ordered expect(dispatcher).to receive(:dispatch).with( Temporal::Workflow::Signal.new(signal_entry.workflow_execution_signaled_event_attributes.signal_name), @@ -173,7 +173,7 @@ class MyWorkflow < Temporal::Workflow; end ] ).once.ordered expect(dispatcher).to receive(:dispatch).with( - Temporal::Workflow::History::EventTarget.workflow, 'started', instance_of(Array) + Temporal::Workflow::History::EventTarget.start_workflow, 'started', instance_of(Array) ).once.ordered state_manager.apply(history.next_window) @@ -204,7 +204,7 @@ class MyWorkflow < Temporal::Workflow; end it 'marker handled first' do activity_target = nil - dispatcher.register_handler(Temporal::Workflow::History::EventTarget.workflow, 'started') do + dispatcher.register_handler(Temporal::Workflow::History::EventTarget.start_workflow, 'started') do activity_target, = state_manager.schedule( Temporal::Workflow::Command::ScheduleActivity.new( activity_id: activity_entry.event_id, @@ -249,7 +249,7 @@ def test_order(signal_first) activity_target = nil signaled = false - dispatcher.register_handler(Temporal::Workflow::History::EventTarget.workflow, 'started') do + dispatcher.register_handler(Temporal::Workflow::History::EventTarget.start_workflow, 'started') do activity_target, = state_manager.schedule( Temporal::Workflow::Command::ScheduleActivity.new( activity_id: activity_entry.event_id, From dc937e8cc75bc80f5123453f90d9aa5cb9f64b06 Mon Sep 17 00:00:00 2001 From: DeRauk Gibble Date: Wed, 17 Jul 2024 16:59:14 -0400 Subject: [PATCH 118/125] Specify Protobuf Version To Fix Build (#308) --- Gemfile | 2 ++ examples/Gemfile | 1 + 2 files changed, 3 insertions(+) diff --git a/Gemfile b/Gemfile index fa75df15..c55c81ac 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ source 'https://rubygems.org' +gem 'google-protobuf', '~> 3.19.6' + gemspec diff --git a/examples/Gemfile b/examples/Gemfile index 38523465..4a744598 100644 --- a/examples/Gemfile +++ b/examples/Gemfile @@ -4,5 +4,6 @@ gem 'temporal-ruby', path: '../' gem 'dry-types', '>= 1.7.2' gem 'dry-struct', '~> 1.6.0' +gem 'google-protobuf', '~> 3.19.6' gem 'rspec', group: :test From 0c9a0c741370e80076e1225c6c148cb0fe1194c7 Mon Sep 17 00:00:00 2001 From: Carolyn Duan <108763985+cduanfigma@users.noreply.github.com> Date: Mon, 22 Jul 2024 10:09:57 -0700 Subject: [PATCH 119/125] Add pagination to get_workflow_history (#290) * Add pagination to get_workflow_history * Fix CI --- lib/temporal/client.rb | 28 +++++++++++------ spec/unit/lib/temporal/client_spec.rb | 44 +++++++++++++++++++++------ 2 files changed, 54 insertions(+), 18 deletions(-) diff --git a/lib/temporal/client.rb b/lib/temporal/client.rb index e738e2b6..a49b5ffb 100644 --- a/lib/temporal/client.rb +++ b/lib/temporal/client.rb @@ -400,13 +400,23 @@ def fail_activity(async_token, exception) # # @return [Temporal::Workflow::History] workflow's execution history def get_workflow_history(namespace: nil, workflow_id:, run_id:) - history_response = connection.get_workflow_execution_history( - namespace: namespace || config.default_execution_options.namespace, - workflow_id: workflow_id, - run_id: run_id - ) + next_page_token = nil + events = [] + loop do + response = + connection.get_workflow_execution_history( + namespace: namespace || config.default_execution_options.namespace, + workflow_id: workflow_id, + run_id: run_id, + next_page_token: next_page_token, + ) + events.concat(response.history.events.to_a) + next_page_token = response.next_page_token + + break if next_page_token.empty? + end - Workflow::History.new(history_response.history.events) + Workflow::History.new(events) end # Fetch workflow's execution history as JSON. This output can be used for replay testing. @@ -459,12 +469,12 @@ def list_closed_workflow_executions(namespace, from, to = Time.now, filter: {}, def query_workflow_executions(namespace, query, filter: {}, next_page_token: nil, max_page_size: nil) validate_filter(filter, :status, :workflow, :workflow_id) - + Temporal::Workflow::Executions.new(connection: connection, status: :all, request_options: { namespace: namespace, query: query, next_page_token: next_page_token, max_page_size: max_page_size }.merge(filter)) end # Count the number of workflows matching the provided query - # + # # @param namespace [String] # @param query [String] # @@ -500,7 +510,7 @@ def remove_custom_search_attributes(*attribute_names, namespace: nil) def list_schedules(namespace, maximum_page_size:, next_page_token: '') connection.list_schedules(namespace: namespace, maximum_page_size: maximum_page_size, next_page_token: next_page_token) end - + # Describe a schedule in a namespace # # @param namespace [String] namespace to list schedules in diff --git a/spec/unit/lib/temporal/client_spec.rb b/spec/unit/lib/temporal/client_spec.rb index 1dd4995d..bc970f31 100644 --- a/spec/unit/lib/temporal/client_spec.rb +++ b/spec/unit/lib/temporal/client_spec.rb @@ -300,7 +300,7 @@ def expect_signal_with_start(expected_arguments, expected_signal_argument) it 'raises when signal_input is given but signal_name is not' do expect do subject.start_workflow( - TestStartWorkflow, + TestStartWorkflow, [42, 54], [43, 55], options: { signal_input: 'what do you get if you multiply six by nine?', } @@ -361,7 +361,7 @@ def expect_signal_with_start(expected_arguments, expected_signal_argument) describe '#describe_namespace' do before { allow(connection).to receive(:describe_namespace).and_return(Temporalio::Api::WorkflowService::V1::DescribeNamespaceResponse.new) } - + it 'passes the namespace to the connection' do result = subject.describe_namespace('new-namespace') @@ -381,7 +381,7 @@ def expect_signal_with_start(expected_arguments, expected_signal_argument) .to have_received(:signal_workflow_execution) .with( namespace: 'default-test-namespace', - signal: 'signal', + signal: 'signal', workflow_id: 'workflow_id', run_id: 'run_id', input: nil, @@ -395,7 +395,7 @@ def expect_signal_with_start(expected_arguments, expected_signal_argument) .to have_received(:signal_workflow_execution) .with( namespace: 'default-test-namespace', - signal: 'signal', + signal: 'signal', workflow_id: 'workflow_id', run_id: 'run_id', input: 'input', @@ -409,7 +409,7 @@ def expect_signal_with_start(expected_arguments, expected_signal_argument) .to have_received(:signal_workflow_execution) .with( namespace: 'other-test-namespace', - signal: 'signal', + signal: 'signal', workflow_id: 'workflow_id', run_id: 'run_id', input: nil, @@ -449,7 +449,7 @@ class NamespacedWorkflow < Temporal::Workflow ) end - it 'can override the namespace' do + it 'can override the namespace' do completed_event = Fabricate(:workflow_completed_event, result: nil) response = Fabricate(:workflow_execution_history, events: [completed_event]) @@ -534,7 +534,7 @@ class NamespacedWorkflow < Temporal::Workflow end.to raise_error(Temporal::WorkflowCanceled) end - it 'raises TimeoutError when the server times out' do + it 'raises TimeoutError when the server times out' do response = Fabricate(:workflow_execution_history, events: []) expect(connection) .to receive(:get_workflow_execution_history) @@ -895,6 +895,32 @@ class NamespacedWorkflow < Temporal::Workflow end end + describe '#get_workflow_history' do + it 'gets full history with pagination' do + completed_event = Fabricate(:workflow_completed_event, result: nil) + response_1 = Fabricate(:workflow_execution_history, events: [completed_event], next_page_token: 'a') + response_2 = Fabricate(:workflow_execution_history, events: [completed_event], next_page_token: '') + + allow(connection) + .to receive(:get_workflow_execution_history) + .and_return(response_1, response_2) + + subject.get_workflow_history(namespace: namespace, workflow_id: workflow_id, run_id: run_id) + + expect(connection) + .to have_received(:get_workflow_execution_history) + .with(namespace: namespace, workflow_id: workflow_id, run_id: run_id, next_page_token: nil) + .ordered + + expect(connection) + .to have_received(:get_workflow_execution_history) + .with(namespace: namespace, workflow_id: workflow_id, run_id: run_id, next_page_token: 'a') + .ordered + + expect(connection).to have_received(:get_workflow_execution_history).exactly(2).times + end + end + describe '#list_open_workflow_executions' do let(:from) { Time.now - 600 } let(:now) { Time.now } @@ -977,7 +1003,7 @@ class NamespacedWorkflow < Temporal::Workflow end end - it 'returns the next page token and paginates correctly' do + it 'returns the next page token and paginates correctly' do executions1 = subject.list_open_workflow_executions(namespace, from, max_page_size: 10) executions1.map do |execution| expect(execution).to be_an_instance_of(Temporal::Workflow::ExecutionInfo) @@ -1009,7 +1035,7 @@ class NamespacedWorkflow < Temporal::Workflow .once end - it 'returns the next page and paginates correctly' do + it 'returns the next page and paginates correctly' do executions1 = subject.list_open_workflow_executions(namespace, from, max_page_size: 10) executions1.map do |execution| expect(execution).to be_an_instance_of(Temporal::Workflow::ExecutionInfo) From c3991a946733924ad367f672e61b7a49513077b5 Mon Sep 17 00:00:00 2001 From: jazev-stripe <128553781+jazev-stripe@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:04:54 -0700 Subject: [PATCH 120/125] Relax version specifier for 'google-protobuf' to fix build errors on Apple Silicon machines (#310) * relax the version requirement for 'google-protobuf' to allow using newer 'grpc' gem * use major.minor to have min minor version requirement --- Gemfile | 2 +- examples/Gemfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index c55c81ac..a98c51b0 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,5 @@ source 'https://rubygems.org' -gem 'google-protobuf', '~> 3.19.6' +gem 'google-protobuf', '~> 3.19' gemspec diff --git a/examples/Gemfile b/examples/Gemfile index 4a744598..c6fc0199 100644 --- a/examples/Gemfile +++ b/examples/Gemfile @@ -4,6 +4,6 @@ gem 'temporal-ruby', path: '../' gem 'dry-types', '>= 1.7.2' gem 'dry-struct', '~> 1.6.0' -gem 'google-protobuf', '~> 3.19.6' +gem 'google-protobuf', '~> 3.19' gem 'rspec', group: :test From 0d3a8bb9037d4b736172b8afcbe6dbe65edb7cf8 Mon Sep 17 00:00:00 2001 From: jazev-stripe <128553781+jazev-stripe@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:20:22 -0700 Subject: [PATCH 121/125] Support passing activity task rate limit on worker options (#311) * support passing activity task rate limit on worker options * remove extra space in README --- README.md | 3 +- lib/temporal/activity/poller.rb | 10 ++++- lib/temporal/connection/grpc.rb | 8 +++- lib/temporal/worker.rb | 19 ++++++-- .../unit/lib/temporal/activity/poller_spec.rb | 30 +++++++++++++ spec/unit/lib/temporal/grpc_spec.rb | 43 +++++++++++++++++++ spec/unit/lib/temporal/worker_spec.rb | 35 ++++++++++++--- 7 files changed, 136 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index ff47a2d5..63888d8b 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,8 @@ Temporal::Worker.new( workflow_thread_pool_size: 10, # how many threads poll for workflows binary_checksum: nil, # identifies the version of workflow worker code activity_poll_retry_seconds: 0, # how many seconds to wait after unsuccessful poll for activities - workflow_poll_retry_seconds: 0 # how many seconds to wait after unsuccessful poll for workflows + workflow_poll_retry_seconds: 0, # how many seconds to wait after unsuccessful poll for workflows + activity_max_tasks_per_second: 0 # rate-limit for starting activity tasks (new activities + retries) on the task queue ) ``` diff --git a/lib/temporal/activity/poller.rb b/lib/temporal/activity/poller.rb index 29c0977d..859fb688 100644 --- a/lib/temporal/activity/poller.rb +++ b/lib/temporal/activity/poller.rb @@ -11,7 +11,8 @@ class Activity class Poller DEFAULT_OPTIONS = { thread_pool_size: 20, - poll_retry_seconds: 0 + poll_retry_seconds: 0, + max_tasks_per_second: 0 # unlimited }.freeze def initialize(namespace, task_queue, activity_lookup, config, middleware = [], options = {}) @@ -91,7 +92,8 @@ def poll_loop end def poll_for_task - connection.poll_activity_task_queue(namespace: namespace, task_queue: task_queue) + connection.poll_activity_task_queue(namespace: namespace, task_queue: task_queue, + max_tasks_per_second: max_tasks_per_second) rescue ::GRPC::Cancelled # We're shutting down and we've already reported that in the logs nil @@ -115,6 +117,10 @@ def poll_retry_seconds @options[:poll_retry_seconds] end + def max_tasks_per_second + @options[:max_tasks_per_second] + end + def thread_pool @thread_pool ||= ThreadPool.new( options[:thread_pool_size], diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index fa246e9f..282a262c 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -256,7 +256,7 @@ def respond_workflow_task_failed(namespace:, task_token:, cause:, exception:, bi client.respond_workflow_task_failed(request) end - def poll_activity_task_queue(namespace:, task_queue:) + def poll_activity_task_queue(namespace:, task_queue:, max_tasks_per_second: 0) request = Temporalio::Api::WorkflowService::V1::PollActivityTaskQueueRequest.new( identity: identity, namespace: namespace, @@ -265,6 +265,12 @@ def poll_activity_task_queue(namespace:, task_queue:) ) ) + if max_tasks_per_second > 0 + request.task_queue_metadata = Temporalio::Api::TaskQueue::V1::TaskQueueMetadata.new( + max_tasks_per_second: Google::Protobuf::DoubleValue.new(value: max_tasks_per_second) + ) + end + poll_mutex.synchronize do return unless can_poll? diff --git a/lib/temporal/worker.rb b/lib/temporal/worker.rb index 5d84df6e..e9a3b2f3 100644 --- a/lib/temporal/worker.rb +++ b/lib/temporal/worker.rb @@ -9,7 +9,7 @@ module Temporal class Worker # activity_thread_pool_size: number of threads that the poller can use to run activities. # can be set to 1 if you want no paralellism in your activities, at the cost of throughput. - + # # binary_checksum: The binary checksum identifies the version of workflow worker code. It is set on each completed or failed workflow # task. It is present in API responses that return workflow execution info, and is shown in temporal-web and tctl. # It is traditionally a checksum of the application binary. However, Temporal server treats this as an opaque @@ -21,13 +21,25 @@ class Worker # from workers with these bad versions. # # See https://docs.temporal.io/docs/tctl/how-to-use-tctl/#recovery-from-bad-deployment----auto-reset-workflow + # + # activity_max_tasks_per_second: Optional: Sets the rate limiting on number of activities that can be executed per second + # + # This limits new activities being started and activity attempts being scheduled. It does NOT + # limit the number of concurrent activities being executed on this task queue. + # + # This is managed by the server and controls activities per second for the entire task queue + # across all the workers. Notice that the number is represented in double, so that you can set + # it to less than 1 if needed. For example, set the number to 0.1 means you want your activity + # to be executed once every 10 seconds. This can be used to protect down stream services from + # flooding. The zero value of this uses the default value. Default is unlimited. def initialize( config = Temporal.configuration, activity_thread_pool_size: Temporal::Activity::Poller::DEFAULT_OPTIONS[:thread_pool_size], workflow_thread_pool_size: Temporal::Workflow::Poller::DEFAULT_OPTIONS[:thread_pool_size], binary_checksum: Temporal::Workflow::Poller::DEFAULT_OPTIONS[:binary_checksum], activity_poll_retry_seconds: Temporal::Activity::Poller::DEFAULT_OPTIONS[:poll_retry_seconds], - workflow_poll_retry_seconds: Temporal::Workflow::Poller::DEFAULT_OPTIONS[:poll_retry_seconds] + workflow_poll_retry_seconds: Temporal::Workflow::Poller::DEFAULT_OPTIONS[:poll_retry_seconds], + activity_max_tasks_per_second: Temporal::Activity::Poller::DEFAULT_OPTIONS[:max_tasks_per_second] ) @config = config @workflows = Hash.new { |hash, key| hash[key] = ExecutableLookup.new } @@ -39,7 +51,8 @@ def initialize( @shutting_down = false @activity_poller_options = { thread_pool_size: activity_thread_pool_size, - poll_retry_seconds: activity_poll_retry_seconds + poll_retry_seconds: activity_poll_retry_seconds, + max_tasks_per_second: activity_max_tasks_per_second } @workflow_poller_options = { thread_pool_size: workflow_thread_pool_size, diff --git a/spec/unit/lib/temporal/activity/poller_spec.rb b/spec/unit/lib/temporal/activity/poller_spec.rb index 76d8396a..3e5d24c7 100644 --- a/spec/unit/lib/temporal/activity/poller_spec.rb +++ b/spec/unit/lib/temporal/activity/poller_spec.rb @@ -199,6 +199,36 @@ def call(_); end end end + context 'when max_tasks_per_second is set' do + subject do + described_class.new( + namespace, + task_queue, + lookup, + config, + middleware, + { + max_tasks_per_second: 32 + } + ) + end + + it 'sends PollActivityTaskQueue requests with the configured task rate-limit' do + times = poll(nil, times: 2) + expect(times).to be >= 2 + + expect(connection).to have_received(:poll_activity_task_queue) + .with( + namespace: namespace, + task_queue: task_queue, + max_tasks_per_second: 32 + ) + .at_least(2) + .times + end + end + + context 'when connection is unable to poll and poll_retry_seconds is set' do subject do described_class.new( diff --git a/spec/unit/lib/temporal/grpc_spec.rb b/spec/unit/lib/temporal/grpc_spec.rb index 0799c5d0..cb97469f 100644 --- a/spec/unit/lib/temporal/grpc_spec.rb +++ b/spec/unit/lib/temporal/grpc_spec.rb @@ -646,6 +646,49 @@ class TestDeserializer end end + describe '#poll_activity_task_queue' do + let(:task_queue) { 'test-task-queue' } + let(:temporal_response) do + Temporalio::Api::WorkflowService::V1::PollActivityTaskQueueResponse.new + end + let(:poll_request) do + instance_double( + "GRPC::ActiveCall::Operation", + execute: temporal_response + ) + end + + before do + allow(grpc_stub).to receive(:poll_activity_task_queue).with(anything, return_op: true).and_return(poll_request) + end + + it 'makes an API request' do + subject.poll_activity_task_queue(namespace: namespace, task_queue: task_queue) + + expect(grpc_stub).to have_received(:poll_activity_task_queue) do |request| + expect(request).to be_an_instance_of(Temporalio::Api::WorkflowService::V1::PollActivityTaskQueueRequest) + expect(request.namespace).to eq(namespace) + expect(request.task_queue.name).to eq(task_queue) + expect(request.identity).to eq(identity) + expect(request.task_queue_metadata).to be_nil + end + end + + it 'makes an API request with max_tasks_per_second in the metadata' do + subject.poll_activity_task_queue(namespace: namespace, task_queue: task_queue, max_tasks_per_second: 10) + + expect(grpc_stub).to have_received(:poll_activity_task_queue) do |request| + expect(request).to be_an_instance_of(Temporalio::Api::WorkflowService::V1::PollActivityTaskQueueRequest) + expect(request.namespace).to eq(namespace) + expect(request.task_queue.name).to eq(task_queue) + expect(request.identity).to eq(identity) + expect(request.task_queue_metadata).to_not be_nil + expect(request.task_queue_metadata.max_tasks_per_second).to_not be_nil + expect(request.task_queue_metadata.max_tasks_per_second.value).to eq(10) + end + end + end + describe '#add_custom_search_attributes' do it 'calls GRPC service with supplied arguments' do allow(grpc_operator_stub).to receive(:add_search_attributes) diff --git a/spec/unit/lib/temporal/worker_spec.rb b/spec/unit/lib/temporal/worker_spec.rb index 685e07a0..4fffc5d8 100644 --- a/spec/unit/lib/temporal/worker_spec.rb +++ b/spec/unit/lib/temporal/worker_spec.rb @@ -293,7 +293,8 @@ def start_and_stop(worker) config, [], thread_pool_size: 20, - poll_retry_seconds: 0 + poll_retry_seconds: 0, + max_tasks_per_second: 0 ) .and_return(activity_poller_1) @@ -306,7 +307,8 @@ def start_and_stop(worker) config, [], thread_pool_size: 20, - poll_retry_seconds: 0 + poll_retry_seconds: 0, + max_tasks_per_second: 0 ) .and_return(activity_poller_2) @@ -333,7 +335,7 @@ def start_and_stop(worker) an_instance_of(Temporal::ExecutableLookup), an_instance_of(Temporal::Configuration), [], - {thread_pool_size: 10, poll_retry_seconds: 0} + {thread_pool_size: 10, poll_retry_seconds: 0, max_tasks_per_second: 0} ) .and_return(activity_poller) @@ -406,7 +408,7 @@ def start_and_stop(worker) an_instance_of(Temporal::ExecutableLookup), an_instance_of(Temporal::Configuration), [], - {thread_pool_size: 20, poll_retry_seconds: 10} + {thread_pool_size: 20, poll_retry_seconds: 10, max_tasks_per_second: 0} ) .and_return(activity_poller) @@ -441,6 +443,28 @@ def start_and_stop(worker) expect(workflow_poller).to have_received(:start) end + it 'can have an activity poller that registers a task rate limit' do + activity_poller = instance_double(Temporal::Activity::Poller, start: nil, stop_polling: nil, cancel_pending_requests: nil, wait: nil) + expect(Temporal::Activity::Poller) + .to receive(:new) + .with( + 'default-namespace', + 'default-task-queue', + an_instance_of(Temporal::ExecutableLookup), + an_instance_of(Temporal::Configuration), + [], + {thread_pool_size: 20, poll_retry_seconds: 0, max_tasks_per_second: 5} + ) + .and_return(activity_poller) + + worker = Temporal::Worker.new(activity_max_tasks_per_second: 5) + worker.register_activity(TestWorkerActivity) + + start_and_stop(worker) + + expect(activity_poller).to have_received(:start) + end + context 'when middleware is configured' do let(:entry_1) { instance_double(Temporal::Middleware::Entry) } let(:entry_2) { instance_double(Temporal::Middleware::Entry) } @@ -492,7 +516,8 @@ def start_and_stop(worker) config, [entry_2], thread_pool_size: 20, - poll_retry_seconds: 0 + poll_retry_seconds: 0, + max_tasks_per_second: 0 ) .and_return(activity_poller_1) From e3c351f2b8e1ec4dc6934ce26a3504dc3efe707f Mon Sep 17 00:00:00 2001 From: Anthony D Date: Tue, 3 Sep 2024 16:44:05 +0100 Subject: [PATCH 122/125] Fix integration specs (#315) --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2456cb5d..022d8b57 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,7 +37,7 @@ jobs: - name: Start dependencies run: | - docker-compose \ + docker compose \ -f examples/docker-compose.yml \ up -d @@ -82,4 +82,4 @@ jobs: env: USE_ERROR_SERIALIZATION_V2: 1 run: | - cd examples && bundle exec rspec \ No newline at end of file + cd examples && bundle exec rspec From c73a07e236124a9319f1cc245a1b2d75c487d5ef Mon Sep 17 00:00:00 2001 From: Anthony D Date: Wed, 4 Sep 2024 17:36:28 +0100 Subject: [PATCH 123/125] [Refactor] Remove Temporal::Concerns::Payloads (#314) * Implement ConverterWrapper as a replacement for Concern::Payloads * Wrap concerter & payload codec with ConverterWrapper in Configuration * Use ConverterWrapper in Connection::GRPC * Fix failing specs * Remove Concerns::Payload from worker and client * Remove Concerns::Payloads dependency from all the serializers * Remove Concerns::Payloads from Metadata * fixup! Remove Concerns::Payloads dependency from all the serializers * Remove Concerns::Payloads from Errors * Remove Concerns::Payloads from Executions * Remove Concerns::Payloads from fabricators * Remove Concerns::Payloads --- .../call_failing_activity_workflow_spec.rb | 5 - examples/spec/integration/converter_spec.rb | 18 +- lib/temporal.rb | 3 + lib/temporal/activity/task_processor.rb | 7 +- lib/temporal/client.rb | 20 +- lib/temporal/concerns/payloads.rb | 86 --------- lib/temporal/configuration.rb | 24 ++- lib/temporal/connection.rb | 3 +- lib/temporal/connection/grpc.rb | 84 ++++----- lib/temporal/connection/serializer.rb | 4 +- .../connection/serializer/backfill.rb | 2 +- lib/temporal/connection/serializer/base.rb | 5 +- .../serializer/complete_workflow.rb | 5 +- .../connection/serializer/continue_as_new.rb | 13 +- .../connection/serializer/fail_workflow.rb | 2 +- lib/temporal/connection/serializer/failure.rb | 13 +- .../connection/serializer/query_answer.rb | 5 +- .../connection/serializer/record_marker.rb | 5 +- .../connection/serializer/schedule.rb | 8 +- .../connection/serializer/schedule_action.rb | 11 +- .../serializer/schedule_activity.rb | 9 +- .../serializer/schedule_policies.rb | 2 +- .../serializer/signal_external_workflow.rb | 5 +- .../serializer/start_child_workflow.rb | 15 +- .../serializer/upsert_search_attributes.rb | 5 +- lib/temporal/converter_wrapper.rb | 87 +++++++++ lib/temporal/metadata.rb | 15 +- lib/temporal/workflow/errors.rb | 8 +- lib/temporal/workflow/execution_info.rb | 9 +- lib/temporal/workflow/executions.rb | 7 +- lib/temporal/workflow/executor.rb | 2 +- lib/temporal/workflow/state_manager.rb | 30 ++- lib/temporal/workflow/task_processor.rb | 10 +- spec/config/test_converter.rb | 8 + .../grpc/activity_task_fabricator.rb | 4 +- .../grpc/application_failure_fabricator.rb | 6 +- .../grpc/history_event_fabricator.rb | 17 +- spec/fabricators/grpc/memo_fabricator.rb | 2 +- spec/fabricators/grpc/payload_fabricator.rb | 20 ++ spec/fabricators/grpc/payloads_fabricator.rb | 9 + .../grpc/search_attributes_fabricator.rb | 2 +- ...ion_started_event_attributes_fabricator.rb | 2 +- .../grpc/workflow_query_fabricator.rb | 2 +- .../temporal/activity/task_processor_spec.rb | 4 +- spec/unit/lib/temporal/configuration_spec.rb | 48 ++++- .../connection/serializer/backfill_spec.rb | 10 +- .../serializer/continue_as_new_spec.rb | 9 +- .../connection/serializer/failure_spec.rb | 33 ++-- .../serializer/query_answer_spec.rb | 12 +- .../serializer/query_failure_spec.rb | 9 +- .../serializer/retry_policy_spec.rb | 9 +- .../serializer/schedule_action_spec.rb | 10 +- .../serializer/schedule_policies_spec.rb | 10 +- .../serializer/schedule_spec_spec.rb | 8 +- .../serializer/schedule_state_spec.rb | 8 +- .../serializer/start_child_workflow_spec.rb | 10 +- .../upsert_search_attributes_spec.rb | 15 +- .../workflow_id_reuse_policy_spec.rb | 11 +- spec/unit/lib/temporal/connection_spec.rb | 4 + .../lib/temporal/converter_wrapper_spec.rb | 175 ++++++++++++++++++ spec/unit/lib/temporal/grpc_spec.rb | 25 +-- spec/unit/lib/temporal/metadata_spec.rb | 11 +- .../unit/lib/temporal/workflow/errors_spec.rb | 19 +- .../temporal/workflow/execution_info_spec.rb | 10 +- .../lib/temporal/workflow/executor_spec.rb | 6 +- .../temporal/workflow/state_manager_spec.rb | 2 +- 66 files changed, 687 insertions(+), 370 deletions(-) delete mode 100644 lib/temporal/concerns/payloads.rb create mode 100644 lib/temporal/converter_wrapper.rb create mode 100644 spec/config/test_converter.rb create mode 100644 spec/fabricators/grpc/payloads_fabricator.rb create mode 100644 spec/unit/lib/temporal/converter_wrapper_spec.rb diff --git a/examples/spec/integration/call_failing_activity_workflow_spec.rb b/examples/spec/integration/call_failing_activity_workflow_spec.rb index c39853a6..090dd312 100644 --- a/examples/spec/integration/call_failing_activity_workflow_spec.rb +++ b/examples/spec/integration/call_failing_activity_workflow_spec.rb @@ -1,11 +1,6 @@ require 'workflows/call_failing_activity_workflow' describe CallFailingActivityWorkflow, :integration do - - class TestDeserializer - include Temporal::Concerns::Payloads - end - it 'correctly re-raises an activity-thrown exception in the workflow' do workflow_id = SecureRandom.uuid expected_message = "a failure message" diff --git a/examples/spec/integration/converter_spec.rb b/examples/spec/integration/converter_spec.rb index 0a97f075..6c6672c4 100644 --- a/examples/spec/integration/converter_spec.rb +++ b/examples/spec/integration/converter_spec.rb @@ -3,16 +3,20 @@ require 'grpc/errors' describe 'Converter', :integration do + let(:codec) do + Temporal::Connection::Converter::Codec::Chain.new( + payload_codecs: [ + Temporal::CryptPayloadCodec.new + ] + ) + end + around(:each) do |example| task_queue = Temporal.configuration.task_queue Temporal.configure do |config| config.task_queue = 'crypt' - config.payload_codec = Temporal::Connection::Converter::Codec::Chain.new( - payload_codecs: [ - Temporal::CryptPayloadCodec.new - ] - ) + config.payload_codec = codec end example.run @@ -67,8 +71,6 @@ completion_event = events[:EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED].first result = completion_event.workflow_execution_completed_event_attributes.result - payload_codec = Temporal.configuration.payload_codec - - expect(payload_codec.decodes(result).payloads.first.data).to eq('"Hello World, Tom"') + expect(codec.decodes(result).payloads.first.data).to eq('"Hello World, Tom"') end end diff --git a/lib/temporal.rb b/lib/temporal.rb index 078defe0..b9f49d55 100644 --- a/lib/temporal.rb +++ b/lib/temporal.rb @@ -51,6 +51,9 @@ module Temporal class << self def configure(&block) yield config + # Reset the singleton client after configuration was altered to ensure + # it is initialized with the latest attributes + @default_client = nil end def configuration diff --git a/lib/temporal/activity/task_processor.rb b/lib/temporal/activity/task_processor.rb index 35f4bbc6..ef20780b 100644 --- a/lib/temporal/activity/task_processor.rb +++ b/lib/temporal/activity/task_processor.rb @@ -2,7 +2,6 @@ require 'temporal/error_handler' require 'temporal/errors' require 'temporal/activity/context' -require 'temporal/concerns/payloads' require 'temporal/connection/retryer' require 'temporal/connection' require 'temporal/metric_keys' @@ -10,13 +9,11 @@ module Temporal class Activity class TaskProcessor - include Concerns::Payloads - def initialize(task, task_queue, namespace, activity_lookup, middleware_chain, config, heartbeat_thread_pool) @task = task @task_queue = task_queue @namespace = namespace - @metadata = Metadata.generate_activity_metadata(task, namespace) + @metadata = Metadata.generate_activity_metadata(task, namespace, config.converter) @task_token = task.task_token @activity_name = task.activity_type.name @activity_class = activity_lookup.find(activity_name) @@ -38,7 +35,7 @@ def process end result = middleware_chain.invoke(metadata) do - activity_class.execute_in_context(context, from_payloads(task.input)) + activity_class.execute_in_context(context, config.converter.from_payloads(task.input)) end # Do not complete asynchronous activities, these should be completed manually diff --git a/lib/temporal/client.rb b/lib/temporal/client.rb index a49b5ffb..9b537a9d 100644 --- a/lib/temporal/client.rb +++ b/lib/temporal/client.rb @@ -16,6 +16,7 @@ module Temporal class Client def initialize(config) @config = config + @converter = config.converter end # Start a workflow with an optional signal @@ -251,7 +252,7 @@ def await_workflow_result(workflow, workflow_id:, run_id: nil, timeout: nil, nam case closed_event.type when 'WORKFLOW_EXECUTION_COMPLETED' payloads = closed_event.attributes.result - return ResultConverter.from_result_payloads(payloads) + return converter.from_result_payloads(payloads) when 'WORKFLOW_EXECUTION_TIMED_OUT' raise Temporal::WorkflowTimedOut when 'WORKFLOW_EXECUTION_TERMINATED' @@ -259,7 +260,7 @@ def await_workflow_result(workflow, workflow_id:, run_id: nil, timeout: nil, nam when 'WORKFLOW_EXECUTION_CANCELED' raise Temporal::WorkflowCanceled when 'WORKFLOW_EXECUTION_FAILED' - raise Temporal::Workflow::Errors.generate_error(closed_event.attributes.failure) + raise Temporal::Workflow::Errors.generate_error(closed_event.attributes.failure, converter) when 'WORKFLOW_EXECUTION_CONTINUED_AS_NEW' new_run_id = closed_event.attributes.new_execution_run_id # Throw to let the caller know they're not getting the result @@ -355,7 +356,7 @@ def fetch_workflow_execution_info(namespace, workflow_id, run_id) run_id: run_id ) - Workflow::ExecutionInfo.generate_from(response.workflow_execution_info) + Workflow::ExecutionInfo.generate_from(response.workflow_execution_info, converter) end # Manually complete an activity @@ -458,19 +459,19 @@ def get_workflow_history_protobuf(namespace: nil, workflow_id:, run_id: nil) def list_open_workflow_executions(namespace, from, to = Time.now, filter: {}, next_page_token: nil, max_page_size: nil) validate_filter(filter, :workflow, :workflow_id) - Temporal::Workflow::Executions.new(connection: connection, status: :open, request_options: { namespace: namespace, from: from, to: to, next_page_token: next_page_token, max_page_size: max_page_size}.merge(filter)) + Temporal::Workflow::Executions.new(converter, connection: connection, status: :open, request_options: { namespace: namespace, from: from, to: to, next_page_token: next_page_token, max_page_size: max_page_size}.merge(filter)) end def list_closed_workflow_executions(namespace, from, to = Time.now, filter: {}, next_page_token: nil, max_page_size: nil) validate_filter(filter, :status, :workflow, :workflow_id) - Temporal::Workflow::Executions.new(connection: connection, status: :closed, request_options: { namespace: namespace, from: from, to: to, next_page_token: next_page_token, max_page_size: max_page_size}.merge(filter)) + Temporal::Workflow::Executions.new(converter, connection: connection, status: :closed, request_options: { namespace: namespace, from: from, to: to, next_page_token: next_page_token, max_page_size: max_page_size}.merge(filter)) end def query_workflow_executions(namespace, query, filter: {}, next_page_token: nil, max_page_size: nil) validate_filter(filter, :status, :workflow, :workflow_id) - Temporal::Workflow::Executions.new(connection: connection, status: :all, request_options: { namespace: namespace, query: query, next_page_token: next_page_token, max_page_size: max_page_size }.merge(filter)) + Temporal::Workflow::Executions.new(converter, connection: connection, status: :all, request_options: { namespace: namespace, query: query, next_page_token: next_page_token, max_page_size: max_page_size }.merge(filter)) end # Count the number of workflows matching the provided query @@ -598,14 +599,9 @@ def connection @connection ||= Temporal::Connection.generate(config.for_connection) end - class ResultConverter - extend Concerns::Payloads - end - private_constant :ResultConverter - private - attr_reader :config + attr_reader :config, :converter def compute_run_timeout(execution_options) execution_options.timeouts[:run] || execution_options.timeouts[:execution] diff --git a/lib/temporal/concerns/payloads.rb b/lib/temporal/concerns/payloads.rb deleted file mode 100644 index 5c771e21..00000000 --- a/lib/temporal/concerns/payloads.rb +++ /dev/null @@ -1,86 +0,0 @@ -module Temporal - module Concerns - module Payloads - def from_payloads(payloads) - payloads = payload_codec.decodes(payloads) - payload_converter.from_payloads(payloads) - end - - def from_payload(payload) - payload = payload_codec.decode(payload) - payload_converter.from_payload(payload) - end - - def from_payload_map_without_codec(payload_map) - payload_map.map { |key, value| [key, payload_converter.from_payload(value)] }.to_h - end - - def from_result_payloads(payloads) - from_payloads(payloads)&.first - end - - def from_details_payloads(payloads) - from_payloads(payloads)&.first - end - - def from_signal_payloads(payloads) - from_payloads(payloads)&.first - end - - def from_query_payloads(payloads) - from_payloads(payloads)&.first - end - - def from_payload_map(payload_map) - payload_map.map { |key, value| [key, from_payload(value)] }.to_h - end - - def to_payloads(data) - payloads = payload_converter.to_payloads(data) - payload_codec.encodes(payloads) - end - - def to_payload(data) - payload = payload_converter.to_payload(data) - payload_codec.encode(payload) - end - - def to_payload_map_without_codec(data) - # skips the payload_codec step because search attributes don't use this pipeline - data.transform_values do |value| - payload_converter.to_payload(value) - end - end - - def to_result_payloads(data) - to_payloads([data]) - end - - def to_details_payloads(data) - to_payloads([data]) - end - - def to_signal_payloads(data) - to_payloads([data]) - end - - def to_query_payloads(data) - to_payloads([data]) - end - - def to_payload_map(data) - data.transform_values(&method(:to_payload)) - end - - private - - def payload_converter - Temporal.configuration.converter - end - - def payload_codec - Temporal.configuration.payload_codec - end - end - end -end diff --git a/lib/temporal/configuration.rb b/lib/temporal/configuration.rb index 136c73ce..101ad956 100644 --- a/lib/temporal/configuration.rb +++ b/lib/temporal/configuration.rb @@ -1,4 +1,5 @@ require 'temporal/capabilities' +require 'temporal/converter_wrapper' require 'temporal/logger' require 'temporal/metrics_adapters/null' require 'temporal/middleware/header_propagator_chain' @@ -12,13 +13,13 @@ module Temporal class Configuration - Connection = Struct.new(:type, :host, :port, :credentials, :identity, :connection_options, keyword_init: true) + Connection = Struct.new(:type, :host, :port, :credentials, :identity, :converter, :connection_options, keyword_init: true) Execution = Struct.new(:namespace, :task_queue, :timeouts, :headers, :search_attributes, keyword_init: true) - attr_reader :timeouts, :error_handlers, :capabilities - attr_accessor :connection_type, :converter, :use_error_serialization_v2, :host, :port, :credentials, :identity, + attr_reader :timeouts, :error_handlers, :capabilities, :payload_codec + attr_accessor :connection_type, :use_error_serialization_v2, :host, :port, :credentials, :identity, :logger, :metrics_adapter, :namespace, :task_queue, :headers, :search_attributes, :header_propagators, - :payload_codec, :legacy_signals, :no_signals_in_first_task, :connection_options, :log_on_workflow_replay + :legacy_signals, :no_signals_in_first_task, :connection_options, :log_on_workflow_replay # See https://docs.temporal.io/blog/activity-timeouts/ for general docs. # We want an infinite execution timeout for cron schedules and other perpetual workflows. @@ -124,6 +125,7 @@ def for_connection port: port, credentials: credentials, identity: identity || default_identity, + converter: converter, connection_options: connection_options.merge(use_error_serialization_v2: @use_error_serialization_v2) ).freeze end @@ -148,6 +150,20 @@ def header_propagator_chain Middleware::HeaderPropagatorChain.new(header_propagators) end + def converter + @converter_wrapper ||= ConverterWrapper.new(@converter, @payload_codec) + end + + def converter=(new_converter) + @converter = new_converter + @converter_wrapper = nil + end + + def payload_codec=(new_codec) + @payload_codec = new_codec + @converter_wrapper = nil + end + private def default_identity diff --git a/lib/temporal/connection.rb b/lib/temporal/connection.rb index a36fe091..6ee1bcc7 100644 --- a/lib/temporal/connection.rb +++ b/lib/temporal/connection.rb @@ -12,9 +12,10 @@ def self.generate(configuration) port = configuration.port credentials = configuration.credentials identity = configuration.identity + converter = configuration.converter options = configuration.connection_options - connection_class.new(host, port, identity, credentials, options) + connection_class.new(host, port, identity, credentials, converter, options) end end end diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index 282a262c..5392f62a 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -15,13 +15,10 @@ require 'temporal/connection/serializer/backfill' require 'temporal/connection/serializer/schedule' require 'temporal/connection/serializer/workflow_id_reuse_policy' -require 'temporal/concerns/payloads' module Temporal module Connection class GRPC - include Concerns::Payloads - HISTORY_EVENT_FILTER = { all: Temporalio::Api::Enums::V1::HistoryEventFilterType::HISTORY_EVENT_FILTER_TYPE_ALL_EVENT, close: Temporalio::Api::Enums::V1::HistoryEventFilterType::HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT @@ -58,10 +55,11 @@ class GRPC CONNECTION_TIMEOUT_SECONDS = 60 - def initialize(host, port, identity, credentials, options = {}) + def initialize(host, port, identity, credentials, converter, options = {}) @url = "#{host}:#{port}" @identity = identity @credentials = credentials + @converter = converter @poll = true @poll_mutex = Mutex.new @poll_request = nil @@ -131,24 +129,24 @@ def start_workflow_execution( name: workflow_name ), workflow_id: workflow_id, - workflow_id_reuse_policy: Temporal::Connection::Serializer::WorkflowIdReusePolicy.new(workflow_id_reuse_policy).to_proto, + workflow_id_reuse_policy: Temporal::Connection::Serializer::WorkflowIdReusePolicy.new(workflow_id_reuse_policy, converter).to_proto, task_queue: Temporalio::Api::TaskQueue::V1::TaskQueue.new( name: task_queue ), - input: to_payloads(input), + input: converter.to_payloads(input), workflow_execution_timeout: execution_timeout, workflow_run_timeout: run_timeout, workflow_task_timeout: task_timeout, request_id: SecureRandom.uuid, header: Temporalio::Api::Common::V1::Header.new( - fields: to_payload_map(headers || {}) + fields: converter.to_payload_map(headers || {}) ), cron_schedule: cron_schedule, memo: Temporalio::Api::Common::V1::Memo.new( - fields: to_payload_map(memo || {}) + fields: converter.to_payload_map(memo || {}) ), search_attributes: Temporalio::Api::Common::V1::SearchAttributes.new( - indexed_fields: to_payload_map_without_codec(search_attributes || {}) + indexed_fields: converter.to_payload_map_without_codec(search_attributes || {}) ) ) @@ -213,7 +211,7 @@ def poll_workflow_task_queue(namespace:, task_queue:, binary_checksum:) end def respond_query_task_completed(namespace:, task_token:, query_result:) - query_result_proto = Serializer.serialize(query_result) + query_result_proto = Serializer.serialize(query_result, converter) request = Temporalio::Api::WorkflowService::V1::RespondQueryTaskCompletedRequest.new( task_token: task_token, namespace: namespace, @@ -230,8 +228,8 @@ def respond_workflow_task_completed(namespace:, task_token:, commands:, binary_c namespace: namespace, identity: identity, task_token: task_token, - commands: Array(commands).map { |(_, command)| Serializer.serialize(command) }, - query_results: query_results.transform_values { |value| Serializer.serialize(value) }, + commands: Array(commands).map { |(_, command)| Serializer.serialize(command, converter) }, + query_results: query_results.transform_values { |value| Serializer.serialize(value, converter) }, binary_checksum: binary_checksum, sdk_metadata: if new_sdk_flags_used.any? Temporalio::Api::Sdk::V1::WorkflowTaskCompletedMetadata.new( @@ -250,7 +248,7 @@ def respond_workflow_task_failed(namespace:, task_token:, cause:, exception:, bi identity: identity, task_token: task_token, cause: cause, - failure: Serializer::Failure.new(exception).to_proto, + failure: Serializer::Failure.new(exception, converter).to_proto, binary_checksum: binary_checksum ) client.respond_workflow_task_failed(request) @@ -284,7 +282,7 @@ def record_activity_task_heartbeat(namespace:, task_token:, details: nil) request = Temporalio::Api::WorkflowService::V1::RecordActivityTaskHeartbeatRequest.new( namespace: namespace, task_token: task_token, - details: to_details_payloads(details), + details: converter.to_details_payloads(details), identity: identity ) client.record_activity_task_heartbeat(request) @@ -299,7 +297,7 @@ def respond_activity_task_completed(namespace:, task_token:, result:) namespace: namespace, identity: identity, task_token: task_token, - result: to_result_payloads(result) + result: converter.to_result_payloads(result) ) client.respond_activity_task_completed(request) end @@ -311,7 +309,7 @@ def respond_activity_task_completed_by_id(namespace:, activity_id:, workflow_id: workflow_id: workflow_id, run_id: run_id, activity_id: activity_id, - result: to_result_payloads(result) + result: converter.to_result_payloads(result) ) client.respond_activity_task_completed_by_id(request) end @@ -322,7 +320,7 @@ def respond_activity_task_failed(namespace:, task_token:, exception:) namespace: namespace, identity: identity, task_token: task_token, - failure: Serializer::Failure.new(exception, serialize_whole_error: serialize_whole_error).to_proto + failure: Serializer::Failure.new(exception, converter, serialize_whole_error: serialize_whole_error).to_proto ) client.respond_activity_task_failed(request) end @@ -334,7 +332,7 @@ def respond_activity_task_failed_by_id(namespace:, activity_id:, workflow_id:, r workflow_id: workflow_id, run_id: run_id, activity_id: activity_id, - failure: Serializer::Failure.new(exception).to_proto + failure: Serializer::Failure.new(exception, converter).to_proto ) client.respond_activity_task_failed_by_id(request) end @@ -343,7 +341,7 @@ def respond_activity_task_canceled(namespace:, task_token:, details: nil) request = Temporalio::Api::WorkflowService::V1::RespondActivityTaskCanceledRequest.new( namespace: namespace, task_token: task_token, - details: to_details_payloads(details), + details: converter.to_details_payloads(details), identity: identity ) client.respond_activity_task_canceled(request) @@ -365,7 +363,7 @@ def signal_workflow_execution(namespace:, workflow_id:, run_id:, signal:, input: run_id: run_id ), signal_name: signal, - input: to_signal_payloads(input), + input: converter.to_signal_payloads(input), identity: identity ) client.signal_workflow_execution(request) @@ -384,9 +382,9 @@ def signal_with_start_workflow_execution( search_attributes: nil ) proto_header_fields = if headers.nil? - to_payload_map({}) + converter.to_payload_map({}) elsif headers.instance_of?(Hash) - to_payload_map(headers) + converter.to_payload_map(headers) else # Preserve backward compatability for headers specified using proto objects warn '[DEPRECATION] Specify headers using a hash rather than protobuf objects' @@ -400,11 +398,11 @@ def signal_with_start_workflow_execution( name: workflow_name ), workflow_id: workflow_id, - workflow_id_reuse_policy: Temporal::Connection::Serializer::WorkflowIdReusePolicy.new(workflow_id_reuse_policy).to_proto, + workflow_id_reuse_policy: Temporal::Connection::Serializer::WorkflowIdReusePolicy.new(workflow_id_reuse_policy, converter).to_proto, task_queue: Temporalio::Api::TaskQueue::V1::TaskQueue.new( name: task_queue ), - input: to_payloads(input), + input: converter.to_payloads(input), workflow_execution_timeout: execution_timeout, workflow_run_timeout: run_timeout, workflow_task_timeout: task_timeout, @@ -414,12 +412,12 @@ def signal_with_start_workflow_execution( ), cron_schedule: cron_schedule, signal_name: signal_name, - signal_input: to_signal_payloads(signal_input), + signal_input: converter.to_signal_payloads(signal_input), memo: Temporalio::Api::Common::V1::Memo.new( - fields: to_payload_map(memo || {}) + fields: converter.to_payload_map(memo || {}) ), search_attributes: Temporalio::Api::Common::V1::SearchAttributes.new( - indexed_fields: to_payload_map_without_codec(search_attributes || {}) + indexed_fields: converter.to_payload_map_without_codec(search_attributes || {}) ) ) @@ -463,7 +461,7 @@ def terminate_workflow_execution( run_id: run_id ), reason: reason, - details: to_details_payloads(details) + details: converter.to_details_payloads(details) ) client.terminate_workflow_execution(request) @@ -577,7 +575,7 @@ def query_workflow(namespace:, workflow_id:, run_id:, query:, args: nil, query_r ), query: Temporalio::Api::Query::V1::WorkflowQuery.new( query_type: query, - query_args: to_query_payloads(args) + query_args: converter.to_query_payloads(args) ) ) if query_reject_condition @@ -599,7 +597,7 @@ def query_workflow(namespace:, workflow_id:, run_id:, query:, args: nil, query_r elsif !response.query_result raise Temporal::QueryFailed, 'Invalid response from server' else - from_query_payloads(response.query_result) + converter.from_query_payloads(response.query_result) end end @@ -649,8 +647,8 @@ def list_schedules(namespace:, maximum_page_size:, next_page_token:) schedules: resp.schedules.map do |schedule| Temporal::Schedule::ScheduleListEntry.new( schedule_id: schedule.schedule_id, - memo: from_payload_map(schedule.memo&.fields || {}), - search_attributes: from_payload_map_without_codec(schedule.search_attributes&.indexed_fields || {}), + memo: converter.from_payload_map(schedule.memo&.fields || {}), + search_attributes: converter.from_payload_map_without_codec(schedule.search_attributes&.indexed_fields || {}), info: schedule.info ) end, @@ -674,8 +672,8 @@ def describe_schedule(namespace:, schedule_id:) Temporal::Schedule::DescribeScheduleResponse.new( schedule: resp.schedule, info: resp.info, - memo: from_payload_map(resp.memo&.fields || {}), - search_attributes: from_payload_map_without_codec(resp.search_attributes&.indexed_fields || {}), + memo: converter.from_payload_map(resp.memo&.fields || {}), + search_attributes: converter.from_payload_map_without_codec(resp.search_attributes&.indexed_fields || {}), conflict_token: resp.conflict_token ) end @@ -695,27 +693,28 @@ def create_schedule( if trigger_immediately initial_patch.trigger_immediately = Temporalio::Api::Schedule::V1::TriggerImmediatelyRequest.new( overlap_policy: Temporal::Connection::Serializer::ScheduleOverlapPolicy.new( - schedule.policies&.overlap_policy + schedule.policies&.overlap_policy, + converter ).to_proto ) end if backfill - initial_patch.backfill_request += [Temporal::Connection::Serializer::Backfill.new(backfill).to_proto] + initial_patch.backfill_request += [Temporal::Connection::Serializer::Backfill.new(backfill, converter).to_proto] end end request = Temporalio::Api::WorkflowService::V1::CreateScheduleRequest.new( namespace: namespace, schedule_id: schedule_id, - schedule: Temporal::Connection::Serializer::Schedule.new(schedule).to_proto, + schedule: Temporal::Connection::Serializer::Schedule.new(schedule, converter).to_proto, identity: identity, request_id: SecureRandom.uuid, memo: Temporalio::Api::Common::V1::Memo.new( - fields: to_payload_map(memo || {}) + fields: converter.to_payload_map(memo || {}) ), search_attributes: Temporalio::Api::Common::V1::SearchAttributes.new( - indexed_fields: to_payload_map_without_codec(search_attributes || {}) + indexed_fields: converter.to_payload_map_without_codec(search_attributes || {}) ) ) client.create_schedule(request) @@ -739,7 +738,7 @@ def update_schedule(namespace:, schedule_id:, schedule:, conflict_token: nil) request = Temporalio::Api::WorkflowService::V1::UpdateScheduleRequest.new( namespace: namespace, schedule_id: schedule_id, - schedule: Temporal::Connection::Serializer::Schedule.new(schedule).to_proto, + schedule: Temporal::Connection::Serializer::Schedule.new(schedule, converter).to_proto, conflict_token: conflict_token, identity: identity, request_id: SecureRandom.uuid @@ -759,7 +758,8 @@ def trigger_schedule(namespace:, schedule_id:, overlap_policy: nil) patch: Temporalio::Api::Schedule::V1::SchedulePatch.new( trigger_immediately: Temporalio::Api::Schedule::V1::TriggerImmediatelyRequest.new( overlap_policy: Temporal::Connection::Serializer::ScheduleOverlapPolicy.new( - overlap_policy + overlap_policy, + converter ).to_proto ), ), @@ -799,7 +799,7 @@ def pause_schedule(namespace:, schedule_id:, should_pause:, note: nil) private - attr_reader :url, :identity, :credentials, :options, :poll_mutex, :poll_request + attr_reader :url, :identity, :credentials, :converter, :options, :poll_mutex, :poll_request def client return @client if @client diff --git a/lib/temporal/connection/serializer.rb b/lib/temporal/connection/serializer.rb index 46070c66..b31c1005 100644 --- a/lib/temporal/connection/serializer.rb +++ b/lib/temporal/connection/serializer.rb @@ -33,9 +33,9 @@ module Serializer Workflow::QueryResult::Failure => Serializer::QueryFailure, }.freeze - def self.serialize(object) + def self.serialize(object, converter) serializer = SERIALIZERS_MAP[object.class] - serializer.new(object).to_proto + serializer.new(object, converter).to_proto end end end diff --git a/lib/temporal/connection/serializer/backfill.rb b/lib/temporal/connection/serializer/backfill.rb index 04998cfb..7abb40a5 100644 --- a/lib/temporal/connection/serializer/backfill.rb +++ b/lib/temporal/connection/serializer/backfill.rb @@ -11,7 +11,7 @@ def to_proto Temporalio::Api::Schedule::V1::BackfillRequest.new( start_time: serialize_time(object.start_time), end_time: serialize_time(object.end_time), - overlap_policy: Temporal::Connection::Serializer::ScheduleOverlapPolicy.new(object.overlap_policy).to_proto + overlap_policy: Temporal::Connection::Serializer::ScheduleOverlapPolicy.new(object.overlap_policy, converter).to_proto ) end diff --git a/lib/temporal/connection/serializer/base.rb b/lib/temporal/connection/serializer/base.rb index 9fcd49c5..79e8767a 100644 --- a/lib/temporal/connection/serializer/base.rb +++ b/lib/temporal/connection/serializer/base.rb @@ -6,8 +6,9 @@ module Temporal module Connection module Serializer class Base - def initialize(object) + def initialize(object, converter) @object = object + @converter = converter end def to_proto @@ -16,7 +17,7 @@ def to_proto private - attr_reader :object + attr_reader :object, :converter end end end diff --git a/lib/temporal/connection/serializer/complete_workflow.rb b/lib/temporal/connection/serializer/complete_workflow.rb index beb3b0ed..8eaa3ed4 100644 --- a/lib/temporal/connection/serializer/complete_workflow.rb +++ b/lib/temporal/connection/serializer/complete_workflow.rb @@ -1,18 +1,15 @@ require 'temporal/connection/serializer/base' -require 'temporal/concerns/payloads' module Temporal module Connection module Serializer class CompleteWorkflow < Base - include Concerns::Payloads - def to_proto Temporalio::Api::Command::V1::Command.new( command_type: Temporalio::Api::Enums::V1::CommandType::COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, complete_workflow_execution_command_attributes: Temporalio::Api::Command::V1::CompleteWorkflowExecutionCommandAttributes.new( - result: to_result_payloads(object.result) + result: converter.to_result_payloads(object.result) ) ) end diff --git a/lib/temporal/connection/serializer/continue_as_new.rb b/lib/temporal/connection/serializer/continue_as_new.rb index 6573c8ec..989ff2a9 100644 --- a/lib/temporal/connection/serializer/continue_as_new.rb +++ b/lib/temporal/connection/serializer/continue_as_new.rb @@ -1,13 +1,10 @@ require 'temporal/connection/serializer/base' require 'temporal/connection/serializer/retry_policy' -require 'temporal/concerns/payloads' module Temporal module Connection module Serializer class ContinueAsNew < Base - include Concerns::Payloads - def to_proto Temporalio::Api::Command::V1::Command.new( command_type: Temporalio::Api::Enums::V1::CommandType::COMMAND_TYPE_CONTINUE_AS_NEW_WORKFLOW_EXECUTION, @@ -15,10 +12,10 @@ def to_proto Temporalio::Api::Command::V1::ContinueAsNewWorkflowExecutionCommandAttributes.new( workflow_type: Temporalio::Api::Common::V1::WorkflowType.new(name: object.workflow_type), task_queue: Temporalio::Api::TaskQueue::V1::TaskQueue.new(name: object.task_queue), - input: to_payloads(object.input), + input: converter.to_payloads(object.input), workflow_run_timeout: object.timeouts[:run], workflow_task_timeout: object.timeouts[:task], - retry_policy: Temporal::Connection::Serializer::RetryPolicy.new(object.retry_policy).to_proto, + retry_policy: Temporal::Connection::Serializer::RetryPolicy.new(object.retry_policy, converter).to_proto, header: serialize_headers(object.headers), memo: serialize_memo(object.memo), search_attributes: serialize_search_attributes(object.search_attributes), @@ -31,19 +28,19 @@ def to_proto def serialize_headers(headers) return unless headers - Temporalio::Api::Common::V1::Header.new(fields: to_payload_map(headers)) + Temporalio::Api::Common::V1::Header.new(fields: converter.to_payload_map(headers)) end def serialize_memo(memo) return unless memo - Temporalio::Api::Common::V1::Memo.new(fields: to_payload_map(memo)) + Temporalio::Api::Common::V1::Memo.new(fields: converter.to_payload_map(memo)) end def serialize_search_attributes(search_attributes) return unless search_attributes - Temporalio::Api::Common::V1::SearchAttributes.new(indexed_fields: to_payload_map_without_codec(search_attributes)) + Temporalio::Api::Common::V1::SearchAttributes.new(indexed_fields: converter.to_payload_map_without_codec(search_attributes)) end end end diff --git a/lib/temporal/connection/serializer/fail_workflow.rb b/lib/temporal/connection/serializer/fail_workflow.rb index a6ef9ea0..2bedb688 100644 --- a/lib/temporal/connection/serializer/fail_workflow.rb +++ b/lib/temporal/connection/serializer/fail_workflow.rb @@ -10,7 +10,7 @@ def to_proto command_type: Temporalio::Api::Enums::V1::CommandType::COMMAND_TYPE_FAIL_WORKFLOW_EXECUTION, fail_workflow_execution_command_attributes: Temporalio::Api::Command::V1::FailWorkflowExecutionCommandAttributes.new( - failure: Failure.new(object.exception).to_proto + failure: Failure.new(object.exception, converter).to_proto ) ) end diff --git a/lib/temporal/connection/serializer/failure.rb b/lib/temporal/connection/serializer/failure.rb index ddfeb2e3..2d17e949 100644 --- a/lib/temporal/connection/serializer/failure.rb +++ b/lib/temporal/connection/serializer/failure.rb @@ -1,21 +1,18 @@ require 'temporal/connection/serializer/base' -require 'temporal/concerns/payloads' module Temporal module Connection module Serializer class Failure < Base - include Concerns::Payloads - - def initialize(error, serialize_whole_error: false, max_bytes: 200_000) + def initialize(error, converter, serialize_whole_error: false, max_bytes: 200_000) @serialize_whole_error = serialize_whole_error @max_bytes = max_bytes - super(error) + super(error, converter) end def to_proto if @serialize_whole_error - details = to_details_payloads(object) + details = converter.to_details_payloads(object) if details.payloads.first.data.size > @max_bytes Temporal.logger.error( "Could not serialize exception because it's too large, so we are using a fallback that may not "\ @@ -25,10 +22,10 @@ def to_proto ) # Fallback to a more conservative serialization if the payload is too big to avoid # sending a huge amount of data to temporal and putting it in the history. - details = to_details_payloads(object.message) + details = converter.to_details_payloads(object.message) end else - details = to_details_payloads(object.message) + details = converter.to_details_payloads(object.message) end Temporalio::Api::Failure::V1::Failure.new( message: object.message, diff --git a/lib/temporal/connection/serializer/query_answer.rb b/lib/temporal/connection/serializer/query_answer.rb index 746c50c0..0c98b010 100644 --- a/lib/temporal/connection/serializer/query_answer.rb +++ b/lib/temporal/connection/serializer/query_answer.rb @@ -1,16 +1,13 @@ require 'temporal/connection/serializer/base' -require 'temporal/concerns/payloads' module Temporal module Connection module Serializer class QueryAnswer < Base - include Concerns::Payloads - def to_proto Temporalio::Api::Query::V1::WorkflowQueryResult.new( result_type: Temporalio::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_ANSWERED, - answer: to_query_payloads(object.result) + answer: converter.to_query_payloads(object.result) ) end end diff --git a/lib/temporal/connection/serializer/record_marker.rb b/lib/temporal/connection/serializer/record_marker.rb index b29040f3..99fddb8c 100644 --- a/lib/temporal/connection/serializer/record_marker.rb +++ b/lib/temporal/connection/serializer/record_marker.rb @@ -1,12 +1,9 @@ require 'temporal/connection/serializer/base' -require 'temporal/concerns/payloads' module Temporal module Connection module Serializer class RecordMarker < Base - include Concerns::Payloads - def to_proto Temporalio::Api::Command::V1::Command.new( command_type: Temporalio::Api::Enums::V1::CommandType::COMMAND_TYPE_RECORD_MARKER, @@ -14,7 +11,7 @@ def to_proto Temporalio::Api::Command::V1::RecordMarkerCommandAttributes.new( marker_name: object.name, details: { - 'data' => to_details_payloads(object.details) + 'data' => converter.to_details_payloads(object.details) } ) ) diff --git a/lib/temporal/connection/serializer/schedule.rb b/lib/temporal/connection/serializer/schedule.rb index 32c206fd..3e2fc264 100644 --- a/lib/temporal/connection/serializer/schedule.rb +++ b/lib/temporal/connection/serializer/schedule.rb @@ -10,10 +10,10 @@ module Serializer class Schedule < Base def to_proto Temporalio::Api::Schedule::V1::Schedule.new( - spec: Temporal::Connection::Serializer::ScheduleSpec.new(object.spec).to_proto, - action: Temporal::Connection::Serializer::ScheduleAction.new(object.action).to_proto, - policies: Temporal::Connection::Serializer::SchedulePolicies.new(object.policies).to_proto, - state: Temporal::Connection::Serializer::ScheduleState.new(object.state).to_proto + spec: Temporal::Connection::Serializer::ScheduleSpec.new(object.spec, converter).to_proto, + action: Temporal::Connection::Serializer::ScheduleAction.new(object.action, converter).to_proto, + policies: Temporal::Connection::Serializer::SchedulePolicies.new(object.policies, converter).to_proto, + state: Temporal::Connection::Serializer::ScheduleState.new(object.state, converter).to_proto ) end end diff --git a/lib/temporal/connection/serializer/schedule_action.rb b/lib/temporal/connection/serializer/schedule_action.rb index ab4ce4c0..b79942be 100644 --- a/lib/temporal/connection/serializer/schedule_action.rb +++ b/lib/temporal/connection/serializer/schedule_action.rb @@ -1,12 +1,9 @@ require "temporal/connection/serializer/base" -require "temporal/concerns/payloads" module Temporal module Connection module Serializer class ScheduleAction < Base - include Concerns::Payloads - def to_proto unless object.is_a?(Temporal::Schedule::StartWorkflowAction) raise ArgumentError, "Unknown action type #{object.class}" @@ -21,18 +18,18 @@ def to_proto task_queue: Temporalio::Api::TaskQueue::V1::TaskQueue.new( name: object.task_queue ), - input: to_payloads(object.input), + input: converter.to_payloads(object.input), workflow_execution_timeout: object.execution_timeout, workflow_run_timeout: object.run_timeout, workflow_task_timeout: object.task_timeout, header: Temporalio::Api::Common::V1::Header.new( - fields: to_payload_map(object.headers || {}) + fields: converter.to_payload_map(object.headers || {}) ), memo: Temporalio::Api::Common::V1::Memo.new( - fields: to_payload_map(object.memo || {}) + fields: converter.to_payload_map(object.memo || {}) ), search_attributes: Temporalio::Api::Common::V1::SearchAttributes.new( - indexed_fields: to_payload_map_without_codec(object.search_attributes || {}) + indexed_fields: converter.to_payload_map_without_codec(object.search_attributes || {}) ) ) ) diff --git a/lib/temporal/connection/serializer/schedule_activity.rb b/lib/temporal/connection/serializer/schedule_activity.rb index 10b26570..b3640639 100644 --- a/lib/temporal/connection/serializer/schedule_activity.rb +++ b/lib/temporal/connection/serializer/schedule_activity.rb @@ -1,13 +1,10 @@ require 'temporal/connection/serializer/base' require 'temporal/connection/serializer/retry_policy' -require 'temporal/concerns/payloads' module Temporal module Connection module Serializer class ScheduleActivity < Base - include Concerns::Payloads - def to_proto Temporalio::Api::Command::V1::Command.new( command_type: Temporalio::Api::Enums::V1::CommandType::COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, @@ -15,13 +12,13 @@ def to_proto Temporalio::Api::Command::V1::ScheduleActivityTaskCommandAttributes.new( activity_id: object.activity_id.to_s, activity_type: Temporalio::Api::Common::V1::ActivityType.new(name: object.activity_type), - input: to_payloads(object.input), + input: converter.to_payloads(object.input), task_queue: Temporalio::Api::TaskQueue::V1::TaskQueue.new(name: object.task_queue), schedule_to_close_timeout: object.timeouts[:schedule_to_close], schedule_to_start_timeout: object.timeouts[:schedule_to_start], start_to_close_timeout: object.timeouts[:start_to_close], heartbeat_timeout: object.timeouts[:heartbeat], - retry_policy: Temporal::Connection::Serializer::RetryPolicy.new(object.retry_policy).to_proto, + retry_policy: Temporal::Connection::Serializer::RetryPolicy.new(object.retry_policy, converter).to_proto, header: serialize_headers(object.headers) ) ) @@ -32,7 +29,7 @@ def to_proto def serialize_headers(headers) return unless headers - Temporalio::Api::Common::V1::Header.new(fields: to_payload_map(headers)) + Temporalio::Api::Common::V1::Header.new(fields: converter.to_payload_map(headers)) end end end diff --git a/lib/temporal/connection/serializer/schedule_policies.rb b/lib/temporal/connection/serializer/schedule_policies.rb index 4a92c226..42558899 100644 --- a/lib/temporal/connection/serializer/schedule_policies.rb +++ b/lib/temporal/connection/serializer/schedule_policies.rb @@ -9,7 +9,7 @@ def to_proto return unless object Temporalio::Api::Schedule::V1::SchedulePolicies.new( - overlap_policy: Temporal::Connection::Serializer::ScheduleOverlapPolicy.new(object.overlap_policy).to_proto, + overlap_policy: Temporal::Connection::Serializer::ScheduleOverlapPolicy.new(object.overlap_policy, converter).to_proto, catchup_window: object.catchup_window, pause_on_failure: object.pause_on_failure ) diff --git a/lib/temporal/connection/serializer/signal_external_workflow.rb b/lib/temporal/connection/serializer/signal_external_workflow.rb index 5cc640fd..ff229ddb 100644 --- a/lib/temporal/connection/serializer/signal_external_workflow.rb +++ b/lib/temporal/connection/serializer/signal_external_workflow.rb @@ -1,12 +1,9 @@ require 'temporal/connection/serializer/base' -require 'temporal/concerns/payloads' module Temporal module Connection module Serializer class SignalExternalWorkflow < Base - include Concerns::Payloads - def to_proto Temporalio::Api::Command::V1::Command.new( command_type: Temporalio::Api::Enums::V1::CommandType::COMMAND_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION, @@ -15,7 +12,7 @@ def to_proto namespace: object.namespace, execution: serialize_execution(object.execution), signal_name: object.signal_name, - input: to_signal_payloads(object.input), + input: converter.to_signal_payloads(object.input), control: "", # deprecated child_workflow_only: object.child_workflow_only ) diff --git a/lib/temporal/connection/serializer/start_child_workflow.rb b/lib/temporal/connection/serializer/start_child_workflow.rb index 90d08c79..dcb2fbf0 100644 --- a/lib/temporal/connection/serializer/start_child_workflow.rb +++ b/lib/temporal/connection/serializer/start_child_workflow.rb @@ -1,14 +1,11 @@ require 'temporal/connection/serializer/base' require 'temporal/connection/serializer/retry_policy' require 'temporal/connection/serializer/workflow_id_reuse_policy' -require 'temporal/concerns/payloads' module Temporal module Connection module Serializer class StartChildWorkflow < Base - include Concerns::Payloads - PARENT_CLOSE_POLICY = { terminate: Temporalio::Api::Enums::V1::ParentClosePolicy::PARENT_CLOSE_POLICY_TERMINATE, abandon: Temporalio::Api::Enums::V1::ParentClosePolicy::PARENT_CLOSE_POLICY_ABANDON, @@ -24,16 +21,16 @@ def to_proto workflow_id: object.workflow_id.to_s, workflow_type: Temporalio::Api::Common::V1::WorkflowType.new(name: object.workflow_type), task_queue: Temporalio::Api::TaskQueue::V1::TaskQueue.new(name: object.task_queue), - input: to_payloads(object.input), + input: converter.to_payloads(object.input), workflow_execution_timeout: object.timeouts[:execution], workflow_run_timeout: object.timeouts[:run], workflow_task_timeout: object.timeouts[:task], - retry_policy: Temporal::Connection::Serializer::RetryPolicy.new(object.retry_policy).to_proto, + retry_policy: Temporal::Connection::Serializer::RetryPolicy.new(object.retry_policy, converter).to_proto, parent_close_policy: serialize_parent_close_policy(object.parent_close_policy), header: serialize_headers(object.headers), cron_schedule: object.cron_schedule, memo: serialize_memo(object.memo), - workflow_id_reuse_policy: Temporal::Connection::Serializer::WorkflowIdReusePolicy.new(object.workflow_id_reuse_policy).to_proto, + workflow_id_reuse_policy: Temporal::Connection::Serializer::WorkflowIdReusePolicy.new(object.workflow_id_reuse_policy, converter).to_proto, search_attributes: serialize_search_attributes(object.search_attributes), ) ) @@ -44,13 +41,13 @@ def to_proto def serialize_headers(headers) return unless headers - Temporalio::Api::Common::V1::Header.new(fields: to_payload_map(headers)) + Temporalio::Api::Common::V1::Header.new(fields: converter.to_payload_map(headers)) end def serialize_memo(memo) return unless memo - Temporalio::Api::Common::V1::Memo.new(fields: to_payload_map(memo)) + Temporalio::Api::Common::V1::Memo.new(fields: converter.to_payload_map(memo)) end def serialize_parent_close_policy(parent_close_policy) @@ -66,7 +63,7 @@ def serialize_parent_close_policy(parent_close_policy) def serialize_search_attributes(search_attributes) return unless search_attributes - Temporalio::Api::Common::V1::SearchAttributes.new(indexed_fields: to_payload_map_without_codec(search_attributes)) + Temporalio::Api::Common::V1::SearchAttributes.new(indexed_fields: converter.to_payload_map_without_codec(search_attributes)) end end end diff --git a/lib/temporal/connection/serializer/upsert_search_attributes.rb b/lib/temporal/connection/serializer/upsert_search_attributes.rb index e8aa652c..b1b0395a 100644 --- a/lib/temporal/connection/serializer/upsert_search_attributes.rb +++ b/lib/temporal/connection/serializer/upsert_search_attributes.rb @@ -1,19 +1,16 @@ require 'temporal/connection/serializer/base' -require 'temporal/concerns/payloads' module Temporal module Connection module Serializer class UpsertSearchAttributes < Base - include Concerns::Payloads - def to_proto Temporalio::Api::Command::V1::Command.new( command_type: Temporalio::Api::Enums::V1::CommandType::COMMAND_TYPE_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES, upsert_workflow_search_attributes_command_attributes: Temporalio::Api::Command::V1::UpsertWorkflowSearchAttributesCommandAttributes.new( search_attributes: Temporalio::Api::Common::V1::SearchAttributes.new( - indexed_fields: to_payload_map_without_codec(object.search_attributes || {}) + indexed_fields: converter.to_payload_map_without_codec(object.search_attributes || {}) ), ) ) diff --git a/lib/temporal/converter_wrapper.rb b/lib/temporal/converter_wrapper.rb new file mode 100644 index 00000000..a14e2abf --- /dev/null +++ b/lib/temporal/converter_wrapper.rb @@ -0,0 +1,87 @@ +# This class provides convenience methods for accessing the converter/codec. It is fully backwards +# compatible with Temporal::Connection::Converter::Base interface, however it adds new convenience +# methods specific to different conversion scenarios. + +module Temporal + class ConverterWrapper + def initialize(converter, codec) + @converter = converter + @codec = codec + end + + def from_payloads(payloads) + payloads = codec.decodes(payloads) + converter.from_payloads(payloads) + end + + def from_payload(payload) + payload = codec.decode(payload) + converter.from_payload(payload) + end + + def from_payload_map_without_codec(payload_map) + payload_map.map { |key, value| [key, converter.from_payload(value)] }.to_h + end + + def from_result_payloads(payloads) + from_payloads(payloads)&.first + end + + def from_details_payloads(payloads) + from_payloads(payloads)&.first + end + + def from_signal_payloads(payloads) + from_payloads(payloads)&.first + end + + def from_query_payloads(payloads) + from_payloads(payloads)&.first + end + + def from_payload_map(payload_map) + payload_map.map { |key, value| [key, from_payload(value)] }.to_h + end + + def to_payloads(data) + payloads = converter.to_payloads(data) + codec.encodes(payloads) + end + + def to_payload(data) + payload = converter.to_payload(data) + codec.encode(payload) + end + + def to_payload_map_without_codec(data) + # skips the codec step because search attributes don't use this pipeline + data.transform_values do |value| + converter.to_payload(value) + end + end + + def to_result_payloads(data) + to_payloads([data]) + end + + def to_details_payloads(data) + to_payloads([data]) + end + + def to_signal_payloads(data) + to_payloads([data]) + end + + def to_query_payloads(data) + to_payloads([data]) + end + + def to_payload_map(data) + data.transform_values(&method(:to_payload)) + end + + private + + attr_reader :converter, :codec + end +end diff --git a/lib/temporal/metadata.rb b/lib/temporal/metadata.rb index 7be46b31..5439029f 100644 --- a/lib/temporal/metadata.rb +++ b/lib/temporal/metadata.rb @@ -2,15 +2,12 @@ require 'temporal/metadata/activity' require 'temporal/metadata/workflow' require 'temporal/metadata/workflow_task' -require 'temporal/concerns/payloads' module Temporal module Metadata class << self - include Concerns::Payloads - - def generate_activity_metadata(task, namespace) + def generate_activity_metadata(task, namespace, converter) Metadata::Activity.new( namespace: namespace, id: task.activity_id, @@ -20,8 +17,8 @@ def generate_activity_metadata(task, namespace) workflow_run_id: task.workflow_execution.run_id, workflow_id: task.workflow_execution.workflow_id, workflow_name: task.workflow_type.name, - headers: from_payload_map(task.header&.fields || {}), - heartbeat_details: from_details_payloads(task.heartbeat_details), + headers: converter.from_payload_map(task.header&.fields || {}), + heartbeat_details: converter.from_details_payloads(task.heartbeat_details), scheduled_at: task.scheduled_time.to_time, current_attempt_scheduled_at: task.current_attempt_scheduled_time.to_time, heartbeat_timeout: task.heartbeat_timeout.seconds @@ -44,7 +41,7 @@ def generate_workflow_task_metadata(task, namespace) # @param event [Temporal::Workflow::History::Event] Workflow started history event # @param task_metadata [Temporal::Metadata::WorkflowTask] workflow task metadata - def generate_workflow_metadata(event, task_metadata) + def generate_workflow_metadata(event, task_metadata, converter) Metadata::Workflow.new( name: event.attributes.workflow_type.name, id: task_metadata.workflow_id, @@ -54,9 +51,9 @@ def generate_workflow_metadata(event, task_metadata) attempt: event.attributes.attempt, namespace: task_metadata.namespace, task_queue: event.attributes.task_queue.name, - headers: from_payload_map(event.attributes.header&.fields || {}), + headers: converter.from_payload_map(event.attributes.header&.fields || {}), run_started_at: event.timestamp, - memo: from_payload_map(event.attributes.memo&.fields || {}), + memo: converter.from_payload_map(event.attributes.memo&.fields || {}), ) end end diff --git a/lib/temporal/workflow/errors.rb b/lib/temporal/workflow/errors.rb index 42157376..832c2ac3 100644 --- a/lib/temporal/workflow/errors.rb +++ b/lib/temporal/workflow/errors.rb @@ -3,11 +3,9 @@ module Temporal class Workflow class Errors - extend Concerns::Payloads - # Convert a failure returned from the server to an Error to raise to the client # failure: Temporalio::Api::Failure::V1::Failure - def self.generate_error(failure, default_exception_class = StandardError) + def self.generate_error(failure, converter, default_exception_class = StandardError) case failure.failure_info when :application_failure_info @@ -25,7 +23,7 @@ def self.generate_error(failure, default_exception_class = StandardError) end begin details = failure.application_failure_info.details - exception_or_message = from_details_payloads(details) + exception_or_message = converter.from_details_payloads(details) # v1 serialization only supports StandardErrors with a single "message" argument. # v2 serialization supports complex errors using our converters to serialize them. # enable v2 serialization in activities with Temporal.configuration.use_error_serialization_v2 @@ -59,7 +57,7 @@ def self.generate_error(failure, default_exception_class = StandardError) TimeoutError.new("Timeout type: #{failure.timeout_failure_info.timeout_type.to_s}") when :canceled_failure_info # TODO: Distinguish between different entity cancellations - StandardError.new(from_payloads(failure.canceled_failure_info.details)) + StandardError.new(converter.from_payloads(failure.canceled_failure_info.details)) else StandardError.new(failure.message) end diff --git a/lib/temporal/workflow/execution_info.rb b/lib/temporal/workflow/execution_info.rb index e3f70021..77a27332 100644 --- a/lib/temporal/workflow/execution_info.rb +++ b/lib/temporal/workflow/execution_info.rb @@ -1,12 +1,9 @@ -require 'temporal/concerns/payloads' require 'temporal/workflow/status' module Temporal class Workflow class ExecutionInfo < Struct.new(:workflow, :workflow_id, :run_id, :start_time, :close_time, :status, :history_length, :memo, :search_attributes, keyword_init: true) - extend Concerns::Payloads - STATUSES = [ Temporal::Workflow::Status::RUNNING, Temporal::Workflow::Status::COMPLETED, @@ -17,8 +14,8 @@ class ExecutionInfo < Struct.new(:workflow, :workflow_id, :run_id, :start_time, Temporal::Workflow::Status::TIMED_OUT ] - def self.generate_from(response) - search_attributes = response.search_attributes.nil? ? {} : from_payload_map_without_codec(response.search_attributes.indexed_fields) + def self.generate_from(response, converter) + search_attributes = response.search_attributes.nil? ? {} : converter.from_payload_map_without_codec(response.search_attributes.indexed_fields) new( workflow: response.type.name, workflow_id: response.execution.workflow_id, @@ -27,7 +24,7 @@ def self.generate_from(response) close_time: response.close_time&.to_time, status: Temporal::Workflow::Status::API_STATUS_MAP.fetch(response.status), history_length: response.history_length, - memo: from_payload_map(response.memo.fields), + memo: converter.from_payload_map(response.memo.fields), search_attributes: search_attributes ).freeze end diff --git a/lib/temporal/workflow/executions.rb b/lib/temporal/workflow/executions.rb index 83079b1a..15fb9109 100644 --- a/lib/temporal/workflow/executions.rb +++ b/lib/temporal/workflow/executions.rb @@ -9,7 +9,8 @@ class Executions next_page_token: nil }.freeze - def initialize(connection:, status:, request_options:) + def initialize(converter, connection:, status:, request_options:) + @converter = converter @connection = connection @status = status @request_options = DEFAULT_REQUEST_OPTIONS.merge(request_options) @@ -20,7 +21,7 @@ def next_page_token end def next_page - self.class.new(connection: @connection, status: @status, request_options: @request_options.merge(next_page_token: next_page_token)) + self.class.new(@converter, connection: @connection, status: @status, request_options: @request_options.merge(next_page_token: next_page_token)) end def each @@ -42,7 +43,7 @@ def each ) paginated_executions = response.executions.map do |raw_execution| - execution = Temporal::Workflow::ExecutionInfo.generate_from(raw_execution) + execution = Temporal::Workflow::ExecutionInfo.generate_from(raw_execution, @converter) if block_given? yield execution end diff --git a/lib/temporal/workflow/executor.rb b/lib/temporal/workflow/executor.rb index 6233feb0..f40fef3b 100644 --- a/lib/temporal/workflow/executor.rb +++ b/lib/temporal/workflow/executor.rb @@ -71,7 +71,7 @@ def process_query(query) end def execute_workflow(input, workflow_started_event) - metadata = Metadata.generate_workflow_metadata(workflow_started_event, task_metadata) + metadata = Metadata.generate_workflow_metadata(workflow_started_event, task_metadata, config.converter) context = Workflow::Context.new(state_manager, dispatcher, workflow_class, metadata, config, query_registry, track_stack_trace) diff --git a/lib/temporal/workflow/state_manager.rb b/lib/temporal/workflow/state_manager.rb index f8c5cc64..c90ed3de 100644 --- a/lib/temporal/workflow/state_manager.rb +++ b/lib/temporal/workflow/state_manager.rb @@ -4,7 +4,6 @@ require 'temporal/workflow/command_state_machine' require 'temporal/workflow/history/event_target' require 'temporal/workflow/history/size' -require 'temporal/concerns/payloads' require 'temporal/workflow/errors' require 'temporal/workflow/sdk_flags' require 'temporal/workflow/signal' @@ -12,8 +11,6 @@ module Temporal class Workflow class StateManager - include Concerns::Payloads - SIDE_EFFECT_MARKER = 'SIDE_EFFECT'.freeze RELEASE_MARKER = 'RELEASE'.freeze @@ -34,6 +31,7 @@ def initialize(dispatcher, config) @replay = false @search_attributes = {} @config = config + @converter = config.converter # Current flags in use, built up from workflow task completed history entries @sdk_flags = Set.new @@ -167,7 +165,7 @@ def history_size private - attr_reader :commands, :dispatcher, :command_tracker, :marker_ids, :side_effects, :releases, :config + attr_reader :commands, :dispatcher, :command_tracker, :marker_ids, :side_effects, :releases, :config, :converter def use_signals_first(raw_events) # The presence of SAVE_FIRST_TASK_SIGNALS implies HANDLE_SIGNALS_FIRST @@ -250,14 +248,14 @@ def apply_event(event) case event.type when 'WORKFLOW_EXECUTION_STARTED' unless event.attributes.search_attributes.nil? - search_attributes.merge!(from_payload_map(event.attributes.search_attributes&.indexed_fields || {})) + search_attributes.merge!(converter.from_payload_map(event.attributes.search_attributes&.indexed_fields || {})) end state_machine.start dispatch( History::EventTarget.start_workflow, 'started', - from_payloads(event.attributes.input), + converter.from_payloads(event.attributes.input), event ) @@ -296,16 +294,16 @@ def apply_event(event) when 'ACTIVITY_TASK_COMPLETED' state_machine.complete - dispatch(history_target, 'completed', from_result_payloads(event.attributes.result)) + dispatch(history_target, 'completed', converter.from_result_payloads(event.attributes.result)) when 'ACTIVITY_TASK_FAILED' state_machine.fail dispatch(history_target, 'failed', - Temporal::Workflow::Errors.generate_error(event.attributes.failure, ActivityException)) + Temporal::Workflow::Errors.generate_error(event.attributes.failure, converter, ActivityException)) when 'ACTIVITY_TASK_TIMED_OUT' state_machine.time_out - dispatch(history_target, 'failed', Temporal::Workflow::Errors.generate_error(event.attributes.failure)) + dispatch(history_target, 'failed', Temporal::Workflow::Errors.generate_error(event.attributes.failure, converter)) when 'ACTIVITY_TASK_CANCEL_REQUESTED' state_machine.requested @@ -319,7 +317,7 @@ def apply_event(event) when 'ACTIVITY_TASK_CANCELED' state_machine.cancel dispatch(history_target, 'failed', - Temporal::ActivityCanceled.new(from_details_payloads(event.attributes.details))) + Temporal::ActivityCanceled.new(converter.from_details_payloads(event.attributes.details))) when 'TIMER_STARTED' state_machine.start @@ -356,13 +354,13 @@ def apply_event(event) when 'MARKER_RECORDED' state_machine.complete - handle_marker(event.id, event.attributes.marker_name, from_details_payloads(event.attributes.details['data'])) + handle_marker(event.id, event.attributes.marker_name, converter.from_details_payloads(event.attributes.details['data'])) when 'WORKFLOW_EXECUTION_SIGNALED' # relies on Signal#== for matching in Dispatcher signal_target = Signal.new(event.attributes.signal_name) dispatch(signal_target, 'signaled', event.attributes.signal_name, - from_signal_payloads(event.attributes.input)) + converter.from_signal_payloads(event.attributes.input)) when 'WORKFLOW_EXECUTION_TERMINATED' # todo @@ -388,15 +386,15 @@ def apply_event(event) when 'CHILD_WORKFLOW_EXECUTION_COMPLETED' state_machine.complete - dispatch(history_target, 'completed', from_result_payloads(event.attributes.result)) + dispatch(history_target, 'completed', converter.from_result_payloads(event.attributes.result)) when 'CHILD_WORKFLOW_EXECUTION_FAILED' state_machine.fail - dispatch(history_target, 'failed', Temporal::Workflow::Errors.generate_error(event.attributes.failure)) + dispatch(history_target, 'failed', Temporal::Workflow::Errors.generate_error(event.attributes.failure, converter)) when 'CHILD_WORKFLOW_EXECUTION_CANCELED' state_machine.cancel - dispatch(history_target, 'failed', Temporal::Workflow::Errors.generate_error(event.attributes.failure)) + dispatch(history_target, 'failed', Temporal::Workflow::Errors.generate_error(event.attributes.failure, converter)) when 'CHILD_WORKFLOW_EXECUTION_TIMED_OUT' state_machine.time_out @@ -427,7 +425,7 @@ def apply_event(event) dispatch(history_target, 'completed') when 'UPSERT_WORKFLOW_SEARCH_ATTRIBUTES' - search_attributes.merge!(from_payload_map(event.attributes.search_attributes&.indexed_fields || {})) + search_attributes.merge!(converter.from_payload_map(event.attributes.search_attributes&.indexed_fields || {})) # no need to track state; this is just a synchronous API call. discard_command(history_target) diff --git a/lib/temporal/workflow/task_processor.rb b/lib/temporal/workflow/task_processor.rb index f415aed5..b3620ad8 100644 --- a/lib/temporal/workflow/task_processor.rb +++ b/lib/temporal/workflow/task_processor.rb @@ -9,15 +9,13 @@ module Temporal class Workflow class TaskProcessor - Query = Struct.new(:query) do - include Concerns::Payloads - + Query = Struct.new(:query, :converter) do def query_type query.query_type end def query_args - from_query_payloads(query.query_args) + converter.from_query_payloads(query.query_args) end end @@ -125,10 +123,10 @@ def legacy_query_task? def parse_queries # Support for deprecated query style if legacy_query_task? - { LEGACY_QUERY_KEY => Query.new(task.query) } + { LEGACY_QUERY_KEY => Query.new(task.query, config.converter) } else task.queries.each_with_object({}) do |(query_id, query), result| - result[query_id] = Query.new(query) + result[query_id] = Query.new(query, config.converter) end end end diff --git a/spec/config/test_converter.rb b/spec/config/test_converter.rb new file mode 100644 index 00000000..6cb9fce5 --- /dev/null +++ b/spec/config/test_converter.rb @@ -0,0 +1,8 @@ +require 'temporal/converter_wrapper' + +# This is a barebones default converter that can be used in tests +# where default conversion behaviour is expected +TEST_CONVERTER = Temporal::ConverterWrapper.new( + Temporal::Configuration::DEFAULT_CONVERTER, + Temporal::Configuration::DEFAULT_PAYLOAD_CODEC +).freeze diff --git a/spec/fabricators/grpc/activity_task_fabricator.rb b/spec/fabricators/grpc/activity_task_fabricator.rb index 6d2a531d..82e0886f 100644 --- a/spec/fabricators/grpc/activity_task_fabricator.rb +++ b/spec/fabricators/grpc/activity_task_fabricator.rb @@ -6,7 +6,7 @@ activity_id { SecureRandom.uuid } task_token { |attrs| attrs[:task_token] || SecureRandom.uuid } activity_type { Fabricate(:api_activity_type) } - input { Temporal.configuration.converter.to_payloads(nil) } + input { TEST_CONVERTER.to_payloads(nil) } workflow_type { Fabricate(:api_workflow_type) } workflow_execution { Fabricate(:api_workflow_execution) } current_attempt_scheduled_time { Google::Protobuf::Timestamp.new.tap { |t| t.from_time(Time.now) } } @@ -15,7 +15,7 @@ current_attempt_scheduled_time { Google::Protobuf::Timestamp.new.tap { |t| t.from_time(Time.now) } } header do |attrs| fields = (attrs[:headers] || {}).each_with_object({}) do |(field, value), h| - h[field] = Temporal.configuration.converter.to_payload(value) + h[field] = TEST_CONVERTER.to_payload(value) end Temporalio::Api::Common::V1::Header.new(fields: fields) end diff --git a/spec/fabricators/grpc/application_failure_fabricator.rb b/spec/fabricators/grpc/application_failure_fabricator.rb index 9d1396d8..95089cb7 100644 --- a/spec/fabricators/grpc/application_failure_fabricator.rb +++ b/spec/fabricators/grpc/application_failure_fabricator.rb @@ -1,7 +1,3 @@ -require 'temporal/concerns/payloads' -class TestDeserializer - include Temporal::Concerns::Payloads -end # Simulates Temporal::Connection::Serializer::Failure Fabricator(:api_application_failure, from: Temporalio::Api::Failure::V1::Failure) do transient :error_class, :backtrace @@ -10,7 +6,7 @@ class TestDeserializer application_failure_info do |attrs| Temporalio::Api::Failure::V1::ApplicationFailureInfo.new( type: attrs[:error_class], - details: TestDeserializer.new.to_details_payloads(attrs[:message]), + details: TEST_CONVERTER.to_details_payloads(attrs[:message]), ) end end diff --git a/spec/fabricators/grpc/history_event_fabricator.rb b/spec/fabricators/grpc/history_event_fabricator.rb index 4562d7ef..ad9a55e8 100644 --- a/spec/fabricators/grpc/history_event_fabricator.rb +++ b/spec/fabricators/grpc/history_event_fabricator.rb @@ -1,11 +1,4 @@ require 'securerandom' -require 'temporal/concerns/payloads' - -class TestSerializer - extend Temporal::Concerns::Payloads -end - -include Temporal::Concerns::Payloads Fabricator(:api_history_event, from: Temporalio::Api::History::V1::HistoryEvent) do event_id { 1 } @@ -17,9 +10,9 @@ class TestSerializer event_type { Temporalio::Api::Enums::V1::EventType::EVENT_TYPE_WORKFLOW_EXECUTION_STARTED } event_time { Time.now } workflow_execution_started_event_attributes do |attrs| - header_fields = to_payload_map(attrs[:headers] || {}) + header_fields = TEST_CONVERTER.to_payload_map(attrs[:headers] || {}) header = Temporalio::Api::Common::V1::Header.new(fields: header_fields) - indexed_fields = attrs[:search_attributes] ? to_payload_map(attrs[:search_attributes]) : nil + indexed_fields = attrs[:search_attributes] ? TEST_CONVERTER.to_payload_map(attrs[:search_attributes]) : nil Temporalio::Api::History::V1::WorkflowExecutionStartedEventAttributes.new( workflow_type: Fabricate(:api_workflow_type), @@ -142,7 +135,7 @@ class TestSerializer event_type { Temporalio::Api::Enums::V1::EventType::EVENT_TYPE_ACTIVITY_TASK_CANCELED } activity_task_canceled_event_attributes do |attrs| Temporalio::Api::History::V1::ActivityTaskCanceledEventAttributes.new( - details: TestSerializer.to_details_payloads('ACTIVITY_ID_NOT_STARTED'), + details: TEST_CONVERTER.to_details_payloads('ACTIVITY_ID_NOT_STARTED'), scheduled_event_id: attrs[:event_id] - 2, started_event_id: nil, identity: 'test-worker@test-host' @@ -197,7 +190,7 @@ class TestSerializer transient :search_attributes event_type { Temporalio::Api::Enums::V1::EventType::EVENT_TYPE_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES } upsert_workflow_search_attributes_event_attributes do |attrs| - indexed_fields = attrs[:search_attributes] ? to_payload_map(attrs[:search_attributes]) : nil + indexed_fields = attrs[:search_attributes] ? TEST_CONVERTER.to_payload_map(attrs[:search_attributes]) : nil Temporalio::Api::History::V1::UpsertWorkflowSearchAttributesEventAttributes.new( workflow_task_completed_event_id: attrs[:event_id] - 1, search_attributes: Temporalio::Api::Common::V1::SearchAttributes.new( @@ -213,7 +206,7 @@ class TestSerializer Temporalio::Api::History::V1::MarkerRecordedEventAttributes.new( workflow_task_completed_event_id: attrs[:event_id] - 1, marker_name: 'SIDE_EFFECT', - details: to_payload_map({}) + details: TEST_CONVERTER.to_payload_map({}) ) end end diff --git a/spec/fabricators/grpc/memo_fabricator.rb b/spec/fabricators/grpc/memo_fabricator.rb index 38f764f2..cf499c8a 100644 --- a/spec/fabricators/grpc/memo_fabricator.rb +++ b/spec/fabricators/grpc/memo_fabricator.rb @@ -1,7 +1,7 @@ Fabricator(:memo, from: Temporalio::Api::Common::V1::Memo) do fields do Google::Protobuf::Map.new(:string, :message, Temporalio::Api::Common::V1::Payload).tap do |m| - m['foo'] = Temporal.configuration.converter.to_payload('bar') + m['foo'] = TEST_CONVERTER.to_payload('bar') end end end diff --git a/spec/fabricators/grpc/payload_fabricator.rb b/spec/fabricators/grpc/payload_fabricator.rb index badd8f36..9312da42 100644 --- a/spec/fabricators/grpc/payload_fabricator.rb +++ b/spec/fabricators/grpc/payload_fabricator.rb @@ -1,3 +1,23 @@ Fabricator(:api_payload, from: Temporalio::Api::Common::V1::Payload) do metadata { Google::Protobuf::Map.new(:string, :bytes) } end + +Fabricator(:api_payload_nil, from: :api_payload) do + metadata do + Google::Protobuf::Map.new(:string, :bytes).tap do |m| + m['encoding'] = Temporal::Connection::Converter::Payload::Nil::ENCODING + end + end +end + +Fabricator(:api_payload_bytes, from: :api_payload) do + transient :bytes + + metadata do + Google::Protobuf::Map.new(:string, :bytes).tap do |m| + m['encoding'] = Temporal::Connection::Converter::Payload::Bytes::ENCODING + end + end + + data { |attrs| attrs.fetch(:bytes, 'foobar') } +end diff --git a/spec/fabricators/grpc/payloads_fabricator.rb b/spec/fabricators/grpc/payloads_fabricator.rb new file mode 100644 index 00000000..a8f3aff0 --- /dev/null +++ b/spec/fabricators/grpc/payloads_fabricator.rb @@ -0,0 +1,9 @@ +Fabricator(:api_payloads, from: Temporalio::Api::Common::V1::Payloads) do + transient :payloads_array + + payloads do |attrs| + Google::Protobuf::RepeatedField.new(:message, Temporalio::Api::Common::V1::Payload).tap do |m| + m.concat(Array(attrs.fetch(:payloads_array, Fabricate(:api_payload)))) + end + end +end diff --git a/spec/fabricators/grpc/search_attributes_fabricator.rb b/spec/fabricators/grpc/search_attributes_fabricator.rb index 16a33675..1e98516e 100644 --- a/spec/fabricators/grpc/search_attributes_fabricator.rb +++ b/spec/fabricators/grpc/search_attributes_fabricator.rb @@ -1,7 +1,7 @@ Fabricator(:search_attributes, from: Temporalio::Api::Common::V1::SearchAttributes) do indexed_fields do Google::Protobuf::Map.new(:string, :message, Temporalio::Api::Common::V1::Payload).tap do |m| - m['foo'] = Temporal.configuration.converter.to_payload('bar') + m['foo'] = TEST_CONVERTER.to_payload('bar') end end end diff --git a/spec/fabricators/grpc/workflow_execution_started_event_attributes_fabricator.rb b/spec/fabricators/grpc/workflow_execution_started_event_attributes_fabricator.rb index 172bd7a5..0c1449fe 100644 --- a/spec/fabricators/grpc/workflow_execution_started_event_attributes_fabricator.rb +++ b/spec/fabricators/grpc/workflow_execution_started_event_attributes_fabricator.rb @@ -12,7 +12,7 @@ task_queue { Fabricate(:api_task_queue) } header do |attrs| fields = (attrs[:headers] || {}).each_with_object({}) do |(field, value), h| - h[field] = Temporal.configuration.converter.to_payload(value) + h[field] = TEST_CONVERTER.to_payload(value) end Temporalio::Api::Common::V1::Header.new(fields: fields) end diff --git a/spec/fabricators/grpc/workflow_query_fabricator.rb b/spec/fabricators/grpc/workflow_query_fabricator.rb index 024cdd59..f8831d49 100644 --- a/spec/fabricators/grpc/workflow_query_fabricator.rb +++ b/spec/fabricators/grpc/workflow_query_fabricator.rb @@ -1,4 +1,4 @@ Fabricator(:api_workflow_query, from: Temporalio::Api::Query::V1::WorkflowQuery) do query_type { 'state' } - query_args { Temporal.configuration.converter.to_payloads(['']) } + query_args { TEST_CONVERTER.to_payloads(['']) } end diff --git a/spec/unit/lib/temporal/activity/task_processor_spec.rb b/spec/unit/lib/temporal/activity/task_processor_spec.rb index e4ccdb2a..6999ba60 100644 --- a/spec/unit/lib/temporal/activity/task_processor_spec.rb +++ b/spec/unit/lib/temporal/activity/task_processor_spec.rb @@ -17,7 +17,7 @@ input: config.converter.to_payloads(input) ) end - let(:metadata) { Temporal::Metadata.generate_activity_metadata(task, namespace) } + let(:metadata) { Temporal::Metadata.generate_activity_metadata(task, namespace, config.converter) } let(:workflow_name) { task.workflow_type.name } let(:activity_name) { 'TestActivity' } let(:connection) { instance_double('Temporal::Connection::GRPC') } @@ -40,7 +40,7 @@ .and_return(connection) allow(Temporal::Metadata) .to receive(:generate_activity_metadata) - .with(task, namespace) + .with(task, namespace, config.converter) .and_return(metadata) allow(Temporal::Activity::Context).to receive(:new).with(connection, metadata, config, heartbeat_thread_pool).and_return(context) diff --git a/spec/unit/lib/temporal/configuration_spec.rb b/spec/unit/lib/temporal/configuration_spec.rb index c1024e34..8ab2e282 100644 --- a/spec/unit/lib/temporal/configuration_spec.rb +++ b/spec/unit/lib/temporal/configuration_spec.rb @@ -62,4 +62,50 @@ def inject!(_); end expect(subject.for_connection).to have_attributes(identity: new_identity) end end -end \ No newline at end of file + + describe '#converter' do + it 'wraps the provided converter and codec' do + converter_wrapper = subject.converter + + expect(converter_wrapper).to be_a(Temporal::ConverterWrapper) + expect(converter_wrapper.send(:converter)).to eq(described_class::DEFAULT_CONVERTER) + expect(converter_wrapper.send(:codec)).to eq(described_class::DEFAULT_PAYLOAD_CODEC) + end + end + + describe '#converter=' do + let(:converter) { instance_double(Temporal::Connection::Converter::Composite) } + + it 'resets the wrapper when converter has changed' do + old_converter_wrapper = subject.converter + + expect(old_converter_wrapper).to be_a(Temporal::ConverterWrapper) + expect(old_converter_wrapper.send(:converter)).to eq(described_class::DEFAULT_CONVERTER) + + subject.converter = converter + new_converter_wrapper = subject.converter + + expect(new_converter_wrapper).to be_a(Temporal::ConverterWrapper) + expect(new_converter_wrapper.send(:converter)).to eq(converter) + expect(new_converter_wrapper.send(:codec)).to eq(old_converter_wrapper.send(:codec)) + end + end + + describe '#payload_codec=' do + let(:codec) { Temporal::Connection::Converter::Codec::Base.new } + + it 'resets the wrapper when converter has changed' do + old_converter_wrapper = subject.converter + + expect(old_converter_wrapper).to be_a(Temporal::ConverterWrapper) + expect(old_converter_wrapper.send(:codec)).to eq(described_class::DEFAULT_PAYLOAD_CODEC) + + subject.payload_codec = codec + new_converter_wrapper = subject.converter + + expect(new_converter_wrapper).to be_a(Temporal::ConverterWrapper) + expect(new_converter_wrapper.send(:codec)).to eq(codec) + expect(new_converter_wrapper.send(:converter)).to eq(old_converter_wrapper.send(:converter)) + end + end +end diff --git a/spec/unit/lib/temporal/connection/serializer/backfill_spec.rb b/spec/unit/lib/temporal/connection/serializer/backfill_spec.rb index e7d980f3..b4505a57 100644 --- a/spec/unit/lib/temporal/connection/serializer/backfill_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/backfill_spec.rb @@ -3,6 +3,12 @@ require "temporal/connection/serializer/backfill" describe Temporal::Connection::Serializer::Backfill do + let(:converter) do + Temporal::ConverterWrapper.new( + Temporal::Configuration::DEFAULT_CONVERTER, + Temporal::Configuration::DEFAULT_PAYLOAD_CODEC + ) + end let(:example_backfill) do Temporal::Schedule::Backfill.new( start_time: Time.new(2000, 1, 1, 0, 0, 0), @@ -15,13 +21,13 @@ it "raises an error if an invalid overlap_policy is specified" do invalid = Temporal::Schedule::Backfill.new(overlap_policy: :foobar) expect do - described_class.new(invalid).to_proto + described_class.new(invalid, converter).to_proto end .to(raise_error(Temporal::Connection::ArgumentError, "Unknown schedule overlap policy specified: foobar")) end it "produces well-formed protobuf" do - result = described_class.new(example_backfill).to_proto + result = described_class.new(example_backfill, converter).to_proto expect(result).to(be_a(Temporalio::Api::Schedule::V1::BackfillRequest)) expect(result.overlap_policy).to(eq(:SCHEDULE_OVERLAP_POLICY_BUFFER_ALL)) diff --git a/spec/unit/lib/temporal/connection/serializer/continue_as_new_spec.rb b/spec/unit/lib/temporal/connection/serializer/continue_as_new_spec.rb index 046b066c..398231da 100644 --- a/spec/unit/lib/temporal/connection/serializer/continue_as_new_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/continue_as_new_spec.rb @@ -2,6 +2,13 @@ require 'temporal/workflow/command' describe Temporal::Connection::Serializer::ContinueAsNew do + let(:converter) do + Temporal::ConverterWrapper.new( + Temporal::Configuration::DEFAULT_CONVERTER, + Temporal::Configuration::DEFAULT_PAYLOAD_CODEC + ) + end + describe 'to_proto' do it 'produces a protobuf' do timeouts = { @@ -19,7 +26,7 @@ search_attributes: {'foo-search-attribute': 'qux'}, ) - result = described_class.new(command).to_proto + result = described_class.new(command, converter).to_proto expect(result).to be_an_instance_of(Temporalio::Api::Command::V1::Command) expect(result.command_type).to eql( diff --git a/spec/unit/lib/temporal/connection/serializer/failure_spec.rb b/spec/unit/lib/temporal/connection/serializer/failure_spec.rb index 4242554e..2bde0337 100644 --- a/spec/unit/lib/temporal/connection/serializer/failure_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/failure_spec.rb @@ -1,14 +1,17 @@ require 'temporal/connection/serializer/failure' require 'temporal/workflow/command' -class TestDeserializer - include Temporal::Concerns::Payloads -end - describe Temporal::Connection::Serializer::Failure do + let(:converter) do + Temporal::ConverterWrapper.new( + Temporal::Configuration::DEFAULT_CONVERTER, + Temporal::Configuration::DEFAULT_PAYLOAD_CODEC + ) + end + describe 'to_proto' do it 'produces a protobuf' do - result = described_class.new(StandardError.new('test')).to_proto + result = described_class.new(StandardError.new('test'), converter).to_proto expect(result).to be_an_instance_of(Temporalio::Api::Failure::V1::Failure) end @@ -31,10 +34,10 @@ def initialize(foo, bar, bad_class:) it 'Serializes round-trippable full errors when asked to' do # Make sure serializing various bits round-trips e = MyError.new(['seven', 'three'], "Bar", bad_class: NaughtyClass) - failure_proto = described_class.new(e, serialize_whole_error: true).to_proto + failure_proto = described_class.new(e, converter, serialize_whole_error: true).to_proto expect(failure_proto.application_failure_info.type).to eq("MyError") - deserialized_error = TestDeserializer.new.from_details_payloads(failure_proto.application_failure_info.details) + deserialized_error = converter.from_details_payloads(failure_proto.application_failure_info.details) expect(deserialized_error).to be_an_instance_of(MyError) expect(deserialized_error.message).to eq("Hello, Bar!") expect(deserialized_error.foo).to eq(['seven', 'three']) @@ -53,23 +56,23 @@ def initialize(message) it 'deals with too-large serialization using the old path' do e = MyBigError.new('Uh oh!') # Normal serialization path - failure_proto = described_class.new(e, serialize_whole_error: true, max_bytes: 1000).to_proto + failure_proto = described_class.new(e, converter, serialize_whole_error: true, max_bytes: 1000).to_proto expect(failure_proto.application_failure_info.type).to eq('MyBigError') - deserialized_error = TestDeserializer.new.from_details_payloads(failure_proto.application_failure_info.details) + deserialized_error = converter.from_details_payloads(failure_proto.application_failure_info.details) expect(deserialized_error).to be_an_instance_of(MyBigError) expect(deserialized_error.big_payload).to eq('123456789012345678901234567890123456789012345678901234567890') # Exercise legacy serialization mechanism - failure_proto = described_class.new(e, serialize_whole_error: false).to_proto + failure_proto = described_class.new(e, converter, serialize_whole_error: false).to_proto expect(failure_proto.application_failure_info.type).to eq('MyBigError') - old_style_deserialized_error = MyBigError.new(TestDeserializer.new.from_details_payloads(failure_proto.application_failure_info.details)) + old_style_deserialized_error = MyBigError.new(converter.from_details_payloads(failure_proto.application_failure_info.details)) expect(old_style_deserialized_error).to be_an_instance_of(MyBigError) expect(old_style_deserialized_error.message).to eq('Uh oh!') # If the payload size exceeds the max_bytes, we fallback to the old-style serialization. - failure_proto = described_class.new(e, serialize_whole_error: true, max_bytes: 50).to_proto + failure_proto = described_class.new(e, converter, serialize_whole_error: true, max_bytes: 50).to_proto expect(failure_proto.application_failure_info.type).to eq('MyBigError') - avoids_truncation_error = MyBigError.new(TestDeserializer.new.from_details_payloads(failure_proto.application_failure_info.details)) + avoids_truncation_error = MyBigError.new(converter.from_details_payloads(failure_proto.application_failure_info.details)) expect(avoids_truncation_error).to be_an_instance_of(MyBigError) expect(avoids_truncation_error.message).to eq('Uh oh!') @@ -82,7 +85,7 @@ def initialize(message) allow(Temporal.logger).to receive(:error) max_bytes = 50 - described_class.new(e, serialize_whole_error: true, max_bytes: max_bytes).to_proto + described_class.new(e, converter, serialize_whole_error: true, max_bytes: max_bytes).to_proto expect(Temporal.logger) .to have_received(:error) .with( @@ -99,7 +102,7 @@ def initialize; end it 'successfully processes an error with no constructor arguments' do e = MyArglessError.new - failure_proto = described_class.new(e, serialize_whole_error: true).to_proto + failure_proto = described_class.new(e, converter, serialize_whole_error: true).to_proto expect(failure_proto.application_failure_info.type).to eq('MyArglessError') end diff --git a/spec/unit/lib/temporal/connection/serializer/query_answer_spec.rb b/spec/unit/lib/temporal/connection/serializer/query_answer_spec.rb index 62028824..5e912206 100644 --- a/spec/unit/lib/temporal/connection/serializer/query_answer_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/query_answer_spec.rb @@ -1,23 +1,25 @@ require 'temporal/connection/serializer/query_failure' require 'temporal/workflow/query_result' -require 'temporal/concerns/payloads' describe Temporal::Connection::Serializer::QueryAnswer do - class TestDeserializer - extend Temporal::Concerns::Payloads + let(:converter) do + Temporal::ConverterWrapper.new( + Temporal::Configuration::DEFAULT_CONVERTER, + Temporal::Configuration::DEFAULT_PAYLOAD_CODEC + ) end describe 'to_proto' do let(:query_result) { Temporal::Workflow::QueryResult.answer(42) } it 'produces a protobuf' do - result = described_class.new(query_result).to_proto + result = described_class.new(query_result, converter).to_proto expect(result).to be_a(Temporalio::Api::Query::V1::WorkflowQueryResult) expect(result.result_type).to eq(Temporalio::Api::Enums::V1::QueryResultType.lookup( Temporalio::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_ANSWERED) ) - expect(result.answer).to eq(TestDeserializer.to_query_payloads(42)) + expect(result.answer).to eq(converter.to_query_payloads(42)) end end end diff --git a/spec/unit/lib/temporal/connection/serializer/query_failure_spec.rb b/spec/unit/lib/temporal/connection/serializer/query_failure_spec.rb index 0590c0c4..62926aea 100644 --- a/spec/unit/lib/temporal/connection/serializer/query_failure_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/query_failure_spec.rb @@ -2,12 +2,19 @@ require 'temporal/workflow/query_result' describe Temporal::Connection::Serializer::QueryFailure do + let(:converter) do + Temporal::ConverterWrapper.new( + Temporal::Configuration::DEFAULT_CONVERTER, + Temporal::Configuration::DEFAULT_PAYLOAD_CODEC + ) + end + describe 'to_proto' do let(:exception) { StandardError.new('Test query failure') } let(:query_result) { Temporal::Workflow::QueryResult.failure(exception) } it 'produces a protobuf' do - result = described_class.new(query_result).to_proto + result = described_class.new(query_result, converter).to_proto expect(result).to be_a(Temporalio::Api::Query::V1::WorkflowQueryResult) expect(result.result_type).to eq(Temporalio::Api::Enums::V1::QueryResultType.lookup( diff --git a/spec/unit/lib/temporal/connection/serializer/retry_policy_spec.rb b/spec/unit/lib/temporal/connection/serializer/retry_policy_spec.rb index 211f807f..5e27503f 100644 --- a/spec/unit/lib/temporal/connection/serializer/retry_policy_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/retry_policy_spec.rb @@ -2,6 +2,13 @@ require 'temporal/connection/serializer/retry_policy' describe Temporal::Connection::Serializer::RetryPolicy do + let(:converter) do + Temporal::ConverterWrapper.new( + Temporal::Configuration::DEFAULT_CONVERTER, + Temporal::Configuration::DEFAULT_PAYLOAD_CODEC + ) + end + describe 'to_proto' do let(:example_policy) do Temporal::RetryPolicy.new( @@ -14,7 +21,7 @@ end it 'converts to proto' do - proto = described_class.new(example_policy).to_proto + proto = described_class.new(example_policy, converter).to_proto expect(proto.initial_interval.seconds).to eq(1) expect(proto.backoff_coefficient).to eq(1.5) expect(proto.maximum_interval.seconds).to eq(5) diff --git a/spec/unit/lib/temporal/connection/serializer/schedule_action_spec.rb b/spec/unit/lib/temporal/connection/serializer/schedule_action_spec.rb index 275bb8e0..93f9e87c 100644 --- a/spec/unit/lib/temporal/connection/serializer/schedule_action_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/schedule_action_spec.rb @@ -3,6 +3,12 @@ require "temporal/connection/serializer/schedule_action" describe Temporal::Connection::Serializer::ScheduleAction do + let(:converter) do + Temporal::ConverterWrapper.new( + Temporal::Configuration::DEFAULT_CONVERTER, + Temporal::Configuration::DEFAULT_PAYLOAD_CODEC + ) + end let(:timeouts) { {run: 100, task: 10} } let(:example_action) do @@ -24,7 +30,7 @@ describe "to_proto" do it "raises an error if an invalid action is specified" do expect do - described_class.new(123).to_proto + described_class.new(123, converter).to_proto end .to(raise_error(Temporal::Connection::ArgumentError)) do |e| expect(e.message).to(eq("Unknown action type Integer")) @@ -32,7 +38,7 @@ end it "produces well-formed protobuf" do - result = described_class.new(example_action).to_proto + result = described_class.new(example_action, converter).to_proto expect(result).to(be_a(Temporalio::Api::Schedule::V1::ScheduleAction)) diff --git a/spec/unit/lib/temporal/connection/serializer/schedule_policies_spec.rb b/spec/unit/lib/temporal/connection/serializer/schedule_policies_spec.rb index cf64ed98..2b51cee3 100644 --- a/spec/unit/lib/temporal/connection/serializer/schedule_policies_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/schedule_policies_spec.rb @@ -2,6 +2,12 @@ require "temporal/connection/serializer/schedule_policies" describe Temporal::Connection::Serializer::SchedulePolicies do + let(:converter) do + Temporal::ConverterWrapper.new( + Temporal::Configuration::DEFAULT_CONVERTER, + Temporal::Configuration::DEFAULT_PAYLOAD_CODEC + ) + end let(:example_policies) do Temporal::Schedule::SchedulePolicies.new( overlap_policy: :buffer_one, @@ -12,7 +18,7 @@ describe "to_proto" do it "produces well-formed protobuf" do - result = described_class.new(example_policies).to_proto + result = described_class.new(example_policies, converter).to_proto expect(result).to(be_a(Temporalio::Api::Schedule::V1::SchedulePolicies)) expect(result.overlap_policy).to(eq(:SCHEDULE_OVERLAP_POLICY_BUFFER_ONE)) @@ -23,7 +29,7 @@ it "should raise if an unknown overlap policy is specified" do invalid_policies = Temporal::Schedule::SchedulePolicies.new(overlap_policy: :foobar) expect do - described_class.new(invalid_policies).to_proto + described_class.new(invalid_policies, converter).to_proto end .to(raise_error(Temporal::Connection::ArgumentError, "Unknown schedule overlap policy specified: foobar")) end diff --git a/spec/unit/lib/temporal/connection/serializer/schedule_spec_spec.rb b/spec/unit/lib/temporal/connection/serializer/schedule_spec_spec.rb index c0aa636f..ee0cd0f8 100644 --- a/spec/unit/lib/temporal/connection/serializer/schedule_spec_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/schedule_spec_spec.rb @@ -4,6 +4,12 @@ require "temporal/connection/serializer/schedule_spec" describe Temporal::Connection::Serializer::ScheduleSpec do + let(:converter) do + Temporal::ConverterWrapper.new( + Temporal::Configuration::DEFAULT_CONVERTER, + Temporal::Configuration::DEFAULT_PAYLOAD_CODEC + ) + end let(:example_spec) do Temporal::Schedule::ScheduleSpec.new( cron_expressions: ["@hourly"], @@ -33,7 +39,7 @@ describe "to_proto" do it "produces well-formed protobuf" do - result = described_class.new(example_spec).to_proto + result = described_class.new(example_spec, converter).to_proto expect(result).to(be_a(Temporalio::Api::Schedule::V1::ScheduleSpec)) expect(result.cron_string).to(eq(["@hourly"])) diff --git a/spec/unit/lib/temporal/connection/serializer/schedule_state_spec.rb b/spec/unit/lib/temporal/connection/serializer/schedule_state_spec.rb index 16c47732..3fbe8051 100644 --- a/spec/unit/lib/temporal/connection/serializer/schedule_state_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/schedule_state_spec.rb @@ -2,6 +2,12 @@ require "temporal/connection/serializer/schedule_state" describe Temporal::Connection::Serializer::ScheduleState do + let(:converter) do + Temporal::ConverterWrapper.new( + Temporal::Configuration::DEFAULT_CONVERTER, + Temporal::Configuration::DEFAULT_PAYLOAD_CODEC + ) + end let(:example_state) do Temporal::Schedule::ScheduleState.new( notes: "some notes", @@ -13,7 +19,7 @@ describe "to_proto" do it "produces well-formed protobuf" do - result = described_class.new(example_state).to_proto + result = described_class.new(example_state, converter).to_proto expect(result).to(be_a(Temporalio::Api::Schedule::V1::ScheduleState)) expect(result.notes).to(eq("some notes")) diff --git a/spec/unit/lib/temporal/connection/serializer/start_child_workflow_spec.rb b/spec/unit/lib/temporal/connection/serializer/start_child_workflow_spec.rb index 2e72951c..ae26f88f 100644 --- a/spec/unit/lib/temporal/connection/serializer/start_child_workflow_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/start_child_workflow_spec.rb @@ -3,6 +3,12 @@ require 'temporal/connection/serializer/start_child_workflow' describe Temporal::Connection::Serializer::StartChildWorkflow do + let(:converter) do + Temporal::ConverterWrapper.new( + Temporal::Configuration::DEFAULT_CONVERTER, + Temporal::Configuration::DEFAULT_PAYLOAD_CODEC + ) + end let(:example_command) do Temporal::Workflow::Command::StartChildWorkflow.new( workflow_id: SecureRandom.uuid, @@ -24,7 +30,7 @@ command.parent_close_policy = :invalid expect do - described_class.new(command).to_proto + described_class.new(command, converter).to_proto end.to raise_error(Temporal::Connection::ArgumentError) do |e| expect(e.message).to eq("Unknown parent_close_policy '#{command.parent_close_policy}' specified") end @@ -40,7 +46,7 @@ command = example_command command.parent_close_policy = policy_name - result = described_class.new(command).to_proto + result = described_class.new(command, converter).to_proto attribs = result.start_child_workflow_execution_command_attributes expect(attribs.parent_close_policy).to eq(expected_parent_close_policy) end diff --git a/spec/unit/lib/temporal/connection/serializer/upsert_search_attributes_spec.rb b/spec/unit/lib/temporal/connection/serializer/upsert_search_attributes_spec.rb index bc94128f..5bdace1a 100644 --- a/spec/unit/lib/temporal/connection/serializer/upsert_search_attributes_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/upsert_search_attributes_spec.rb @@ -3,11 +3,14 @@ require 'temporal/connection/serializer/upsert_search_attributes' require 'temporal/workflow/command' -class TestDeserializer - extend Temporal::Concerns::Payloads -end - describe Temporal::Connection::Serializer::UpsertSearchAttributes do + let(:converter) do + Temporal::ConverterWrapper.new( + Temporal::Configuration::DEFAULT_CONVERTER, + Temporal::Configuration::DEFAULT_PAYLOAD_CODEC + ) + end + it 'produces a protobuf that round-trips' do expected_attributes = { 'CustomStringField' => 'moo', @@ -22,14 +25,14 @@ class TestDeserializer search_attributes: expected_attributes ) - result = described_class.new(command).to_proto + result = described_class.new(command, converter).to_proto expect(result).to be_an_instance_of(Temporalio::Api::Command::V1::Command) expect(result.command_type).to eql( :COMMAND_TYPE_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES ) command_attributes = result.upsert_workflow_search_attributes_command_attributes expect(command_attributes).not_to be_nil - actual_attributes = TestDeserializer.from_payload_map_without_codec(command_attributes&.search_attributes&.indexed_fields) + actual_attributes = converter.from_payload_map_without_codec(command_attributes&.search_attributes&.indexed_fields) expect(actual_attributes).to eql(expected_attributes) end diff --git a/spec/unit/lib/temporal/connection/serializer/workflow_id_reuse_policy_spec.rb b/spec/unit/lib/temporal/connection/serializer/workflow_id_reuse_policy_spec.rb index ce139325..b1ee6cad 100644 --- a/spec/unit/lib/temporal/connection/serializer/workflow_id_reuse_policy_spec.rb +++ b/spec/unit/lib/temporal/connection/serializer/workflow_id_reuse_policy_spec.rb @@ -2,6 +2,13 @@ require 'temporal/connection/serializer/retry_policy' describe Temporal::Connection::Serializer::WorkflowIdReusePolicy do + let(:converter) do + Temporal::ConverterWrapper.new( + Temporal::Configuration::DEFAULT_CONVERTER, + Temporal::Configuration::DEFAULT_PAYLOAD_CODEC + ) + end + describe 'to_proto' do SYM_TO_PROTO = { allow_failed: Temporalio::Api::Enums::V1::WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY, @@ -12,7 +19,7 @@ def self.test_valid_policy(policy_sym) it "serializes #{policy_sym}" do - proto_enum = described_class.new(policy_sym).to_proto + proto_enum = described_class.new(policy_sym, converter).to_proto expected = SYM_TO_PROTO[policy_sym] expect(proto_enum).to eq(expected) end @@ -25,7 +32,7 @@ def self.test_valid_policy(policy_sym) it "rejects invalid policies" do expect do - described_class.new(:not_a_valid_policy).to_proto + described_class.new(:not_a_valid_policy, converter).to_proto end.to raise_error(Temporal::Connection::ArgumentError, 'Unknown workflow_id_reuse_policy specified: not_a_valid_policy') end end diff --git a/spec/unit/lib/temporal/connection_spec.rb b/spec/unit/lib/temporal/connection_spec.rb index f334d6d8..a3e5642f 100644 --- a/spec/unit/lib/temporal/connection_spec.rb +++ b/spec/unit/lib/temporal/connection_spec.rb @@ -25,6 +25,7 @@ expect(subject).to be_kind_of(Temporal::Connection::GRPC) expect(subject.send(:identity)).not_to be_nil expect(subject.send(:credentials)).to eq(:this_channel_is_insecure) + expect(subject.send(:converter)).to eq(config.converter) end end @@ -35,6 +36,7 @@ expect(subject).to be_kind_of(Temporal::Connection::GRPC) expect(subject.send(:identity)).not_to be_nil expect(subject.send(:credentials)).to be_kind_of(GRPC::Core::ChannelCredentials) + expect(subject.send(:converter)).to eq(config.converter) end end @@ -45,6 +47,7 @@ expect(subject).to be_kind_of(Temporal::Connection::GRPC) expect(subject.send(:identity)).not_to be_nil expect(subject.send(:credentials)).to be_kind_of(GRPC::Core::CallCredentials) + expect(subject.send(:converter)).to eq(config.converter) end end @@ -61,6 +64,7 @@ expect(subject).to be_kind_of(Temporal::Connection::GRPC) expect(subject.send(:identity)).not_to be_nil expect(subject.send(:credentials)).to be_kind_of(GRPC::Core::ChannelCredentials) + expect(subject.send(:converter)).to eq(config.converter) end end end diff --git a/spec/unit/lib/temporal/converter_wrapper_spec.rb b/spec/unit/lib/temporal/converter_wrapper_spec.rb new file mode 100644 index 00000000..f5b06af4 --- /dev/null +++ b/spec/unit/lib/temporal/converter_wrapper_spec.rb @@ -0,0 +1,175 @@ +require 'temporal/converter_wrapper' +require 'temporal/connection/converter/payload/bytes' +require 'temporal/connection/converter/payload/nil' +require 'temporal/connection/converter/composite' + +describe Temporal::ConverterWrapper do + class TestCodec < Temporal::Connection::Converter::Codec::Base + def encode(payload) + return payload + end + + def decode(payload) + return payload + end + end + + subject { described_class.new(converter, codec) } + let(:converter) do + Temporal::Connection::Converter::Composite.new(payload_converters: [ + Temporal::Connection::Converter::Payload::Bytes.new, + Temporal::Connection::Converter::Payload::Nil.new + ]) + end + let(:codec) { Temporal::Connection::Converter::Codec::Chain.new(payload_codecs: [TestCodec.new]) } + let(:payloads) { Fabricate(:api_payloads, payloads_array: [payload_bytes, payload_nil]) } + let(:payload_bytes) { Fabricate(:api_payload_bytes, bytes: 'test-payload') } + let(:payload_nil) { Fabricate(:api_payload_nil) } + + before do + allow(codec).to receive(:encode).and_call_original + allow(codec).to receive(:encodes).and_call_original + allow(codec).to receive(:decode).and_call_original + allow(codec).to receive(:decodes).and_call_original + end + + describe '#from_payloads' do + it 'decodes and converts' do + expect(subject.from_payloads(payloads)).to eq(['test-payload', nil]) + expect(codec).to have_received(:decodes) + end + end + + describe '#from_payload' do + it 'decodes and converts' do + expect(subject.from_payload(payload_bytes)).to eq('test-payload') + expect(codec).to have_received(:decode) + end + end + + describe '#from_payload_map_without_codec' do + let(:payload_map) do + Google::Protobuf::Map.new(:string, :message, Temporalio::Api::Common::V1::Payload).tap do |m| + m['first'] = payload_bytes + m['second'] = payload_nil + end + end + + it 'converts' do + expect(subject.from_payload_map_without_codec(payload_map)) + .to eq('first' => 'test-payload', 'second' => nil) + expect(codec).not_to have_received(:decode) + end + end + + describe '#from_result_payloads' do + it 'decodes and converts' do + expect(subject.from_result_payloads(payloads)).to eq('test-payload') + expect(codec).to have_received(:decodes) + end + end + + describe '#from_details_payloads' do + it 'decodes and converts first payload' do + expect(subject.from_details_payloads(payloads)).to eq('test-payload') + expect(codec).to have_received(:decodes) + end + end + + describe '#from_signal_payloads' do + it 'decodes and converts first payload' do + expect(subject.from_signal_payloads(payloads)).to eq('test-payload') + expect(codec).to have_received(:decodes) + end + end + + describe '#from_query_payloads' do + it 'decodes and converts first payload' do + expect(subject.from_query_payloads(payloads)).to eq('test-payload') + expect(codec).to have_received(:decodes) + end + end + + describe '#from_payload_map' do + let(:payload_map) do + Google::Protobuf::Map.new(:string, :message, Temporalio::Api::Common::V1::Payload).tap do |m| + m['first'] = payload_bytes + m['second'] = payload_nil + end + end + + it 'decodes and converts first payload' do + expect(subject.from_payload_map(payload_map)) + .to eq('first' => 'test-payload', 'second' => nil) + expect(codec).to have_received(:decode).twice + end + end + + describe '#to_payloads' do + it 'converts and encodes' do + expect(subject.to_payloads(['test-payload'.b, nil])).to eq(payloads) + expect(codec).to have_received(:encodes) + end + end + + describe '#to_payload' do + it 'converts and encodes' do + expect(subject.to_payload('test-payload'.b)).to eq(payload_bytes) + expect(codec).to have_received(:encode) + end + end + + describe '#to_payload_map_without_codec' do + let(:payload_map) { { first: payload_bytes, second: payload_nil } } + + it 'converts' do + expect(subject.to_payload_map_without_codec(first: 'test-payload'.b, second: nil)).to eq(payload_map) + expect(codec).not_to have_received(:encode) + end + end + + describe '#to_result_payloads' do + let(:payloads) { Fabricate(:api_payloads, payloads_array: [payload_bytes]) } + + it 'converts and encodes' do + expect(subject.to_result_payloads('test-payload'.b)).to eq(payloads) + expect(codec).to have_received(:encodes) + end + end + + describe '#to_details_payloads' do + let(:payloads) { Fabricate(:api_payloads, payloads_array: [payload_bytes]) } + + it 'converts and encodes' do + expect(subject.to_details_payloads('test-payload'.b)).to eq(payloads) + expect(codec).to have_received(:encodes) + end + end + + describe '#to_signal_payloads' do + let(:payloads) { Fabricate(:api_payloads, payloads_array: [payload_bytes]) } + + it 'converts and encodes' do + expect(subject.to_signal_payloads('test-payload'.b)).to eq(payloads) + expect(codec).to have_received(:encodes) + end + end + + describe '#to_query_payloads' do + let(:payloads) { Fabricate(:api_payloads, payloads_array: [payload_bytes]) } + + it 'converts and encodes' do + expect(subject.to_query_payloads('test-payload'.b)).to eq(payloads) + expect(codec).to have_received(:encodes) + end + end + + describe '#to_payload_map' do + let(:payload_map) { { first: payload_bytes, second: payload_nil } } + + it 'converts and encodes' do + expect(subject.to_payload_map(first: 'test-payload'.b, second: nil)).to eq(payload_map) + expect(codec).to have_received(:encode).twice + end + end +end diff --git a/spec/unit/lib/temporal/grpc_spec.rb b/spec/unit/lib/temporal/grpc_spec.rb index cb97469f..9d0fe528 100644 --- a/spec/unit/lib/temporal/grpc_spec.rb +++ b/spec/unit/lib/temporal/grpc_spec.rb @@ -1,8 +1,15 @@ require 'temporal/connection/grpc' +require 'temporal/converter_wrapper' require 'temporal/workflow/query_result' describe Temporal::Connection::GRPC do let(:identity) { 'my-identity' } + let(:converter) do + Temporal::ConverterWrapper.new( + Temporal::Configuration::DEFAULT_CONVERTER, + Temporal::Configuration::DEFAULT_PAYLOAD_CODEC + ) + end let(:binary_checksum) { 'v1.0.0' } let(:grpc_stub) { double('grpc stub') } let(:grpc_operator_stub) { double('grpc stub') } @@ -10,12 +17,9 @@ let(:workflow_id) { SecureRandom.uuid } let(:run_id) { SecureRandom.uuid } let(:now) { Time.now} + let(:options) { {} } - subject { Temporal::Connection::GRPC.new(nil, nil, identity, :this_channel_is_insecure) } - - class TestDeserializer - extend Temporal::Concerns::Payloads - end + subject { Temporal::Connection::GRPC.new(nil, nil, identity, :this_channel_is_insecure, converter, options) } before do allow(subject).to receive(:client).and_return(grpc_stub) @@ -535,7 +539,7 @@ class TestDeserializer expect(request.completed_type).to eq(Temporalio::Api::Enums::V1::QueryResultType.lookup( Temporalio::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_ANSWERED) ) - expect(request.query_result).to eq(TestDeserializer.to_query_payloads(42)) + expect(request.query_result).to eq(converter.to_query_payloads(42)) expect(request.error_message).to eq('') end end @@ -606,7 +610,7 @@ class TestDeserializer expect(request.query_results['1'].result_type).to eq(Temporalio::Api::Enums::V1::QueryResultType.lookup( Temporalio::Api::Enums::V1::QueryResultType::QUERY_RESULT_TYPE_ANSWERED) ) - expect(request.query_results['1'].answer).to eq(TestDeserializer.to_query_payloads(42)) + expect(request.query_results['1'].answer).to eq(converter.to_query_payloads(42)) expect(request.query_results['2']).to be_a(Temporalio::Api::Query::V1::WorkflowQueryResult) expect(request.query_results['2'].result_type).to eq(Temporalio::Api::Enums::V1::QueryResultType.lookup( @@ -880,7 +884,7 @@ class TestDeserializer end context "when keepalive_time_ms is passed" do - subject { Temporal::Connection::GRPC.new(nil, nil, identity, :this_channel_is_insecure, keepalive_time_ms: 30_000) } + let(:options) { { keepalive_time_ms: 30_000 } } it "passes the option to the channel args" do expect(Temporalio::Api::WorkflowService::V1::WorkflowService::Stub).to receive(:new).with( @@ -897,7 +901,7 @@ class TestDeserializer end context "when passing retry_connection" do - subject { Temporal::Connection::GRPC.new(nil, nil, identity, :this_channel_is_insecure, retry_connection: true) } + let(:options) { { retry_connection: true } } it "passes the option to the channel args" do expect(Temporalio::Api::WorkflowService::V1::WorkflowService::Stub).to receive(:new).with( @@ -932,8 +936,7 @@ class TestDeserializer end context "when passing a custom retry policy" do - subject { Temporal::Connection::GRPC.new(nil, nil, identity, :this_channel_is_insecure, retry_policy: retry_policy) } - + let(:options) { { retry_policy: retry_policy } } let(:retry_policy) do { retryableStatusCodes: ["UNAVAILABLE", "INTERNAL"], diff --git a/spec/unit/lib/temporal/metadata_spec.rb b/spec/unit/lib/temporal/metadata_spec.rb index cd21fb76..b3f02955 100644 --- a/spec/unit/lib/temporal/metadata_spec.rb +++ b/spec/unit/lib/temporal/metadata_spec.rb @@ -1,8 +1,15 @@ require 'temporal/metadata' describe Temporal::Metadata do + let(:converter) do + Temporal::ConverterWrapper.new( + Temporal::Configuration::DEFAULT_CONVERTER, + Temporal::Configuration::DEFAULT_PAYLOAD_CODEC + ) + end + describe '.generate_activity_metadata' do - subject { described_class.generate_activity_metadata(data, namespace) } + subject { described_class.generate_activity_metadata(data, namespace, converter) } let(:data) { Fabricate(:api_activity_task) } let(:namespace) { 'test-namespace' } @@ -46,7 +53,7 @@ end context '.generate_workflow_metadata' do - subject { described_class.generate_workflow_metadata(event, task_metadata) } + subject { described_class.generate_workflow_metadata(event, task_metadata, converter) } let(:event) { Temporal::Workflow::History::Event.new(Fabricate(:api_workflow_execution_started_event)) } let(:task_metadata) { Fabricate(:workflow_task_metadata) } let(:namespace) { nil } diff --git a/spec/unit/lib/temporal/workflow/errors_spec.rb b/spec/unit/lib/temporal/workflow/errors_spec.rb index 53d86b68..82fb6924 100644 --- a/spec/unit/lib/temporal/workflow/errors_spec.rb +++ b/spec/unit/lib/temporal/workflow/errors_spec.rb @@ -27,6 +27,13 @@ def initialize(foo, bar) end describe Temporal::Workflow::Errors do + let(:converter) do + Temporal::ConverterWrapper.new( + Temporal::Configuration::DEFAULT_CONVERTER, + Temporal::Configuration::DEFAULT_PAYLOAD_CODEC + ) + end + describe '.generate_error' do it "instantiates properly when the client has the error" do message = "An error message" @@ -38,7 +45,7 @@ def initialize(foo, bar) error_class: SomeError.to_s ) - e = Temporal::Workflow::Errors.generate_error(failure) + e = Temporal::Workflow::Errors.generate_error(failure, converter) expect(e).to be_a(SomeError) expect(e.message).to eq(message) expect(e.backtrace).to eq(stack_trace) @@ -47,9 +54,9 @@ def initialize(foo, bar) it 'correctly deserializes a complex error' do error = MyFancyError.new('foo', 'bar') - failure = Temporal::Connection::Serializer::Failure.new(error, serialize_whole_error: true).to_proto + failure = Temporal::Connection::Serializer::Failure.new(error, converter, serialize_whole_error: true).to_proto - e = Temporal::Workflow::Errors.generate_error(failure) + e = Temporal::Workflow::Errors.generate_error(failure, converter) expect(e).to be_a(MyFancyError) expect(e.foo).to eq('foo') expect(e.bar).to eq('bar') @@ -68,7 +75,7 @@ def initialize(foo, bar) error_class: 'NonexistentError', ) - e = Temporal::Workflow::Errors.generate_error(failure) + e = Temporal::Workflow::Errors.generate_error(failure, converter) expect(e).to be_a(StandardError) expect(e.message).to eq("NonexistentError: An error message") expect(e.backtrace).to eq(stack_trace) @@ -94,7 +101,7 @@ def initialize(foo, bar) error_class: ErrorWithTwoArgs.to_s, ) - e = Temporal::Workflow::Errors.generate_error(failure) + e = Temporal::Workflow::Errors.generate_error(failure, converter) expect(e).to be_a(StandardError) expect(e.message).to eq("ErrorWithTwoArgs: An error message") expect(e.backtrace).to eq(stack_trace) @@ -127,7 +134,7 @@ def initialize(foo, bar) error_class: ErrorThatRaisesInInitialize.to_s, ) - e = Temporal::Workflow::Errors.generate_error(failure) + e = Temporal::Workflow::Errors.generate_error(failure, converter) expect(e).to be_a(StandardError) expect(e.message).to eq("ErrorThatRaisesInInitialize: An error message") expect(e.backtrace).to eq(stack_trace) diff --git a/spec/unit/lib/temporal/workflow/execution_info_spec.rb b/spec/unit/lib/temporal/workflow/execution_info_spec.rb index ad3368f2..6bef7b2d 100644 --- a/spec/unit/lib/temporal/workflow/execution_info_spec.rb +++ b/spec/unit/lib/temporal/workflow/execution_info_spec.rb @@ -1,7 +1,13 @@ require 'temporal/workflow/execution_info' describe Temporal::Workflow::ExecutionInfo do - subject { described_class.generate_from(api_info) } + subject { described_class.generate_from(api_info, converter) } + let(:converter) do + Temporal::ConverterWrapper.new( + Temporal::Configuration::DEFAULT_CONVERTER, + Temporal::Configuration::DEFAULT_PAYLOAD_CODEC + ) + end let(:api_info) { Fabricate(:api_workflow_execution_info, workflow: 'TestWorkflow', workflow_id: '') } describe '.generate_for' do @@ -25,7 +31,7 @@ it 'deserializes if search_attributes is nil' do api_info.search_attributes = nil - result = described_class.generate_from(api_info) + result = described_class.generate_from(api_info, converter) expect(result.search_attributes).to eq({}) end end diff --git a/spec/unit/lib/temporal/workflow/executor_spec.rb b/spec/unit/lib/temporal/workflow/executor_spec.rb index ee567a8b..714dc72b 100644 --- a/spec/unit/lib/temporal/workflow/executor_spec.rb +++ b/spec/unit/lib/temporal/workflow/executor_spec.rb @@ -134,9 +134,9 @@ def execute let(:query_2_error) { StandardError.new('Test query failure') } let(:queries) do { - '1' => Temporal::Workflow::TaskProcessor::Query.new(Fabricate(:api_workflow_query, query_type: 'success')), - '2' => Temporal::Workflow::TaskProcessor::Query.new(Fabricate(:api_workflow_query, query_type: 'failure')), - '3' => Temporal::Workflow::TaskProcessor::Query.new(Fabricate(:api_workflow_query, query_type: 'unknown')) + '1' => Temporal::Workflow::TaskProcessor::Query.new(Fabricate(:api_workflow_query, query_type: 'success'), config.converter), + '2' => Temporal::Workflow::TaskProcessor::Query.new(Fabricate(:api_workflow_query, query_type: 'failure'), config.converter), + '3' => Temporal::Workflow::TaskProcessor::Query.new(Fabricate(:api_workflow_query, query_type: 'unknown'), config.converter) } end diff --git a/spec/unit/lib/temporal/workflow/state_manager_spec.rb b/spec/unit/lib/temporal/workflow/state_manager_spec.rb index a1caaa2c..8aa8f9aa 100644 --- a/spec/unit/lib/temporal/workflow/state_manager_spec.rb +++ b/spec/unit/lib/temporal/workflow/state_manager_spec.rb @@ -229,7 +229,7 @@ class MyWorkflow < Temporal::Workflow; end state_manager.schedule( Temporal::Workflow::Command::RecordMarker.new( name: marker_entry.marker_recorded_event_attributes.marker_name, - details: to_payload_map({}) + details: TEST_CONVERTER.to_payload_map({}) ) ) From f41efb7bb0c9e755a07382b94e00a4f31bcd33c1 Mon Sep 17 00:00:00 2001 From: Anthony D Date: Wed, 4 Sep 2024 23:18:35 +0100 Subject: [PATCH 124/125] [Refactor] Remove global config references (#317) * Remove global config from client specs * Remove global configuration from client and connection * Remove global configuration from integration specs * Remove global config from error messages and comments * Remove global config from specs * Remove global config from worker spec * Add README section on global vs local configuration --- README.md | 41 +++++++++++++ examples/init.rb | 7 ++- examples/spec/helpers.rb | 8 ++- examples/spec/integration/converter_spec.rb | 4 +- .../spec/integration/create_schedule_spec.rb | 4 +- .../spec/integration/delete_schedule_spec.rb | 2 +- ...handling_structured_error_workflow_spec.rb | 4 +- .../spec/integration/list_schedules_spec.rb | 2 +- .../integration/metadata_workflow_spec.rb | 2 +- .../spec/integration/pause_schedule_spec.rb | 2 +- .../spec/integration/reset_workflow_spec.rb | 12 ++-- .../spec/integration/start_workflow_spec.rb | 12 ++-- .../spec/integration/trigger_schedule_spec.rb | 2 +- .../spec/integration/update_schedule_spec.rb | 4 +- lib/temporal/client.rb | 2 +- lib/temporal/configuration.rb | 2 +- lib/temporal/connection/grpc.rb | 2 +- lib/temporal/errors.rb | 2 +- lib/temporal/workflow/errors.rb | 4 +- spec/config/temporal.rb | 5 -- spec/unit/lib/temporal/client_spec.rb | 58 +++++++++---------- .../testing/local_workflow_context_spec.rb | 6 +- spec/unit/lib/temporal/worker_spec.rb | 10 ++-- .../lib/temporal/workflow/context_spec.rb | 2 +- .../unit/lib/temporal/workflow/errors_spec.rb | 4 +- spec/unit/lib/temporal_spec.rb | 9 ++- 26 files changed, 129 insertions(+), 83 deletions(-) delete mode 100644 spec/config/temporal.rb diff --git a/README.md b/README.md index 63888d8b..a64c1159 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,47 @@ Temporal.configure do |config| end ``` +## Configuration + +This gem is optimised for the smoothest out-of-the-box experience, which is achieved using a global +configuration: + +```ruby +Temporal.configure do |config| + config.host = '127.0.0.1' # sets global host + ... +end + +Temporal::Worker.new # uses global host +Temporal.start_workflow(...) # uses global host +``` + +This will work just fine for simpler use-cases, however at some point you might need to setup +multiple clients and workers within the same instance of your app (e.g. you have different Temporal +hosts, need to use different codecs/converters for different parts of your app, etc). Should this be +the case we recommend using explicit local configurations for each client/worker: + +```ruby +config_1 = Temporal::Configuration.new +config_1.host = 'temporal-01' + +config_2 = Temporal::Configuration.new +config_2.host = 'temporal-01' + +worker_1 = Temporal::Worker.new(config_1) +worker_2 = Temporal::Worker.new(config_2) + +client_1 = Temporal::Client.new(config_1) +client_1.start_workflow(...) + +client_2 = Temporal::Client.new(config_2) +client_2.start_workflow(...) +``` + +*NOTE: Almost all the methods on the `Temporal` module are delegated to the default client that's +initialized using global configuration. The same methods can be used directly on your own client +instances.* + ## Workflows A workflow is defined using pure Ruby code, however it should contain only a high-level diff --git a/examples/init.rb b/examples/init.rb index ab4e1b3a..053c0e14 100644 --- a/examples/init.rb +++ b/examples/init.rb @@ -8,10 +8,13 @@ metrics_logger = Logger.new(STDOUT, progname: 'metrics') +DEFAULT_NAMESPACE = 'ruby-samples'.freeze +DEFAULT_TASK_QUEUE = 'general'.freeze + Temporal.configure do |config| config.host = ENV.fetch('TEMPORAL_HOST', 'localhost') config.port = ENV.fetch('TEMPORAL_PORT', 7233).to_i - config.namespace = ENV.fetch('TEMPORAL_NAMESPACE', 'ruby-samples') - config.task_queue = ENV.fetch('TEMPORAL_TASK_QUEUE', 'general') + config.namespace = ENV.fetch('TEMPORAL_NAMESPACE', DEFAULT_NAMESPACE) + config.task_queue = ENV.fetch('TEMPORAL_TASK_QUEUE', DEFAULT_TASK_QUEUE) config.metrics_adapter = Temporal::MetricsAdapters::Log.new(metrics_logger) end diff --git a/examples/spec/helpers.rb b/examples/spec/helpers.rb index f2e614e4..4d4c65a4 100644 --- a/examples/spec/helpers.rb +++ b/examples/spec/helpers.rb @@ -21,7 +21,7 @@ def wait_for_workflow_completion(workflow_id, run_id) def fetch_history(workflow_id, run_id, options = {}) connection = Temporal.send(:default_client).send(:connection) options = { - namespace: Temporal.configuration.namespace, + namespace: integration_spec_namespace, workflow_id: workflow_id, run_id: run_id, }.merge(options) @@ -30,6 +30,10 @@ def fetch_history(workflow_id, run_id, options = {}) end def integration_spec_namespace - ENV.fetch('TEMPORAL_NAMESPACE', 'ruby-samples') + ENV.fetch('TEMPORAL_NAMESPACE', DEFAULT_NAMESPACE) + end + + def integration_spec_task_queue + ENV.fetch('TEMPORAL_TASK_QUEUE', DEFAULT_TASK_QUEUE) end end diff --git a/examples/spec/integration/converter_spec.rb b/examples/spec/integration/converter_spec.rb index 6c6672c4..576ff55f 100644 --- a/examples/spec/integration/converter_spec.rb +++ b/examples/spec/integration/converter_spec.rb @@ -12,8 +12,6 @@ end around(:each) do |example| - task_queue = Temporal.configuration.task_queue - Temporal.configure do |config| config.task_queue = 'crypt' config.payload_codec = codec @@ -22,7 +20,7 @@ example.run ensure Temporal.configure do |config| - config.task_queue = task_queue + config.task_queue = integration_spec_task_queue config.payload_codec = Temporal::Configuration::DEFAULT_PAYLOAD_CODEC end end diff --git a/examples/spec/integration/create_schedule_spec.rb b/examples/spec/integration/create_schedule_spec.rb index 02f3e0b4..a7ae3a40 100644 --- a/examples/spec/integration/create_schedule_spec.rb +++ b/examples/spec/integration/create_schedule_spec.rb @@ -25,7 +25,7 @@ "Test", options: { workflow_id: workflow_id, - task_queue: Temporal.configuration.task_queue + task_queue: integration_spec_task_queue } ), policies: Temporal::Schedule::SchedulePolicies.new( @@ -74,7 +74,7 @@ action: Temporal::Schedule::StartWorkflowAction.new( "HelloWorldWorkflow", "Test", - options: {task_queue: Temporal.configuration.task_queue} + options: {task_queue: integration_spec_task_queue} ) ) diff --git a/examples/spec/integration/delete_schedule_spec.rb b/examples/spec/integration/delete_schedule_spec.rb index b12d7220..c621710d 100644 --- a/examples/spec/integration/delete_schedule_spec.rb +++ b/examples/spec/integration/delete_schedule_spec.rb @@ -15,7 +15,7 @@ "HelloWorldWorkflow", "Test", options: { - task_queue: Temporal.configuration.task_queue + task_queue: integration_spec_task_queue } ) ) diff --git a/examples/spec/integration/handling_structured_error_workflow_spec.rb b/examples/spec/integration/handling_structured_error_workflow_spec.rb index 094fb139..91096453 100644 --- a/examples/spec/integration/handling_structured_error_workflow_spec.rb +++ b/examples/spec/integration/handling_structured_error_workflow_spec.rb @@ -5,8 +5,6 @@ # That worker runs a task queue, error_serialization_v2. This setup code will # route workflow requests to that task queue. around(:each) do |example| - task_queue = Temporal.configuration.task_queue - Temporal.configure do |config| config.task_queue = 'error_serialization_v2' end @@ -14,7 +12,7 @@ example.run ensure Temporal.configure do |config| - config.task_queue = task_queue + config.task_queue = integration_spec_task_queue end end diff --git a/examples/spec/integration/list_schedules_spec.rb b/examples/spec/integration/list_schedules_spec.rb index cfdc97b6..abd6b862 100644 --- a/examples/spec/integration/list_schedules_spec.rb +++ b/examples/spec/integration/list_schedules_spec.rb @@ -22,7 +22,7 @@ "HelloWorldWorkflow", "Test", options: { - task_queue: Temporal.configuration.task_queue + task_queue: integration_spec_task_queue } ) ) diff --git a/examples/spec/integration/metadata_workflow_spec.rb b/examples/spec/integration/metadata_workflow_spec.rb index 508c3af8..2fd0b1e6 100644 --- a/examples/spec/integration/metadata_workflow_spec.rb +++ b/examples/spec/integration/metadata_workflow_spec.rb @@ -16,7 +16,7 @@ run_id: run_id, ) - expect(actual_result.task_queue).to eq(Temporal.configuration.task_queue) + expect(actual_result.task_queue).to eq(integration_spec_task_queue) end it 'workflow can retrieve its headers' do diff --git a/examples/spec/integration/pause_schedule_spec.rb b/examples/spec/integration/pause_schedule_spec.rb index 12a750f4..46e8b8ce 100644 --- a/examples/spec/integration/pause_schedule_spec.rb +++ b/examples/spec/integration/pause_schedule_spec.rb @@ -17,7 +17,7 @@ "HelloWorldWorkflow", "Test", options: { - task_queue: Temporal.configuration.task_queue + task_queue: integration_spec_task_queue } ) ) diff --git a/examples/spec/integration/reset_workflow_spec.rb b/examples/spec/integration/reset_workflow_spec.rb index 57f41d82..7305fae4 100644 --- a/examples/spec/integration/reset_workflow_spec.rb +++ b/examples/spec/integration/reset_workflow_spec.rb @@ -2,7 +2,7 @@ require 'workflows/query_workflow' require 'temporal/reset_reapply_type' -describe 'Temporal.reset_workflow' do +describe 'Temporal.reset_workflow', :integration do it 'can reset a closed workflow to the beginning' do workflow_id = SecureRandom.uuid original_run_id = Temporal.start_workflow( @@ -19,7 +19,7 @@ expect(original_result).to eq('Hello World, Test') new_run_id = Temporal.reset_workflow( - Temporal.configuration.namespace, + integration_spec_namespace, workflow_id, original_run_id, strategy: Temporal::ResetStrategy::FIRST_WORKFLOW_TASK @@ -36,7 +36,7 @@ def reset_hello_world_workflow_twice(workflow_id, original_run_id, request_id:) 2.times.map do new_run_id = Temporal.reset_workflow( - Temporal.configuration.namespace, + integration_spec_namespace, workflow_id, original_run_id, strategy: Temporal::ResetStrategy::FIRST_WORKFLOW_TASK, @@ -130,7 +130,7 @@ def start_query_workflow_and_signal_three_times workflow_id, original_run_id = start_query_workflow_and_signal_three_times.values_at(:workflow_id, :run_id) new_run_id = Temporal.reset_workflow( - Temporal.configuration.namespace, + integration_spec_namespace, workflow_id, original_run_id, strategy: Temporal::ResetStrategy::FIRST_WORKFLOW_TASK, @@ -147,7 +147,7 @@ def start_query_workflow_and_signal_three_times workflow_id, original_run_id = start_query_workflow_and_signal_three_times.values_at(:workflow_id, :run_id) new_run_id = Temporal.reset_workflow( - Temporal.configuration.namespace, + integration_spec_namespace, workflow_id, original_run_id, strategy: Temporal::ResetStrategy::FIRST_WORKFLOW_TASK, @@ -160,4 +160,4 @@ def start_query_workflow_and_signal_three_times Temporal.terminate_workflow(workflow_id, run_id: new_run_id) end end - \ No newline at end of file + diff --git a/examples/spec/integration/start_workflow_spec.rb b/examples/spec/integration/start_workflow_spec.rb index 99d0d7c4..8cf6a46c 100644 --- a/examples/spec/integration/start_workflow_spec.rb +++ b/examples/spec/integration/start_workflow_spec.rb @@ -1,7 +1,7 @@ require 'workflows/hello_world_workflow' require 'workflows/long_workflow' -describe 'Temporal.start_workflow' do +describe 'Temporal.start_workflow', :integration do let(:workflow_id) { SecureRandom.uuid } it 'starts a workflow using a class reference' do @@ -21,15 +21,15 @@ it 'starts a workflow using a string reference' do run_id = Temporal.start_workflow('HelloWorldWorkflow', 'Test', options: { workflow_id: workflow_id, - namespace: Temporal.configuration.namespace, - task_queue: Temporal.configuration.task_queue + namespace: integration_spec_namespace, + task_queue: integration_spec_task_queue }) result = Temporal.await_workflow_result( 'HelloWorldWorkflow', workflow_id: workflow_id, run_id: run_id, - namespace: Temporal.configuration.namespace + namespace: integration_spec_namespace ) expect(result).to eq('Hello World, Test') @@ -82,11 +82,11 @@ }) execution_1 = Temporal.fetch_workflow_execution_info( - Temporal.configuration.namespace, + integration_spec_namespace, workflow_id, run_id_1) execution_2 = Temporal.fetch_workflow_execution_info( - Temporal.configuration.namespace, + integration_spec_namespace, workflow_id, run_id_2) diff --git a/examples/spec/integration/trigger_schedule_spec.rb b/examples/spec/integration/trigger_schedule_spec.rb index ffa3e5c8..f90c8f0b 100644 --- a/examples/spec/integration/trigger_schedule_spec.rb +++ b/examples/spec/integration/trigger_schedule_spec.rb @@ -17,7 +17,7 @@ "HelloWorldWorkflow", "Test", options: { - task_queue: Temporal.configuration.task_queue + task_queue: integration_spec_task_queue } ) ) diff --git a/examples/spec/integration/update_schedule_spec.rb b/examples/spec/integration/update_schedule_spec.rb index 4aa358c6..5623894d 100644 --- a/examples/spec/integration/update_schedule_spec.rb +++ b/examples/spec/integration/update_schedule_spec.rb @@ -18,7 +18,7 @@ "HelloWorldWorkflow", "Test", options: { - task_queue: Temporal.configuration.task_queue + task_queue: integration_spec_task_queue } ), policies: Temporal::Schedule::SchedulePolicies.new( @@ -42,7 +42,7 @@ "HelloWorldWorkflow", "UpdatedInput", options: { - task_queue: Temporal.configuration.task_queue + task_queue: integration_spec_task_queue } ), policies: Temporal::Schedule::SchedulePolicies.new( diff --git a/lib/temporal/client.rb b/lib/temporal/client.rb index 9b537a9d..fc390e92 100644 --- a/lib/temporal/client.rb +++ b/lib/temporal/client.rb @@ -331,7 +331,7 @@ def reset_workflow(namespace, workflow_id, run_id, strategy: nil, workflow_task_ # for reference # @param details [String, Array, nil] optional details to be stored in history def terminate_workflow(workflow_id, namespace: nil, run_id: nil, reason: nil, details: nil) - namespace ||= Temporal.configuration.namespace + namespace ||= config.namespace connection.terminate_workflow_execution( namespace: namespace, diff --git a/lib/temporal/configuration.rb b/lib/temporal/configuration.rb index 101ad956..0506b61f 100644 --- a/lib/temporal/configuration.rb +++ b/lib/temporal/configuration.rb @@ -126,7 +126,7 @@ def for_connection credentials: credentials, identity: identity || default_identity, converter: converter, - connection_options: connection_options.merge(use_error_serialization_v2: @use_error_serialization_v2) + connection_options: connection_options.merge(use_error_serialization_v2: use_error_serialization_v2) ).freeze end diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index 5392f62a..1f817f51 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -315,7 +315,7 @@ def respond_activity_task_completed_by_id(namespace:, activity_id:, workflow_id: end def respond_activity_task_failed(namespace:, task_token:, exception:) - serialize_whole_error = options.fetch(:use_error_serialization_v2, Temporal.configuration.use_error_serialization_v2) + serialize_whole_error = options.fetch(:use_error_serialization_v2) request = Temporalio::Api::WorkflowService::V1::RespondActivityTaskFailedRequest.new( namespace: namespace, identity: identity, diff --git a/lib/temporal/errors.rb b/lib/temporal/errors.rb index a13ada62..1c423a6c 100644 --- a/lib/temporal/errors.rb +++ b/lib/temporal/errors.rb @@ -26,7 +26,7 @@ class ChildWorkflowTerminatedError < Error; end # A superclass for activity exceptions raised explicitly # with the intent to propagate to a workflow - # With v2 serialization (set with Temporal.configuration set with use_error_serialization_v2=true) you can + # With v2 serialization (set with Temporal::Configuration#use_error_serialization_v2=true) you can # throw any exception from an activity and expect that it can be handled by the workflow. class ActivityException < ClientError; end diff --git a/lib/temporal/workflow/errors.rb b/lib/temporal/workflow/errors.rb index 832c2ac3..f13f03bf 100644 --- a/lib/temporal/workflow/errors.rb +++ b/lib/temporal/workflow/errors.rb @@ -26,7 +26,7 @@ def self.generate_error(failure, converter, default_exception_class = StandardEr exception_or_message = converter.from_details_payloads(details) # v1 serialization only supports StandardErrors with a single "message" argument. # v2 serialization supports complex errors using our converters to serialize them. - # enable v2 serialization in activities with Temporal.configuration.use_error_serialization_v2 + # enable v2 serialization in activities with Temporal::Configuration#use_error_serialization_v2 if exception_or_message.is_a?(Exception) exception = exception_or_message else @@ -37,7 +37,7 @@ def self.generate_error(failure, converter, default_exception_class = StandardEr exception = default_exception_class.new(message) Temporal.logger.error( "Could not instantiate original error. Defaulting to StandardError. Make sure the worker running " \ - "your activities is setting Temporal.configuration.use_error_serialization_v2. If so, make sure the " \ + "your activities is configured with use_error_serialization_v2. If so, make sure the " \ "original error serialized by searching your logs for 'unserializable_error'. If not, you're using "\ "legacy serialization, and it's likely that "\ "your error's initializer takes something other than exactly one positional argument.", diff --git a/spec/config/temporal.rb b/spec/config/temporal.rb deleted file mode 100644 index 0d868ffe..00000000 --- a/spec/config/temporal.rb +++ /dev/null @@ -1,5 +0,0 @@ -RSpec.configure do |config| - config.before(:each) do - Temporal.configuration.error_handlers.clear - end -end \ No newline at end of file diff --git a/spec/unit/lib/temporal/client_spec.rb b/spec/unit/lib/temporal/client_spec.rb index bc970f31..a0ba4dc1 100644 --- a/spec/unit/lib/temporal/client_spec.rb +++ b/spec/unit/lib/temporal/client_spec.rb @@ -58,9 +58,9 @@ def inject!(header) workflow_name: 'TestStartWorkflow', task_queue: 'default-test-task-queue', input: [42], - task_timeout: Temporal.configuration.timeouts[:task], - run_timeout: Temporal.configuration.timeouts[:run], - execution_timeout: Temporal.configuration.timeouts[:execution], + task_timeout: config.timeouts[:task], + run_timeout: config.timeouts[:run], + execution_timeout: config.timeouts[:execution], workflow_id_reuse_policy: nil, headers: { 'test' => 'asdf' }, memo: {}, @@ -87,9 +87,9 @@ def inject!(header) workflow_name: 'TestStartWorkflow', task_queue: 'default-test-task-queue', input: [42], - task_timeout: Temporal.configuration.timeouts[:task], - run_timeout: Temporal.configuration.timeouts[:run], - execution_timeout: Temporal.configuration.timeouts[:execution], + task_timeout: config.timeouts[:task], + run_timeout: config.timeouts[:run], + execution_timeout: config.timeouts[:execution], workflow_id_reuse_policy: nil, headers: {}, memo: {}, @@ -120,9 +120,9 @@ def inject!(header) workflow_name: 'test-workflow', task_queue: 'test-task-queue', input: [42], - task_timeout: Temporal.configuration.timeouts[:task], - run_timeout: Temporal.configuration.timeouts[:run], - execution_timeout: Temporal.configuration.timeouts[:execution], + task_timeout: config.timeouts[:task], + run_timeout: config.timeouts[:run], + execution_timeout: config.timeouts[:execution], workflow_id_reuse_policy: :reject, headers: { 'Foo' => 'Bar' }, memo: { 'MemoKey1' => 'MemoValue1' }, @@ -147,9 +147,9 @@ def inject!(header) workflow_name: 'test-workflow', task_queue: 'default-test-task-queue', input: [42, { arg_1: 1, arg_2: 2 }], - task_timeout: Temporal.configuration.timeouts[:task], - run_timeout: Temporal.configuration.timeouts[:run], - execution_timeout: Temporal.configuration.timeouts[:execution], + task_timeout: config.timeouts[:task], + run_timeout: config.timeouts[:run], + execution_timeout: config.timeouts[:execution], workflow_id_reuse_policy: nil, headers: {}, memo: {}, @@ -168,9 +168,9 @@ def inject!(header) workflow_name: 'TestStartWorkflow', task_queue: 'default-test-task-queue', input: [42], - task_timeout: Temporal.configuration.timeouts[:task], - run_timeout: Temporal.configuration.timeouts[:run], - execution_timeout: Temporal.configuration.timeouts[:execution], + task_timeout: config.timeouts[:task], + run_timeout: config.timeouts[:run], + execution_timeout: config.timeouts[:execution], workflow_id_reuse_policy: nil, headers: {}, memo: {}, @@ -191,9 +191,9 @@ def inject!(header) workflow_name: 'TestStartWorkflow', task_queue: 'default-test-task-queue', input: [42], - task_timeout: Temporal.configuration.timeouts[:task], - run_timeout: Temporal.configuration.timeouts[:run], - execution_timeout: Temporal.configuration.timeouts[:execution], + task_timeout: config.timeouts[:task], + run_timeout: config.timeouts[:run], + execution_timeout: config.timeouts[:execution], workflow_id_reuse_policy: :allow, headers: {}, memo: {}, @@ -218,9 +218,9 @@ def inject!(header) workflow_name: 'test-workflow', task_queue: 'test-task-queue', input: [42], - task_timeout: Temporal.configuration.timeouts[:task], - run_timeout: Temporal.configuration.timeouts[:run], - execution_timeout: Temporal.configuration.timeouts[:execution], + task_timeout: config.timeouts[:task], + run_timeout: config.timeouts[:run], + execution_timeout: config.timeouts[:execution], workflow_id_reuse_policy: nil, headers: {}, memo: {}, @@ -246,9 +246,9 @@ def expect_signal_with_start(expected_arguments, expected_signal_argument) workflow_name: 'TestStartWorkflow', task_queue: 'default-test-task-queue', input: expected_arguments, - task_timeout: Temporal.configuration.timeouts[:task], - run_timeout: Temporal.configuration.timeouts[:run], - execution_timeout: Temporal.configuration.timeouts[:execution], + task_timeout: config.timeouts[:task], + run_timeout: config.timeouts[:run], + execution_timeout: config.timeouts[:execution], workflow_id_reuse_policy: nil, headers: {}, memo: {}, @@ -328,9 +328,9 @@ def expect_signal_with_start(expected_arguments, expected_signal_argument) task_queue: 'default-test-task-queue', cron_schedule: '* * * * *', input: [42], - task_timeout: Temporal.configuration.timeouts[:task], - run_timeout: Temporal.configuration.timeouts[:run], - execution_timeout: Temporal.configuration.timeouts[:execution], + task_timeout: config.timeouts[:task], + run_timeout: config.timeouts[:run], + execution_timeout: config.timeouts[:execution], workflow_id_reuse_policy: nil, memo: {}, search_attributes: {}, @@ -482,7 +482,7 @@ class NamespacedWorkflow < Temporal::Workflow it "completes and returns a #{type}" do payload = Temporalio::Api::Common::V1::Payloads.new( payloads: [ - Temporal.configuration.converter.to_payload(expected_result) + config.converter.to_payload(expected_result) ], ) completed_event = Fabricate(:workflow_completed_event, result: payload) @@ -759,7 +759,7 @@ class NamespacedWorkflow < Temporal::Workflow expect(connection) .to have_received(:terminate_workflow_execution) .with( - namespace: 'default-namespace', + namespace: 'default-test-namespace', workflow_id: 'my-workflow', reason: 'just stop it', details: nil, diff --git a/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb b/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb index 66c68769..75600fb3 100644 --- a/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb +++ b/spec/unit/lib/temporal/testing/local_workflow_context_spec.rb @@ -9,6 +9,7 @@ let(:run_id) { 'run_id_1' } let(:execution) { Temporal::Testing::WorkflowExecution.new } let(:task_queue) { 'my_test_queue' } + let(:config) { Temporal::Configuration.new } let(:workflow_context) do Temporal::Testing::LocalWorkflowContext.new( execution, @@ -27,13 +28,14 @@ headers: {}, run_started_at: Time.now, memo: {}, - ) + ), + config ) end let(:async_token) do # Generate the async token Temporal::Activity::AsyncToken.encode( - Temporal.configuration.namespace, + config.namespace, 1, # activity ID starts at 1 for each workflow workflow_id, run_id diff --git a/spec/unit/lib/temporal/worker_spec.rb b/spec/unit/lib/temporal/worker_spec.rb index 4fffc5d8..2c379567 100644 --- a/spec/unit/lib/temporal/worker_spec.rb +++ b/spec/unit/lib/temporal/worker_spec.rb @@ -344,7 +344,7 @@ def start_and_stop(worker) .to receive(:new) .and_return(workflow_poller) - worker = Temporal::Worker.new(activity_thread_pool_size: 10) + worker = Temporal::Worker.new(config, activity_thread_pool_size: 10) worker.register_workflow(TestWorkerWorkflow) worker.register_activity(TestWorkerActivity) @@ -389,7 +389,7 @@ def start_and_stop(worker) ) .and_return(workflow_poller) - worker = Temporal::Worker.new(binary_checksum: binary_checksum) + worker = Temporal::Worker.new(config, binary_checksum: binary_checksum) worker.register_workflow(TestWorkerWorkflow) worker.register_activity(TestWorkerActivity) @@ -412,7 +412,7 @@ def start_and_stop(worker) ) .and_return(activity_poller) - worker = Temporal::Worker.new(activity_poll_retry_seconds: 10) + worker = Temporal::Worker.new(config, activity_poll_retry_seconds: 10) worker.register_activity(TestWorkerActivity) start_and_stop(worker) @@ -435,7 +435,7 @@ def start_and_stop(worker) ) .and_return(workflow_poller) - worker = Temporal::Worker.new(workflow_poll_retry_seconds: 10) + worker = Temporal::Worker.new(config, workflow_poll_retry_seconds: 10) worker.register_workflow(TestWorkerWorkflow) start_and_stop(worker) @@ -457,7 +457,7 @@ def start_and_stop(worker) ) .and_return(activity_poller) - worker = Temporal::Worker.new(activity_max_tasks_per_second: 5) + worker = Temporal::Worker.new(config, activity_max_tasks_per_second: 5) worker.register_activity(TestWorkerActivity) start_and_stop(worker) diff --git a/spec/unit/lib/temporal/workflow/context_spec.rb b/spec/unit/lib/temporal/workflow/context_spec.rb index 6dddf3b2..05a61282 100644 --- a/spec/unit/lib/temporal/workflow/context_spec.rb +++ b/spec/unit/lib/temporal/workflow/context_spec.rb @@ -27,7 +27,7 @@ def execute end let(:metadata_hash) { Fabricate(:workflow_metadata).to_h } let(:metadata) { Temporal::Metadata::Workflow.new(**metadata_hash) } - let(:config) { Temporal.configuration } + let(:config) { Temporal::Configuration.new } let(:workflow_context) do Temporal::Workflow::Context.new( diff --git a/spec/unit/lib/temporal/workflow/errors_spec.rb b/spec/unit/lib/temporal/workflow/errors_spec.rb index 82fb6924..bce9d477 100644 --- a/spec/unit/lib/temporal/workflow/errors_spec.rb +++ b/spec/unit/lib/temporal/workflow/errors_spec.rb @@ -109,7 +109,7 @@ def initialize(foo, bar) .to have_received(:error) .with( "Could not instantiate original error. Defaulting to StandardError. "\ - "Make sure the worker running your activities is setting Temporal.configuration.use_error_serialization_v2. "\ + "Make sure the worker running your activities is configured with use_error_serialization_v2. "\ "If so, make sure the original error serialized by searching your logs for 'unserializable_error'. "\ "If not, you're using legacy serialization, and it's likely that "\ "your error's initializer takes something other than exactly one positional argument.", @@ -142,7 +142,7 @@ def initialize(foo, bar) .to have_received(:error) .with( "Could not instantiate original error. Defaulting to StandardError. "\ - "Make sure the worker running your activities is setting Temporal.configuration.use_error_serialization_v2. "\ + "Make sure the worker running your activities is configured with use_error_serialization_v2. "\ "If so, make sure the original error serialized by searching your logs for 'unserializable_error'. "\ "If not, you're using legacy serialization, and it's likely that "\ "your error's initializer takes something other than exactly one positional argument.", diff --git a/spec/unit/lib/temporal_spec.rb b/spec/unit/lib/temporal_spec.rb index 47ccd73d..49e57664 100644 --- a/spec/unit/lib/temporal_spec.rb +++ b/spec/unit/lib/temporal_spec.rb @@ -67,19 +67,24 @@ it 'calls a block with the configuration' do expect do |block| described_class.configure(&block) - end.to yield_with_args(described_class.configuration) + end.to yield_with_args(described_class.send(:config)) end end describe '.configuration' do + before { allow(described_class).to receive(:warn) } + it 'returns Temporal::Configuration object' do expect(described_class.configuration).to be_an_instance_of(Temporal::Configuration) + expect(described_class) + .to have_received(:warn) + .with('[DEPRECATION] This method is now deprecated without a substitution') end end describe '.logger' do it 'returns preconfigured Temporal logger' do - expect(described_class.logger).to eq(described_class.configuration.logger) + expect(described_class.logger).to eq(described_class.send(:config).logger) end end From b5efd2cef802be2fa97d5bab04839413726ac06e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Dold=C3=A1n?= Date: Thu, 5 Dec 2024 14:43:18 -0300 Subject: [PATCH 125/125] Add workflow start delay option (#294) --- lib/temporal/client.rb | 5 ++- lib/temporal/connection/grpc.rb | 8 +++- lib/temporal/execution_options.rb | 4 +- spec/unit/lib/temporal/client_spec.rb | 37 ++++++++++++------- .../lib/temporal/execution_options_spec.rb | 10 +++-- spec/unit/lib/temporal/grpc_spec.rb | 4 ++ 6 files changed, 46 insertions(+), 22 deletions(-) diff --git a/lib/temporal/client.rb b/lib/temporal/client.rb index fc390e92..c2e83e1f 100644 --- a/lib/temporal/client.rb +++ b/lib/temporal/client.rb @@ -41,6 +41,7 @@ def initialize(config) # @option options [Hash] :timeouts check Temporal::Configuration::DEFAULT_TIMEOUTS # @option options [Hash] :headers # @option options [Hash] :search_attributes + # @option options [Integer] :start_delay determines the amount of seconds to wait before initiating a Workflow # # @return [String] workflow's run ID def start_workflow(workflow, *input, options: {}, **args) @@ -67,6 +68,7 @@ def start_workflow(workflow, *input, options: {}, **args) headers: config.header_propagator_chain.inject(execution_options.headers), memo: execution_options.memo, search_attributes: Workflow::Context::Helpers.process_search_attributes(execution_options.search_attributes), + start_delay: execution_options.start_delay ) else raise ArgumentError, 'If signal_input is provided, you must also provide signal_name' if signal_name.nil? @@ -85,7 +87,8 @@ def start_workflow(workflow, *input, options: {}, **args) memo: execution_options.memo, search_attributes: Workflow::Context::Helpers.process_search_attributes(execution_options.search_attributes), signal_name: signal_name, - signal_input: signal_input + signal_input: signal_input, + start_delay: execution_options.start_delay ) end diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index 1f817f51..f35d2dc3 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -120,7 +120,8 @@ def start_workflow_execution( headers: nil, cron_schedule: nil, memo: nil, - search_attributes: nil + search_attributes: nil, + start_delay: nil ) request = Temporalio::Api::WorkflowService::V1::StartWorkflowExecutionRequest.new( identity: identity, @@ -137,6 +138,7 @@ def start_workflow_execution( workflow_execution_timeout: execution_timeout, workflow_run_timeout: run_timeout, workflow_task_timeout: task_timeout, + workflow_start_delay: start_delay, request_id: SecureRandom.uuid, header: Temporalio::Api::Common::V1::Header.new( fields: converter.to_payload_map(headers || {}) @@ -379,7 +381,8 @@ def signal_with_start_workflow_execution( headers: nil, cron_schedule: nil, memo: nil, - search_attributes: nil + search_attributes: nil, + start_delay: nil ) proto_header_fields = if headers.nil? converter.to_payload_map({}) @@ -406,6 +409,7 @@ def signal_with_start_workflow_execution( workflow_execution_timeout: execution_timeout, workflow_run_timeout: run_timeout, workflow_task_timeout: task_timeout, + workflow_start_delay: start_delay, request_id: SecureRandom.uuid, header: Temporalio::Api::Common::V1::Header.new( fields: proto_header_fields diff --git a/lib/temporal/execution_options.rb b/lib/temporal/execution_options.rb index 65f0031c..d3319cb8 100644 --- a/lib/temporal/execution_options.rb +++ b/lib/temporal/execution_options.rb @@ -3,7 +3,8 @@ module Temporal class ExecutionOptions - attr_reader :name, :namespace, :task_queue, :retry_policy, :timeouts, :headers, :memo, :search_attributes + attr_reader :name, :namespace, :task_queue, :retry_policy, :timeouts, :headers, :memo, :search_attributes, + :start_delay def initialize(object, options, defaults = nil) # Options are treated as overrides and take precedence @@ -15,6 +16,7 @@ def initialize(object, options, defaults = nil) @headers = options[:headers] || {} @memo = options[:memo] || {} @search_attributes = options[:search_attributes] || {} + @start_delay = options[:start_delay] || 0 # For Temporal::Workflow and Temporal::Activity use defined values as the next option if has_executable_concern?(object) diff --git a/spec/unit/lib/temporal/client_spec.rb b/spec/unit/lib/temporal/client_spec.rb index a0ba4dc1..31dc4a78 100644 --- a/spec/unit/lib/temporal/client_spec.rb +++ b/spec/unit/lib/temporal/client_spec.rb @@ -52,20 +52,21 @@ def inject!(header) subject.start_workflow(TestStartWorkflow, 42) expect(connection) .to have_received(:start_workflow_execution) - .with( - namespace: 'default-test-namespace', - workflow_id: an_instance_of(String), - workflow_name: 'TestStartWorkflow', - task_queue: 'default-test-task-queue', - input: [42], - task_timeout: config.timeouts[:task], - run_timeout: config.timeouts[:run], - execution_timeout: config.timeouts[:execution], - workflow_id_reuse_policy: nil, - headers: { 'test' => 'asdf' }, - memo: {}, - search_attributes: {}, - ) + .with( + namespace: 'default-test-namespace', + workflow_id: an_instance_of(String), + workflow_name: 'TestStartWorkflow', + task_queue: 'default-test-task-queue', + input: [42], + task_timeout: config.timeouts[:task], + run_timeout: config.timeouts[:run], + execution_timeout: config.timeouts[:execution], + workflow_id_reuse_policy: nil, + headers: { 'test' => 'asdf' }, + memo: {}, + search_attributes: {}, + start_delay: 0 + ) end end @@ -94,6 +95,7 @@ def inject!(header) headers: {}, memo: {}, search_attributes: {}, + start_delay: 0 ) end @@ -109,6 +111,7 @@ def inject!(header) workflow_id_reuse_policy: :reject, memo: { 'MemoKey1' => 'MemoValue1' }, search_attributes: { 'SearchAttribute1' => 256 }, + start_delay: 10 } ) @@ -127,6 +130,7 @@ def inject!(header) headers: { 'Foo' => 'Bar' }, memo: { 'MemoKey1' => 'MemoValue1' }, search_attributes: { 'SearchAttribute1' => 256 }, + start_delay: 10 ) end @@ -154,6 +158,7 @@ def inject!(header) headers: {}, memo: {}, search_attributes: {}, + start_delay: 0 ) end @@ -175,6 +180,7 @@ def inject!(header) headers: {}, memo: {}, search_attributes: {}, + start_delay: 0 ) end @@ -198,6 +204,7 @@ def inject!(header) headers: {}, memo: {}, search_attributes: {}, + start_delay: 0 ) end end @@ -225,6 +232,7 @@ def inject!(header) headers: {}, memo: {}, search_attributes: {}, + start_delay: 0 ) end end @@ -255,6 +263,7 @@ def expect_signal_with_start(expected_arguments, expected_signal_argument) search_attributes: {}, signal_name: 'the question', signal_input: expected_signal_argument, + start_delay: 0 ) end diff --git a/spec/unit/lib/temporal/execution_options_spec.rb b/spec/unit/lib/temporal/execution_options_spec.rb index 98fbe380..d0c9d017 100644 --- a/spec/unit/lib/temporal/execution_options_spec.rb +++ b/spec/unit/lib/temporal/execution_options_spec.rb @@ -99,10 +99,11 @@ class TestExecutionOptionsWorkflow < Temporal::Workflow task_queue: 'test-task-queue', retry_policy: { interval: 1, backoff: 2, max_attempts: 5 }, timeouts: { start_to_close: 10 }, - headers: { 'TestHeader' => 'Test' } + headers: { 'TestHeader' => 'Test' }, + start_delay: 10 } end - + it 'is initialized with full options' do expect(subject.name).to eq(options[:name]) expect(subject.namespace).to eq(options[:namespace]) @@ -113,12 +114,13 @@ class TestExecutionOptionsWorkflow < Temporal::Workflow expect(subject.retry_policy.max_attempts).to eq(options[:retry_policy][:max_attempts]) expect(subject.timeouts).to eq(options[:timeouts]) expect(subject.headers).to eq(options[:headers]) + expect(subject.start_delay).to eq(options[:start_delay]) end end - + context 'when retry policy options are invalid' do let(:options) { { retry_policy: { max_attempts: 10 } } } - + it 'raises' do expect { subject }.to raise_error( Temporal::RetryPolicy::InvalidRetryPolicy, diff --git a/spec/unit/lib/temporal/grpc_spec.rb b/spec/unit/lib/temporal/grpc_spec.rb index 9d0fe528..5639a0e9 100644 --- a/spec/unit/lib/temporal/grpc_spec.rb +++ b/spec/unit/lib/temporal/grpc_spec.rb @@ -66,6 +66,7 @@ execution_timeout: 1, run_timeout: 2, task_timeout: 3, + start_delay: 10, memo: {}, search_attributes: { 'foo-int-attribute' => 256, @@ -90,6 +91,7 @@ expect(request.workflow_execution_timeout.seconds).to eq(1) expect(request.workflow_run_timeout.seconds).to eq(2) expect(request.workflow_task_timeout.seconds).to eq(3) + expect(request.workflow_start_delay.seconds).to eq(10) expect(request.workflow_id_reuse_policy).to eq(:WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE) expect(request.search_attributes.indexed_fields).to eq({ 'foo-int-attribute' => Temporalio::Api::Common::V1::Payload.new(data: '256', metadata: { 'encoding' => 'json/plain' }), @@ -138,6 +140,7 @@ execution_timeout: 1, run_timeout: 2, task_timeout: 3, + start_delay: 10, workflow_id_reuse_policy: :allow, signal_name: 'the question', signal_input: 'what do you get if you multiply six by nine?' @@ -153,6 +156,7 @@ expect(request.workflow_execution_timeout.seconds).to eq(1) expect(request.workflow_run_timeout.seconds).to eq(2) expect(request.workflow_task_timeout.seconds).to eq(3) + expect(request.workflow_start_delay.seconds).to eq(10) expect(request.signal_name).to eq('the question') expect(request.signal_input.payloads[0].data).to eq('"what do you get if you multiply six by nine?"') expect(request.workflow_id_reuse_policy).to eq(:WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE)