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.
@@ -0,0 +1,504 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "rbconfig"
5
+ require "socket"
6
+ require "time"
7
+
8
+ require_relative "worker/version"
9
+ require_relative "protocol"
10
+ require_relative "errors"
11
+ require_relative "options"
12
+ require_relative "context"
13
+ require_relative "log_buffer"
14
+ require_relative "processor"
15
+ require_relative "thread_pool"
16
+ require_relative "metrics"
17
+ require_relative "job"
18
+ require_relative "cancellation_listener"
19
+
20
+ module Milvaion
21
+ # A Milvaion worker. Register jobs with #job, then call #run (blocking) or #start / #stop.
22
+ #
23
+ # worker = Milvaion::Worker.new(worker_id: "report-worker")
24
+ # worker.job("SendEmailJob") { |ctx| ctx.log("hi"); { sent: true } }
25
+ # worker.run
26
+ class Worker
27
+ attr_reader :options
28
+
29
+ def initialize(options = nil, **kwargs)
30
+ @options = options || WorkerOptions.new(**kwargs)
31
+ @jobs = {}
32
+ @cancellations = CancellationRegistry.new
33
+ @sampler = ResourceSampler.new
34
+ @publisher = AmqpPublisher.new
35
+ @consumers = []
36
+ @state = :idle
37
+ @state_mutex = Mutex.new
38
+ @heartbeat_mutex = Mutex.new
39
+ @heartbeat_changed = ConditionVariable.new
40
+ @heartbeat_requested = false
41
+ @heartbeat_stopped = false
42
+ @last_immediate = 0.0
43
+ @stop_thread = nil
44
+ end
45
+
46
+ def logger
47
+ options.logger
48
+ end
49
+
50
+ def worker_id
51
+ options.worker_id
52
+ end
53
+
54
+ def instance_id
55
+ options.instance_id
56
+ end
57
+
58
+ def state
59
+ @state_mutex.synchronize { @state }
60
+ end
61
+
62
+ def running_jobs
63
+ @processor ? @processor.running : 0
64
+ end
65
+
66
+ # Registers a job. Either a name and a block (or any object that responds to call):
67
+ #
68
+ # worker.job("SendEmailJob", max_retries: 5) { |ctx| ... }
69
+ #
70
+ # or a Milvaion::Job subclass, optionally with options that override its job_options:
71
+ #
72
+ # worker.job(SendEmailJob)
73
+ #
74
+ # Names are unique within a worker and matched case-insensitively.
75
+ def job(name_or_class, handler = nil, **options, &block)
76
+ if name_or_class.is_a?(Class)
77
+ klass = name_or_class
78
+ raise ArgumentError, "#{klass} must define perform(ctx)" unless klass.method_defined?(:perform)
79
+
80
+ name = klass.respond_to?(:job_name) ? klass.job_name : klass.name.to_s.split("::").last
81
+ defaults = klass.respond_to?(:job_options) ? klass.job_options : {}
82
+
83
+ return add_job(name, klass, JobOptions.new(**defaults, **options))
84
+ end
85
+
86
+ callable = handler || block
87
+ raise ArgumentError, "Pass a block or a handler that responds to call." unless callable.respond_to?(:call)
88
+
89
+ add_job(name_or_class, callable, JobOptions.new(**options))
90
+ end
91
+
92
+ # Registers a handler (anything that responds to call(ctx), or a Job class) under the name the
93
+ # dashboard dispatches the job by (JobNameInWorker).
94
+ def add_job(name, handler, job_options = JobOptions.new)
95
+ raise Error, "Jobs must be added before the worker starts." unless state == :idle
96
+
97
+ name = name.to_s.strip
98
+ raise ArgumentError, "Job name is required." if name.empty?
99
+
100
+ if @jobs.key?(name.downcase)
101
+ raise ArgumentError, "Job #{name.inspect} is already registered. Job names must be unique within a worker (case-insensitive)."
102
+ end
103
+
104
+ @jobs[name.downcase] = Milvaion.resolve_job(name, handler, job_options, options)
105
+ self
106
+ end
107
+
108
+ # Starts the worker and blocks until SIGINT or SIGTERM, then shuts down gracefully.
109
+ def run
110
+ reader, writer = IO.pipe
111
+ previous = {}
112
+
113
+ %w[INT TERM].each do |signal|
114
+ previous[signal] = Signal.trap(signal) do
115
+ writer.write_nonblock(".")
116
+ rescue StandardError
117
+ nil
118
+ end
119
+ end
120
+
121
+ begin
122
+ start
123
+ reader.read(1)
124
+ logger.info("Shutdown signal received.")
125
+ ensure
126
+ stop
127
+ previous.each { |signal, handler| Signal.trap(signal, handler || "DEFAULT") }
128
+ reader.close
129
+ writer.close
130
+ end
131
+ end
132
+
133
+ # Connects, registers this instance and starts consuming. Raises if RabbitMQ or Redis cannot be
134
+ # reached within startup_timeout.
135
+ def start
136
+ @state_mutex.synchronize do
137
+ raise Error, "Worker cannot be started from state #{@state}." unless @state == :idle
138
+ raise Error, "Add at least one job before starting the worker." if @jobs.empty?
139
+
140
+ @state = :starting
141
+ end
142
+
143
+ logger.info("Starting worker #{worker_id} (instance #{instance_id}) with #{@jobs.size} job type(s).")
144
+
145
+ @logs = LogBuffer.new(->(batch) { send_logs(batch) }, logger).start
146
+ @processor = Processor.new(options, @jobs, @publisher, @logs, @cancellations)
147
+ @processor.on_count_changed = -> { request_immediate_heartbeat }
148
+ @pool = ThreadPool.new(options.max_parallel_jobs, logger).start
149
+
150
+ begin
151
+ if options.redis_url
152
+ @listener = CancellationListener.new(options.redis_url, options.cancellation_channel, @cancellations, logger)
153
+ @listener.start(options.startup_timeout)
154
+ else
155
+ logger.warn("Redis is disabled: jobs cannot be cancelled from the dashboard.")
156
+ end
157
+
158
+ connect
159
+ rescue Exception # rubocop:disable Lint/RescueException -- clean up on any failure, then re-raise
160
+ set_state(:stopped)
161
+ close
162
+ raise
163
+ end
164
+
165
+ @heartbeat_thread = Thread.new do
166
+ Thread.current.name = "milvaion-heartbeat"
167
+ heartbeat_loop
168
+ end
169
+
170
+ set_state(:running)
171
+ self
172
+ end
173
+
174
+ # Stops taking new jobs, lets running ones finish for up to shutdown_timeout, returns the rest to
175
+ # the queue and disconnects. Safe to call more than once.
176
+ def stop
177
+ thread = @state_mutex.synchronize do
178
+ return if %i[idle stopped].include?(@state)
179
+
180
+ @stop_thread ||= Thread.new { shutdown }
181
+ end
182
+
183
+ thread.join
184
+ nil
185
+ end
186
+
187
+ # The registration message. Public for tests.
188
+ def build_registration
189
+ jobs = @jobs.values
190
+ schema = ->(value) { value.is_a?(String) ? value : JSON.generate(value) }
191
+
192
+ metadata = {
193
+ "IsExternal" => false,
194
+ "ExternalScheduler" => nil,
195
+ "ProcessorCount" => Etc.nprocessors,
196
+ "OSVersion" => os_version,
197
+ "RuntimeVersion" => "Ruby #{RUBY_VERSION}",
198
+ "HeartbeatInterval" => options.heartbeat_interval.to_i,
199
+ "Sdk" => "ruby",
200
+ "SdkVersion" => VERSION,
201
+ "JobConfigs" => jobs.map do |j|
202
+ { "JobType" => j.name, "ConsumerId" => j.consumer_id, "MaxParallelJobs" => j.max_parallel_jobs, "ExecutionTimeoutSeconds" => j.execution_timeout.to_i }
203
+ end
204
+ }
205
+
206
+ {
207
+ "workerId" => worker_id,
208
+ "instanceId" => instance_id,
209
+ "displayName" => "#{worker_id} (#{instance_id})",
210
+ "hostName" => Socket.gethostname,
211
+ "ipAddress" => ResourceSampler.local_ip_address,
212
+ "routingPatterns" => jobs.to_h { |j| [j.name, j.routing_pattern] },
213
+ "jobDataDefinitions" => jobs.reject { |j| j.data_schema.nil? }.to_h { |j| [j.name, schema.call(j.data_schema)] },
214
+ "jobResultDefinitions" => jobs.reject { |j| j.result_schema.nil? }.to_h { |j| [j.name, schema.call(j.result_schema)] },
215
+ "jobTypes" => jobs.map(&:name),
216
+ "maxParallelJobs" => options.max_parallel_jobs,
217
+ "version" => options.version,
218
+ # The API reads this case-sensitively, so its properties are PascalCase.
219
+ "metadata" => JSON.generate(metadata)
220
+ }
221
+ end
222
+
223
+ private
224
+
225
+ def set_state(value)
226
+ @state_mutex.synchronize { @state = value }
227
+ end
228
+
229
+ def monotonic
230
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
231
+ end
232
+
233
+ def os_version
234
+ uname = Etc.uname
235
+ "#{uname[:sysname]} #{uname[:release]}"
236
+ rescue StandardError
237
+ RbConfig::CONFIG["host_os"]
238
+ end
239
+
240
+ def connect
241
+ require "bunny"
242
+
243
+ deadline = monotonic + options.startup_timeout
244
+
245
+ begin
246
+ connection = Bunny.new(
247
+ options.rabbitmq_url,
248
+ heartbeat: 60,
249
+ connection_timeout: 10,
250
+ automatically_recover: true,
251
+ network_recovery_interval: 2,
252
+ recovery_attempts: nil,
253
+ recover_from_connection_close: true,
254
+ connection_name: instance_id,
255
+ logger: logger
256
+ )
257
+ connection.start
258
+ rescue StandardError => e
259
+ raise ConnectionError, "Could not connect to RabbitMQ within #{options.startup_timeout}s: #{e.message}" if monotonic + 2 > deadline
260
+
261
+ logger.warn("Could not connect to RabbitMQ: #{e.message}")
262
+ Kernel.sleep(2)
263
+ retry
264
+ end
265
+
266
+ @connection = connection
267
+ # Bunny restores channels, queues, bindings and consumers itself; the API also needs a fresh registration.
268
+ connection.after_recovery_completed { Thread.new { on_reconnect } }
269
+
270
+ setup(connection)
271
+ logger.info("Connected to RabbitMQ.")
272
+ end
273
+
274
+ def setup(connection)
275
+ queue_args = options.queue_type == "quorum" ? { "x-queue-type" => "quorum" } : {}
276
+ channel = connection.create_channel
277
+ channel.confirm_select
278
+
279
+ [Protocol::QUEUE_STATUS_UPDATES, Protocol::QUEUE_WORKER_LOGS, Protocol::QUEUE_WORKER_HEARTBEAT, Protocol::QUEUE_WORKER_REGISTRATION].each do |queue|
280
+ channel.queue(queue, durable: true, arguments: queue_args)
281
+ end
282
+
283
+ channel.topic(Protocol::EXCHANGE_JOBS, durable: true)
284
+ dead_letter = channel.direct(Protocol::EXCHANGE_DEAD_LETTER, durable: true)
285
+ channel.queue(Protocol::QUEUE_FAILED_OCCURRENCES, durable: true, arguments: queue_args)
286
+ .bind(dead_letter, routing_key: Protocol::DEAD_LETTER_ROUTING_KEY)
287
+
288
+ # Before consuming, so the first delivery can already report its status.
289
+ @publisher.attach(channel, { Protocol::EXCHANGE_DEAD_LETTER => dead_letter })
290
+
291
+ @jobs.each_value do |job|
292
+ consumer_channel = connection.create_channel(nil, 1)
293
+ consumer_channel.prefetch(job.max_parallel_jobs)
294
+ settle_lock = Mutex.new
295
+
296
+ arguments = { "x-dead-letter-exchange" => Protocol::EXCHANGE_DEAD_LETTER, "x-dead-letter-routing-key" => Protocol::DEAD_LETTER_ROUTING_KEY }.merge(queue_args)
297
+ queue = consumer_channel.queue(job.queue, durable: true, arguments: arguments)
298
+ queue.bind(Protocol::EXCHANGE_JOBS, routing_key: job.routing_pattern)
299
+
300
+ consumer = queue.subscribe(manual_ack: true, block: false, consumer_tag: "#{instance_id}.#{job.consumer_id}") do |info, properties, body|
301
+ on_delivery(job, consumer_channel, settle_lock, info, properties, body)
302
+ end
303
+
304
+ @consumers << consumer
305
+ logger.info("Consuming #{job.name} from #{job.queue} (pattern #{job.routing_pattern}, prefetch #{job.max_parallel_jobs}).")
306
+ end
307
+
308
+ register
309
+ end
310
+
311
+ def on_delivery(job, channel, settle_lock, info, properties, body)
312
+ tag = info.delivery_tag
313
+
314
+ delivery = Delivery.new(
315
+ body: body,
316
+ headers: properties.headers || {},
317
+ correlation_id: properties.correlation_id,
318
+ redelivered: info.redelivered? ? true : false,
319
+ ack: -> { settle_lock.synchronize { channel.ack(tag) } },
320
+ nack: ->(requeue) { settle_lock.synchronize { channel.nack(tag, false, requeue) } }
321
+ )
322
+
323
+ return if @pool.post(delivery) { |item| @processor.handle(item, job) }
324
+
325
+ # The worker is stopping: give the message back.
326
+ begin
327
+ delivery.nack.call(true)
328
+ rescue StandardError => e
329
+ logger.debug("Could not return a message to the queue: #{e.message}")
330
+ end
331
+ end
332
+
333
+ def on_reconnect
334
+ logger.info("Reconnected to RabbitMQ.")
335
+ register
336
+ rescue StandardError => e
337
+ logger.warn("Re-registering after a reconnect failed: #{e.message}")
338
+ end
339
+
340
+ def register
341
+ @publisher.publish("", Protocol::QUEUE_WORKER_REGISTRATION, JSON.generate(build_registration), persistent: true)
342
+ send_heartbeat(false)
343
+ logger.info("Registered #{worker_id} (instance #{instance_id}).")
344
+ end
345
+
346
+ def heartbeat_loop
347
+ interval = options.heartbeat_interval.to_f
348
+ next_regular = monotonic + interval
349
+
350
+ loop do
351
+ @heartbeat_mutex.synchronize do
352
+ loop do
353
+ return if @heartbeat_stopped
354
+
355
+ now = monotonic
356
+
357
+ if @heartbeat_requested && now >= @last_immediate + 1
358
+ @heartbeat_requested = false
359
+ @last_immediate = now
360
+ break
361
+ end
362
+
363
+ if now >= next_regular
364
+ next_regular = now + interval
365
+ break
366
+ end
367
+
368
+ wake = [next_regular, (@last_immediate + 1 if @heartbeat_requested)].compact.min
369
+ @heartbeat_changed.wait(@heartbeat_mutex, wake - now)
370
+ end
371
+ end
372
+
373
+ send_heartbeat(false)
374
+ end
375
+ end
376
+
377
+ # Reports a changed job count at most once a second; the last change is always sent.
378
+ def request_immediate_heartbeat
379
+ return unless %i[running stopping].include?(state)
380
+
381
+ @heartbeat_mutex.synchronize do
382
+ @heartbeat_requested = true
383
+ @heartbeat_changed.signal
384
+ end
385
+ end
386
+
387
+ def send_heartbeat(stopping)
388
+ body = JSON.generate({
389
+ "workerId" => worker_id,
390
+ "instanceId" => instance_id,
391
+ "currentJobs" => stopping ? 0 : running_jobs,
392
+ "memoryBytes" => @sampler.memory_bytes,
393
+ "cpuUsagePercent" => @sampler.cpu_usage_percent,
394
+ "timestamp" => Milvaion.utc_now,
395
+ "isStopping" => stopping
396
+ })
397
+
398
+ @publisher.publish("", Protocol::QUEUE_WORKER_HEARTBEAT, body, persistent: false)
399
+ rescue StandardError => e
400
+ logger.debug("Heartbeat failed; will retry on the next interval: #{e.message}")
401
+ end
402
+
403
+ def send_logs(batch)
404
+ @publisher.publish("", Protocol::QUEUE_WORKER_LOGS, JSON.generate(batch), persistent: true)
405
+ end
406
+
407
+ def shutdown
408
+ set_state(:stopping)
409
+ logger.info("Stopping worker instance #{instance_id}...")
410
+
411
+ @consumers.each do |consumer|
412
+ consumer.cancel
413
+ rescue StandardError
414
+ nil
415
+ end
416
+ @consumers.clear
417
+
418
+ # Deliveries waiting for a pool thread have not started: give them straight back.
419
+ @pool.close.each do |delivery|
420
+ delivery.nack.call(true)
421
+ rescue StandardError
422
+ nil
423
+ end
424
+
425
+ @processor.begin_shutdown
426
+
427
+ unless @processor.wait_for_idle(options.shutdown_timeout)
428
+ logger.warn("#{@processor.running} job(s) still running after #{options.shutdown_timeout}s. Returning them to the queue.")
429
+ @cancellations.abort_all(AbortReason::SHUTDOWN, "Worker is shutting down.")
430
+ @processor.wait_for_idle(10)
431
+ end
432
+
433
+ @heartbeat_mutex.synchronize do
434
+ @heartbeat_stopped = true
435
+ @heartbeat_changed.broadcast
436
+ end
437
+ @heartbeat_thread&.join(5)
438
+
439
+ send_heartbeat(true)
440
+
441
+ begin
442
+ @logs.flush
443
+ rescue StandardError => e
444
+ logger.warn("Some job logs could not be sent before shutdown: #{e.message}")
445
+ end
446
+
447
+ close
448
+ set_state(:stopped)
449
+ logger.info("Worker instance #{instance_id} stopped.")
450
+ end
451
+
452
+ def close
453
+ @listener&.stop
454
+ @logs&.close
455
+ @publisher.detach
456
+ @pool&.close
457
+ @pool&.join(5)
458
+
459
+ begin
460
+ @connection&.close
461
+ rescue StandardError
462
+ nil
463
+ end
464
+ end
465
+ end
466
+
467
+ # Publishes on the confirm channel and waits for the broker's confirmation. Thread-safe.
468
+ class AmqpPublisher
469
+ def initialize
470
+ @mutex = Mutex.new
471
+ @channel = nil
472
+ @exchanges = {}
473
+ end
474
+
475
+ def attach(channel, exchanges)
476
+ @mutex.synchronize do
477
+ @channel = channel
478
+ @exchanges = exchanges
479
+ end
480
+ end
481
+
482
+ def detach
483
+ @mutex.synchronize do
484
+ @channel = nil
485
+ @exchanges = {}
486
+ end
487
+ end
488
+
489
+ def publish(exchange, routing_key, body, persistent: true, correlation_id: nil, headers: nil)
490
+ @mutex.synchronize do
491
+ channel = @channel
492
+ raise ConnectionError, "Not connected to RabbitMQ." if channel.nil? || !channel.open?
493
+
494
+ target = exchange.empty? ? channel.default_exchange : @exchanges.fetch(exchange) { channel.exchange(exchange, no_declare: true) }
495
+ properties = { routing_key: routing_key, persistent: persistent, content_type: "application/json" }
496
+ properties[:correlation_id] = correlation_id if correlation_id
497
+ properties[:headers] = headers if headers
498
+
499
+ target.publish(body, **properties)
500
+ raise ConnectionError, "RabbitMQ did not confirm the message to #{routing_key}." unless channel.wait_for_confirms
501
+ end
502
+ end
503
+ end
504
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "milvaion/worker"
data/lib/milvaion.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "milvaion/worker"
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "milvaion-worker"
5
+ # Kept equal to Milvaion::Worker::VERSION by a spec; the publish workflow checks it against the tag.
6
+ spec.version = "1.0.0"
7
+ spec.authors = ["Milvasoft"]
8
+ spec.summary = "Ruby worker SDK for the Milvaion distributed job scheduler."
9
+ spec.description = "Run Milvaion jobs in Ruby: registration, heartbeats, live logs, retries with " \
10
+ "exponential backoff, dead lettering, timeouts, cancellation and graceful shutdown " \
11
+ "over RabbitMQ and Redis."
12
+ spec.homepage = "https://github.com/Milvasoft/milvaion"
13
+ spec.license = "Apache-2.0"
14
+ spec.required_ruby_version = ">= 3.1"
15
+
16
+ spec.metadata = {
17
+ "homepage_uri" => spec.homepage,
18
+ "source_code_uri" => "https://github.com/Milvasoft/milvaion/tree/master/sdks/ruby/milvaion-worker",
19
+ "documentation_uri" => "https://github.com/Milvasoft/milvaion/blob/master/sdks/ruby/milvaion-worker/README.md",
20
+ "bug_tracker_uri" => "https://github.com/Milvasoft/milvaion/issues"
21
+ }
22
+
23
+ spec.files = Dir["lib/**/*.rb", "README.md", "milvaion-worker.gemspec"]
24
+ spec.require_paths = ["lib"]
25
+
26
+ spec.add_dependency "bunny", ">= 2.22", "< 3"
27
+ spec.add_dependency "logger", ">= 1.4"
28
+ spec.add_dependency "redis", ">= 4.8", "< 6"
29
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: milvaion-worker
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Milvasoft
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-11-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bunny
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '2.22'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '3'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '2.22'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '3'
33
+ - !ruby/object:Gem::Dependency
34
+ name: logger
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '1.4'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '1.4'
47
+ - !ruby/object:Gem::Dependency
48
+ name: redis
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '4.8'
54
+ - - "<"
55
+ - !ruby/object:Gem::Version
56
+ version: '6'
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '4.8'
64
+ - - "<"
65
+ - !ruby/object:Gem::Version
66
+ version: '6'
67
+ description: 'Run Milvaion jobs in Ruby: registration, heartbeats, live logs, retries
68
+ with exponential backoff, dead lettering, timeouts, cancellation and graceful shutdown
69
+ over RabbitMQ and Redis.'
70
+ email:
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - README.md
76
+ - lib/milvaion-worker.rb
77
+ - lib/milvaion.rb
78
+ - lib/milvaion/cancellation_listener.rb
79
+ - lib/milvaion/context.rb
80
+ - lib/milvaion/errors.rb
81
+ - lib/milvaion/job.rb
82
+ - lib/milvaion/log_buffer.rb
83
+ - lib/milvaion/metrics.rb
84
+ - lib/milvaion/options.rb
85
+ - lib/milvaion/processor.rb
86
+ - lib/milvaion/protocol.rb
87
+ - lib/milvaion/thread_pool.rb
88
+ - lib/milvaion/worker.rb
89
+ - lib/milvaion/worker/version.rb
90
+ - milvaion-worker.gemspec
91
+ homepage: https://github.com/Milvasoft/milvaion
92
+ licenses:
93
+ - Apache-2.0
94
+ metadata:
95
+ homepage_uri: https://github.com/Milvasoft/milvaion
96
+ source_code_uri: https://github.com/Milvasoft/milvaion/tree/master/sdks/ruby/milvaion-worker
97
+ documentation_uri: https://github.com/Milvasoft/milvaion/blob/master/sdks/ruby/milvaion-worker/README.md
98
+ bug_tracker_uri: https://github.com/Milvasoft/milvaion/issues
99
+ post_install_message:
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '3.1'
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubygems_version: 3.5.22
115
+ signing_key:
116
+ specification_version: 4
117
+ summary: Ruby worker SDK for the Milvaion distributed job scheduler.
118
+ test_files: []