temporalio 0.2.0-x86_64-darwin
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +23 -0
- data/Rakefile +387 -0
- data/lib/temporalio/activity/complete_async_error.rb +11 -0
- data/lib/temporalio/activity/context.rb +107 -0
- data/lib/temporalio/activity/definition.rb +77 -0
- data/lib/temporalio/activity/info.rb +63 -0
- data/lib/temporalio/activity.rb +69 -0
- data/lib/temporalio/api/batch/v1/message.rb +31 -0
- data/lib/temporalio/api/cloud/cloudservice/v1/request_response.rb +93 -0
- data/lib/temporalio/api/cloud/cloudservice/v1/service.rb +25 -0
- data/lib/temporalio/api/cloud/cloudservice.rb +3 -0
- data/lib/temporalio/api/cloud/identity/v1/message.rb +36 -0
- data/lib/temporalio/api/cloud/namespace/v1/message.rb +35 -0
- data/lib/temporalio/api/cloud/operation/v1/message.rb +27 -0
- data/lib/temporalio/api/cloud/region/v1/message.rb +23 -0
- data/lib/temporalio/api/command/v1/message.rb +46 -0
- data/lib/temporalio/api/common/v1/grpc_status.rb +23 -0
- data/lib/temporalio/api/common/v1/message.rb +41 -0
- data/lib/temporalio/api/enums/v1/batch_operation.rb +22 -0
- data/lib/temporalio/api/enums/v1/command_type.rb +21 -0
- data/lib/temporalio/api/enums/v1/common.rb +26 -0
- data/lib/temporalio/api/enums/v1/event_type.rb +21 -0
- data/lib/temporalio/api/enums/v1/failed_cause.rb +26 -0
- data/lib/temporalio/api/enums/v1/namespace.rb +23 -0
- data/lib/temporalio/api/enums/v1/query.rb +22 -0
- data/lib/temporalio/api/enums/v1/reset.rb +23 -0
- data/lib/temporalio/api/enums/v1/schedule.rb +21 -0
- data/lib/temporalio/api/enums/v1/task_queue.rb +25 -0
- data/lib/temporalio/api/enums/v1/update.rb +22 -0
- data/lib/temporalio/api/enums/v1/workflow.rb +30 -0
- data/lib/temporalio/api/errordetails/v1/message.rb +42 -0
- data/lib/temporalio/api/export/v1/message.rb +24 -0
- data/lib/temporalio/api/failure/v1/message.rb +35 -0
- data/lib/temporalio/api/filter/v1/message.rb +27 -0
- data/lib/temporalio/api/history/v1/message.rb +90 -0
- data/lib/temporalio/api/namespace/v1/message.rb +31 -0
- data/lib/temporalio/api/nexus/v1/message.rb +40 -0
- data/lib/temporalio/api/operatorservice/v1/request_response.rb +49 -0
- data/lib/temporalio/api/operatorservice/v1/service.rb +23 -0
- data/lib/temporalio/api/operatorservice.rb +3 -0
- data/lib/temporalio/api/protocol/v1/message.rb +23 -0
- data/lib/temporalio/api/query/v1/message.rb +27 -0
- data/lib/temporalio/api/replication/v1/message.rb +26 -0
- data/lib/temporalio/api/schedule/v1/message.rb +42 -0
- data/lib/temporalio/api/sdk/v1/enhanced_stack_trace.rb +25 -0
- data/lib/temporalio/api/sdk/v1/task_complete_metadata.rb +21 -0
- data/lib/temporalio/api/sdk/v1/user_metadata.rb +23 -0
- data/lib/temporalio/api/sdk/v1/workflow_metadata.rb +23 -0
- data/lib/temporalio/api/taskqueue/v1/message.rb +45 -0
- data/lib/temporalio/api/update/v1/message.rb +33 -0
- data/lib/temporalio/api/version/v1/message.rb +26 -0
- data/lib/temporalio/api/workflow/v1/message.rb +43 -0
- data/lib/temporalio/api/workflowservice/v1/request_response.rb +189 -0
- data/lib/temporalio/api/workflowservice/v1/service.rb +23 -0
- data/lib/temporalio/api/workflowservice.rb +3 -0
- data/lib/temporalio/api.rb +13 -0
- data/lib/temporalio/cancellation.rb +150 -0
- data/lib/temporalio/client/activity_id_reference.rb +32 -0
- data/lib/temporalio/client/async_activity_handle.rb +110 -0
- data/lib/temporalio/client/connection/cloud_service.rb +648 -0
- data/lib/temporalio/client/connection/operator_service.rb +249 -0
- data/lib/temporalio/client/connection/service.rb +41 -0
- data/lib/temporalio/client/connection/workflow_service.rb +1218 -0
- data/lib/temporalio/client/connection.rb +270 -0
- data/lib/temporalio/client/interceptor.rb +316 -0
- data/lib/temporalio/client/workflow_execution.rb +103 -0
- data/lib/temporalio/client/workflow_execution_count.rb +36 -0
- data/lib/temporalio/client/workflow_execution_status.rb +18 -0
- data/lib/temporalio/client/workflow_handle.rb +446 -0
- data/lib/temporalio/client/workflow_query_reject_condition.rb +14 -0
- data/lib/temporalio/client/workflow_update_handle.rb +67 -0
- data/lib/temporalio/client/workflow_update_wait_stage.rb +17 -0
- data/lib/temporalio/client.rb +404 -0
- data/lib/temporalio/common_enums.rb +24 -0
- data/lib/temporalio/converters/data_converter.rb +102 -0
- data/lib/temporalio/converters/failure_converter.rb +200 -0
- data/lib/temporalio/converters/payload_codec.rb +26 -0
- data/lib/temporalio/converters/payload_converter/binary_null.rb +34 -0
- data/lib/temporalio/converters/payload_converter/binary_plain.rb +35 -0
- data/lib/temporalio/converters/payload_converter/binary_protobuf.rb +42 -0
- data/lib/temporalio/converters/payload_converter/composite.rb +62 -0
- data/lib/temporalio/converters/payload_converter/encoding.rb +35 -0
- data/lib/temporalio/converters/payload_converter/json_plain.rb +44 -0
- data/lib/temporalio/converters/payload_converter/json_protobuf.rb +41 -0
- data/lib/temporalio/converters/payload_converter.rb +73 -0
- data/lib/temporalio/converters.rb +9 -0
- data/lib/temporalio/error/failure.rb +219 -0
- data/lib/temporalio/error.rb +147 -0
- data/lib/temporalio/internal/bridge/3.1/temporalio_bridge.bundle +0 -0
- data/lib/temporalio/internal/bridge/3.2/temporalio_bridge.bundle +0 -0
- data/lib/temporalio/internal/bridge/3.3/temporalio_bridge.bundle +0 -0
- data/lib/temporalio/internal/bridge/api/activity_result/activity_result.rb +34 -0
- data/lib/temporalio/internal/bridge/api/activity_task/activity_task.rb +31 -0
- data/lib/temporalio/internal/bridge/api/child_workflow/child_workflow.rb +33 -0
- data/lib/temporalio/internal/bridge/api/common/common.rb +26 -0
- data/lib/temporalio/internal/bridge/api/core_interface.rb +36 -0
- data/lib/temporalio/internal/bridge/api/external_data/external_data.rb +27 -0
- data/lib/temporalio/internal/bridge/api/workflow_activation/workflow_activation.rb +52 -0
- data/lib/temporalio/internal/bridge/api/workflow_commands/workflow_commands.rb +54 -0
- data/lib/temporalio/internal/bridge/api/workflow_completion/workflow_completion.rb +30 -0
- data/lib/temporalio/internal/bridge/api.rb +3 -0
- data/lib/temporalio/internal/bridge/client.rb +90 -0
- data/lib/temporalio/internal/bridge/runtime.rb +53 -0
- data/lib/temporalio/internal/bridge/testing.rb +46 -0
- data/lib/temporalio/internal/bridge/worker.rb +83 -0
- data/lib/temporalio/internal/bridge.rb +36 -0
- data/lib/temporalio/internal/client/implementation.rb +525 -0
- data/lib/temporalio/internal/proto_utils.rb +54 -0
- data/lib/temporalio/internal/worker/activity_worker.rb +345 -0
- data/lib/temporalio/internal/worker/multi_runner.rb +169 -0
- data/lib/temporalio/internal.rb +7 -0
- data/lib/temporalio/retry_policy.rb +51 -0
- data/lib/temporalio/runtime.rb +271 -0
- data/lib/temporalio/scoped_logger.rb +96 -0
- data/lib/temporalio/search_attributes.rb +300 -0
- data/lib/temporalio/testing/activity_environment.rb +132 -0
- data/lib/temporalio/testing/workflow_environment.rb +137 -0
- data/lib/temporalio/testing.rb +10 -0
- data/lib/temporalio/version.rb +5 -0
- data/lib/temporalio/worker/activity_executor/fiber.rb +49 -0
- data/lib/temporalio/worker/activity_executor/thread_pool.rb +254 -0
- data/lib/temporalio/worker/activity_executor.rb +55 -0
- data/lib/temporalio/worker/interceptor.rb +88 -0
- data/lib/temporalio/worker/tuner.rb +151 -0
- data/lib/temporalio/worker.rb +426 -0
- data/lib/temporalio/workflow_history.rb +22 -0
- data/lib/temporalio.rb +7 -0
- data/temporalio.gemspec +28 -0
- metadata +191 -0
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'temporalio/activity'
|
4
|
+
require 'temporalio/cancellation'
|
5
|
+
require 'temporalio/converters/payload_converter'
|
6
|
+
require 'temporalio/worker/activity_executor'
|
7
|
+
|
8
|
+
module Temporalio
|
9
|
+
module Testing
|
10
|
+
# Test environment for testing activities.
|
11
|
+
#
|
12
|
+
# Users can create this environment and then use {run} to execute activities on it. Often, since mutable things like
|
13
|
+
# cancellation can be set, users create this for each activity that is run. There is no real performance penalty for
|
14
|
+
# creating an environment for every run.
|
15
|
+
class ActivityEnvironment
|
16
|
+
# @return [Activity::Info] The activity info used by default. This is frozen, but can be dup'd and mutated to pass
|
17
|
+
# in to {initialize}.
|
18
|
+
def self.default_info
|
19
|
+
@default_info ||= Activity::Info.new(
|
20
|
+
activity_id: 'test',
|
21
|
+
activity_type: 'unknown',
|
22
|
+
attempt: 1,
|
23
|
+
current_attempt_scheduled_time: Time.at(0),
|
24
|
+
heartbeat_details: [],
|
25
|
+
heartbeat_timeout: nil,
|
26
|
+
local?: false,
|
27
|
+
schedule_to_close_timeout: 1.0,
|
28
|
+
scheduled_time: Time.at(0),
|
29
|
+
start_to_close_timeout: 1.0,
|
30
|
+
started_time: Time.at(0),
|
31
|
+
task_queue: 'test',
|
32
|
+
task_token: String.new('test', encoding: Encoding::ASCII_8BIT),
|
33
|
+
workflow_id: 'test',
|
34
|
+
workflow_namespace: 'default',
|
35
|
+
workflow_run_id: 'test-run',
|
36
|
+
workflow_type: 'test'
|
37
|
+
).freeze
|
38
|
+
end
|
39
|
+
|
40
|
+
# Create a test environment for activities.
|
41
|
+
#
|
42
|
+
# @param info [Activity::Info] Value for {Activity::Context#info}.
|
43
|
+
# @param on_heartbeat [Proc(Array), nil] Proc that is called with all heartbeat details when
|
44
|
+
# {Activity::Context#heartbeat} is called.
|
45
|
+
# @param cancellation [Cancellation] Value for {Activity::Context#cancellation}.
|
46
|
+
# @param worker_shutdown_cancellation [Cancellation] Value for {Activity::Context#worker_shutdown_cancellation}.
|
47
|
+
# @param payload_converter [Converters::PayloadConverter] Value for {Activity::Context#payload_converter}.
|
48
|
+
# @param logger [Logger] Value for {Activity::Context#logger}.
|
49
|
+
# @param activity_executors [Hash<Symbol, Worker::ActivityExecutor>] Executors that activities can run within.
|
50
|
+
def initialize(
|
51
|
+
info: ActivityEnvironment.default_info,
|
52
|
+
on_heartbeat: nil,
|
53
|
+
cancellation: Cancellation.new,
|
54
|
+
worker_shutdown_cancellation: Cancellation.new,
|
55
|
+
payload_converter: Converters::PayloadConverter.default,
|
56
|
+
logger: Logger.new(nil),
|
57
|
+
activity_executors: Worker::ActivityExecutor.defaults
|
58
|
+
)
|
59
|
+
@info = info
|
60
|
+
@on_heartbeat = on_heartbeat
|
61
|
+
@cancellation = cancellation
|
62
|
+
@worker_shutdown_cancellation = worker_shutdown_cancellation
|
63
|
+
@payload_converter = payload_converter
|
64
|
+
@logger = logger
|
65
|
+
@activity_executors = activity_executors
|
66
|
+
end
|
67
|
+
|
68
|
+
# Run an activity and returns its result or raises its exception.
|
69
|
+
#
|
70
|
+
# @param activity [Activity, Class<Activity>, Activity::Definition] Activity to run.
|
71
|
+
# @param args [Array<Object>] Arguments to the activity.
|
72
|
+
# @return Activity result.
|
73
|
+
def run(activity, *args)
|
74
|
+
defn = Activity::Definition.from_activity(activity)
|
75
|
+
executor = @activity_executors[defn.executor]
|
76
|
+
raise ArgumentError, "Unknown executor: #{defn.executor}" if executor.nil?
|
77
|
+
|
78
|
+
queue = Queue.new
|
79
|
+
executor.execute_activity(defn) do
|
80
|
+
Activity::Context._current_executor = executor
|
81
|
+
executor.set_activity_context(defn, Context.new(
|
82
|
+
info: @info.dup,
|
83
|
+
on_heartbeat: @on_heartbeat,
|
84
|
+
cancellation: @cancellation,
|
85
|
+
worker_shutdown_cancellation: @worker_shutdown_cancellation,
|
86
|
+
payload_converter: @payload_converter,
|
87
|
+
logger: @logger
|
88
|
+
))
|
89
|
+
queue.push([defn.proc.call(*args), nil])
|
90
|
+
rescue Exception => e # rubocop:disable Lint/RescueException Intentionally capturing all exceptions
|
91
|
+
queue.push([nil, e])
|
92
|
+
ensure
|
93
|
+
executor.set_activity_context(defn, nil)
|
94
|
+
Activity::Context._current_executor = nil
|
95
|
+
end
|
96
|
+
|
97
|
+
result, err = queue.pop
|
98
|
+
raise err unless err.nil?
|
99
|
+
|
100
|
+
result
|
101
|
+
end
|
102
|
+
|
103
|
+
# @!visibility private
|
104
|
+
class Context < Activity::Context
|
105
|
+
attr_reader :info, :cancellation, :worker_shutdown_cancellation, :payload_converter, :logger
|
106
|
+
|
107
|
+
def initialize( # rubocop:disable Lint/MissingSuper
|
108
|
+
info: ActivityEnvironment.default_info,
|
109
|
+
on_heartbeat: nil,
|
110
|
+
cancellation: Cancellation.new,
|
111
|
+
worker_shutdown_cancellation: Cancellation.new,
|
112
|
+
payload_converter: Converters::PayloadConverter.default,
|
113
|
+
logger: Logger.new(nil)
|
114
|
+
)
|
115
|
+
@info = info
|
116
|
+
@on_heartbeat = on_heartbeat
|
117
|
+
@cancellation = cancellation
|
118
|
+
@worker_shutdown_cancellation = worker_shutdown_cancellation
|
119
|
+
@payload_converter = payload_converter
|
120
|
+
@logger = logger
|
121
|
+
end
|
122
|
+
|
123
|
+
# @!visibility private
|
124
|
+
def heartbeat(*details)
|
125
|
+
@on_heartbeat&.call(details)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
private_constant :Context
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'temporalio/client'
|
4
|
+
require 'temporalio/converters'
|
5
|
+
require 'temporalio/internal/bridge/testing'
|
6
|
+
require 'temporalio/runtime'
|
7
|
+
require 'temporalio/version'
|
8
|
+
|
9
|
+
module Temporalio
|
10
|
+
module Testing
|
11
|
+
# Test environment with a Temporal server for running workflows and more.
|
12
|
+
class WorkflowEnvironment
|
13
|
+
# @return [Client] Client for the server.
|
14
|
+
attr_reader :client
|
15
|
+
|
16
|
+
# Start a local dev server. This is a full Temporal dev server from the CLI that by default downloaded to tmp if
|
17
|
+
# not already present. The dev server is run as a child process. All options that start with +dev_server_+ are for
|
18
|
+
# this specific implementation and therefore are not stable and may be changed as the underlying implementation
|
19
|
+
# changes.
|
20
|
+
#
|
21
|
+
# If a block is given it is passed the environment and the environment is shut down after. If a block is not
|
22
|
+
# given, the environment is returned and {shutdown} needs to be called manually.
|
23
|
+
#
|
24
|
+
# @param namespace [String] Namespace for the server.
|
25
|
+
# @param data_converter [Converters::DataConverter] Data converter for the client.
|
26
|
+
# @param interceptors [Array<Client::Interceptor>] Interceptors for the client.
|
27
|
+
# @param logger [Logger] Logger for the client.
|
28
|
+
# @param default_workflow_query_reject_condition [WorkflowQueryRejectCondition, nil] Default rejection condition
|
29
|
+
# for the client.
|
30
|
+
# @param ip [String] IP to bind to.
|
31
|
+
# @param port [Integer, nil] Port to bind on, or +nil+ for random.
|
32
|
+
# @param ui [Boolean] If +true+, also starts the UI.
|
33
|
+
# @param runtime [Runtime] Runtime for the server and client.
|
34
|
+
# @param dev_server_existing_path [String, nil] Existing CLI path to use instead of downloading and caching to
|
35
|
+
# tmp.
|
36
|
+
# @param dev_server_database_filename [String, nil] Persistent SQLite filename to use across local server runs.
|
37
|
+
# Default of +nil+ means in-memory only.
|
38
|
+
# @param dev_server_log_format [String] Log format for CLI dev server.
|
39
|
+
# @param dev_server_log_level [String] Log level for CLI dev server.
|
40
|
+
# @param dev_server_download_version [String] Version of dev server to download and cache.
|
41
|
+
# @param dev_server_download_dest_dir [String, nil] Where to download. Defaults to tmp.
|
42
|
+
# @param dev_server_extra_args [Array<String>] Any extra arguments for the CLI dev server.
|
43
|
+
#
|
44
|
+
# @yield [environment] If a block is given, it is called with the environment and upon complete the environment is
|
45
|
+
# shutdown.
|
46
|
+
# @yieldparam environment [WorkflowEnvironment] Environment that is shut down upon block completion.
|
47
|
+
#
|
48
|
+
# @return [WorkflowEnvironment, Object] Started local server environment with client if there was no block given,
|
49
|
+
# or block result if block was given.
|
50
|
+
def self.start_local(
|
51
|
+
namespace: 'default',
|
52
|
+
data_converter: Converters::DataConverter.default,
|
53
|
+
interceptors: [],
|
54
|
+
logger: Logger.new($stdout, level: Logger::WARN),
|
55
|
+
default_workflow_query_reject_condition: nil,
|
56
|
+
ip: '127.0.0.1',
|
57
|
+
port: nil,
|
58
|
+
ui: false, # rubocop:disable Naming/MethodParameterName
|
59
|
+
runtime: Runtime.default,
|
60
|
+
dev_server_existing_path: nil,
|
61
|
+
dev_server_database_filename: nil,
|
62
|
+
dev_server_log_format: 'pretty',
|
63
|
+
dev_server_log_level: 'warn',
|
64
|
+
dev_server_download_version: 'default',
|
65
|
+
dev_server_download_dest_dir: nil,
|
66
|
+
dev_server_extra_args: []
|
67
|
+
)
|
68
|
+
server_options = Internal::Bridge::Testing::EphemeralServer::StartDevServerOptions.new(
|
69
|
+
existing_path: dev_server_existing_path,
|
70
|
+
sdk_name: 'sdk-ruby',
|
71
|
+
sdk_version: VERSION,
|
72
|
+
download_version: dev_server_download_version,
|
73
|
+
download_dest_dir: dev_server_download_dest_dir,
|
74
|
+
namespace:,
|
75
|
+
ip:,
|
76
|
+
port:,
|
77
|
+
database_filename: dev_server_database_filename,
|
78
|
+
ui:,
|
79
|
+
log_format: dev_server_log_format,
|
80
|
+
log_level: dev_server_log_level,
|
81
|
+
extra_args: dev_server_extra_args
|
82
|
+
)
|
83
|
+
core_server = Internal::Bridge::Testing::EphemeralServer.start_dev_server(runtime._core_runtime, server_options)
|
84
|
+
# Try to connect, shutdown if we can't
|
85
|
+
begin
|
86
|
+
client = Client.connect(
|
87
|
+
core_server.target,
|
88
|
+
namespace,
|
89
|
+
data_converter:,
|
90
|
+
interceptors:,
|
91
|
+
logger:,
|
92
|
+
default_workflow_query_reject_condition:,
|
93
|
+
runtime:
|
94
|
+
)
|
95
|
+
server = Ephemeral.new(client, core_server)
|
96
|
+
rescue StandardError
|
97
|
+
core_server.shutdown
|
98
|
+
raise
|
99
|
+
end
|
100
|
+
if block_given?
|
101
|
+
begin
|
102
|
+
yield server
|
103
|
+
ensure
|
104
|
+
server.shutdown
|
105
|
+
end
|
106
|
+
else
|
107
|
+
server
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Create workflow environment to an existing server with the given client.
|
112
|
+
#
|
113
|
+
# @param client [Client] Client to existing server.
|
114
|
+
def initialize(client)
|
115
|
+
@client = client
|
116
|
+
end
|
117
|
+
|
118
|
+
# Shutdown this workflow environment.
|
119
|
+
def shutdown
|
120
|
+
# Do nothing by default
|
121
|
+
end
|
122
|
+
|
123
|
+
# @!visibility private
|
124
|
+
class Ephemeral < WorkflowEnvironment
|
125
|
+
def initialize(client, core_server)
|
126
|
+
super(client)
|
127
|
+
@core_server = core_server
|
128
|
+
end
|
129
|
+
|
130
|
+
# @!visibility private
|
131
|
+
def shutdown
|
132
|
+
@core_server.shutdown
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'temporalio/error'
|
4
|
+
require 'temporalio/worker/activity_executor'
|
5
|
+
|
6
|
+
module Temporalio
|
7
|
+
class Worker
|
8
|
+
class ActivityExecutor
|
9
|
+
# Activity executor for scheduling activites as fibers.
|
10
|
+
class Fiber
|
11
|
+
# @return [Fiber] Default/shared Fiber executor instance.
|
12
|
+
def self.default
|
13
|
+
@default ||= new
|
14
|
+
end
|
15
|
+
|
16
|
+
# @see ActivityExecutor.initialize_activity
|
17
|
+
def initialize_activity(defn)
|
18
|
+
# If there is not a current scheduler, we're going to preemptively
|
19
|
+
# fail the registration
|
20
|
+
return unless ::Fiber.current_scheduler.nil?
|
21
|
+
|
22
|
+
raise ArgumentError, "Activity '#{defn.name}' wants a fiber executor but no current fiber scheduler"
|
23
|
+
end
|
24
|
+
|
25
|
+
# @see ActivityExecutor.initialize_activity
|
26
|
+
def execute_activity(_defn, &)
|
27
|
+
::Fiber.schedule(&)
|
28
|
+
end
|
29
|
+
|
30
|
+
# @see ActivityExecutor.activity_context
|
31
|
+
def activity_context
|
32
|
+
::Fiber[:temporal_activity_context]
|
33
|
+
end
|
34
|
+
|
35
|
+
# @see ActivityExecutor.set_activity_context
|
36
|
+
def set_activity_context(defn, context)
|
37
|
+
::Fiber[:temporal_activity_context] = context
|
38
|
+
# If they have opted in to raising on cancel, wire that up
|
39
|
+
return unless defn.cancel_raise
|
40
|
+
|
41
|
+
fiber = ::Fiber.current
|
42
|
+
context&.cancellation&.add_cancel_callback do
|
43
|
+
fiber.raise(Error::CanceledError.new('Activity canceled'))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,254 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Much of this logic taken from
|
4
|
+
# https://github.com/ruby-concurrency/concurrent-ruby/blob/044020f44b36930b863b930f3ee8fa1e9f750469/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb,
|
5
|
+
# see MIT license at
|
6
|
+
# https://github.com/ruby-concurrency/concurrent-ruby/blob/044020f44b36930b863b930f3ee8fa1e9f750469/LICENSE.txt
|
7
|
+
|
8
|
+
module Temporalio
|
9
|
+
class Worker
|
10
|
+
class ActivityExecutor
|
11
|
+
# Activity executor for scheduling activities in their own thread. This implementation is a stripped down form of
|
12
|
+
# Concurrent Ruby's `CachedThreadPool`.
|
13
|
+
class ThreadPool < ActivityExecutor
|
14
|
+
# @return [ThreadPool] Default/shared thread pool executor instance with unlimited max threads.
|
15
|
+
def self.default
|
16
|
+
@default ||= new
|
17
|
+
end
|
18
|
+
|
19
|
+
# @!visibility private
|
20
|
+
def self._monotonic_time
|
21
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Create a new thread pool executor that creates threads as needed.
|
25
|
+
#
|
26
|
+
# @param max_threads [Integer, nil] Maximum number of thread workers to create, or nil for unlimited max.
|
27
|
+
# @param idle_timeout [Float] Number of seconds before a thread worker with no work should be stopped. Note,
|
28
|
+
# the check of whether a thread worker is idle is only done on each new activity.
|
29
|
+
def initialize(max_threads: nil, idle_timeout: 20) # rubocop:disable Lint/MissingSuper
|
30
|
+
@max_threads = max_threads
|
31
|
+
@idle_timeout = idle_timeout
|
32
|
+
|
33
|
+
@mutex = Mutex.new
|
34
|
+
@pool = []
|
35
|
+
@ready = []
|
36
|
+
@queue = []
|
37
|
+
@scheduled_task_count = 0
|
38
|
+
@completed_task_count = 0
|
39
|
+
@largest_length = 0
|
40
|
+
@workers_counter = 0
|
41
|
+
@prune_interval = @idle_timeout / 2
|
42
|
+
@next_prune_time = ThreadPool._monotonic_time + @prune_interval
|
43
|
+
end
|
44
|
+
|
45
|
+
# @see ActivityExecutor.execute_activity
|
46
|
+
def execute_activity(_defn, &block)
|
47
|
+
@mutex.synchronize do
|
48
|
+
locked_assign_worker(&block) || locked_enqueue(&block)
|
49
|
+
@scheduled_task_count += 1
|
50
|
+
locked_prune_pool if @next_prune_time < ThreadPool._monotonic_time
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# @see ActivityExecutor.activity_context
|
55
|
+
def activity_context
|
56
|
+
Thread.current[:temporal_activity_context]
|
57
|
+
end
|
58
|
+
|
59
|
+
# @see ActivityExecutor.set_activity_context
|
60
|
+
def set_activity_context(defn, context)
|
61
|
+
Thread.current[:temporal_activity_context] = context
|
62
|
+
# If they have opted in to raising on cancel, wire that up
|
63
|
+
return unless defn.cancel_raise
|
64
|
+
|
65
|
+
thread = Thread.current
|
66
|
+
context&.cancellation&.add_cancel_callback do
|
67
|
+
thread.raise(Error::CanceledError.new('Activity canceled')) if thread[:temporal_activity_context] == context
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [Integer] The largest number of threads that have been created in the pool since construction.
|
72
|
+
def largest_length
|
73
|
+
@mutex.synchronize { @largest_length }
|
74
|
+
end
|
75
|
+
|
76
|
+
# @return [Integer] The number of tasks that have been scheduled for execution on the pool since construction.
|
77
|
+
def scheduled_task_count
|
78
|
+
@mutex.synchronize { @scheduled_task_count }
|
79
|
+
end
|
80
|
+
|
81
|
+
# @return [Integer] The number of tasks that have been completed by the pool since construction.
|
82
|
+
def completed_task_count
|
83
|
+
@mutex.synchronize { @completed_task_count }
|
84
|
+
end
|
85
|
+
|
86
|
+
# @return [Integer] The number of threads that are actively executing tasks.
|
87
|
+
def active_count
|
88
|
+
@mutex.synchronize { @pool.length - @ready.length }
|
89
|
+
end
|
90
|
+
|
91
|
+
# @return [Integer] The number of threads currently in the pool.
|
92
|
+
def length
|
93
|
+
@mutex.synchronize { @pool.length }
|
94
|
+
end
|
95
|
+
|
96
|
+
# @return [Integer] The number of tasks in the queue awaiting execution.
|
97
|
+
def queue_length
|
98
|
+
@mutex.synchronize { @queue.length }
|
99
|
+
end
|
100
|
+
|
101
|
+
# Gracefully shutdown each thread when it is done with its current task. This should not be called until all
|
102
|
+
# workers using this executor are complete. This does not need to be called at all on program exit (e.g. for the
|
103
|
+
# global default).
|
104
|
+
def shutdown
|
105
|
+
@mutex.synchronize do
|
106
|
+
# Stop all workers
|
107
|
+
@pool.each(&:stop)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Kill each thread. This should not be called until all workers using this executor are complete. This does not
|
112
|
+
# need to be called at all on program exit (e.g. for the global default).
|
113
|
+
def kill
|
114
|
+
@mutex.synchronize do
|
115
|
+
# Kill all workers
|
116
|
+
@pool.each(&:kill)
|
117
|
+
@pool.clear
|
118
|
+
@ready.clear
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# @!visibility private
|
123
|
+
def _remove_busy_worker(worker)
|
124
|
+
@mutex.synchronize { locked_remove_busy_worker(worker) }
|
125
|
+
end
|
126
|
+
|
127
|
+
# @!visibility private
|
128
|
+
def _ready_worker(worker, last_message)
|
129
|
+
@mutex.synchronize { locked_ready_worker(worker, last_message) }
|
130
|
+
end
|
131
|
+
|
132
|
+
# @!visibility private
|
133
|
+
def _worker_died(worker)
|
134
|
+
@mutex.synchronize { locked_worker_died(worker) }
|
135
|
+
end
|
136
|
+
|
137
|
+
# @!visibility private
|
138
|
+
def _worker_task_completed
|
139
|
+
@mutex.synchronize { @completed_task_count += 1 }
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
def locked_assign_worker(&block)
|
145
|
+
# keep growing if the pool is not at the minimum yet
|
146
|
+
worker, = @ready.pop || locked_add_busy_worker
|
147
|
+
if worker
|
148
|
+
worker << block
|
149
|
+
true
|
150
|
+
else
|
151
|
+
false
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def locked_enqueue(&block)
|
156
|
+
@queue << block
|
157
|
+
end
|
158
|
+
|
159
|
+
def locked_add_busy_worker
|
160
|
+
return if @max_threads && @pool.size >= @max_threads
|
161
|
+
|
162
|
+
@workers_counter += 1
|
163
|
+
@pool << (worker = Worker.new(self, @workers_counter))
|
164
|
+
@largest_length = @pool.length if @pool.length > @largest_length
|
165
|
+
worker
|
166
|
+
end
|
167
|
+
|
168
|
+
def locked_prune_pool
|
169
|
+
now = ThreadPool._monotonic_time
|
170
|
+
stopped_workers = 0
|
171
|
+
while !@ready.empty? && (@pool.size - stopped_workers).positive?
|
172
|
+
worker, last_message = @ready.first
|
173
|
+
break unless now - last_message > @idle_timeout
|
174
|
+
|
175
|
+
stopped_workers += 1
|
176
|
+
@ready.shift
|
177
|
+
worker << :stop
|
178
|
+
|
179
|
+
end
|
180
|
+
|
181
|
+
@next_prune_time = ThreadPool._monotonic_time + @prune_interval
|
182
|
+
end
|
183
|
+
|
184
|
+
def locked_remove_busy_worker(worker)
|
185
|
+
@pool.delete(worker)
|
186
|
+
end
|
187
|
+
|
188
|
+
def locked_ready_worker(worker, last_message)
|
189
|
+
block = @queue.shift
|
190
|
+
if block
|
191
|
+
worker << block
|
192
|
+
else
|
193
|
+
@ready.push([worker, last_message])
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def locked_worker_died(worker)
|
198
|
+
locked_remove_busy_worker(worker)
|
199
|
+
replacement_worker = locked_add_busy_worker
|
200
|
+
locked_ready_worker(replacement_worker, ThreadPool._monotonic_time) if replacement_worker
|
201
|
+
end
|
202
|
+
|
203
|
+
# @!visibility private
|
204
|
+
class Worker
|
205
|
+
def initialize(pool, id)
|
206
|
+
@queue = Queue.new
|
207
|
+
@thread = Thread.new(@queue, pool) do |my_queue, my_pool|
|
208
|
+
catch(:stop) do
|
209
|
+
loop do
|
210
|
+
case block = my_queue.pop
|
211
|
+
when :stop
|
212
|
+
pool._remove_busy_worker(self)
|
213
|
+
throw :stop
|
214
|
+
else
|
215
|
+
begin
|
216
|
+
block.call
|
217
|
+
my_pool._worker_task_completed
|
218
|
+
my_pool._ready_worker(self, ThreadPool._monotonic_time)
|
219
|
+
rescue StandardError => e
|
220
|
+
# Ignore
|
221
|
+
warn("Unexpected activity block error: #{e}")
|
222
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
223
|
+
warn("Unexpected activity block exception: #{e}")
|
224
|
+
my_pool._worker_died(self)
|
225
|
+
throw :stop
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
@thread.name = "activity-thread-#{id}"
|
232
|
+
end
|
233
|
+
|
234
|
+
# @!visibility private
|
235
|
+
def <<(block)
|
236
|
+
@queue << block
|
237
|
+
end
|
238
|
+
|
239
|
+
# @!visibility private
|
240
|
+
def stop
|
241
|
+
@queue << :stop
|
242
|
+
end
|
243
|
+
|
244
|
+
# @!visibility private
|
245
|
+
def kill
|
246
|
+
@thread.kill
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
private_constant :Worker
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'temporalio/worker/activity_executor/fiber'
|
4
|
+
require 'temporalio/worker/activity_executor/thread_pool'
|
5
|
+
|
6
|
+
module Temporalio
|
7
|
+
class Worker
|
8
|
+
# Base class to be extended by activity executor implementations. Most users will not use this, but rather keep with
|
9
|
+
# the two defaults of thread pool and fiber executors.
|
10
|
+
class ActivityExecutor
|
11
|
+
# @return [Hash<Symbol, ActivityExecutor>] Default set of executors (immutable).
|
12
|
+
def self.defaults
|
13
|
+
@defaults ||= {
|
14
|
+
default: ThreadPool.default,
|
15
|
+
thread_pool: ThreadPool.default,
|
16
|
+
fiber: Fiber.default
|
17
|
+
}.freeze
|
18
|
+
end
|
19
|
+
|
20
|
+
# Initialize an activity. This is called on worker initialize for every activity that will use this executor. This
|
21
|
+
# allows executor implementations to do eager validation based on the definition. This does not have to be
|
22
|
+
# implemented and the default is a no-op.
|
23
|
+
#
|
24
|
+
# @param defn [Activity::Definition] Activity definition.
|
25
|
+
def initialize_activity(defn)
|
26
|
+
# Default no-op
|
27
|
+
end
|
28
|
+
|
29
|
+
# Execute the given block in the executor. The block is built to never raise and need no arguments. Implementers
|
30
|
+
# must implement this.
|
31
|
+
#
|
32
|
+
# @param defn [Activity::Definition] Activity definition.
|
33
|
+
# @yield Block to execute.
|
34
|
+
def execute_activity(defn, &)
|
35
|
+
raise NotImplementedError
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [Activity::Context, nil] Get the current activity context. This is called by users from inside the
|
39
|
+
# activity. Implementers must implement this.
|
40
|
+
def activity_context
|
41
|
+
raise NotImplementedError
|
42
|
+
end
|
43
|
+
|
44
|
+
# Set the current activity context (or unset if nil). This is called by the system from within the block given to
|
45
|
+
# {execute_activity} with a context before user code is executed and with nil after user code is complete.
|
46
|
+
# Implementers must implement this.
|
47
|
+
#
|
48
|
+
# @param defn [Activity::Definition] Activity definition.
|
49
|
+
# @param context [Activity::Context, nil] The value to set.
|
50
|
+
def set_activity_context(defn, context)
|
51
|
+
raise NotImplementedError
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|