temporalio 0.2.0-x86_64-linux

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.
Files changed (128) hide show
  1. checksums.yaml +7 -0
  2. data/lib/temporalio/activity/complete_async_error.rb +11 -0
  3. data/lib/temporalio/activity/context.rb +107 -0
  4. data/lib/temporalio/activity/definition.rb +77 -0
  5. data/lib/temporalio/activity/info.rb +63 -0
  6. data/lib/temporalio/activity.rb +69 -0
  7. data/lib/temporalio/api/batch/v1/message.rb +31 -0
  8. data/lib/temporalio/api/cloud/cloudservice/v1/request_response.rb +93 -0
  9. data/lib/temporalio/api/cloud/cloudservice/v1/service.rb +25 -0
  10. data/lib/temporalio/api/cloud/cloudservice.rb +3 -0
  11. data/lib/temporalio/api/cloud/identity/v1/message.rb +36 -0
  12. data/lib/temporalio/api/cloud/namespace/v1/message.rb +35 -0
  13. data/lib/temporalio/api/cloud/operation/v1/message.rb +27 -0
  14. data/lib/temporalio/api/cloud/region/v1/message.rb +23 -0
  15. data/lib/temporalio/api/command/v1/message.rb +46 -0
  16. data/lib/temporalio/api/common/v1/grpc_status.rb +23 -0
  17. data/lib/temporalio/api/common/v1/message.rb +41 -0
  18. data/lib/temporalio/api/enums/v1/batch_operation.rb +22 -0
  19. data/lib/temporalio/api/enums/v1/command_type.rb +21 -0
  20. data/lib/temporalio/api/enums/v1/common.rb +26 -0
  21. data/lib/temporalio/api/enums/v1/event_type.rb +21 -0
  22. data/lib/temporalio/api/enums/v1/failed_cause.rb +26 -0
  23. data/lib/temporalio/api/enums/v1/namespace.rb +23 -0
  24. data/lib/temporalio/api/enums/v1/query.rb +22 -0
  25. data/lib/temporalio/api/enums/v1/reset.rb +23 -0
  26. data/lib/temporalio/api/enums/v1/schedule.rb +21 -0
  27. data/lib/temporalio/api/enums/v1/task_queue.rb +25 -0
  28. data/lib/temporalio/api/enums/v1/update.rb +22 -0
  29. data/lib/temporalio/api/enums/v1/workflow.rb +30 -0
  30. data/lib/temporalio/api/errordetails/v1/message.rb +42 -0
  31. data/lib/temporalio/api/export/v1/message.rb +24 -0
  32. data/lib/temporalio/api/failure/v1/message.rb +35 -0
  33. data/lib/temporalio/api/filter/v1/message.rb +27 -0
  34. data/lib/temporalio/api/history/v1/message.rb +90 -0
  35. data/lib/temporalio/api/namespace/v1/message.rb +31 -0
  36. data/lib/temporalio/api/nexus/v1/message.rb +40 -0
  37. data/lib/temporalio/api/operatorservice/v1/request_response.rb +49 -0
  38. data/lib/temporalio/api/operatorservice/v1/service.rb +23 -0
  39. data/lib/temporalio/api/operatorservice.rb +3 -0
  40. data/lib/temporalio/api/protocol/v1/message.rb +23 -0
  41. data/lib/temporalio/api/query/v1/message.rb +27 -0
  42. data/lib/temporalio/api/replication/v1/message.rb +26 -0
  43. data/lib/temporalio/api/schedule/v1/message.rb +42 -0
  44. data/lib/temporalio/api/sdk/v1/enhanced_stack_trace.rb +25 -0
  45. data/lib/temporalio/api/sdk/v1/task_complete_metadata.rb +21 -0
  46. data/lib/temporalio/api/sdk/v1/user_metadata.rb +23 -0
  47. data/lib/temporalio/api/sdk/v1/workflow_metadata.rb +23 -0
  48. data/lib/temporalio/api/taskqueue/v1/message.rb +45 -0
  49. data/lib/temporalio/api/update/v1/message.rb +33 -0
  50. data/lib/temporalio/api/version/v1/message.rb +26 -0
  51. data/lib/temporalio/api/workflow/v1/message.rb +43 -0
  52. data/lib/temporalio/api/workflowservice/v1/request_response.rb +189 -0
  53. data/lib/temporalio/api/workflowservice/v1/service.rb +23 -0
  54. data/lib/temporalio/api/workflowservice.rb +3 -0
  55. data/lib/temporalio/api.rb +13 -0
  56. data/lib/temporalio/cancellation.rb +150 -0
  57. data/lib/temporalio/client/activity_id_reference.rb +32 -0
  58. data/lib/temporalio/client/async_activity_handle.rb +110 -0
  59. data/lib/temporalio/client/connection/cloud_service.rb +648 -0
  60. data/lib/temporalio/client/connection/operator_service.rb +249 -0
  61. data/lib/temporalio/client/connection/service.rb +41 -0
  62. data/lib/temporalio/client/connection/workflow_service.rb +1218 -0
  63. data/lib/temporalio/client/connection.rb +270 -0
  64. data/lib/temporalio/client/interceptor.rb +316 -0
  65. data/lib/temporalio/client/workflow_execution.rb +103 -0
  66. data/lib/temporalio/client/workflow_execution_count.rb +36 -0
  67. data/lib/temporalio/client/workflow_execution_status.rb +18 -0
  68. data/lib/temporalio/client/workflow_handle.rb +446 -0
  69. data/lib/temporalio/client/workflow_query_reject_condition.rb +14 -0
  70. data/lib/temporalio/client/workflow_update_handle.rb +67 -0
  71. data/lib/temporalio/client/workflow_update_wait_stage.rb +17 -0
  72. data/lib/temporalio/client.rb +404 -0
  73. data/lib/temporalio/common_enums.rb +24 -0
  74. data/lib/temporalio/converters/data_converter.rb +102 -0
  75. data/lib/temporalio/converters/failure_converter.rb +200 -0
  76. data/lib/temporalio/converters/payload_codec.rb +26 -0
  77. data/lib/temporalio/converters/payload_converter/binary_null.rb +34 -0
  78. data/lib/temporalio/converters/payload_converter/binary_plain.rb +35 -0
  79. data/lib/temporalio/converters/payload_converter/binary_protobuf.rb +42 -0
  80. data/lib/temporalio/converters/payload_converter/composite.rb +62 -0
  81. data/lib/temporalio/converters/payload_converter/encoding.rb +35 -0
  82. data/lib/temporalio/converters/payload_converter/json_plain.rb +44 -0
  83. data/lib/temporalio/converters/payload_converter/json_protobuf.rb +41 -0
  84. data/lib/temporalio/converters/payload_converter.rb +73 -0
  85. data/lib/temporalio/converters.rb +9 -0
  86. data/lib/temporalio/error/failure.rb +219 -0
  87. data/lib/temporalio/error.rb +147 -0
  88. data/lib/temporalio/internal/bridge/3.1/temporalio_bridge.so +0 -0
  89. data/lib/temporalio/internal/bridge/3.2/temporalio_bridge.so +0 -0
  90. data/lib/temporalio/internal/bridge/3.3/temporalio_bridge.so +0 -0
  91. data/lib/temporalio/internal/bridge/api/activity_result/activity_result.rb +34 -0
  92. data/lib/temporalio/internal/bridge/api/activity_task/activity_task.rb +31 -0
  93. data/lib/temporalio/internal/bridge/api/child_workflow/child_workflow.rb +33 -0
  94. data/lib/temporalio/internal/bridge/api/common/common.rb +26 -0
  95. data/lib/temporalio/internal/bridge/api/core_interface.rb +36 -0
  96. data/lib/temporalio/internal/bridge/api/external_data/external_data.rb +27 -0
  97. data/lib/temporalio/internal/bridge/api/workflow_activation/workflow_activation.rb +52 -0
  98. data/lib/temporalio/internal/bridge/api/workflow_commands/workflow_commands.rb +54 -0
  99. data/lib/temporalio/internal/bridge/api/workflow_completion/workflow_completion.rb +30 -0
  100. data/lib/temporalio/internal/bridge/api.rb +3 -0
  101. data/lib/temporalio/internal/bridge/client.rb +90 -0
  102. data/lib/temporalio/internal/bridge/runtime.rb +53 -0
  103. data/lib/temporalio/internal/bridge/testing.rb +46 -0
  104. data/lib/temporalio/internal/bridge/worker.rb +83 -0
  105. data/lib/temporalio/internal/bridge.rb +36 -0
  106. data/lib/temporalio/internal/client/implementation.rb +525 -0
  107. data/lib/temporalio/internal/proto_utils.rb +54 -0
  108. data/lib/temporalio/internal/worker/activity_worker.rb +345 -0
  109. data/lib/temporalio/internal/worker/multi_runner.rb +169 -0
  110. data/lib/temporalio/internal.rb +7 -0
  111. data/lib/temporalio/retry_policy.rb +51 -0
  112. data/lib/temporalio/runtime.rb +271 -0
  113. data/lib/temporalio/scoped_logger.rb +96 -0
  114. data/lib/temporalio/search_attributes.rb +300 -0
  115. data/lib/temporalio/testing/activity_environment.rb +132 -0
  116. data/lib/temporalio/testing/workflow_environment.rb +137 -0
  117. data/lib/temporalio/testing.rb +10 -0
  118. data/lib/temporalio/version.rb +5 -0
  119. data/lib/temporalio/worker/activity_executor/fiber.rb +49 -0
  120. data/lib/temporalio/worker/activity_executor/thread_pool.rb +254 -0
  121. data/lib/temporalio/worker/activity_executor.rb +55 -0
  122. data/lib/temporalio/worker/interceptor.rb +88 -0
  123. data/lib/temporalio/worker/tuner.rb +151 -0
  124. data/lib/temporalio/worker.rb +426 -0
  125. data/lib/temporalio/workflow_history.rb +22 -0
  126. data/lib/temporalio.rb +7 -0
  127. data/temporalio.gemspec +28 -0
  128. metadata +189 -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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'temporalio/testing/activity_environment'
4
+ require 'temporalio/testing/workflow_environment'
5
+
6
+ module Temporalio
7
+ # Module for all testing environments.
8
+ module Testing
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Temporalio
4
+ VERSION = '0.2.0'
5
+ 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