milvaion-worker 1.0.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/README.md +190 -0
- data/lib/milvaion/cancellation_listener.rb +136 -0
- data/lib/milvaion/context.rb +191 -0
- data/lib/milvaion/errors.rb +45 -0
- data/lib/milvaion/job.rb +43 -0
- data/lib/milvaion/log_buffer.rb +120 -0
- data/lib/milvaion/metrics.rb +115 -0
- data/lib/milvaion/options.rb +205 -0
- data/lib/milvaion/processor.rb +558 -0
- data/lib/milvaion/protocol.rb +45 -0
- data/lib/milvaion/thread_pool.rb +85 -0
- data/lib/milvaion/worker/version.rb +8 -0
- data/lib/milvaion/worker.rb +504 -0
- data/lib/milvaion-worker.rb +3 -0
- data/lib/milvaion.rb +3 -0
- data/milvaion-worker.gemspec +29 -0
- metadata +118 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Milvaion
|
|
4
|
+
# Batches occurrence logs and sends them from a background thread every flush_interval seconds,
|
|
5
|
+
# or as soon as batch_size entries are waiting. Safe to add to from any thread.
|
|
6
|
+
#
|
|
7
|
+
# A batch that fails to send is kept and sent first next time, so logs keep their order.
|
|
8
|
+
class LogBuffer
|
|
9
|
+
attr_reader :batch_size, :flush_interval, :max_buffered
|
|
10
|
+
|
|
11
|
+
# sender is called with one batch hash ({"logs", "batchTimestamp", "count"}) and raises if it cannot send it.
|
|
12
|
+
def initialize(sender, logger, batch_size: 50, flush_interval: 0.5, max_buffered: 10_000)
|
|
13
|
+
@sender = sender
|
|
14
|
+
@logger = logger
|
|
15
|
+
@batch_size = batch_size
|
|
16
|
+
@flush_interval = flush_interval
|
|
17
|
+
@max_buffered = max_buffered
|
|
18
|
+
@mutex = Mutex.new
|
|
19
|
+
@changed = ConditionVariable.new
|
|
20
|
+
@flush_mutex = Mutex.new
|
|
21
|
+
@buffer = []
|
|
22
|
+
@unsent = nil
|
|
23
|
+
@dropped = 0
|
|
24
|
+
@closed = false
|
|
25
|
+
@thread = nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def start
|
|
29
|
+
@thread ||= Thread.new do
|
|
30
|
+
Thread.current.name = "milvaion-logs"
|
|
31
|
+
run
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def add(occurrence_id, instance_id, log)
|
|
38
|
+
@mutex.synchronize do
|
|
39
|
+
if @buffer.size >= max_buffered
|
|
40
|
+
@buffer.shift
|
|
41
|
+
@dropped += 1
|
|
42
|
+
@logger.warn("Log buffer is full; dropped #{@dropped} oldest log(s) so far.") if @dropped % 1000 == 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
@buffer << { "occurrenceId" => occurrence_id, "workerId" => instance_id, "log" => log, "messageTimestamp" => Milvaion.utc_now }
|
|
46
|
+
@changed.broadcast
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def pending
|
|
51
|
+
@mutex.synchronize { @buffer.size + (@unsent&.size || 0) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Sends everything buffered so far. Raises if a batch cannot be sent; it stays buffered.
|
|
55
|
+
def flush
|
|
56
|
+
@flush_mutex.synchronize do
|
|
57
|
+
per_round = batch_size * 2
|
|
58
|
+
rounds = ((pending + per_round - 1) / per_round) + 1
|
|
59
|
+
|
|
60
|
+
rounds.times do
|
|
61
|
+
batch = @mutex.synchronize do
|
|
62
|
+
if @unsent.nil?
|
|
63
|
+
taken = @buffer.shift(per_round)
|
|
64
|
+
@unsent = taken unless taken.empty?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
@unsent
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
return if batch.nil?
|
|
71
|
+
|
|
72
|
+
@sender.call({ "logs" => batch, "batchTimestamp" => Milvaion.utc_now, "count" => batch.size })
|
|
73
|
+
@mutex.synchronize { @unsent = nil }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def close
|
|
79
|
+
@mutex.synchronize do
|
|
80
|
+
@closed = true
|
|
81
|
+
@changed.broadcast
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
@thread&.join(5)
|
|
85
|
+
@thread = nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def run
|
|
91
|
+
loop do
|
|
92
|
+
@mutex.synchronize do
|
|
93
|
+
@changed.wait(@mutex) while !@closed && @buffer.empty? && @unsent.nil?
|
|
94
|
+
# Give more entries a moment to arrive, unless a full batch is already waiting.
|
|
95
|
+
deadline = monotonic + flush_interval
|
|
96
|
+
|
|
97
|
+
while !@closed && @buffer.size < batch_size
|
|
98
|
+
remaining = deadline - monotonic
|
|
99
|
+
break if remaining <= 0
|
|
100
|
+
|
|
101
|
+
@changed.wait(@mutex, remaining)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
break if @mutex.synchronize { @closed }
|
|
106
|
+
|
|
107
|
+
begin
|
|
108
|
+
flush
|
|
109
|
+
rescue StandardError => e
|
|
110
|
+
@logger.debug("Failed to send logs; will retry: #{e.message}")
|
|
111
|
+
Kernel.sleep(flush_interval)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def monotonic
|
|
117
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "etc"
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
6
|
+
module Milvaion
|
|
7
|
+
# Process resource usage for heartbeats. Inside a Linux container the cgroup figures are used,
|
|
8
|
+
# matching what `docker stats` reports, as the .NET SDK does.
|
|
9
|
+
class ResourceSampler
|
|
10
|
+
def initialize
|
|
11
|
+
@last_cpu = nil
|
|
12
|
+
@last_sample = 0.0
|
|
13
|
+
@cores = Etc.nprocessors
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def memory_bytes
|
|
17
|
+
cgroup_memory || proc_rss || ps_rss || 0
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# CPU used since the previous call, summed across cores (one busy core = 100).
|
|
21
|
+
def cpu_usage_percent
|
|
22
|
+
cpu = cgroup_cpu_seconds || Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID)
|
|
23
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
24
|
+
|
|
25
|
+
if @last_cpu.nil?
|
|
26
|
+
@last_cpu = cpu
|
|
27
|
+
@last_sample = now
|
|
28
|
+
return 0.0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
elapsed = now - @last_sample
|
|
32
|
+
used = cpu - @last_cpu
|
|
33
|
+
@last_cpu = cpu
|
|
34
|
+
@last_sample = now
|
|
35
|
+
|
|
36
|
+
return 0.0 if elapsed <= 0
|
|
37
|
+
|
|
38
|
+
(used / elapsed * 100).round(2).clamp(0.0, 100.0 * @cores)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The address used for outbound traffic, or "Unknown".
|
|
42
|
+
def self.local_ip_address
|
|
43
|
+
UDPSocket.open do |socket|
|
|
44
|
+
socket.connect("10.255.255.255", 1) # no packet is sent
|
|
45
|
+
socket.addr.last
|
|
46
|
+
end
|
|
47
|
+
rescue StandardError
|
|
48
|
+
"Unknown"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def read_int(path)
|
|
54
|
+
Integer(File.read(path).strip, 10)
|
|
55
|
+
rescue StandardError
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def read_stat(path, key)
|
|
60
|
+
File.foreach(path) do |line|
|
|
61
|
+
name, value = line.split
|
|
62
|
+
return Integer(value, 10) if name == key && value
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
nil
|
|
66
|
+
rescue StandardError
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def cgroup_memory
|
|
71
|
+
current = read_int("/sys/fs/cgroup/memory.current")
|
|
72
|
+
return [0, current - (read_stat("/sys/fs/cgroup/memory.stat", "inactive_file") || 0)].max if current
|
|
73
|
+
|
|
74
|
+
usage = read_int("/sys/fs/cgroup/memory/memory.usage_in_bytes")
|
|
75
|
+
return [0, usage - (read_stat("/sys/fs/cgroup/memory/memory.stat", "total_inactive_file") || 0)].max if usage
|
|
76
|
+
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def cgroup_cpu_seconds
|
|
81
|
+
usage = read_stat("/sys/fs/cgroup/cpu.stat", "usage_usec")
|
|
82
|
+
return usage / 1_000_000.0 if usage
|
|
83
|
+
|
|
84
|
+
["/sys/fs/cgroup/cpu/cpuacct.usage", "/sys/fs/cgroup/cpuacct/cpuacct.usage"].each do |path|
|
|
85
|
+
nanos = read_int(path)
|
|
86
|
+
return nanos / 1_000_000_000.0 if nanos
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def proc_rss
|
|
93
|
+
pages = Integer(File.read("/proc/self/statm").split[1], 10)
|
|
94
|
+
pages * page_size
|
|
95
|
+
rescue StandardError
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def page_size
|
|
100
|
+
Etc.sysconf(Etc::SC_PAGESIZE)
|
|
101
|
+
rescue StandardError
|
|
102
|
+
4096
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# macOS and other systems without /proc.
|
|
106
|
+
def ps_rss
|
|
107
|
+
return nil if Gem.win_platform?
|
|
108
|
+
|
|
109
|
+
kilobytes = Integer(IO.popen(["ps", "-o", "rss=", "-p", Process.pid.to_s], err: File::NULL, &:read).strip, 10)
|
|
110
|
+
kilobytes * 1024
|
|
111
|
+
rescue StandardError
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "etc"
|
|
4
|
+
require "logger"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
|
|
7
|
+
module Milvaion
|
|
8
|
+
# Worker configuration. Only worker_id is required.
|
|
9
|
+
class WorkerOptions
|
|
10
|
+
DEFAULTS = {
|
|
11
|
+
rabbitmq_url: "amqp://guest:guest@localhost:5672/",
|
|
12
|
+
queue_type: "classic",
|
|
13
|
+
redis_url: "redis://localhost:6379",
|
|
14
|
+
redis_key_prefix: Protocol::DEFAULT_KEY_PREFIX,
|
|
15
|
+
max_parallel_jobs: nil,
|
|
16
|
+
execution_timeout: 3600,
|
|
17
|
+
heartbeat_interval: 30,
|
|
18
|
+
job_heartbeat_interval: 60,
|
|
19
|
+
shutdown_timeout: 30,
|
|
20
|
+
startup_timeout: 30,
|
|
21
|
+
version: "1.0.0",
|
|
22
|
+
instance_id: nil,
|
|
23
|
+
logger: nil
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
# worker_id: identifies the worker application. Use the same value on every replica.
|
|
27
|
+
# rabbitmq_url: AMQP URL of the broker the API uses.
|
|
28
|
+
# queue_type: "classic" or "quorum". Must match the API's MilvaionConfig:RabbitMQ:QueueType.
|
|
29
|
+
# redis_url: Redis only carries cancellation requests. nil disables them.
|
|
30
|
+
# redis_key_prefix: must match the API's MilvaionConfig:Redis:KeyPrefix.
|
|
31
|
+
# max_parallel_jobs: size of the job thread pool, reported as this instance's capacity.
|
|
32
|
+
# execution_timeout: default job timeout in seconds. nil or 0 disables it.
|
|
33
|
+
# heartbeat_interval: seconds between instance heartbeats.
|
|
34
|
+
# job_heartbeat_interval: seconds between Running updates of a long job, so it is not flagged as a zombie. 0 disables it.
|
|
35
|
+
# shutdown_timeout: how long stop lets running jobs finish before returning them to the queue.
|
|
36
|
+
# startup_timeout: how long start waits for RabbitMQ and Redis.
|
|
37
|
+
# version: shown in the dashboard.
|
|
38
|
+
# instance_id: overrides the generated "{worker_id}-{8 hex}" id. Must be unique per process.
|
|
39
|
+
# logger: a Logger. Defaults to one that writes to $stdout at INFO.
|
|
40
|
+
attr_reader :worker_id, :rabbitmq_url, :queue_type, :redis_url, :redis_key_prefix, :max_parallel_jobs,
|
|
41
|
+
:execution_timeout, :heartbeat_interval, :job_heartbeat_interval, :shutdown_timeout,
|
|
42
|
+
:startup_timeout, :version, :instance_id, :logger
|
|
43
|
+
|
|
44
|
+
def initialize(worker_id:, **options)
|
|
45
|
+
unknown = options.keys - DEFAULTS.keys
|
|
46
|
+
raise ArgumentError, "Unknown worker option(s): #{unknown.join(', ')}" unless unknown.empty?
|
|
47
|
+
|
|
48
|
+
values = DEFAULTS.merge(options)
|
|
49
|
+
|
|
50
|
+
@worker_id = worker_id.to_s.strip
|
|
51
|
+
raise ArgumentError, "worker_id is required" if @worker_id.empty?
|
|
52
|
+
|
|
53
|
+
@rabbitmq_url = values[:rabbitmq_url]
|
|
54
|
+
@queue_type = (values[:queue_type] || "classic").to_s.strip.downcase
|
|
55
|
+
raise ArgumentError, %(queue_type must be "classic" or "quorum", got #{values[:queue_type].inspect}) unless %w[classic quorum].include?(@queue_type)
|
|
56
|
+
|
|
57
|
+
@redis_url = values[:redis_url]
|
|
58
|
+
@redis_key_prefix = values[:redis_key_prefix]
|
|
59
|
+
@max_parallel_jobs = Integer(values[:max_parallel_jobs] || Etc.nprocessors * 2)
|
|
60
|
+
raise ArgumentError, "max_parallel_jobs must be positive, got #{@max_parallel_jobs}" unless @max_parallel_jobs.positive?
|
|
61
|
+
|
|
62
|
+
@execution_timeout = values[:execution_timeout]
|
|
63
|
+
@heartbeat_interval = values[:heartbeat_interval]
|
|
64
|
+
@job_heartbeat_interval = values[:job_heartbeat_interval]
|
|
65
|
+
@shutdown_timeout = values[:shutdown_timeout]
|
|
66
|
+
@startup_timeout = values[:startup_timeout]
|
|
67
|
+
@version = values[:version]
|
|
68
|
+
@instance_id = values[:instance_id].to_s.empty? ? "#{@worker_id}-#{SecureRandom.hex(4)}" : values[:instance_id]
|
|
69
|
+
@logger = values[:logger] || Logger.new($stdout, progname: "milvaion", level: Logger::INFO)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def cancellation_channel
|
|
73
|
+
"#{redis_key_prefix}cancellation_channel"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
ENV_MAPPING = {
|
|
77
|
+
"MILVAION_WORKER_ID" => [:worker_id, :string],
|
|
78
|
+
"MILVAION_RABBITMQ_URL" => [:rabbitmq_url, :string],
|
|
79
|
+
"MILVAION_RABBITMQ_QUEUE_TYPE" => [:queue_type, :string],
|
|
80
|
+
"MILVAION_REDIS_KEY_PREFIX" => [:redis_key_prefix, :string],
|
|
81
|
+
"MILVAION_MAX_PARALLEL_JOBS" => [:max_parallel_jobs, :integer],
|
|
82
|
+
"MILVAION_EXECUTION_TIMEOUT_SECONDS" => [:execution_timeout, :float],
|
|
83
|
+
"MILVAION_HEARTBEAT_INTERVAL_SECONDS" => [:heartbeat_interval, :float]
|
|
84
|
+
}.freeze
|
|
85
|
+
|
|
86
|
+
# Reads MILVAION_WORKER_ID, MILVAION_RABBITMQ_URL, MILVAION_RABBITMQ_QUEUE_TYPE, MILVAION_REDIS_URL
|
|
87
|
+
# ("false" disables Redis), MILVAION_REDIS_KEY_PREFIX, MILVAION_MAX_PARALLEL_JOBS,
|
|
88
|
+
# MILVAION_EXECUTION_TIMEOUT_SECONDS and MILVAION_HEARTBEAT_INTERVAL_SECONDS.
|
|
89
|
+
# Keyword arguments win over the environment.
|
|
90
|
+
def self.from_env(env = ENV, **overrides)
|
|
91
|
+
values = {}
|
|
92
|
+
|
|
93
|
+
ENV_MAPPING.each do |key, (name, type)|
|
|
94
|
+
raw = env[key].to_s.strip
|
|
95
|
+
next if raw.empty?
|
|
96
|
+
|
|
97
|
+
values[name] =
|
|
98
|
+
case type
|
|
99
|
+
when :integer then Integer(raw, 10)
|
|
100
|
+
when :float then Float(raw)
|
|
101
|
+
else raw
|
|
102
|
+
end
|
|
103
|
+
rescue ArgumentError
|
|
104
|
+
raise ArgumentError, "#{key} must be a number, got #{raw.inspect}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
redis_url = env["MILVAION_REDIS_URL"].to_s.strip
|
|
108
|
+
|
|
109
|
+
if redis_url.casecmp?("false")
|
|
110
|
+
values[:redis_url] = nil
|
|
111
|
+
elsif !redis_url.empty?
|
|
112
|
+
values[:redis_url] = redis_url
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
values.merge!(overrides)
|
|
116
|
+
values[:worker_id] ||= ""
|
|
117
|
+
|
|
118
|
+
new(**values)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Configuration of one job type. Defaults apply to options left out.
|
|
123
|
+
#
|
|
124
|
+
# routing_pattern: default "{worker_id}.{name without Job}.*".
|
|
125
|
+
# consumer_id: default "{name without Job}-consumer".
|
|
126
|
+
# max_parallel_jobs: jobs of this type that may run at once on this instance (prefetch). Default: min(10, worker max).
|
|
127
|
+
# execution_timeout: seconds. A timeout set on the job in the dashboard wins. 0 disables it. Default: the worker default.
|
|
128
|
+
# max_retries: retries after the first failure before the job goes to the dead letter queue. Default 3.
|
|
129
|
+
# base_retry_delay: retry n waits base_retry_delay * 2^(n-1) seconds. Default 5.
|
|
130
|
+
# data_schema: JSON Schema of the job data, as a Hash or a JSON string. The dashboard builds the job data form from it.
|
|
131
|
+
# result_schema: JSON Schema of the result.
|
|
132
|
+
# interrupt_on_abort: raise JobAborted inside the handler thread on cancel or timeout (Thread#raise, like
|
|
133
|
+
# the Timeout module, with the same risks). Default false: the handler must check the context.
|
|
134
|
+
class JobOptions
|
|
135
|
+
KEYS = %i[routing_pattern consumer_id max_parallel_jobs execution_timeout max_retries base_retry_delay data_schema result_schema interrupt_on_abort].freeze
|
|
136
|
+
|
|
137
|
+
attr_reader(*KEYS)
|
|
138
|
+
|
|
139
|
+
def initialize(**options)
|
|
140
|
+
unknown = options.keys - KEYS
|
|
141
|
+
raise ArgumentError, "Unknown job option(s): #{unknown.join(', ')}" unless unknown.empty?
|
|
142
|
+
|
|
143
|
+
@routing_pattern = options[:routing_pattern]
|
|
144
|
+
@consumer_id = options[:consumer_id]
|
|
145
|
+
@max_parallel_jobs = options[:max_parallel_jobs]
|
|
146
|
+
@execution_timeout = options[:execution_timeout]
|
|
147
|
+
@max_retries = options.fetch(:max_retries, 3)
|
|
148
|
+
@base_retry_delay = options.fetch(:base_retry_delay, 5)
|
|
149
|
+
@data_schema = options[:data_schema]
|
|
150
|
+
@result_schema = options[:result_schema]
|
|
151
|
+
@interrupt_on_abort = options.fetch(:interrupt_on_abort, false)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def to_h
|
|
155
|
+
KEYS.to_h { |key| [key, public_send(key)] }
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# A job type with all defaults applied.
|
|
160
|
+
ResolvedJob = Struct.new(:name, :handler, :routing_pattern, :queue, :consumer_id, :max_parallel_jobs,
|
|
161
|
+
:execution_timeout, :max_retries, :base_retry_delay, :data_schema, :result_schema, :interrupt_on_abort,
|
|
162
|
+
keyword_init: true)
|
|
163
|
+
|
|
164
|
+
# The routing pattern a job gets when it does not set one, identical to the .NET SDK:
|
|
165
|
+
# "SendEmailJob" in worker "Mail-Worker" becomes "mail-worker.sendemail.*".
|
|
166
|
+
def self.generate_routing_pattern(job_name, worker_id)
|
|
167
|
+
"#{worker_id.downcase}.#{trim_job_suffix(job_name).downcase}.*"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# The queue a job type is consumed from.
|
|
171
|
+
def self.queue_name_for_pattern(pattern)
|
|
172
|
+
"#{Protocol::QUEUE_JOBS}.#{pattern.gsub('*', 'wildcard').gsub('#', 'all')}"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def self.trim_job_suffix(name)
|
|
176
|
+
name.sub(/job\z/i, "")
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def self.resolve_job(name, handler, options, worker)
|
|
180
|
+
max_parallel = Integer(options.max_parallel_jobs || [10, worker.max_parallel_jobs].min)
|
|
181
|
+
|
|
182
|
+
unless max_parallel.between?(1, 65_535)
|
|
183
|
+
raise ArgumentError, "max_parallel_jobs of job #{name.inspect} must be between 1 and 65535, got #{max_parallel}"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
pattern = options.routing_pattern.to_s.strip
|
|
187
|
+
pattern = generate_routing_pattern(name, worker.worker_id) if pattern.empty?
|
|
188
|
+
timeout = options.execution_timeout.nil? ? worker.execution_timeout : options.execution_timeout
|
|
189
|
+
|
|
190
|
+
ResolvedJob.new(
|
|
191
|
+
name: name,
|
|
192
|
+
handler: handler,
|
|
193
|
+
routing_pattern: pattern,
|
|
194
|
+
queue: queue_name_for_pattern(pattern),
|
|
195
|
+
consumer_id: options.consumer_id || "#{trim_job_suffix(name).downcase}-consumer",
|
|
196
|
+
max_parallel_jobs: max_parallel,
|
|
197
|
+
execution_timeout: timeout || 0,
|
|
198
|
+
max_retries: [0, Integer(options.max_retries)].max,
|
|
199
|
+
base_retry_delay: [0.0, Float(options.base_retry_delay)].max,
|
|
200
|
+
data_schema: options.data_schema,
|
|
201
|
+
result_schema: options.result_schema,
|
|
202
|
+
interrupt_on_abort: options.interrupt_on_abort ? true : false
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
end
|