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,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "adapter"
4
+ require_relative "external_operation"
5
+ require "active_support/core_ext/string/inflections"
6
+
7
+ module ActiveJob
8
+ module Temporal
9
+ module JobPayloadChainBuilder
10
+ ChainStepRoutingJob = Struct.new(:queue_name, :priority)
11
+
12
+ private
13
+
14
+ def apply_chain(payload, job)
15
+ chain = chain_payloads_for(job)
16
+ payload[:chain] = chain if chain.any?
17
+ end
18
+
19
+ def chain_payloads_for(job)
20
+ Array(job.respond_to?(:temporal_chain) ? job.temporal_chain : nil).each_with_index.map do |chain_step, index|
21
+ chain_payload_for(job, chain_step, index + 1)
22
+ end
23
+ end
24
+
25
+ def chain_payload_for(root_job, chain_step, position)
26
+ external_operation = ExternalOperation.normalize(chain_step)
27
+ return external_chain_payload(external_operation) if external_operation
28
+
29
+ job_class = chain_step_job_class(chain_step)
30
+ options = chain_step_options(chain_step)
31
+ queue_name = chain_step_queue_name(job_class, options)
32
+ job_id = "#{root_job.job_id}:chain:#{position}"
33
+ payload = base_chain_payload(job_class, job_id, queue_name, options)
34
+
35
+ apply_chain_step_retry_policy(payload, job_class, job_id, queue_name)
36
+ apply_temporal_options(payload, job_class)
37
+ apply_rate_limits_for_class(payload, job_class)
38
+ apply_workflow_interactions(payload, job_class)
39
+ payload
40
+ end
41
+
42
+ def external_chain_payload(external_operation)
43
+ {
44
+ temporal_operation: external_operation.fetch(:temporal_operation),
45
+ temporal_type: external_operation.fetch(:temporal_type),
46
+ options: external_operation.fetch(:options)
47
+ }
48
+ end
49
+
50
+ def base_chain_payload(job_class, job_id, queue_name, options)
51
+ {
52
+ job_class: job_class.name,
53
+ job_id: job_id,
54
+ queue_name: queue_name,
55
+ arguments: [],
56
+ executions: 0,
57
+ exception_executions: {},
58
+ default_activity_options: default_activity_options,
59
+ activity_task_queue: chain_step_activity_task_queue(queue_name, options)
60
+ }
61
+ end
62
+
63
+ def chain_step_job_class(chain_step)
64
+ job_class_name = chain_step[:job_class] || chain_step["job_class"]
65
+ job_class = job_class_name.constantize
66
+ return job_class if job_class < ActiveJob::Base
67
+
68
+ raise ArgumentError, "chain entries must be ActiveJob classes or configured jobs"
69
+ end
70
+
71
+ def chain_step_options(chain_step)
72
+ chain_step[:options] || chain_step["options"] || {}
73
+ end
74
+
75
+ def chain_step_queue_name(job_class, options)
76
+ queue_name = options[:queue] || options["queue"] || job_class.queue_name
77
+ queue_name.to_s
78
+ end
79
+
80
+ def chain_step_activity_task_queue(queue_name, options)
81
+ priority = options[:priority] || options["priority"]
82
+ routing_job = ChainStepRoutingJob.new(queue_name, priority)
83
+ Adapter.resolve_task_queue(routing_job, config: @config)
84
+ end
85
+
86
+ def apply_chain_step_retry_policy(payload, job_class, job_id, queue_name)
87
+ retry_policy = retry_policy_for(job_class)
88
+ payload[:retry_policy] = retry_policy
89
+ return unless dead_letter_enabled?
90
+
91
+ payload[:dead_letter] = dead_letter_metadata(
92
+ job_class.name,
93
+ job_id,
94
+ queue_name,
95
+ retry_policy,
96
+ task_queue: payload.fetch(:activity_task_queue)
97
+ )
98
+ end
99
+
100
+ def apply_rate_limits_for_class(payload, job_class)
101
+ rate_limits = rate_limits_for(job_class)
102
+ payload[:rate_limits] = rate_limits if rate_limits.any?
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string/inflections"
4
+ require "time"
5
+
6
+ require_relative "adapter"
7
+ require_relative "external_operation"
8
+ require_relative "workflow_id_builder"
9
+
10
+ module ActiveJob
11
+ module Temporal
12
+ module JobPayloadChildWorkflows
13
+ ChildWorkflowRoutingJob = Struct.new(:queue_name, :priority)
14
+
15
+ private
16
+
17
+ def apply_child_workflows(payload, job)
18
+ child_workflows = child_workflow_payloads_for(job)
19
+ payload[:child_workflows] = child_workflows if child_workflows.any?
20
+ end
21
+
22
+ def child_workflow_payloads_for(job)
23
+ child_workflows = job.respond_to?(:temporal_child_workflows) ? job.temporal_child_workflows : nil
24
+ Array(child_workflows).each_with_index.map do |child_workflow, index|
25
+ child_workflow_payload_for(job, child_workflow, index + 1)
26
+ end
27
+ end
28
+
29
+ def child_workflow_payload_for(root_job, child_workflow, position)
30
+ external_operation = ExternalOperation.normalize(child_workflow)
31
+ return external_child_workflow_payload(external_operation) if external_operation
32
+
33
+ job_class = child_workflow_job_class(child_workflow)
34
+ options = child_workflow_options(child_workflow)
35
+ queue_name = child_workflow_queue_name(job_class, options)
36
+ job_id = "#{root_job.job_id}:child:#{position}"
37
+ payload = base_child_workflow_payload(job_class, job_id, queue_name, options)
38
+
39
+ apply_child_workflow_retry_policy(payload, job_class, job_id, queue_name)
40
+ apply_temporal_options(payload, job_class)
41
+ apply_workflow_identity(payload, job_class)
42
+ apply_rate_limits_for_class(payload, job_class)
43
+ apply_workflow_interactions(payload, job_class)
44
+ apply_child_workflow_search_attributes(payload, job_class, job_id, queue_name, options)
45
+ payload
46
+ end
47
+
48
+ def external_child_workflow_payload(external_operation)
49
+ {
50
+ temporal_operation: external_operation.fetch(:temporal_operation),
51
+ temporal_type: external_operation.fetch(:temporal_type),
52
+ options: external_operation.fetch(:options)
53
+ }
54
+ end
55
+
56
+ def base_child_workflow_payload(job_class, job_id, queue_name, options)
57
+ task_queue = child_workflow_task_queue(queue_name, options)
58
+ {
59
+ job_class: job_class.name,
60
+ job_id: job_id,
61
+ workflow_id: child_workflow_id(job_class, job_id),
62
+ queue_name: queue_name,
63
+ arguments: [],
64
+ executions: 0,
65
+ exception_executions: {},
66
+ default_activity_options: default_activity_options,
67
+ activity_task_queue: task_queue,
68
+ workflow_task_queue: task_queue
69
+ }
70
+ end
71
+
72
+ def child_workflow_job_class(child_workflow)
73
+ job_class_name = child_workflow[:job_class] || child_workflow["job_class"]
74
+ job_class = job_class_name.constantize
75
+ return job_class if job_class < ActiveJob::Base
76
+
77
+ raise ArgumentError, "child_workflows entries must be ActiveJob classes or configured jobs"
78
+ end
79
+
80
+ def child_workflow_options(child_workflow)
81
+ child_workflow[:options] || child_workflow["options"] || {}
82
+ end
83
+
84
+ def child_workflow_queue_name(job_class, options)
85
+ queue_name = options[:queue] || options["queue"] || job_class.queue_name
86
+ queue_name.to_s
87
+ end
88
+
89
+ def child_workflow_task_queue(queue_name, options)
90
+ priority = options[:priority] || options["priority"]
91
+ routing_job = ChildWorkflowRoutingJob.new(queue_name, priority)
92
+ Adapter.resolve_task_queue(routing_job, config: @config)
93
+ end
94
+
95
+ def child_workflow_id(job_class, job_id)
96
+ WorkflowIdBuilder.new.build_from_job_class(job_class, job_id)
97
+ end
98
+
99
+ def apply_child_workflow_retry_policy(payload, job_class, job_id, queue_name)
100
+ retry_policy = retry_policy_for(job_class)
101
+ payload[:retry_policy] = retry_policy
102
+ return unless dead_letter_enabled?
103
+
104
+ payload[:dead_letter] = dead_letter_metadata(
105
+ job_class.name,
106
+ job_id,
107
+ queue_name,
108
+ retry_policy,
109
+ task_queue: payload.fetch(:activity_task_queue)
110
+ )
111
+ end
112
+
113
+ def apply_child_workflow_search_attributes(payload, job_class, job_id, queue_name, options)
114
+ return unless @config.respond_to?(:enable_search_attributes) && @config.enable_search_attributes
115
+
116
+ tags = options[:tags] || options["tags"] || []
117
+ payload[:search_attributes] = {
118
+ job_class: job_class.name,
119
+ job_id: job_id,
120
+ queue_name: queue_name,
121
+ enqueued_at: Time.now.utc.iso8601,
122
+ tags: tags
123
+ }
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "workflow_id_builder"
4
+
5
+ module ActiveJob
6
+ module Temporal
7
+ module JobPayloadDependencies
8
+ private
9
+
10
+ def apply_dependencies(payload, job)
11
+ return unless job.respond_to?(:temporal_dependencies)
12
+
13
+ dependencies = Array(job.temporal_dependencies)
14
+ return if dependencies.empty?
15
+
16
+ payload[:dependencies] = dependencies.map { |dependency| enrich_dependency(dependency) }
17
+ policy = job.respond_to?(:temporal_dependency_failure_policy) ? job.temporal_dependency_failure_policy : :fail
18
+ payload[:dependency_failure_policy] = policy.to_s
19
+ end
20
+
21
+ def enrich_dependency(dependency)
22
+ normalized = dependency.each_with_object({}) do |(key, value), enriched_dependency|
23
+ enriched_dependency[key.to_sym] = value
24
+ end
25
+ normalized[:workflow_id] ||= default_dependency_workflow_id(normalized)
26
+ normalized.compact
27
+ end
28
+
29
+ def default_dependency_workflow_id(dependency)
30
+ job_class = dependency[:job_class]
31
+ job_id = dependency[:job_id]
32
+ return unless job_class && job_id
33
+
34
+ workflow_id = "#{WorkflowIdBuilder::DEFAULT_PREFIX}:#{job_class}:#{job_id}"
35
+ WorkflowIdBuilder.validate!(workflow_id)
36
+ workflow_id
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rate_limit_options"
4
+
5
+ module ActiveJob
6
+ module Temporal
7
+ module JobPayloadRateLimits
8
+ private
9
+
10
+ def apply_rate_limits(payload, job)
11
+ rate_limits = rate_limits_for(job.class)
12
+ payload[:rate_limits] = rate_limits if rate_limits.any?
13
+ end
14
+
15
+ def rate_limits_for(job_class)
16
+ rate_limits = [
17
+ configured_global_rate_limit,
18
+ configured_job_rate_limit(job_class)
19
+ ].compact
20
+ validate_rate_limiter!(rate_limits)
21
+ rate_limits
22
+ end
23
+
24
+ def configured_global_rate_limit
25
+ return unless @config.respond_to?(:global_rate_limit) && @config.global_rate_limit
26
+
27
+ normalize_rate_limit(@config.global_rate_limit, default_key: "activejob-temporal:global")
28
+ end
29
+
30
+ def configured_job_rate_limit(job_class)
31
+ return unless job_class.respond_to?(:rate_limit)
32
+
33
+ rate_limit = job_class.rate_limit
34
+ return if rate_limit.empty?
35
+
36
+ normalize_rate_limit(rate_limit, default_key: "activejob-temporal:job:#{job_class.name}")
37
+ end
38
+
39
+ def normalize_rate_limit(rate_limit, default_key:)
40
+ normalized = RateLimitOptions.normalize_hash(rate_limit)
41
+ normalized[:key] ||= default_key
42
+ normalized
43
+ end
44
+
45
+ def validate_rate_limiter!(rate_limits)
46
+ return if rate_limits.empty?
47
+ return if @config.respond_to?(:rate_limiter) && @config.rate_limiter
48
+
49
+ raise ConfigurationError, "rate_limiter is required when rate limits are configured"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module Temporal
5
+ module JobPayloadWorkflowInteractions
6
+ private
7
+
8
+ def apply_workflow_interactions(payload, job_class)
9
+ workflow_interactions = workflow_interactions_for(job_class)
10
+ payload[:workflow_interactions] = workflow_interactions if workflow_interactions
11
+ end
12
+
13
+ def workflow_interactions_for(job_class)
14
+ handlers = {
15
+ signals: handler_names_for(job_class, :temporal_signal_handler_names),
16
+ queries: handler_names_for(job_class, :temporal_query_handler_names),
17
+ updates: handler_names_for(job_class, :temporal_update_handler_names)
18
+ }
19
+ return if handlers.values.all?(&:empty?)
20
+
21
+ { job_class: job_class.name, **handlers }
22
+ end
23
+
24
+ def handler_names_for(job_class, method_name)
25
+ return [] unless job_class.respond_to?(method_name)
26
+
27
+ job_class.public_send(method_name).map(&:to_s).sort
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_job"
4
+
5
+ module ActiveJob
6
+ module Temporal
7
+ # Captures Temporal search tags passed through ActiveJob's set options.
8
+ module JobTags
9
+ attr_reader :temporal_tags
10
+
11
+ def self.normalize(tags)
12
+ return [] if tags.nil?
13
+
14
+ raise ArgumentError, "tags must be an Array of Strings or Symbols" unless tags.is_a?(Array)
15
+
16
+ tags.map do |tag|
17
+ normalize_tag(tag)
18
+ end.uniq
19
+ end
20
+
21
+ def self.normalize_tag(tag)
22
+ return tag if tag.is_a?(String)
23
+ return tag.to_s if tag.is_a?(Symbol)
24
+
25
+ raise ArgumentError, "tags must contain only Strings or Symbols"
26
+ end
27
+
28
+ def set(options = {})
29
+ enqueue_options = options.dup
30
+ normalized_tags = JobTags.normalize(enqueue_options.delete(:tags)) if enqueue_options.key?(:tags)
31
+
32
+ super(enqueue_options).tap do
33
+ @temporal_tags = normalized_tags if options.key?(:tags)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ ActiveJob::Base.prepend(ActiveJob::Temporal::JobTags) if defined?(ActiveJob::Base)
@@ -0,0 +1,126 @@
1
+ en:
2
+ activemodel:
3
+ errors:
4
+ models:
5
+ active_job/temporal/config_validator:
6
+ attributes:
7
+ target:
8
+ target_required: "Target host is required (set ACTIVEJOB_TEMPORAL_TARGET or config.target)"
9
+ invalid_format: "must be in format 'host:port' with DNS-style host labels and TCP port 1-65535 (e.g., 'localhost:7233' or 'temporal.example.com:7233'), got: %{target}"
10
+
11
+ namespace:
12
+ namespace_required: "Namespace is required (set ACTIVEJOB_TEMPORAL_NAMESPACE or config.namespace)"
13
+ invalid_format: "must be 1-1000 characters, start and end with an alphanumeric character, and contain only alphanumeric characters, hyphens, and underscores (got: %{namespace})"
14
+
15
+ default_activity_timeout:
16
+ not_a_duration: "must be a duration (e.g., 10.minutes or 30.seconds), got: %{value}"
17
+ duration_not_positive: "must be positive (got: %{seconds} seconds). Use values like 1.second or 10.minutes"
18
+
19
+ default_retry_initial_interval:
20
+ not_a_duration: "must be a duration (e.g., 10.minutes or 30.seconds), got: %{value}"
21
+ duration_not_positive: "must be positive (got: %{seconds} seconds). Use values like 1.second or 10.minutes"
22
+
23
+ default_retry_backoff:
24
+ retry_backoff_too_small: "must be >= 1.0 (got: %{value}). Use 2.0 for exponential backoff"
25
+
26
+ default_retry_max_attempts:
27
+ retry_max_attempts_negative: "must be >= 0 (got: %{value}). Use 0 for unlimited retries, or positive number for max attempts"
28
+
29
+ max_payload_size_kb:
30
+ payload_size_invalid: "must be between 1 and 2,097,152 KB (got: %{value}). Typical values: 250 KB (default), 500 KB (large), or 1000 KB (very large)"
31
+
32
+ payload_serializer:
33
+ unsupported_serializer: "is not supported. Use one of: json, message_pack, msgpack, marshal"
34
+
35
+ payload_storage_adapter:
36
+ invalid: "must respond to #dump and #load, got: %{value}"
37
+
38
+ payload_storage_threshold_kb:
39
+ required: "is required when payload_storage_adapter is configured"
40
+ requires_adapter: "requires payload_storage_adapter"
41
+ invalid: "must be a positive integer, got: %{value}"
42
+
43
+ max_concurrent_activities:
44
+ concurrent_activities_invalid: "must be positive (got: %{value}). Typical values: 50 (low), 100 (default), 200+ (high throughput)"
45
+
46
+ max_concurrent_workflow_tasks:
47
+ concurrent_workflow_tasks_invalid: "must be positive (got: %{value}). Typical values: 5 (default), 10-50 (medium), 100+ (high throughput)"
48
+
49
+ validation_level:
50
+ invalid_level: "must be one of: strict, warn, none (got: %{value})"
51
+
52
+ observability:
53
+ invalid: "must be an observability configuration, got: %{value}"
54
+
55
+ audit_log:
56
+ not_boolean: "must be true or false, got: %{value}"
57
+
58
+ audit_logger:
59
+ invalid: "must respond to #info, got: %{value}"
60
+
61
+ encrypt_payload:
62
+ not_boolean: "must be true or false, got: %{value}"
63
+
64
+ encryption_key:
65
+ required: "is required when payload encryption is enabled"
66
+ invalid: "must be a Base64-encoded %{bytes}-byte AES-256-GCM key or key metadata with a safe id"
67
+
68
+ encryption_old_keys:
69
+ not_an_array: "must be an array of Base64-encoded keys or key metadata, got: %{value}"
70
+ invalid: "must contain only Base64-encoded %{bytes}-byte AES-256-GCM keys or key metadata with safe ids"
71
+
72
+ workflow_id_generator:
73
+ not_callable: "must respond to #call (for example, ->(job) { \"custom-#{job.job_id}\" }), got: %{value}"
74
+ wrong_arity: "must accept one positional ActiveJob argument (for example, ->(job) { \"custom-#{job.job_id}\" }), got: %{value}"
75
+
76
+ middleware_chain:
77
+ invalid: "must respond to #add and #call, got: %{value}"
78
+
79
+ priority_task_queues:
80
+ not_a_hash: "must be a hash mapping ActiveJob priority values to task queue names, got: %{value}"
81
+ non_integer_priority: "priority keys must be integers because ActiveJob priorities are numeric, got: %{value}"
82
+ blank_queue: "task queue names must be present, got: %{value}"
83
+
84
+ rate_limiter:
85
+ invalid: "must respond to #wait_time_for or #call, got: %{value}"
86
+ wrong_arity: "must accept one rate_limits argument, got: %{value}"
87
+
88
+ global_rate_limit:
89
+ requires_rate_limiter: "requires rate_limiter"
90
+ invalid: "must be a hash like { limit: 100, per: :second }, got: %{value}"
91
+
92
+ dead_letter_queue:
93
+ blank: "must be present when configured"
94
+
95
+ dead_letter_after_attempts:
96
+ requires_queue: "requires dead_letter_queue"
97
+ invalid: "must be greater than 0, got: %{value}"
98
+
99
+ dead_letter_auto_discard_after:
100
+ requires_queue: "requires dead_letter_queue"
101
+ not_a_duration: "must be a duration (e.g., 1.day or 6.hours), got: %{value}"
102
+ duration_not_positive: "must be positive (got: %{seconds} seconds). Use values like 1.hour or 7.days"
103
+
104
+ tls_cert_path:
105
+ requires_key_path: "requires tls_key_path so the client certificate and private key rotate together"
106
+ invalid_path: "must be a non-empty file path, got: %{value}"
107
+ unreadable_path: "must point to a readable, non-symlink file, got: %{value}"
108
+
109
+ tls_key_path:
110
+ invalid_path: "must be a non-empty file path, got: %{value}"
111
+ unreadable_path: "must point to a readable, non-symlink file, got: %{value}"
112
+
113
+ tls_server_root_ca_cert_path:
114
+ invalid_path: "must be a non-empty file path, got: %{value}"
115
+ unreadable_path: "must point to a readable, non-symlink file, got: %{value}"
116
+
117
+ tls_domain:
118
+ blank: "must be present when configured"
119
+
120
+ tls_cert_watch:
121
+ not_boolean: "must be true or false, got: %{value}"
122
+ requires_paths: "requires at least one TLS certificate path to watch"
123
+
124
+ tls_reload_signal:
125
+ blank: "must be present when configured"
126
+ invalid: "must be a signal name like HUP or USR1, got: %{value}"