activejob-temporal 0.1.0
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/CHANGELOG.md +130 -0
- data/LICENSE +21 -0
- data/README.md +198 -0
- data/activejob-temporal.gemspec +58 -0
- data/api/job_payload_schema.json +318 -0
- data/bin/temporal-worker +295 -0
- data/lib/activejob/temporal/active_job_handler_source.rb +84 -0
- data/lib/activejob/temporal/activities/aj_runner_activity.rb +454 -0
- data/lib/activejob/temporal/activities/best_effort_side_effects.rb +49 -0
- data/lib/activejob/temporal/activities/dependency_status_activity.rb +160 -0
- data/lib/activejob/temporal/activities/rate_limit_activity.rb +41 -0
- data/lib/activejob/temporal/adapter.rb +257 -0
- data/lib/activejob/temporal/audit_log.rb +118 -0
- data/lib/activejob/temporal/batch_enqueue_result.rb +110 -0
- data/lib/activejob/temporal/batch_enqueuer.rb +141 -0
- data/lib/activejob/temporal/bind_policy.rb +44 -0
- data/lib/activejob/temporal/cancel/batch_canceller.rb +154 -0
- data/lib/activejob/temporal/cancel/batch_summary.rb +45 -0
- data/lib/activejob/temporal/cancel.rb +236 -0
- data/lib/activejob/temporal/certificate_watcher.rb +76 -0
- data/lib/activejob/temporal/chain_options.rb +83 -0
- data/lib/activejob/temporal/child_workflow_options.rb +102 -0
- data/lib/activejob/temporal/client.rb +215 -0
- data/lib/activejob/temporal/conditional_enqueue.rb +56 -0
- data/lib/activejob/temporal/configurable.rb +55 -0
- data/lib/activejob/temporal/configuration.rb +981 -0
- data/lib/activejob/temporal/configured_job_compatibility.rb +44 -0
- data/lib/activejob/temporal/connection_worker_pool.rb +88 -0
- data/lib/activejob/temporal/dead_letter_payload_validation.rb +34 -0
- data/lib/activejob/temporal/dead_letter_queue.rb +163 -0
- data/lib/activejob/temporal/dependency_options.rb +134 -0
- data/lib/activejob/temporal/external_operation.rb +193 -0
- data/lib/activejob/temporal/health_check_server.rb +159 -0
- data/lib/activejob/temporal/http_line_reader.rb +36 -0
- data/lib/activejob/temporal/inspect.rb +184 -0
- data/lib/activejob/temporal/job_descriptor.rb +37 -0
- data/lib/activejob/temporal/job_payload_builder.rb +209 -0
- data/lib/activejob/temporal/job_payload_chain_builder.rb +106 -0
- data/lib/activejob/temporal/job_payload_child_workflows.rb +127 -0
- data/lib/activejob/temporal/job_payload_dependencies.rb +40 -0
- data/lib/activejob/temporal/job_payload_rate_limits.rb +53 -0
- data/lib/activejob/temporal/job_payload_workflow_interactions.rb +31 -0
- data/lib/activejob/temporal/job_tags.rb +40 -0
- data/lib/activejob/temporal/locales/en.yml +126 -0
- data/lib/activejob/temporal/logger.rb +214 -0
- data/lib/activejob/temporal/metrics_server.rb +150 -0
- data/lib/activejob/temporal/middleware/chain.rb +106 -0
- data/lib/activejob/temporal/middleware.rb +11 -0
- data/lib/activejob/temporal/observability/datadog.rb +167 -0
- data/lib/activejob/temporal/observability/opentelemetry.rb +107 -0
- data/lib/activejob/temporal/observability/prometheus.rb +271 -0
- data/lib/activejob/temporal/observability.rb +260 -0
- data/lib/activejob/temporal/payload.rb +415 -0
- data/lib/activejob/temporal/payload_encryption.rb +215 -0
- data/lib/activejob/temporal/payload_serializers/json.rb +23 -0
- data/lib/activejob/temporal/payload_serializers/marshal.rb +53 -0
- data/lib/activejob/temporal/payload_serializers/message_pack.rb +59 -0
- data/lib/activejob/temporal/payload_serializers.rb +37 -0
- data/lib/activejob/temporal/payload_storage.rb +103 -0
- data/lib/activejob/temporal/rails_environment_loader.rb +143 -0
- data/lib/activejob/temporal/rate_limit_options.rb +94 -0
- data/lib/activejob/temporal/rate_limiters/memory.rb +198 -0
- data/lib/activejob/temporal/reload_signal_queue.rb +40 -0
- data/lib/activejob/temporal/retry_handler_extractor.rb +361 -0
- data/lib/activejob/temporal/retry_mapper.rb +264 -0
- data/lib/activejob/temporal/schedulable.rb +60 -0
- data/lib/activejob/temporal/schedule.rb +181 -0
- data/lib/activejob/temporal/schedule_options.rb +105 -0
- data/lib/activejob/temporal/search_attributes.rb +173 -0
- data/lib/activejob/temporal/signal_query.rb +161 -0
- data/lib/activejob/temporal/signal_query_options.rb +106 -0
- data/lib/activejob/temporal/temporal_options.rb +114 -0
- data/lib/activejob/temporal/tls_file.rb +45 -0
- data/lib/activejob/temporal/transaction_safety.rb +39 -0
- data/lib/activejob/temporal/version.rb +7 -0
- data/lib/activejob/temporal/visibility_query.rb +13 -0
- data/lib/activejob/temporal/worker_client_reloader.rb +34 -0
- data/lib/activejob/temporal/worker_health.rb +117 -0
- data/lib/activejob/temporal/worker_pool.rb +408 -0
- data/lib/activejob/temporal/workflow_enqueuer.rb +271 -0
- data/lib/activejob/temporal/workflow_enqueuer_batch.rb +17 -0
- data/lib/activejob/temporal/workflow_id_builder.rb +155 -0
- data/lib/activejob/temporal/workflow_identity.rb +62 -0
- data/lib/activejob/temporal/workflows/aj_workflow.rb +282 -0
- data/lib/activejob/temporal/workflows/dead_letter_support.rb +134 -0
- data/lib/activejob/temporal/workflows/dead_letter_workflow.rb +114 -0
- data/lib/activejob/temporal/workflows/workflow_chaining.rb +194 -0
- data/lib/activejob/temporal/workflows/workflow_child_workflows.rb +140 -0
- data/lib/activejob/temporal/workflows/workflow_continue_as_new.rb +44 -0
- data/lib/activejob/temporal/workflows/workflow_dependencies.rb +115 -0
- data/lib/activejob/temporal/workflows/workflow_execution_steps.rb +22 -0
- data/lib/activejob/temporal/workflows/workflow_interactions.rb +215 -0
- data/lib/activejob/temporal/workflows/workflow_local_activities.rb +29 -0
- data/lib/activejob/temporal/workflows/workflow_nexus.rb +15 -0
- data/lib/activejob/temporal/workflows/workflow_versioning.rb +21 -0
- data/lib/activejob/temporal.rb +297 -0
- data/lib/activejob-temporal.rb +3 -0
- metadata +423 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_job"
|
|
4
|
+
require "active_support/concern"
|
|
5
|
+
|
|
6
|
+
module ActiveJob
|
|
7
|
+
module Temporal
|
|
8
|
+
module SignalQueryOptions
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
HANDLER_NAME_PATTERN = /\A[a-zA-Z_]\w*\z/
|
|
12
|
+
BUILT_IN_SIGNAL_NAMES = %w[pause resume].freeze
|
|
13
|
+
BUILT_IN_QUERY_NAMES = %w[pause_reason paused phase signals state].freeze
|
|
14
|
+
BUILT_IN_UPDATE_NAMES = [].freeze
|
|
15
|
+
|
|
16
|
+
module ClassMethods
|
|
17
|
+
def temporal_signal(name, &block)
|
|
18
|
+
handler_name = normalize_handler_name(name)
|
|
19
|
+
validate_custom_handler_name!(handler_name, "signal", BUILT_IN_SIGNAL_NAMES)
|
|
20
|
+
local_temporal_signal_handlers[handler_name] = block || default_signal_handler(handler_name)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def temporal_query(name, &block)
|
|
24
|
+
raise ArgumentError, "temporal_query requires a block" unless block
|
|
25
|
+
|
|
26
|
+
handler_name = normalize_handler_name(name)
|
|
27
|
+
validate_custom_handler_name!(handler_name, "query", BUILT_IN_QUERY_NAMES)
|
|
28
|
+
local_temporal_query_handlers[handler_name] = block
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def temporal_update(name, &block)
|
|
32
|
+
raise ArgumentError, "temporal_update requires a block" unless block
|
|
33
|
+
|
|
34
|
+
handler_name = normalize_handler_name(name)
|
|
35
|
+
validate_custom_handler_name!(handler_name, "update", BUILT_IN_UPDATE_NAMES)
|
|
36
|
+
local_temporal_update_handlers[handler_name] = block
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def temporal_signal_handlers
|
|
40
|
+
inherited_temporal_handlers(:temporal_signal_handlers).merge(local_temporal_signal_handlers)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def temporal_query_handlers
|
|
44
|
+
inherited_temporal_handlers(:temporal_query_handlers).merge(local_temporal_query_handlers)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def temporal_update_handlers
|
|
48
|
+
inherited_temporal_handlers(:temporal_update_handlers).merge(local_temporal_update_handlers)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def temporal_signal_handler_names
|
|
52
|
+
temporal_signal_handlers.keys
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def temporal_query_handler_names
|
|
56
|
+
temporal_query_handlers.keys
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def temporal_update_handler_names
|
|
60
|
+
temporal_update_handlers.keys
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def local_temporal_signal_handlers
|
|
66
|
+
@local_temporal_signal_handlers ||= {}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def local_temporal_query_handlers
|
|
70
|
+
@local_temporal_query_handlers ||= {}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def local_temporal_update_handlers
|
|
74
|
+
@local_temporal_update_handlers ||= {}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def normalize_handler_name(name)
|
|
78
|
+
handler_name = name.to_s
|
|
79
|
+
return handler_name if handler_name.match?(HANDLER_NAME_PATTERN)
|
|
80
|
+
|
|
81
|
+
raise ArgumentError, "signal and query names must start with a letter or underscore and contain word chars"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def validate_custom_handler_name!(handler_name, handler_type, built_in_names)
|
|
85
|
+
return unless built_in_names.include?(handler_name)
|
|
86
|
+
|
|
87
|
+
raise ArgumentError, "#{handler_type} name #{handler_name.inspect} is reserved by ActiveJob::Temporal"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def inherited_temporal_handlers(method_name)
|
|
91
|
+
return {} unless superclass.respond_to?(method_name)
|
|
92
|
+
|
|
93
|
+
superclass.public_send(method_name)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def default_signal_handler(handler_name)
|
|
97
|
+
lambda do |state, *args|
|
|
98
|
+
state[handler_name] = args.length == 1 ? args.first : args
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
ActiveJob::Base.include(ActiveJob::Temporal::SignalQueryOptions) if defined?(ActiveJob::Base)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_job"
|
|
4
|
+
|
|
5
|
+
module ActiveJob
|
|
6
|
+
module Temporal
|
|
7
|
+
# Provides per-job timeout configuration via the +temporal_options+ class method.
|
|
8
|
+
#
|
|
9
|
+
# @example Basic timeout override
|
|
10
|
+
# class QuickJob < ApplicationJob
|
|
11
|
+
# temporal_options start_to_close_timeout: 30.seconds
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# @example Long-running job with heartbeat
|
|
15
|
+
# class DataProcessingJob < ApplicationJob
|
|
16
|
+
# temporal_options(
|
|
17
|
+
# start_to_close_timeout: 2.hours,
|
|
18
|
+
# heartbeat_timeout: 30.seconds
|
|
19
|
+
# )
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# @example All timeout types configured
|
|
23
|
+
# class CriticalJob < ApplicationJob
|
|
24
|
+
# temporal_options(
|
|
25
|
+
# start_to_close_timeout: 10.minutes,
|
|
26
|
+
# schedule_to_start_timeout: 1.minute,
|
|
27
|
+
# schedule_to_close_timeout: 15.minutes,
|
|
28
|
+
# heartbeat_timeout: 10.seconds
|
|
29
|
+
# )
|
|
30
|
+
# end
|
|
31
|
+
module TemporalOptions
|
|
32
|
+
extend ActiveSupport::Concern
|
|
33
|
+
|
|
34
|
+
VALID_TIMEOUT_KEYS = %i[
|
|
35
|
+
start_to_close_timeout
|
|
36
|
+
schedule_to_close_timeout
|
|
37
|
+
schedule_to_start_timeout
|
|
38
|
+
heartbeat_timeout
|
|
39
|
+
].freeze
|
|
40
|
+
|
|
41
|
+
# rubocop:disable Metrics/BlockLength
|
|
42
|
+
class_methods do
|
|
43
|
+
# Define Temporal activity timeout options for this job class.
|
|
44
|
+
#
|
|
45
|
+
# @param options [Hash] Timeout configuration options
|
|
46
|
+
# @option options [Integer, ActiveSupport::Duration] :start_to_close_timeout
|
|
47
|
+
# Maximum execution time for a single activity attempt
|
|
48
|
+
# @option options [Integer, ActiveSupport::Duration] :schedule_to_close_timeout
|
|
49
|
+
# Total time including all retries from schedule to completion
|
|
50
|
+
# @option options [Integer, ActiveSupport::Duration] :schedule_to_start_timeout
|
|
51
|
+
# Maximum wait time before activity starts after scheduling
|
|
52
|
+
# @option options [Integer, ActiveSupport::Duration] :heartbeat_timeout
|
|
53
|
+
# Maximum interval between heartbeats before activity is considered failed
|
|
54
|
+
#
|
|
55
|
+
# @return [Hash] The stored timeout options (when called without arguments)
|
|
56
|
+
#
|
|
57
|
+
# @note At least one of +start_to_close_timeout+ or +schedule_to_close_timeout+
|
|
58
|
+
# must be specified. Temporal SDK will validate this requirement.
|
|
59
|
+
#
|
|
60
|
+
# @note Timeout values can be specified as either integers (seconds) or
|
|
61
|
+
# ActiveSupport::Duration objects (e.g., 2.hours, 30.seconds)
|
|
62
|
+
def temporal_options(options = nil)
|
|
63
|
+
if options
|
|
64
|
+
validate_timeout_keys!(options)
|
|
65
|
+
@temporal_options = normalize_timeout_values(options)
|
|
66
|
+
end
|
|
67
|
+
@temporal_options || {}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# Validates that only recognized timeout keys are provided
|
|
73
|
+
#
|
|
74
|
+
# @param options [Hash] The options hash to validate
|
|
75
|
+
# @raise [ArgumentError] If invalid keys are present
|
|
76
|
+
# @api private
|
|
77
|
+
def validate_timeout_keys!(options)
|
|
78
|
+
invalid_keys = options.keys - VALID_TIMEOUT_KEYS
|
|
79
|
+
return if invalid_keys.empty?
|
|
80
|
+
|
|
81
|
+
raise ArgumentError,
|
|
82
|
+
"Invalid temporal_options keys: #{invalid_keys.join(', ')}. " \
|
|
83
|
+
"Valid keys are: #{VALID_TIMEOUT_KEYS.join(', ')}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Normalizes timeout values to numeric seconds
|
|
87
|
+
#
|
|
88
|
+
# Converts ActiveSupport::Duration objects to their numeric second values
|
|
89
|
+
# while preserving integer/float values as-is.
|
|
90
|
+
#
|
|
91
|
+
# @param options [Hash] Raw timeout options with mixed value types
|
|
92
|
+
# @return [Hash] Normalized options with all values as numbers
|
|
93
|
+
# @api private
|
|
94
|
+
def normalize_timeout_values(options)
|
|
95
|
+
options.transform_values do |value|
|
|
96
|
+
case value
|
|
97
|
+
when ActiveSupport::Duration
|
|
98
|
+
value.to_f
|
|
99
|
+
when Numeric
|
|
100
|
+
value
|
|
101
|
+
else
|
|
102
|
+
raise ArgumentError,
|
|
103
|
+
"Timeout values must be numeric or ActiveSupport::Duration, got: #{value.class}"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
# rubocop:enable Metrics/BlockLength
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Automatically include TemporalOptions into ActiveJob::Base when this file is loaded
|
|
114
|
+
ActiveJob::Base.include(ActiveJob::Temporal::TemporalOptions) if defined?(ActiveJob::Base)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveJob
|
|
4
|
+
module Temporal
|
|
5
|
+
module TLSFile
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
OPEN_FLAGS = File::RDONLY | (File.const_defined?(:NOFOLLOW) ? File::NOFOLLOW : 0)
|
|
8
|
+
USES_NOFOLLOW = File.const_defined?(:NOFOLLOW)
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def readable_regular_file?(path)
|
|
13
|
+
expanded_path = File.expand_path(path)
|
|
14
|
+
stat = File.lstat(expanded_path)
|
|
15
|
+
return false if stat.symlink?
|
|
16
|
+
|
|
17
|
+
stat.file? && File.readable?(expanded_path)
|
|
18
|
+
rescue Errno::ENOENT, Errno::ENOTDIR, Errno::EACCES
|
|
19
|
+
false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def read(path)
|
|
23
|
+
return nil if path.nil? || path.to_s.empty?
|
|
24
|
+
|
|
25
|
+
expanded_path = File.expand_path(path)
|
|
26
|
+
reject_symlink!(expanded_path) unless USES_NOFOLLOW
|
|
27
|
+
File.open(expanded_path, OPEN_FLAGS) do |file|
|
|
28
|
+
raise Error, "TLS file path must point to a regular file: #{path}" unless file.stat.file?
|
|
29
|
+
|
|
30
|
+
file.read
|
|
31
|
+
end
|
|
32
|
+
rescue Errno::ELOOP
|
|
33
|
+
raise Error, "TLS file path must not be a symlink: #{path}"
|
|
34
|
+
rescue Errno::ENOENT, Errno::ENOTDIR, Errno::EACCES
|
|
35
|
+
raise Error, "TLS file path is not readable: #{path}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def reject_symlink!(path)
|
|
39
|
+
return unless File.lstat(path).symlink?
|
|
40
|
+
|
|
41
|
+
raise Error, "TLS file path must not be a symlink: #{path}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/lazy_load_hooks"
|
|
4
|
+
|
|
5
|
+
module ActiveJob
|
|
6
|
+
module Temporal
|
|
7
|
+
module TransactionSafety
|
|
8
|
+
module QueueAdapterSetter
|
|
9
|
+
def queue_adapter=(adapter)
|
|
10
|
+
super.tap do
|
|
11
|
+
self.enqueue_after_transaction_commit = true if temporal_queue_adapter?(adapter)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def temporal_queue_adapter?(adapter)
|
|
18
|
+
case adapter
|
|
19
|
+
when Symbol, String
|
|
20
|
+
adapter.to_s == "temporal"
|
|
21
|
+
else
|
|
22
|
+
defined?(ActiveJob::QueueAdapters::TemporalAdapter) &&
|
|
23
|
+
adapter.is_a?(ActiveJob::QueueAdapters::TemporalAdapter)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
def install!
|
|
31
|
+
ActiveSupport.on_load(:active_job) do
|
|
32
|
+
singleton_class.prepend(QueueAdapterSetter) unless singleton_class < QueueAdapterSetter
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
ActiveJob::Temporal::TransactionSafety.install!
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveJob
|
|
4
|
+
module Temporal
|
|
5
|
+
# Rebuilds the Temporal client and swaps it into a running worker.
|
|
6
|
+
class WorkerClientReloader
|
|
7
|
+
def initialize(worker:, logger: ActiveJob::Temporal::Logger, reload_client: ActiveJob::Temporal.method(:reload_client!))
|
|
8
|
+
@worker = worker
|
|
9
|
+
@logger = logger
|
|
10
|
+
@reload_client = reload_client
|
|
11
|
+
@mutex = Mutex.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def reload(source:)
|
|
15
|
+
@mutex.synchronize do
|
|
16
|
+
@logger.log_event("certificate_reload_started", source: source)
|
|
17
|
+
new_client = @reload_client.call do |fresh_client|
|
|
18
|
+
@worker.client = fresh_client
|
|
19
|
+
end
|
|
20
|
+
@logger.log_event("certificate_reload_succeeded", source: source)
|
|
21
|
+
new_client
|
|
22
|
+
rescue StandardError => e
|
|
23
|
+
@logger.error(
|
|
24
|
+
"certificate_reload_failed",
|
|
25
|
+
source: source,
|
|
26
|
+
error_class: e.class.name,
|
|
27
|
+
message: e.message
|
|
28
|
+
)
|
|
29
|
+
raise
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require "temporalio/worker/interceptor"
|
|
5
|
+
|
|
6
|
+
require_relative "observability"
|
|
7
|
+
|
|
8
|
+
module ActiveJob
|
|
9
|
+
module Temporal
|
|
10
|
+
class WorkerHealth
|
|
11
|
+
include Temporalio::Worker::Interceptor::Activity
|
|
12
|
+
|
|
13
|
+
def initialize(task_queue:, namespace:, target:, max_concurrent_activities:, max_concurrent_workflows:)
|
|
14
|
+
@task_queue = task_queue
|
|
15
|
+
@namespace = namespace
|
|
16
|
+
@target = target
|
|
17
|
+
@max_concurrent_activities = max_concurrent_activities
|
|
18
|
+
@max_concurrent_workflows = max_concurrent_workflows
|
|
19
|
+
@mutex = Mutex.new
|
|
20
|
+
@started_at = nil
|
|
21
|
+
@worker_running = false
|
|
22
|
+
@active_tasks = 0
|
|
23
|
+
@last_poll = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def mark_started!
|
|
27
|
+
@mutex.synchronize do
|
|
28
|
+
@started_at ||= Time.now
|
|
29
|
+
@worker_running = true
|
|
30
|
+
end
|
|
31
|
+
Observability.emit(:worker_start, observability_attributes)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def mark_stopped!
|
|
35
|
+
@mutex.synchronize do
|
|
36
|
+
@worker_running = false
|
|
37
|
+
end
|
|
38
|
+
Observability.emit(:worker_stop, observability_attributes)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def record_task_started!(now: Time.now)
|
|
42
|
+
active_tasks = @mutex.synchronize do
|
|
43
|
+
@active_tasks += 1
|
|
44
|
+
@last_poll = now
|
|
45
|
+
@active_tasks
|
|
46
|
+
end
|
|
47
|
+
Observability.emit(:active_tasks, observability_attributes(count: active_tasks))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def record_task_finished!
|
|
51
|
+
active_tasks = @mutex.synchronize do
|
|
52
|
+
@active_tasks = [@active_tasks - 1, 0].max
|
|
53
|
+
@active_tasks
|
|
54
|
+
end
|
|
55
|
+
Observability.emit(:active_tasks, observability_attributes(count: active_tasks))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def snapshot(now: Time.now)
|
|
59
|
+
@mutex.synchronize do
|
|
60
|
+
{
|
|
61
|
+
status: @worker_running ? "ok" : "stopped",
|
|
62
|
+
worker_running: @worker_running,
|
|
63
|
+
started_at: iso8601(@started_at),
|
|
64
|
+
last_poll: iso8601(@last_poll),
|
|
65
|
+
active_tasks: @active_tasks,
|
|
66
|
+
uptime_seconds: uptime_seconds(now),
|
|
67
|
+
task_queue: @task_queue,
|
|
68
|
+
namespace: @namespace,
|
|
69
|
+
target: @target,
|
|
70
|
+
max_concurrent_activities: @max_concurrent_activities,
|
|
71
|
+
max_concurrent_workflows: @max_concurrent_workflows,
|
|
72
|
+
pid: Process.pid
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def intercept_activity(next_interceptor)
|
|
78
|
+
ActivityInbound.new(self, next_interceptor)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def iso8601(time)
|
|
84
|
+
time&.utc&.iso8601
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def uptime_seconds(now)
|
|
88
|
+
return 0 unless @started_at
|
|
89
|
+
|
|
90
|
+
[now - @started_at, 0].max.round
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def observability_attributes(**attributes)
|
|
94
|
+
{
|
|
95
|
+
task_queue: @task_queue,
|
|
96
|
+
namespace: @namespace,
|
|
97
|
+
target: @target,
|
|
98
|
+
worker_id: Process.pid
|
|
99
|
+
}.merge(attributes)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
class ActivityInbound < Temporalio::Worker::Interceptor::Activity::Inbound
|
|
103
|
+
def initialize(worker_health, next_interceptor)
|
|
104
|
+
super(next_interceptor)
|
|
105
|
+
@worker_health = worker_health
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def execute(input)
|
|
109
|
+
@worker_health.record_task_started!
|
|
110
|
+
super
|
|
111
|
+
ensure
|
|
112
|
+
@worker_health.record_task_finished!
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|