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.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +130 -0
  3. data/LICENSE +21 -0
  4. data/README.md +198 -0
  5. data/activejob-temporal.gemspec +58 -0
  6. data/api/job_payload_schema.json +318 -0
  7. data/bin/temporal-worker +295 -0
  8. data/lib/activejob/temporal/active_job_handler_source.rb +84 -0
  9. data/lib/activejob/temporal/activities/aj_runner_activity.rb +454 -0
  10. data/lib/activejob/temporal/activities/best_effort_side_effects.rb +49 -0
  11. data/lib/activejob/temporal/activities/dependency_status_activity.rb +160 -0
  12. data/lib/activejob/temporal/activities/rate_limit_activity.rb +41 -0
  13. data/lib/activejob/temporal/adapter.rb +257 -0
  14. data/lib/activejob/temporal/audit_log.rb +118 -0
  15. data/lib/activejob/temporal/batch_enqueue_result.rb +110 -0
  16. data/lib/activejob/temporal/batch_enqueuer.rb +141 -0
  17. data/lib/activejob/temporal/bind_policy.rb +44 -0
  18. data/lib/activejob/temporal/cancel/batch_canceller.rb +154 -0
  19. data/lib/activejob/temporal/cancel/batch_summary.rb +45 -0
  20. data/lib/activejob/temporal/cancel.rb +236 -0
  21. data/lib/activejob/temporal/certificate_watcher.rb +76 -0
  22. data/lib/activejob/temporal/chain_options.rb +83 -0
  23. data/lib/activejob/temporal/child_workflow_options.rb +102 -0
  24. data/lib/activejob/temporal/client.rb +215 -0
  25. data/lib/activejob/temporal/conditional_enqueue.rb +56 -0
  26. data/lib/activejob/temporal/configurable.rb +55 -0
  27. data/lib/activejob/temporal/configuration.rb +981 -0
  28. data/lib/activejob/temporal/configured_job_compatibility.rb +44 -0
  29. data/lib/activejob/temporal/connection_worker_pool.rb +88 -0
  30. data/lib/activejob/temporal/dead_letter_payload_validation.rb +34 -0
  31. data/lib/activejob/temporal/dead_letter_queue.rb +163 -0
  32. data/lib/activejob/temporal/dependency_options.rb +134 -0
  33. data/lib/activejob/temporal/external_operation.rb +193 -0
  34. data/lib/activejob/temporal/health_check_server.rb +159 -0
  35. data/lib/activejob/temporal/http_line_reader.rb +36 -0
  36. data/lib/activejob/temporal/inspect.rb +184 -0
  37. data/lib/activejob/temporal/job_descriptor.rb +37 -0
  38. data/lib/activejob/temporal/job_payload_builder.rb +209 -0
  39. data/lib/activejob/temporal/job_payload_chain_builder.rb +106 -0
  40. data/lib/activejob/temporal/job_payload_child_workflows.rb +127 -0
  41. data/lib/activejob/temporal/job_payload_dependencies.rb +40 -0
  42. data/lib/activejob/temporal/job_payload_rate_limits.rb +53 -0
  43. data/lib/activejob/temporal/job_payload_workflow_interactions.rb +31 -0
  44. data/lib/activejob/temporal/job_tags.rb +40 -0
  45. data/lib/activejob/temporal/locales/en.yml +126 -0
  46. data/lib/activejob/temporal/logger.rb +214 -0
  47. data/lib/activejob/temporal/metrics_server.rb +150 -0
  48. data/lib/activejob/temporal/middleware/chain.rb +106 -0
  49. data/lib/activejob/temporal/middleware.rb +11 -0
  50. data/lib/activejob/temporal/observability/datadog.rb +167 -0
  51. data/lib/activejob/temporal/observability/opentelemetry.rb +107 -0
  52. data/lib/activejob/temporal/observability/prometheus.rb +271 -0
  53. data/lib/activejob/temporal/observability.rb +260 -0
  54. data/lib/activejob/temporal/payload.rb +415 -0
  55. data/lib/activejob/temporal/payload_encryption.rb +215 -0
  56. data/lib/activejob/temporal/payload_serializers/json.rb +23 -0
  57. data/lib/activejob/temporal/payload_serializers/marshal.rb +53 -0
  58. data/lib/activejob/temporal/payload_serializers/message_pack.rb +59 -0
  59. data/lib/activejob/temporal/payload_serializers.rb +37 -0
  60. data/lib/activejob/temporal/payload_storage.rb +103 -0
  61. data/lib/activejob/temporal/rails_environment_loader.rb +143 -0
  62. data/lib/activejob/temporal/rate_limit_options.rb +94 -0
  63. data/lib/activejob/temporal/rate_limiters/memory.rb +198 -0
  64. data/lib/activejob/temporal/reload_signal_queue.rb +40 -0
  65. data/lib/activejob/temporal/retry_handler_extractor.rb +361 -0
  66. data/lib/activejob/temporal/retry_mapper.rb +264 -0
  67. data/lib/activejob/temporal/schedulable.rb +60 -0
  68. data/lib/activejob/temporal/schedule.rb +181 -0
  69. data/lib/activejob/temporal/schedule_options.rb +105 -0
  70. data/lib/activejob/temporal/search_attributes.rb +173 -0
  71. data/lib/activejob/temporal/signal_query.rb +161 -0
  72. data/lib/activejob/temporal/signal_query_options.rb +106 -0
  73. data/lib/activejob/temporal/temporal_options.rb +114 -0
  74. data/lib/activejob/temporal/tls_file.rb +45 -0
  75. data/lib/activejob/temporal/transaction_safety.rb +39 -0
  76. data/lib/activejob/temporal/version.rb +7 -0
  77. data/lib/activejob/temporal/visibility_query.rb +13 -0
  78. data/lib/activejob/temporal/worker_client_reloader.rb +34 -0
  79. data/lib/activejob/temporal/worker_health.rb +117 -0
  80. data/lib/activejob/temporal/worker_pool.rb +408 -0
  81. data/lib/activejob/temporal/workflow_enqueuer.rb +271 -0
  82. data/lib/activejob/temporal/workflow_enqueuer_batch.rb +17 -0
  83. data/lib/activejob/temporal/workflow_id_builder.rb +155 -0
  84. data/lib/activejob/temporal/workflow_identity.rb +62 -0
  85. data/lib/activejob/temporal/workflows/aj_workflow.rb +282 -0
  86. data/lib/activejob/temporal/workflows/dead_letter_support.rb +134 -0
  87. data/lib/activejob/temporal/workflows/dead_letter_workflow.rb +114 -0
  88. data/lib/activejob/temporal/workflows/workflow_chaining.rb +194 -0
  89. data/lib/activejob/temporal/workflows/workflow_child_workflows.rb +140 -0
  90. data/lib/activejob/temporal/workflows/workflow_continue_as_new.rb +44 -0
  91. data/lib/activejob/temporal/workflows/workflow_dependencies.rb +115 -0
  92. data/lib/activejob/temporal/workflows/workflow_execution_steps.rb +22 -0
  93. data/lib/activejob/temporal/workflows/workflow_interactions.rb +215 -0
  94. data/lib/activejob/temporal/workflows/workflow_local_activities.rb +29 -0
  95. data/lib/activejob/temporal/workflows/workflow_nexus.rb +15 -0
  96. data/lib/activejob/temporal/workflows/workflow_versioning.rb +21 -0
  97. data/lib/activejob/temporal.rb +297 -0
  98. data/lib/activejob-temporal.rb +3 -0
  99. metadata +423 -0
@@ -0,0 +1,318 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "ActiveJob Temporal Payload",
4
+ "type": "object",
5
+ "definitions": {
6
+ "rate_limit": {
7
+ "type": "object",
8
+ "additionalProperties": false,
9
+ "required": ["limit", "interval", "key"],
10
+ "properties": {
11
+ "limit": { "type": "integer", "minimum": 1 },
12
+ "interval": { "type": "number", "exclusiveMinimum": 0 },
13
+ "key": { "type": "string", "minLength": 1 }
14
+ }
15
+ },
16
+ "rate_limits": {
17
+ "type": "array",
18
+ "items": { "$ref": "#/definitions/rate_limit" }
19
+ },
20
+ "active_job_payload": {
21
+ "type": "object",
22
+ "required": ["job_class", "job_id", "queue_name", "arguments"],
23
+ "additionalProperties": true,
24
+ "properties": {
25
+ "job_class": { "type": "string", "minLength": 1 },
26
+ "job_id": { "type": "string", "minLength": 1 },
27
+ "provider_job_id": { "type": ["string", "null"] },
28
+ "queue_name": { "type": "string", "minLength": 1 },
29
+ "priority": { "type": ["integer", "null"] },
30
+ "arguments": { "type": "array" },
31
+ "executions": { "type": "integer", "minimum": 0 },
32
+ "exception_executions": {
33
+ "type": "object",
34
+ "additionalProperties": { "type": "integer", "minimum": 0 }
35
+ },
36
+ "locale": { "type": "string" },
37
+ "timezone": { "type": ["string", "null"] },
38
+ "enqueued_at": { "type": "string", "format": "date-time" },
39
+ "scheduled_at": { "type": ["string", "null"], "format": "date-time" }
40
+ }
41
+ },
42
+ "workflow_interactions": {
43
+ "type": "object",
44
+ "additionalProperties": false,
45
+ "required": ["job_class", "signals", "queries", "updates"],
46
+ "properties": {
47
+ "job_class": { "type": "string", "minLength": 1 },
48
+ "signals": {
49
+ "type": "array",
50
+ "items": { "type": "string", "minLength": 1 },
51
+ "uniqueItems": true
52
+ },
53
+ "queries": {
54
+ "type": "array",
55
+ "items": { "type": "string", "minLength": 1 },
56
+ "uniqueItems": true
57
+ },
58
+ "updates": {
59
+ "type": "array",
60
+ "items": { "type": "string", "minLength": 1 },
61
+ "uniqueItems": true
62
+ }
63
+ }
64
+ },
65
+ "workflow_identity": {
66
+ "type": "object",
67
+ "additionalProperties": false,
68
+ "required": ["workflow_name"],
69
+ "properties": {
70
+ "workflow_name": { "type": "string", "minLength": 1 },
71
+ "workflow_id_prefix": { "type": "string", "minLength": 1 }
72
+ }
73
+ },
74
+ "external_temporal_options": {
75
+ "type": "object",
76
+ "required": ["task_queue"],
77
+ "properties": {
78
+ "task_queue": { "type": "string", "minLength": 1 }
79
+ }
80
+ },
81
+ "active_job_chain_step": {
82
+ "type": "object",
83
+ "additionalProperties": false,
84
+ "required": ["job_class", "job_id", "queue_name", "arguments"],
85
+ "properties": {
86
+ "job_class": { "type": "string", "minLength": 1 },
87
+ "job_id": { "type": "string", "minLength": 1 },
88
+ "queue_name": { "type": "string", "minLength": 1 },
89
+ "arguments": { "type": "array" },
90
+ "active_job": { "$ref": "#/definitions/active_job_payload" },
91
+ "executions": { "type": "integer", "minimum": 0 },
92
+ "exception_executions": {
93
+ "type": "object",
94
+ "additionalProperties": { "type": "integer", "minimum": 0 }
95
+ },
96
+ "default_activity_options": { "type": "object" },
97
+ "retry_policy": { "type": "object" },
98
+ "temporal_options": { "type": "object" },
99
+ "dead_letter": { "type": "object" },
100
+ "workflow_identity": { "$ref": "#/definitions/workflow_identity" },
101
+ "rate_limits": { "$ref": "#/definitions/rate_limits" },
102
+ "workflow_interactions": { "$ref": "#/definitions/workflow_interactions" },
103
+ "activity_task_queue": { "type": "string", "minLength": 1 }
104
+ }
105
+ },
106
+ "external_chain_step": {
107
+ "type": "object",
108
+ "additionalProperties": false,
109
+ "required": ["temporal_operation", "temporal_type", "options"],
110
+ "properties": {
111
+ "temporal_operation": { "type": "string", "enum": ["activity", "workflow"] },
112
+ "temporal_type": { "type": "string", "minLength": 1 },
113
+ "options": { "$ref": "#/definitions/external_temporal_options" }
114
+ }
115
+ },
116
+ "chain_step": {
117
+ "anyOf": [
118
+ { "$ref": "#/definitions/active_job_chain_step" },
119
+ { "$ref": "#/definitions/external_chain_step" }
120
+ ]
121
+ },
122
+ "chain": {
123
+ "type": "array",
124
+ "minItems": 1,
125
+ "items": { "$ref": "#/definitions/chain_step" }
126
+ },
127
+ "child_workflow_search_attributes": {
128
+ "type": "object",
129
+ "additionalProperties": false,
130
+ "required": ["job_class", "job_id", "queue_name", "enqueued_at"],
131
+ "properties": {
132
+ "job_class": { "type": "string", "minLength": 1 },
133
+ "job_id": { "type": "string", "minLength": 1 },
134
+ "queue_name": { "type": "string", "minLength": 1 },
135
+ "enqueued_at": { "type": "string", "format": "date-time" },
136
+ "tags": {
137
+ "type": "array",
138
+ "items": { "type": "string", "minLength": 1 },
139
+ "uniqueItems": true
140
+ }
141
+ }
142
+ },
143
+ "active_job_child_workflow": {
144
+ "type": "object",
145
+ "additionalProperties": false,
146
+ "required": ["job_class", "job_id", "workflow_id", "queue_name", "arguments"],
147
+ "properties": {
148
+ "job_class": { "type": "string", "minLength": 1 },
149
+ "job_id": { "type": "string", "minLength": 1 },
150
+ "workflow_id": { "type": "string", "minLength": 1 },
151
+ "queue_name": { "type": "string", "minLength": 1 },
152
+ "arguments": { "type": "array" },
153
+ "active_job": { "$ref": "#/definitions/active_job_payload" },
154
+ "executions": { "type": "integer", "minimum": 0 },
155
+ "exception_executions": {
156
+ "type": "object",
157
+ "additionalProperties": { "type": "integer", "minimum": 0 }
158
+ },
159
+ "default_activity_options": { "type": "object" },
160
+ "retry_policy": { "type": "object" },
161
+ "temporal_options": { "type": "object" },
162
+ "dead_letter": { "type": "object" },
163
+ "workflow_identity": { "$ref": "#/definitions/workflow_identity" },
164
+ "rate_limits": { "$ref": "#/definitions/rate_limits" },
165
+ "workflow_interactions": { "$ref": "#/definitions/workflow_interactions" },
166
+ "activity_task_queue": { "type": "string", "minLength": 1 },
167
+ "workflow_task_queue": { "type": "string", "minLength": 1 },
168
+ "search_attributes": { "$ref": "#/definitions/child_workflow_search_attributes" }
169
+ }
170
+ },
171
+ "external_child_workflow": {
172
+ "type": "object",
173
+ "additionalProperties": false,
174
+ "required": ["temporal_operation", "temporal_type", "options"],
175
+ "properties": {
176
+ "temporal_operation": { "type": "string", "enum": ["workflow"] },
177
+ "temporal_type": { "type": "string", "minLength": 1 },
178
+ "options": { "$ref": "#/definitions/external_temporal_options" }
179
+ }
180
+ },
181
+ "child_workflow": {
182
+ "anyOf": [
183
+ { "$ref": "#/definitions/active_job_child_workflow" },
184
+ { "$ref": "#/definitions/external_child_workflow" }
185
+ ]
186
+ },
187
+ "child_workflows": {
188
+ "type": "array",
189
+ "minItems": 1,
190
+ "items": { "$ref": "#/definitions/child_workflow" }
191
+ },
192
+ "dependency": {
193
+ "type": "object",
194
+ "additionalProperties": false,
195
+ "anyOf": [
196
+ { "required": ["job_id"] },
197
+ { "required": ["workflow_id"] }
198
+ ],
199
+ "properties": {
200
+ "job_class": { "type": "string", "minLength": 1 },
201
+ "job_id": { "type": "string", "minLength": 1 },
202
+ "workflow_id": { "type": "string", "minLength": 1 }
203
+ }
204
+ },
205
+ "dependencies": {
206
+ "type": "array",
207
+ "minItems": 1,
208
+ "items": { "$ref": "#/definitions/dependency" }
209
+ }
210
+ },
211
+ "oneOf": [
212
+ {
213
+ "additionalProperties": false,
214
+ "required": ["job_class", "job_id", "queue_name", "arguments"],
215
+ "properties": {
216
+ "job_class": { "type": "string" },
217
+ "job_id": { "type": "string" },
218
+ "queue_name": { "type": "string" },
219
+ "arguments": { "type": "array" },
220
+ "active_job": { "$ref": "#/definitions/active_job_payload" },
221
+ "executions": { "type": "integer", "minimum": 0 },
222
+ "exception_executions": {
223
+ "type": "object",
224
+ "additionalProperties": { "type": "integer", "minimum": 0 }
225
+ },
226
+ "scheduled_at": { "type": "string", "format": "date-time" },
227
+ "default_activity_options": { "type": "object" },
228
+ "retry_policy": { "type": "object" },
229
+ "temporal_options": { "type": "object" },
230
+ "dead_letter": { "type": "object" },
231
+ "workflow_identity": { "$ref": "#/definitions/workflow_identity" },
232
+ "rate_limits": { "$ref": "#/definitions/rate_limits" },
233
+ "workflow_interactions": { "$ref": "#/definitions/workflow_interactions" },
234
+ "child_workflows": { "$ref": "#/definitions/child_workflows" },
235
+ "chain": { "$ref": "#/definitions/chain" },
236
+ "dependencies": { "$ref": "#/definitions/dependencies" },
237
+ "dependency_failure_policy": { "type": "string", "enum": ["fail", "ignore"] },
238
+ "activity_task_queue": { "type": "string", "minLength": 1 }
239
+ }
240
+ },
241
+ {
242
+ "additionalProperties": false,
243
+ "required": ["serialized_payload", "payload_serializer", "payload_serializer_version", "serialized_data"],
244
+ "properties": {
245
+ "serialized_payload": { "const": true },
246
+ "payload_serializer": { "type": "string", "enum": ["message_pack", "marshal"] },
247
+ "payload_serializer_version": { "type": "integer", "enum": [1] },
248
+ "serialized_data": { "type": "string" },
249
+ "scheduled_at": { "type": "string", "format": "date-time" },
250
+ "default_activity_options": { "type": "object" },
251
+ "retry_policy": { "type": "object" },
252
+ "temporal_options": { "type": "object" },
253
+ "dead_letter": { "type": "object" },
254
+ "workflow_identity": { "$ref": "#/definitions/workflow_identity" },
255
+ "rate_limits": { "$ref": "#/definitions/rate_limits" },
256
+ "workflow_interactions": { "$ref": "#/definitions/workflow_interactions" },
257
+ "child_workflows": { "$ref": "#/definitions/child_workflows" },
258
+ "chain": { "$ref": "#/definitions/chain" },
259
+ "dependencies": { "$ref": "#/definitions/dependencies" },
260
+ "dependency_failure_policy": { "type": "string", "enum": ["fail", "ignore"] },
261
+ "activity_task_queue": { "type": "string", "minLength": 1 }
262
+ }
263
+ },
264
+ {
265
+ "additionalProperties": false,
266
+ "required": ["encrypted_payload", "encrypted_payload_version", "encrypted_data"],
267
+ "properties": {
268
+ "encrypted_payload": { "const": true },
269
+ "encrypted_payload_version": { "type": "integer", "enum": [1, 2] },
270
+ "encrypted_key_id": { "type": "string", "minLength": 1 },
271
+ "payload_serializer": { "type": "string", "enum": ["message_pack", "marshal"] },
272
+ "payload_serializer_version": { "type": "integer", "enum": [1] },
273
+ "encrypted_data": { "type": "string" },
274
+ "encrypted_iv": { "type": "string" },
275
+ "encrypted_auth_tag": { "type": "string" },
276
+ "scheduled_at": { "type": "string", "format": "date-time" },
277
+ "default_activity_options": { "type": "object" },
278
+ "retry_policy": { "type": "object" },
279
+ "temporal_options": { "type": "object" },
280
+ "dead_letter": { "type": "object" },
281
+ "workflow_identity": { "$ref": "#/definitions/workflow_identity" },
282
+ "rate_limits": { "$ref": "#/definitions/rate_limits" },
283
+ "workflow_interactions": { "$ref": "#/definitions/workflow_interactions" },
284
+ "child_workflows": { "$ref": "#/definitions/child_workflows" },
285
+ "chain": { "$ref": "#/definitions/chain" },
286
+ "dependencies": { "$ref": "#/definitions/dependencies" },
287
+ "dependency_failure_policy": { "type": "string", "enum": ["fail", "ignore"] },
288
+ "activity_task_queue": { "type": "string", "minLength": 1 }
289
+ },
290
+ "dependencies": {
291
+ "payload_serializer": ["payload_serializer_version"],
292
+ "payload_serializer_version": ["payload_serializer"]
293
+ }
294
+ },
295
+ {
296
+ "additionalProperties": false,
297
+ "required": ["external_payload", "external_payload_version", "external_payload_reference"],
298
+ "properties": {
299
+ "external_payload": { "const": true },
300
+ "external_payload_version": { "type": "integer", "enum": [1] },
301
+ "external_payload_reference": {},
302
+ "scheduled_at": { "type": "string", "format": "date-time" },
303
+ "default_activity_options": { "type": "object" },
304
+ "retry_policy": { "type": "object" },
305
+ "temporal_options": { "type": "object" },
306
+ "dead_letter": { "type": "object" },
307
+ "workflow_identity": { "$ref": "#/definitions/workflow_identity" },
308
+ "rate_limits": { "$ref": "#/definitions/rate_limits" },
309
+ "workflow_interactions": { "$ref": "#/definitions/workflow_interactions" },
310
+ "child_workflows": { "$ref": "#/definitions/child_workflows" },
311
+ "chain": { "$ref": "#/definitions/chain" },
312
+ "dependencies": { "$ref": "#/definitions/dependencies" },
313
+ "dependency_failure_policy": { "type": "string", "enum": ["fail", "ignore"] },
314
+ "activity_task_queue": { "type": "string", "minLength": 1 }
315
+ }
316
+ }
317
+ ]
318
+ }
@@ -0,0 +1,295 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/activejob/temporal"
5
+ require "optparse"
6
+
7
+ begin
8
+ require "temporalio/worker"
9
+ rescue LoadError => e
10
+ warn("temporalio-worker gem is required to run this worker: #{e.message}")
11
+ exit(1)
12
+ end
13
+
14
+ metrics_public_bind_env = ENV.fetch("ACTIVEJOB_TEMPORAL_METRICS_ALLOW_PUBLIC_BIND", nil)
15
+ metrics_allow_public_bind = unless metrics_public_bind_env.nil?
16
+ ActiveJob::Temporal::BindPolicy.allow_public_bind?(metrics_public_bind_env)
17
+ end
18
+
19
+ options = {
20
+ health_check_bind: ENV.fetch(
21
+ "ACTIVEJOB_TEMPORAL_HEALTH_CHECK_BIND",
22
+ ActiveJob::Temporal::HealthCheckServer::DEFAULT_BIND_ADDRESS
23
+ ),
24
+ health_check_allow_public_bind: ActiveJob::Temporal::BindPolicy.allow_public_bind?(
25
+ ENV.fetch("ACTIVEJOB_TEMPORAL_HEALTH_CHECK_ALLOW_PUBLIC_BIND", nil)
26
+ ),
27
+ health_check_port: ENV.fetch("ACTIVEJOB_TEMPORAL_HEALTH_CHECK_PORT", nil),
28
+ metrics_bind: ENV.fetch("ACTIVEJOB_TEMPORAL_METRICS_BIND", nil),
29
+ metrics_allow_public_bind: metrics_allow_public_bind,
30
+ metrics_port: ENV.fetch("ACTIVEJOB_TEMPORAL_METRICS_PORT", nil),
31
+ pool_size: ENV.fetch("ACTIVEJOB_TEMPORAL_WORKER_POOL_SIZE", 1)
32
+ }
33
+
34
+ OptionParser.new do |parser|
35
+ parser.banner = "Usage: temporal-worker [options]"
36
+
37
+ parser.on("--health-check-port PORT", Integer, "Expose GET /health on PORT") do |port|
38
+ options[:health_check_port] = port
39
+ end
40
+
41
+ parser.on("--health-check-bind HOST", "Bind health endpoint to HOST") do |host|
42
+ options[:health_check_bind] = host
43
+ end
44
+
45
+ parser.on(
46
+ "--allow-public-health-check-bind",
47
+ "Allow health endpoint to bind non-loopback addresses"
48
+ ) do
49
+ options[:health_check_allow_public_bind] = true
50
+ end
51
+
52
+ parser.on("--metrics-port PORT", Integer, "Expose GET /metrics on PORT") do |port|
53
+ options[:metrics_port] = port
54
+ end
55
+
56
+ parser.on("--metrics-bind HOST", "Bind metrics endpoint to HOST") do |host|
57
+ options[:metrics_bind] = host
58
+ end
59
+
60
+ parser.on("--allow-public-metrics-bind", "Allow metrics endpoint to bind non-loopback addresses") do
61
+ options[:metrics_allow_public_bind] = true
62
+ end
63
+
64
+ parser.on("--pool-size SIZE", Integer, "Run SIZE worker processes under a supervising pool") do |size|
65
+ options[:pool_size] = size
66
+ end
67
+ end.parse!(ARGV)
68
+
69
+ begin
70
+ ActiveJob::Temporal::RailsEnvironmentLoader.load!(rails_root: ENV.fetch("RAILS_ROOT", "."))
71
+ rescue ActiveJob::Temporal::RailsEnvironmentLoader::Error => e
72
+ warn "Error: #{e.message}"
73
+ exit(1)
74
+ end
75
+
76
+ # Read from configuration singleton (already set by Rails initializer)
77
+ config = ActiveJob::Temporal.config
78
+ task_queue = config.task_queue
79
+ max_concurrent_activities = config.max_concurrent_activities
80
+ max_concurrent_workflows = config.max_concurrent_workflow_tasks
81
+ prometheus_adapter = config.observability.adapter(:prometheus)
82
+ metrics_port = options[:metrics_port] || prometheus_adapter&.metrics_server&.port
83
+ if metrics_port && !prometheus_adapter
84
+ warn(
85
+ "Error: Prometheus metrics require the Prometheus observability adapter. " \
86
+ "Add `require \"activejob/temporal/observability/prometheus\"` and enable " \
87
+ "`config.observability.use :prometheus`."
88
+ )
89
+ exit(1)
90
+ end
91
+ metrics_bind = options[:metrics_bind] || prometheus_adapter&.metrics_server&.bind ||
92
+ ActiveJob::Temporal::MetricsServer::DEFAULT_BIND_ADDRESS
93
+ metrics_allow_public_bind = if options[:metrics_allow_public_bind].nil?
94
+ prometheus_adapter&.metrics_server&.allow_public_bind || false
95
+ else
96
+ options[:metrics_allow_public_bind]
97
+ end
98
+ begin
99
+ pool_size = Integer(options[:pool_size])
100
+ rescue ArgumentError, TypeError
101
+ warn "Error: --pool-size must be a positive integer"
102
+ exit(1)
103
+ end
104
+
105
+ if pool_size < 1
106
+ warn "Error: --pool-size must be a positive integer"
107
+ exit(1)
108
+ end
109
+
110
+ reload_signal = config.tls_reload_signal.to_s.sub(/\ASIG/i, "").upcase
111
+ unless Signal.list.key?(reload_signal) && !ActiveJob::Temporal::UNTRAPPABLE_SIGNALS.include?(reload_signal)
112
+ warn(
113
+ "Error: ACTIVEJOB_TEMPORAL_TLS_RELOAD_SIGNAL must be a signal name safe to trap, " \
114
+ "got: #{config.tls_reload_signal.inspect}"
115
+ )
116
+ exit(1)
117
+ end
118
+
119
+ public_bind_errors = []
120
+ if options[:health_check_port] &&
121
+ ActiveJob::Temporal::BindPolicy.public_bind?(options[:health_check_bind]) &&
122
+ !options[:health_check_allow_public_bind]
123
+ public_bind_errors << [
124
+ "health check",
125
+ options[:health_check_bind],
126
+ "--allow-public-health-check-bind",
127
+ "ACTIVEJOB_TEMPORAL_HEALTH_CHECK_ALLOW_PUBLIC_BIND=true"
128
+ ]
129
+ end
130
+ if metrics_port &&
131
+ ActiveJob::Temporal::BindPolicy.public_bind?(metrics_bind) &&
132
+ !metrics_allow_public_bind
133
+ public_bind_errors << [
134
+ "metrics",
135
+ metrics_bind,
136
+ "--allow-public-metrics-bind",
137
+ "ACTIVEJOB_TEMPORAL_METRICS_ALLOW_PUBLIC_BIND=true"
138
+ ]
139
+ end
140
+
141
+ unless public_bind_errors.empty?
142
+ public_bind_errors.each do |endpoint, bind_address, flag, env_var|
143
+ warn(
144
+ "Error: refusing to expose unauthenticated #{endpoint} endpoint on non-loopback address " \
145
+ "#{bind_address.inspect} without explicit public bind opt-in"
146
+ )
147
+ warn "Pass #{flag} or set #{env_var} only when the endpoint is protected from untrusted networks."
148
+ end
149
+ exit(1)
150
+ end
151
+
152
+ if pool_size > 1
153
+ pool = ActiveJob::Temporal::WorkerPool.new(
154
+ size: pool_size,
155
+ health_check_port: options[:health_check_port],
156
+ health_check_bind: options[:health_check_bind],
157
+ health_check_allow_public_bind: options[:health_check_allow_public_bind],
158
+ metrics_port: metrics_port,
159
+ metrics_bind: metrics_bind,
160
+ metrics_allow_public_bind: metrics_allow_public_bind,
161
+ max_concurrent_activities: max_concurrent_activities,
162
+ max_concurrent_workflows: max_concurrent_workflows
163
+ )
164
+
165
+ ActiveJob::Temporal::Logger.log_event(
166
+ "worker_pool_started",
167
+ size: pool_size,
168
+ task_queue: task_queue,
169
+ health_check_port: options[:health_check_port],
170
+ metrics_port: metrics_port
171
+ )
172
+
173
+ pool.start.wait
174
+ exit
175
+ end
176
+
177
+ client = ActiveJob::Temporal.client
178
+ worker_health = ActiveJob::Temporal::WorkerHealth.new(
179
+ task_queue: task_queue,
180
+ namespace: ActiveJob::Temporal.config.namespace,
181
+ target: ActiveJob::Temporal.config.target,
182
+ max_concurrent_activities: max_concurrent_activities,
183
+ max_concurrent_workflows: max_concurrent_workflows
184
+ )
185
+
186
+ worker = Temporalio::Worker.new(
187
+ client: client,
188
+ task_queue: task_queue,
189
+ workflows: [
190
+ ActiveJob::Temporal::Workflows::AjWorkflow,
191
+ ActiveJob::Temporal::Workflows::DeadLetterWorkflow
192
+ ],
193
+ activities: [
194
+ ActiveJob::Temporal::Activities::RateLimitActivity,
195
+ ActiveJob::Temporal::Activities::DependencyStatusActivity,
196
+ ActiveJob::Temporal::Activities::AjRunnerActivity
197
+ ],
198
+ tuner: Temporalio::Worker::Tuner.create_fixed(
199
+ workflow_slots: max_concurrent_workflows,
200
+ activity_slots: max_concurrent_activities,
201
+ local_activity_slots: max_concurrent_activities
202
+ ),
203
+ interceptors: [worker_health]
204
+ )
205
+
206
+ ActiveJob::Temporal::Logger.log_event(
207
+ "worker_started",
208
+ task_queue: task_queue,
209
+ max_concurrent_activities: max_concurrent_activities,
210
+ max_concurrent_workflows: max_concurrent_workflows,
211
+ namespace: ActiveJob::Temporal.config.namespace,
212
+ target: ActiveJob::Temporal.config.target
213
+ )
214
+
215
+ health_check_server = nil
216
+ if options[:health_check_port]
217
+ health_check_server = ActiveJob::Temporal::HealthCheckServer.new(
218
+ port: options[:health_check_port],
219
+ bind_address: options[:health_check_bind],
220
+ allow_public_bind: options[:health_check_allow_public_bind],
221
+ state: worker_health
222
+ ).start
223
+
224
+ ActiveJob::Temporal::Logger.log_event(
225
+ "health_check_server_started",
226
+ bind_address: health_check_server.bind_address,
227
+ port: health_check_server.port
228
+ )
229
+ end
230
+
231
+ metrics_server = nil
232
+ if metrics_port
233
+ metrics_server = prometheus_adapter.start_metrics_server(
234
+ port: metrics_port,
235
+ bind_address: metrics_bind,
236
+ allow_public_bind: metrics_allow_public_bind
237
+ )
238
+
239
+ ActiveJob::Temporal::Logger.log_event(
240
+ "metrics_server_started",
241
+ bind_address: metrics_server.bind_address,
242
+ port: metrics_server.port
243
+ )
244
+ end
245
+
246
+ client_reloader = ActiveJob::Temporal::WorkerClientReloader.new(worker: worker)
247
+ reload_signal_queue = ActiveJob::Temporal::ReloadSignalQueue.new
248
+ reload_signal_thread = Thread.new do
249
+ loop do
250
+ signal = reload_signal_queue.pop
251
+ break unless signal
252
+
253
+ begin
254
+ client_reloader.reload(source: "signal:#{signal}")
255
+ rescue StandardError
256
+ # Reload failures are already logged and should not stop the worker.
257
+ end
258
+ end
259
+ end
260
+ Signal.trap(reload_signal) { reload_signal_queue.push(reload_signal) }
261
+
262
+ certificate_watcher = nil
263
+ if config.tls_cert_watch
264
+ certificate_watcher = ActiveJob::Temporal::CertificateWatcher.new(
265
+ paths: ActiveJob::Temporal::CertificateWatcher.paths_from_config(config),
266
+ reload_callback: lambda {
267
+ begin
268
+ client_reloader.reload(source: "file_watch")
269
+ rescue StandardError
270
+ # Reload failures are already logged and should not stop the watcher.
271
+ end
272
+ }
273
+ ).start
274
+ end
275
+
276
+ begin
277
+ worker_health.mark_started!
278
+ Temporalio::Worker.run_all(
279
+ worker,
280
+ shutdown_signals: %w[SIGINT SIGTERM]
281
+ )
282
+ rescue Interrupt
283
+ # Temporal worker raises Interrupt on shutdown signals; swallow to allow clean exit.
284
+ ensure
285
+ worker_health.mark_stopped!
286
+ certificate_watcher&.stop
287
+ reload_signal_queue.close
288
+ reload_signal_thread.join(1)
289
+ health_check_server&.stop
290
+ metrics_server&.stop
291
+ ActiveJob::Temporal::Logger.log_event(
292
+ "worker_shutdown",
293
+ task_queue: task_queue
294
+ )
295
+ end