diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05bff2a..0ecd2ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,9 +11,9 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-12, macos-latest] + os: [ubuntu-latest, macos-13, macos-latest] # Earliest and latest supported - rubyVersion: ["3.1", "3.3"] + rubyVersion: ["3.2", "3.4"] runs-on: ${{ matrix.os }} steps: - name: Checkout repository diff --git a/.rubocop.yml b/.rubocop.yml index 084ac5e..6055a48 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,4 +1,46 @@ AllCops: NewCops: enable - TargetRubyVersion: 3.1 - SuggestExtensions: false \ No newline at end of file + TargetRubyVersion: 3.2 + SuggestExtensions: false + +# Don't need super for activities or workflows +Lint/MissingSuper: + AllowedParentClasses: + - Temporalio::Activity::Definition + - Temporalio::Workflow::Definition + +# The default is too small and triggers simply setting lots of values on a proto +Metrics/AbcSize: + Max: 200 + +# The default is too small +Metrics/BlockLength: + Max: 100 + +# The default is too small +Metrics/ClassLength: + Max: 1000 + +# The default is too small +Metrics/CyclomaticComplexity: + Max: 100 + +# The default is too small +Metrics/MethodLength: + Max: 100 + +# The default is too small +Metrics/ModuleLength: + Max: 1000 + +# The default is too small +Metrics/PerceivedComplexity: + Max: 40 + +# Don't need API docs for samples +Style/Documentation: + Enabled: false + +# Don't need API docs for samples +Style/DocumentationMethod: + Enabled: false \ No newline at end of file diff --git a/README.md b/README.md index a300284..94e6b51 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ until the SDK is marked stable. Prerequisites: -* Ruby 3.1+ +* Ruby 3.2+ * Local Temporal server running (can [install CLI](https://docs.temporal.io/cli#install) then [run a dev server](https://docs.temporal.io/cli#start-dev-server)) * `bundle install` run in the root @@ -19,7 +19,11 @@ Prerequisites: ## Samples +* [activity_simple](activity_simple) - Simple workflow that calls two activities. * [activity_worker](activity_worker) - Use Ruby activities from a workflow in another language. +* [context_propagation](context_propagation) - Use interceptors to propagate thread/fiber local data from clients + through workflows/activities. +* [message_passing_simple](message_passing_simple) - Simple workflow that accepts signals, queries, and updates. ## Development diff --git a/activity_simple/README.md b/activity_simple/README.md new file mode 100644 index 0000000..6e2481c --- /dev/null +++ b/activity_simple/README.md @@ -0,0 +1,23 @@ +# Activity Simple + +This sample shows calling a couple of simple activities from a simple workflow. + +To run, first see [README.md](../README.md) for prerequisites. Then, in another terminal, start the Ruby worker +from this directory: + + bundle exec ruby worker.rb + +Finally in another terminal, use the Ruby client to the workflow from this directory: + + bundle exec ruby starter.rb + +The Ruby code will invoke the workflow which will execute two activities and return. The output of the final command +should be: + +``` +Executing workflow +Workflow result: some-db-value from table some-db-table +``` + +There is also a [test](../test/activity_simple/my_workflow_test.rb) that demonstrates mocking an activity during the +test. \ No newline at end of file diff --git a/activity_simple/my_activities.rb b/activity_simple/my_activities.rb new file mode 100644 index 0000000..1ae8ad2 --- /dev/null +++ b/activity_simple/my_activities.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'temporalio/activity' + +module ActivitySimple + module MyActivities + # Fake database client + class MyDatabaseClient + def select_value(table) + "some-db-value from table #{table}" + end + end + + # Stateful activity that is created only once by worker creation code + class SelectFromDatabase < Temporalio::Activity::Definition + def initialize(db_client) + @db_client = db_client + end + + def execute(table) + @db_client.select_value(table) + end + end + + # Stateless activity that is passed as class to worker creation code, + # thereby instantiating every attempt + class AppendSuffix < Temporalio::Activity::Definition + def execute(append_to) + "#{append_to} " + end + end + end +end diff --git a/activity_simple/my_workflow.rb b/activity_simple/my_workflow.rb new file mode 100644 index 0000000..1071034 --- /dev/null +++ b/activity_simple/my_workflow.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'temporalio/workflow' +require_relative 'my_activities' + +module ActivitySimple + class MyWorkflow < Temporalio::Workflow::Definition + def execute + # Run an activity that needs some state like a database connection + result1 = Temporalio::Workflow.execute_activity( + MyActivities::SelectFromDatabase, + 'some-db-table', + start_to_close_timeout: 5 * 60 # 5 minutes + ) + Temporalio::Workflow.logger.info("Activity result 1: #{result1}") + + # Run a stateless activity (note no difference on the caller side) + result2 = Temporalio::Workflow.execute_activity( + MyActivities::AppendSuffix, + result1, + start_to_close_timeout: 5 * 60 + ) + Temporalio::Workflow.logger.info("Activity result 2: #{result2}") + + # We'll go ahead and return this result + result2 + end + end +end diff --git a/activity_simple/starter.rb b/activity_simple/starter.rb new file mode 100644 index 0000000..02c8ec5 --- /dev/null +++ b/activity_simple/starter.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'temporalio/client' +require_relative 'my_workflow' + +# Create a client +client = Temporalio::Client.connect('localhost:7233', 'default') + +# Run workflow +puts 'Executing workflow' +result = client.execute_workflow( + ActivitySimple::MyWorkflow, + id: 'activity-simple-sample-workflow-id', + task_queue: 'activity-simple-sample' +) +puts "Workflow result: #{result}" diff --git a/activity_simple/worker.rb b/activity_simple/worker.rb new file mode 100644 index 0000000..2e98370 --- /dev/null +++ b/activity_simple/worker.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require_relative 'my_activities' +require_relative 'my_workflow' +require 'logger' +require 'temporalio/client' +require 'temporalio/worker' + +# Create a Temporal client +client = Temporalio::Client.connect( + 'localhost:7233', + 'default', + logger: Logger.new($stdout, level: Logger::INFO) +) + +# Use an instance for the stateful DB activity, other activity we will pass +# in as class meaning it is instantiated each attempt +db_client = ActivitySimple::MyActivities::MyDatabaseClient.new +select_from_db_activity = ActivitySimple::MyActivities::SelectFromDatabase.new(db_client) + +# Create worker with the activities and workflow +worker = Temporalio::Worker.new( + client:, + task_queue: 'activity-simple-sample', + activities: [select_from_db_activity, ActivitySimple::MyActivities::AppendSuffix], + workflows: [ActivitySimple::MyWorkflow] +) + +# Run the worker until SIGINT +puts 'Starting worker (ctrl+c to exit)' +worker.run(shutdown_signals: ['SIGINT']) diff --git a/activity_worker/activity_worker.rb b/activity_worker/activity_worker.rb index bc2e64e..5e370bc 100644 --- a/activity_worker/activity_worker.rb +++ b/activity_worker/activity_worker.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative 'activity' +require_relative 'say_hello_activity' require 'temporalio/client' require 'temporalio/worker' diff --git a/activity_worker/activity.rb b/activity_worker/say_hello_activity.rb similarity index 76% rename from activity_worker/activity.rb rename to activity_worker/say_hello_activity.rb index 863e2f8..c93fe74 100644 --- a/activity_worker/activity.rb +++ b/activity_worker/say_hello_activity.rb @@ -4,7 +4,7 @@ module ActivityWorker # Activity is a class with execute implemented - class SayHelloActivity < Temporalio::Activity + class SayHelloActivity < Temporalio::Activity::Definition def execute(name) "Hello, #{name}!" end diff --git a/context_propagation/README.md b/context_propagation/README.md new file mode 100644 index 0000000..9bad394 --- /dev/null +++ b/context_propagation/README.md @@ -0,0 +1,16 @@ +# Context Propagation + +This sample shows how a thread/fiber local can be propagated through workflows and activities using an interceptor. + +To run, first see [README.md](../README.md) for prerequisites. Then, in another terminal, start the Ruby worker +from this directory: + + bundle exec ruby worker.rb + +Finally in another terminal, use the Ruby client to the workflow from this directory: + + bundle exec ruby starter.rb + +The Ruby code will invoke the workflow which will execute an activity and return. Note the log output from the worker +that contains logs on which user is calling the workflow/activity, information which we set as thread local on the +client and was automatically propagated through to the workflow and activity. \ No newline at end of file diff --git a/context_propagation/interceptor.rb b/context_propagation/interceptor.rb new file mode 100644 index 0000000..69de715 --- /dev/null +++ b/context_propagation/interceptor.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +require 'temporalio/client/interceptor' +require 'temporalio/worker/interceptor' + +module ContextPropagation + class Interceptor + include Temporalio::Client::Interceptor + include Temporalio::Worker::Interceptor::Workflow + include Temporalio::Worker::Interceptor::Activity + + def initialize(*keys_to_propagate) + @keys_to_propagate = keys_to_propagate + end + + def intercept_client(next_interceptor) + ClientOutbound.new(self, next_interceptor) + end + + def intercept_workflow(next_interceptor) + WorkflowInbound.new(self, next_interceptor) + end + + def intercept_activity(next_interceptor) + ActivityInbound.new(self, next_interceptor) + end + + def context_to_headers(input) + @keys_to_propagate.each do |key| + value = Thread.current[key] + input.headers[key.to_s] = value unless value.nil? + end + end + + def with_context_from_headers(input) + # Grab all original values + orig_values = @keys_to_propagate.map { |key| [key, Thread.current[key]] } + # Replace values, even if they are nil + @keys_to_propagate.each { |key| Thread.current[key] = input.headers[key.to_s] } + begin + yield + ensure + # Put them all back, even if they were nil + orig_values.each { |key, val| Thread.current[key] = val } + end + end + + class ClientOutbound < Temporalio::Client::Interceptor::Outbound + def initialize(root, next_interceptor) + super(next_interceptor) + @root = root + end + + def start_workflow(input) + @root.context_to_headers(input) + super + end + + def signal_workflow(input) + @root.context_to_headers(input) + super + end + + def query_workflow(input) + @root.context_to_headers(input) + super + end + + def start_workflow_update(input) + @root.context_to_headers(input) + super + end + end + + class WorkflowInbound < Temporalio::Worker::Interceptor::Workflow::Inbound + def initialize(root, next_interceptor) + super(next_interceptor) + @root = root + end + + def init(outbound) + super(WorkflowOutbound.new(@root, outbound)) + end + + def execute(input) + @root.with_context_from_headers(input) { super } + end + + def handle_signal(input) + @root.with_context_from_headers(input) { super } + end + + def handle_query(input) + @root.with_context_from_headers(input) { super } + end + + def validate_update(input) + @root.with_context_from_headers(input) { super } + end + + def handle_update(input) + @root.with_context_from_headers(input) { super } + end + end + + class WorkflowOutbound < Temporalio::Worker::Interceptor::Workflow::Outbound + def initialize(root, next_interceptor) + super(next_interceptor) + @root = root + end + + def execute_activity(input) + @root.context_to_headers(input) + super + end + + def execute_local_activity(input) + @root.context_to_headers(input) + super + end + + def signal_child_workflow(input) + @root.context_to_headers(input) + super + end + + def signal_external_workflow(input) + @root.context_to_headers(input) + super + end + + def start_child_workflow(input) + @root.context_to_headers(input) + super + end + end + + class ActivityInbound < Temporalio::Worker::Interceptor::Activity::Inbound + def initialize(root, next_interceptor) + super(next_interceptor) + @root = root + end + + def execute(input) + @root.with_context_from_headers(input) { super } + end + end + end +end diff --git a/context_propagation/say_hello_activity.rb b/context_propagation/say_hello_activity.rb new file mode 100644 index 0000000..d53ec49 --- /dev/null +++ b/context_propagation/say_hello_activity.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'temporalio/activity' + +module ContextPropagation + class SayHelloActivity < Temporalio::Activity::Definition + def execute(name) + Temporalio::Activity::Context.current.logger.info("Activity called by user: #{Thread.current[:my_user]}") + "Hello, #{name}! (called by user #{Thread.current[:my_user]})" + end + end +end diff --git a/context_propagation/say_hello_workflow.rb b/context_propagation/say_hello_workflow.rb new file mode 100644 index 0000000..e032352 --- /dev/null +++ b/context_propagation/say_hello_workflow.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'temporalio/workflow' +require_relative 'say_hello_activity' + +module ContextPropagation + class SayHelloWorkflow < Temporalio::Workflow::Definition + def execute(name) + Temporalio::Workflow.logger.info("Workflow called by user: #{Thread.current[:my_user]}") + + # Wait for signal then run activity + Temporalio::Workflow.wait_condition { @complete } + Temporalio::Workflow.execute_activity(SayHelloActivity, name, start_to_close_timeout: 5 * 60) + end + + workflow_signal + def signal_complete + Temporalio::Workflow.logger.info("Signal called by user: #{Thread.current[:my_user]}") + @complete = true + end + + workflow_query + def complete? + Temporalio::Workflow.logger.info("Query called by user: #{Thread.current[:my_user]}") + @complete + end + end +end diff --git a/context_propagation/starter.rb b/context_propagation/starter.rb new file mode 100644 index 0000000..9faa0df --- /dev/null +++ b/context_propagation/starter.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'logger' +require 'temporalio/client' +require_relative 'interceptor' +require_relative 'say_hello_workflow' + +# Create a Temporal client +client = Temporalio::Client.connect( + 'localhost:7233', + 'default', + logger: Logger.new($stdout, level: Logger::INFO), + # Add the context propagation interceptor to propagate the :my_user + # thread/fiber local + interceptors: [ContextPropagation::Interceptor.new(:my_user)] +) + +# Set user as "Alice" which will get propagated in a distributed way through +# the workflow and activity via Temporal headers +Thread.current[:my_user] = 'Alice' + +# Start workflow, send signal, wait for completion, issue query +puts 'Executing workflow with user "Alice"' +handle = client.start_workflow( + ContextPropagation::SayHelloWorkflow, + 'Bob', + id: 'context-propagation-sample-workflow-id', + task_queue: 'context-propagation-sample' +) +handle.signal(ContextPropagation::SayHelloWorkflow.signal_complete) +result = handle.result +_is_complete = handle.query(ContextPropagation::SayHelloWorkflow.complete?) +puts "Workflow result: #{result}" diff --git a/context_propagation/worker.rb b/context_propagation/worker.rb new file mode 100644 index 0000000..3d7f33f --- /dev/null +++ b/context_propagation/worker.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require_relative 'interceptor' +require_relative 'say_hello_activity' +require_relative 'say_hello_workflow' +require 'logger' +require 'temporalio/client' +require 'temporalio/worker' + +# Create a Temporal client +client = Temporalio::Client.connect( + 'localhost:7233', + 'default', + logger: Logger.new($stdout, level: Logger::INFO), + # Add the context propagation interceptor to propagate the :my_user thread/fiber local + interceptors: [ContextPropagation::Interceptor.new(:my_user)] +) + +# Create worker with the activity and workflow +worker = Temporalio::Worker.new( + client:, + task_queue: 'context-propagation-sample', + activities: [ContextPropagation::SayHelloActivity], + workflows: [ContextPropagation::SayHelloWorkflow] +) + +# Run the worker until SIGINT +puts 'Starting worker (ctrl+c to exit)' +worker.run(shutdown_signals: ['SIGINT']) diff --git a/message_passing_simple/README.md b/message_passing_simple/README.md new file mode 100644 index 0000000..1e90a6c --- /dev/null +++ b/message_passing_simple/README.md @@ -0,0 +1,25 @@ +# Message Passing Simple + +This sample has a simple workflow that accepts signals, queries, and updates. + +To run, first see [README.md](../README.md) for prerequisites. Then, in another terminal, start the Ruby worker +from this directory: + + bundle exec ruby worker.rb + +Finally in another terminal, use the Ruby client to the workflow from this directory: + + bundle exec ruby starter.rb + +The Ruby code will invoke the workflow which will execute two activities and return. The output of the final command +should be: + +``` +Starting workflow +Supported languages: ["chinese", "english"] +Language changed: english -> chinese +Language changed: chinese -> arabic +Workflow result: مرحبا بالعالم +``` + +There are also [tests](../test/message_passing_simple/greeting_workflow_test.rb). \ No newline at end of file diff --git a/message_passing_simple/call_greeting_service.rb b/message_passing_simple/call_greeting_service.rb new file mode 100644 index 0000000..5cbe643 --- /dev/null +++ b/message_passing_simple/call_greeting_service.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'temporalio/activity' + +module MessagePassingSimple + class CallGreetingService < Temporalio::Activity::Definition + def execute(to_language) + # Simulate a network call + sleep(0.2) + # This intentionally returns nil on not found + CallGreetingService.greetings[to_language.to_sym] + end + + def self.greetings + @greetings ||= { + arabic: 'مرحبا بالعالم', + chinese: '你好,世界', + english: 'Hello, world', + french: 'Bonjour, monde', + hindi: 'नमस्ते दुनिया', + portuguese: 'Olá mundo', + spanish: 'Hola mundo' + } + end + end +end diff --git a/message_passing_simple/greeting_workflow.rb b/message_passing_simple/greeting_workflow.rb new file mode 100644 index 0000000..ca0211c --- /dev/null +++ b/message_passing_simple/greeting_workflow.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'temporalio/workflow' +require_relative 'call_greeting_service' + +module MessagePassingSimple + # A workflow that that returns a greeting in one of multiple supported languages. + # + # It exposes a query to obtain the current language, a signal to approve the workflow so that it is allowed to return + # its result, and two updates for changing the current language and receiving the previous language in response. + # + # One of the update handlers only mutates and returns local workflow state; the other update handler executes an + # activity which calls a remote service, giving access to language translations which are not available in local + # workflow state. + class GreetingWorkflow < Temporalio::Workflow::Definition + # This is the equivalent of: + # workflow_query + # def language + # @language + # end + workflow_query_attr_reader :language + + def initialize + @greetings = { chinese: '你好,世界', english: 'Hello, world' } + @language = :english + end + + def execute + # In addition to waiting for the `approve` signal, we also wait for all handlers to finish. Otherwise, the + # workflow might return its result while an async set_language_using_activity update is in progress. + Temporalio::Workflow.wait_condition { @approved_for_release && Temporalio::Workflow.all_handlers_finished? } + @greetings[@language] + end + + workflow_query + def languages(input) + # A query handler returns a value: it can inspect but must not mutate the Workflow state. + if input['include_unsupported'] + CallGreetingService.greetings.keys.sort + else + @greetings.keys.sort + end + end + + workflow_signal + def approve(input) + # A signal handler mutates the workflow state but cannot return a value. + @approved_for_release = true + @approver_name = input['name'] + end + + workflow_update + def set_language(new_language) # rubocop:disable Naming/AccessorMethodName + # An update handler can mutate the workflow state and return a value. + prev = @language.to_sym + @language = new_language.to_sym + prev + end + + workflow_update_validator(:set_language) + def validate_set_language(new_language) + # In an update validator you raise any exception to reject the update. + raise "#{new_language} is not supported" unless @greetings.include?(new_language.to_sym) + end + + workflow_update + def apply_language_with_lookup(new_language) + # Call an activity if it's not there. + unless @greetings.include?(new_language.to_sym) + # We use a mutex so that, if this handler is executed multiple times, each execution can schedule the activity + # only when the previously scheduled activity has completed. This ensures that multiple calls to + # apply_language_with_lookup are processed in order. + @apply_language_mutex ||= Mutex.new + @apply_language_mutex.synchronize do + greeting = Temporalio::Workflow.execute_activity( + CallGreetingService, new_language, start_to_close_timeout: 10 + ) + # The requested language might not be supported by the remote service. If so, we raise ApplicationError, which + # will fail the update. The WorkflowExecutionUpdateAccepted event will still be added to history. (Update + # validators can be used to reject updates before any event is written to history, but they cannot be async, + # and so we cannot use an update validator for this purpose.) + raise Temporalio::Error::ApplicationError, "Greeting service does not support #{new_language}" unless greeting + + @greetings[new_language.to_sym] = greeting + end + end + set_language(new_language) + end + end +end diff --git a/message_passing_simple/starter.rb b/message_passing_simple/starter.rb new file mode 100644 index 0000000..fd18f2e --- /dev/null +++ b/message_passing_simple/starter.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'temporalio/client' +require_relative 'greeting_workflow' + +# Create a client +client = Temporalio::Client.connect('localhost:7233', 'default') + +# Start the workflow +puts 'Starting workflow' +handle = client.start_workflow( + MessagePassingSimple::GreetingWorkflow, + id: 'message-passing-simple-sample-workflow-id', + task_queue: 'message-passing-simple-sample' +) + +# Send a query +supported_languages = handle.query(MessagePassingSimple::GreetingWorkflow.languages, { include_unsupported: false }) +puts "Supported languages: #{supported_languages}" + +# Execute an update +prev_language = handle.execute_update(MessagePassingSimple::GreetingWorkflow.set_language, :chinese) +curr_language = handle.query(MessagePassingSimple::GreetingWorkflow.language) +puts "Language changed: #{prev_language} -> #{curr_language}" + +# Start an update and then wait for it to complete +update_handle = handle.start_update( + MessagePassingSimple::GreetingWorkflow.apply_language_with_lookup, + :arabic, + wait_for_stage: Temporalio::Client::WorkflowUpdateWaitStage::ACCEPTED +) +prev_language = update_handle.result +curr_language = handle.query(MessagePassingSimple::GreetingWorkflow.language) +puts "Language changed: #{prev_language} -> #{curr_language}" + +# Send signal and wait for workflow to complete +handle.signal(MessagePassingSimple::GreetingWorkflow.approve, { name: 'John Q. Approver' }) +puts "Workflow result: #{handle.result}" diff --git a/message_passing_simple/worker.rb b/message_passing_simple/worker.rb new file mode 100644 index 0000000..5eff569 --- /dev/null +++ b/message_passing_simple/worker.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require_relative 'call_greeting_service' +require_relative 'greeting_workflow' +require 'logger' +require 'temporalio/client' +require 'temporalio/worker' + +# Create a client +client = Temporalio::Client.connect('localhost:7233', 'default') + +# Create worker with the activity and workflow +worker = Temporalio::Worker.new( + client:, + task_queue: 'message-passing-simple-sample', + activities: [MessagePassingSimple::CallGreetingService], + workflows: [MessagePassingSimple::GreetingWorkflow] +) + +# Run the worker until SIGINT +puts 'Starting worker (ctrl+c to exit)' +worker.run(shutdown_signals: ['SIGINT']) diff --git a/test/activity_simple/my_workflow_test.rb b/test/activity_simple/my_workflow_test.rb new file mode 100644 index 0000000..1045c58 --- /dev/null +++ b/test/activity_simple/my_workflow_test.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'test' +require 'activity_simple/my_workflow' +require 'securerandom' +require 'temporalio/testing' +require 'temporalio/worker' + +module ActivitySimple + class ActivitySimpleTest < Test + # Demonstrates mocking out activities + class MockSelectFromDatabase < Temporalio::Activity::Definition + activity_name :SelectFromDatabase + + def execute(table) + "mocked value from #{table}" + end + end + + def test_workflow + # Run test server until completion of the block + Temporalio::Testing::WorkflowEnvironment.start_local do |env| + # Run worker until completion of the block + worker = Temporalio::Worker.new( + client: env.client, + task_queue: "tq-#{SecureRandom.uuid}", + activities: [MockSelectFromDatabase, MyActivities::AppendSuffix], + workflows: [MyWorkflow] + ) + worker.run do + # Run workflow + assert_equal( + 'mocked value from some-db-table ', + env.client.execute_workflow(MyWorkflow, id: "wf-#{SecureRandom.uuid}", task_queue: worker.task_queue) + ) + end + end + end + end +end diff --git a/test/activity_worker/activity_worker_test.rb b/test/activity_worker/activity_worker_test.rb index 21a07ef..9489e09 100644 --- a/test/activity_worker/activity_worker_test.rb +++ b/test/activity_worker/activity_worker_test.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'test' -require 'activity_worker/activity' +require 'activity_worker/say_hello_activity' require 'temporalio/testing' module ActivityWorker diff --git a/test/context_propagation/say_hello_workflow_test.rb b/test/context_propagation/say_hello_workflow_test.rb new file mode 100644 index 0000000..c6f9d7a --- /dev/null +++ b/test/context_propagation/say_hello_workflow_test.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'test' +require 'context_propagation/interceptor' +require 'context_propagation/say_hello_activity' +require 'context_propagation/say_hello_workflow' +require 'securerandom' +require 'temporalio/client' +require 'temporalio/testing' +require 'temporalio/worker' + +module ContextPropagation + class SayHelloWorkflowTest < Test + def test_workflow + Temporalio::Testing::WorkflowEnvironment.start_local do |env| + # Add the interceptor to the client + client = Temporalio::Client.new(**env.client.options.with( + interceptors: [Interceptor.new(:my_user)] + ).to_h) + + worker = Temporalio::Worker.new( + client:, + task_queue: "tq-#{SecureRandom.uuid}", + activities: [SayHelloActivity], + workflows: [SayHelloWorkflow] + ) + worker.run do + # Start workflow with thread local, send signal, confirm result + Thread.current[:my_user] = 'some-user' + handle = client.start_workflow( + SayHelloWorkflow, + 'Temporal', + id: "wf-#{SecureRandom.uuid}", + task_queue: worker.task_queue + ) + handle.signal(SayHelloWorkflow.signal_complete) + assert_equal('Hello, Temporal! (called by user some-user)', handle.result) + end + end + end + end +end diff --git a/test/message_passing_simple/greeting_workflow_test.rb b/test/message_passing_simple/greeting_workflow_test.rb new file mode 100644 index 0000000..40f1368 --- /dev/null +++ b/test/message_passing_simple/greeting_workflow_test.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'test' +require 'message_passing_simple/call_greeting_service' +require 'message_passing_simple/greeting_workflow' +require 'securerandom' +require 'temporalio/testing' +require 'temporalio/worker' + +module MessagePassingSimple + class GreetingWorkflowTest < Test + def with_worker_running + Temporalio::Testing::WorkflowEnvironment.start_local do |env| + worker = Temporalio::Worker.new( + client: env.client, + task_queue: "tq-#{SecureRandom.uuid}", + activities: [CallGreetingService], + workflows: [GreetingWorkflow] + ) + worker.run { yield env.client, worker } + end + end + + def test_queries + with_worker_running do |client, worker| + handle = client.start_workflow(GreetingWorkflow, id: "wf-#{SecureRandom.uuid}", task_queue: worker.task_queue) + assert_equal 'english', handle.query(GreetingWorkflow.language) + assert_equal %w[chinese english], handle.query(GreetingWorkflow.languages, { include_unsupported: false }) + assert_equal CallGreetingService.greetings.keys.map(&:to_s).sort, + handle.query(GreetingWorkflow.languages, { include_unsupported: true }) + end + end + + def test_set_language + with_worker_running do |client, worker| + handle = client.start_workflow(GreetingWorkflow, id: "wf-#{SecureRandom.uuid}", task_queue: worker.task_queue) + assert_equal 'english', handle.query(GreetingWorkflow.language) + prev_language = handle.execute_update(GreetingWorkflow.set_language, :chinese) + assert_equal 'english', prev_language + assert_equal 'chinese', handle.query(GreetingWorkflow.language) + end + end + + def test_set_language_invalid + with_worker_running do |client, worker| + handle = client.start_workflow(GreetingWorkflow, id: "wf-#{SecureRandom.uuid}", task_queue: worker.task_queue) + assert_equal 'english', handle.query(GreetingWorkflow.language) + assert_raises(Temporalio::Error::WorkflowUpdateFailedError) do + handle.execute_update(GreetingWorkflow.set_language, :arabic) + end + end + end + + def test_apply_language_with_lookup + with_worker_running do |client, worker| + handle = client.start_workflow(GreetingWorkflow, id: "wf-#{SecureRandom.uuid}", task_queue: worker.task_queue) + prev_language = handle.execute_update(GreetingWorkflow.apply_language_with_lookup, :arabic) + assert_equal 'english', prev_language + assert_equal 'arabic', handle.query(GreetingWorkflow.language) + end + end + end +end