milvaion-worker 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +68 -3
- data/lib/milvaion/kafka.rb +299 -0
- data/lib/milvaion/kafka_transport.rb +649 -0
- data/lib/milvaion/options.rb +26 -5
- data/lib/milvaion/processor.rb +77 -10
- data/lib/milvaion/worker/version.rb +1 -1
- data/lib/milvaion/worker.rb +27 -6
- data/milvaion-worker.gemspec +2 -2
- metadata +4 -2
data/lib/milvaion/options.rb
CHANGED
|
@@ -20,7 +20,9 @@ module Milvaion
|
|
|
20
20
|
startup_timeout: 30,
|
|
21
21
|
version: "1.0.0",
|
|
22
22
|
instance_id: nil,
|
|
23
|
-
logger: nil
|
|
23
|
+
logger: nil,
|
|
24
|
+
transport: "rabbitmq",
|
|
25
|
+
kafka: nil
|
|
24
26
|
}.freeze
|
|
25
27
|
|
|
26
28
|
# worker_id: identifies the worker application. Use the same value on every replica.
|
|
@@ -37,7 +39,9 @@ module Milvaion
|
|
|
37
39
|
# version: shown in the dashboard.
|
|
38
40
|
# instance_id: overrides the generated "{worker_id}-{8 hex}" id. Must be unique per process.
|
|
39
41
|
# logger: a Logger. Defaults to one that writes to $stdout at INFO.
|
|
40
|
-
|
|
42
|
+
# transport: "rabbitmq" (default) or "kafka". The API and every worker of a deployment must use the same one.
|
|
43
|
+
# kafka: Kafka settings (a Hash, see Milvaion::Kafka::Settings::KEYS). Only used when transport is "kafka".
|
|
44
|
+
attr_reader :transport, :kafka, :worker_id, :rabbitmq_url, :queue_type, :redis_url, :redis_key_prefix, :max_parallel_jobs,
|
|
41
45
|
:execution_timeout, :heartbeat_interval, :job_heartbeat_interval, :shutdown_timeout,
|
|
42
46
|
:startup_timeout, :version, :instance_id, :logger
|
|
43
47
|
|
|
@@ -67,6 +71,17 @@ module Milvaion
|
|
|
67
71
|
@version = values[:version]
|
|
68
72
|
@instance_id = values[:instance_id].to_s.empty? ? "#{@worker_id}-#{SecureRandom.hex(4)}" : values[:instance_id]
|
|
69
73
|
@logger = values[:logger] || Logger.new($stdout, progname: "milvaion", level: Logger::INFO)
|
|
74
|
+
|
|
75
|
+
@transport = (values[:transport] || "rabbitmq").to_s.strip.downcase
|
|
76
|
+
@transport = "rabbitmq" if @transport.empty?
|
|
77
|
+
raise ArgumentError, %(transport must be "rabbitmq" or "kafka", got #{values[:transport].inspect}) unless %w[rabbitmq kafka].include?(@transport)
|
|
78
|
+
|
|
79
|
+
# Validated here, so a bad setting fails before anything connects.
|
|
80
|
+
@kafka = kafka? ? Kafka::Settings.new(values[:kafka], @max_parallel_jobs) : nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def kafka?
|
|
84
|
+
transport == "kafka"
|
|
70
85
|
end
|
|
71
86
|
|
|
72
87
|
def cancellation_channel
|
|
@@ -77,6 +92,7 @@ module Milvaion
|
|
|
77
92
|
"MILVAION_WORKER_ID" => [:worker_id, :string],
|
|
78
93
|
"MILVAION_RABBITMQ_URL" => [:rabbitmq_url, :string],
|
|
79
94
|
"MILVAION_RABBITMQ_QUEUE_TYPE" => [:queue_type, :string],
|
|
95
|
+
"MILVAION_TRANSPORT" => [:transport, :string],
|
|
80
96
|
"MILVAION_REDIS_KEY_PREFIX" => [:redis_key_prefix, :string],
|
|
81
97
|
"MILVAION_MAX_PARALLEL_JOBS" => [:max_parallel_jobs, :integer],
|
|
82
98
|
"MILVAION_EXECUTION_TIMEOUT_SECONDS" => [:execution_timeout, :float],
|
|
@@ -85,8 +101,9 @@ module Milvaion
|
|
|
85
101
|
|
|
86
102
|
# Reads MILVAION_WORKER_ID, MILVAION_RABBITMQ_URL, MILVAION_RABBITMQ_QUEUE_TYPE, MILVAION_REDIS_URL
|
|
87
103
|
# ("false" disables Redis), MILVAION_REDIS_KEY_PREFIX, MILVAION_MAX_PARALLEL_JOBS,
|
|
88
|
-
# MILVAION_EXECUTION_TIMEOUT_SECONDS and MILVAION_HEARTBEAT_INTERVAL_SECONDS
|
|
89
|
-
#
|
|
104
|
+
# MILVAION_EXECUTION_TIMEOUT_SECONDS and MILVAION_HEARTBEAT_INTERVAL_SECONDS, and the transport
|
|
105
|
+
# from MILVAION_TRANSPORT and the MILVAION_KAFKA_* variables (see Milvaion::Kafka.options_from_env).
|
|
106
|
+
# Keyword arguments win over the environment; a kafka: Hash wins key by key.
|
|
90
107
|
def self.from_env(env = ENV, **overrides)
|
|
91
108
|
values = {}
|
|
92
109
|
|
|
@@ -112,7 +129,11 @@ module Milvaion
|
|
|
112
129
|
values[:redis_url] = redis_url
|
|
113
130
|
end
|
|
114
131
|
|
|
115
|
-
|
|
132
|
+
kafka = Kafka.options_from_env(env)
|
|
133
|
+
kafka = kafka.merge(overrides[:kafka].to_h.transform_keys(&:to_sym)) if overrides[:kafka]
|
|
134
|
+
values[:kafka] = kafka unless kafka.empty?
|
|
135
|
+
|
|
136
|
+
values.merge!(overrides.except(:kafka))
|
|
116
137
|
values[:worker_id] ||= ""
|
|
117
138
|
|
|
118
139
|
new(**values)
|
data/lib/milvaion/processor.rb
CHANGED
|
@@ -3,8 +3,18 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
|
|
5
5
|
module Milvaion
|
|
6
|
-
# One message taken from a job queue. ack is called with no arguments, nack with the
|
|
7
|
-
|
|
6
|
+
# One message taken from a job queue or topic. ack is called with no arguments, nack with the
|
|
7
|
+
# requeue flag: nack(true) returns the message so it runs again, nack(false) dead-letters it.
|
|
8
|
+
#
|
|
9
|
+
# reject: optional; dead-letters a message that cannot be processed, with the reason.
|
|
10
|
+
# Without it nack(false) is used.
|
|
11
|
+
# schedule_retry: optional; hands a failed job to the broker for a delayed retry instead of
|
|
12
|
+
# waiting in process (Kafka's retry topic). Called with a RetryRequest; returns once
|
|
13
|
+
# the broker has accepted the retry and raises otherwise.
|
|
14
|
+
Delivery = Struct.new(:body, :headers, :correlation_id, :redelivered, :ack, :nack, :reject, :schedule_retry, keyword_init: true)
|
|
15
|
+
|
|
16
|
+
# The next attempt of a failed job. retry_count is that of the next attempt; not_before a Time.
|
|
17
|
+
RetryRequest = Struct.new(:job_id, :occurrence_id, :retry_count, :max_retries, :not_before, keyword_init: true)
|
|
8
18
|
|
|
9
19
|
# Abort signals of the jobs running on this instance, keyed by occurrence id.
|
|
10
20
|
class CancellationRegistry
|
|
@@ -103,7 +113,7 @@ module Milvaion
|
|
|
103
113
|
process(delivery, consumer)
|
|
104
114
|
rescue StandardError => e
|
|
105
115
|
logger.error("Unexpected error while processing a job delivery: #{e.class}: #{e.message}")
|
|
106
|
-
|
|
116
|
+
reject(delivery, "unexpected error while processing the job: #{e.class}: #{e.message}")
|
|
107
117
|
ensure
|
|
108
118
|
change_count(-1)
|
|
109
119
|
end
|
|
@@ -192,7 +202,7 @@ module Milvaion
|
|
|
192
202
|
|
|
193
203
|
if job.nil?
|
|
194
204
|
logger.error("Received a message on #{consumer.queue} that is not a valid job. Sending it to the dead letter queue.")
|
|
195
|
-
|
|
205
|
+
reject(delivery, "the message is not a valid job")
|
|
196
206
|
return
|
|
197
207
|
end
|
|
198
208
|
|
|
@@ -200,7 +210,7 @@ module Milvaion
|
|
|
200
210
|
|
|
201
211
|
if occurrence_id.nil?
|
|
202
212
|
logger.error("Job #{job['id']} arrived without an occurrence id. Sending it to the dead letter queue.")
|
|
203
|
-
|
|
213
|
+
reject(delivery, "the message has no occurrence id")
|
|
204
214
|
return
|
|
205
215
|
end
|
|
206
216
|
|
|
@@ -421,8 +431,11 @@ module Milvaion
|
|
|
421
431
|
end
|
|
422
432
|
|
|
423
433
|
# Waits out the backoff, republishes the job with a higher retry count and acks the original.
|
|
424
|
-
# The original stays unacked meanwhile, so a crash during the wait gets it redelivered.
|
|
434
|
+
# The original stays unacked meanwhile, so a crash during the wait gets it redelivered. A broker
|
|
435
|
+
# that schedules retries itself (Kafka) gets the retry right away instead.
|
|
425
436
|
def retry_later(delivery, consumer, run, delay, max_retries)
|
|
437
|
+
return schedule_retry(delivery, run, delay, max_retries) if delivery.schedule_retry
|
|
438
|
+
|
|
426
439
|
interval = options.job_heartbeat_interval.to_f
|
|
427
440
|
remaining = delay.to_f
|
|
428
441
|
|
|
@@ -444,13 +457,67 @@ module Milvaion
|
|
|
444
457
|
headers: { "CorrelationId" => run.occurrence_id, "x-retry-count" => run.retry_count + 1, "MaxRetries" => max_retries })
|
|
445
458
|
rescue StandardError => e
|
|
446
459
|
logger.error("Failed to republish job #{run.job['id']} for retry. Dead-lettering the message: #{e.message}")
|
|
447
|
-
|
|
460
|
+
reject(delivery, "the job could not be republished for retry: #{e.message}")
|
|
448
461
|
return
|
|
449
462
|
end
|
|
450
463
|
|
|
451
464
|
settle { delivery.ack.call }
|
|
452
465
|
end
|
|
453
466
|
|
|
467
|
+
# Hands the next attempt to the broker, settles the original and keeps the occurrence's job
|
|
468
|
+
# heartbeat going until the attempt is due, since the occurrence stays Running.
|
|
469
|
+
def schedule_retry(delivery, run, delay, max_retries)
|
|
470
|
+
due = Time.now + delay
|
|
471
|
+
|
|
472
|
+
begin
|
|
473
|
+
delivery.schedule_retry.call(RetryRequest.new(job_id: run.job["id"], occurrence_id: run.occurrence_id, retry_count: run.retry_count + 1,
|
|
474
|
+
max_retries: max_retries, not_before: due))
|
|
475
|
+
rescue StandardError => e
|
|
476
|
+
logger.error("Failed to schedule job #{run.job['id']} for retry. Dead-lettering the message: #{e.message}")
|
|
477
|
+
reject(delivery, "the job could not be scheduled for retry: #{e.message}")
|
|
478
|
+
return
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
settle { delivery.ack.call }
|
|
482
|
+
|
|
483
|
+
Thread.new do
|
|
484
|
+
Thread.current.name = "milvaion-retry-heartbeat"
|
|
485
|
+
heartbeat_while_waiting(run, due)
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
public
|
|
490
|
+
|
|
491
|
+
# Sends Running statuses every job_heartbeat_interval for an occurrence that waits for its next
|
|
492
|
+
# attempt, until due, until stop (an AbortSignal) is triggered or the worker stops.
|
|
493
|
+
def heartbeat_while_waiting(run, due, stop = nil)
|
|
494
|
+
interval = options.job_heartbeat_interval.to_f
|
|
495
|
+
return unless interval.positive?
|
|
496
|
+
|
|
497
|
+
loop do
|
|
498
|
+
remaining = due - Time.now
|
|
499
|
+
return if remaining <= 0 || stopping?
|
|
500
|
+
|
|
501
|
+
step = [remaining, interval].min
|
|
502
|
+
return if stop ? stop.wait(step) : @shutdown.wait(step)
|
|
503
|
+
|
|
504
|
+
run.status(Status::RUNNING, reliable: false) if due > Time.now && !stopping?
|
|
505
|
+
end
|
|
506
|
+
rescue StandardError => e
|
|
507
|
+
logger.debug("Job heartbeat while waiting for a retry failed: #{e.message}")
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
private
|
|
511
|
+
|
|
512
|
+
# Dead-letters a message that cannot be processed.
|
|
513
|
+
def reject(delivery, reason)
|
|
514
|
+
if delivery.reject
|
|
515
|
+
settle { delivery.reject.call(reason) }
|
|
516
|
+
else
|
|
517
|
+
settle { delivery.nack.call(false) }
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
|
|
454
521
|
def dead_letter(delivery, run, exception, retry_count, max_retries)
|
|
455
522
|
job = run.job
|
|
456
523
|
body = JSON.generate({
|
|
@@ -465,11 +532,11 @@ module Milvaion
|
|
|
465
532
|
|
|
466
533
|
begin
|
|
467
534
|
publisher.publish(Protocol::EXCHANGE_DEAD_LETTER, Protocol::DEAD_LETTER_ROUTING_KEY, body, persistent: true, correlation_id: run.occurrence_id,
|
|
468
|
-
headers: { "CorrelationId" => run.occurrence_id, "x-retry-count" => retry_count, "MaxRetries" => max_retries })
|
|
535
|
+
key: run.occurrence_id, headers: { "CorrelationId" => run.occurrence_id, "x-retry-count" => retry_count, "MaxRetries" => max_retries })
|
|
469
536
|
rescue StandardError => e
|
|
470
537
|
# The queue dead-letters a rejected message itself, just without the exception details.
|
|
471
538
|
logger.error("Failed to publish job #{job['id']} to the dead letter queue. Rejecting the message instead: #{e.message}")
|
|
472
|
-
|
|
539
|
+
reject(delivery, "the dead letter message could not be published: #{e.message}")
|
|
473
540
|
return
|
|
474
541
|
end
|
|
475
542
|
|
|
@@ -542,7 +609,7 @@ module Milvaion
|
|
|
542
609
|
attempt = 0
|
|
543
610
|
|
|
544
611
|
begin
|
|
545
|
-
@p.publisher.publish("", Protocol::QUEUE_STATUS_UPDATES, body, persistent: true)
|
|
612
|
+
@p.publisher.publish("", Protocol::QUEUE_STATUS_UPDATES, body, persistent: true, key: occurrence_id)
|
|
546
613
|
rescue StandardError => e
|
|
547
614
|
if attempt >= delays.size
|
|
548
615
|
@p.logger.warn("Failed to publish status #{Status.name_of(status)} for occurrence #{occurrence_id}: #{e.message}")
|
data/lib/milvaion/worker.rb
CHANGED
|
@@ -8,6 +8,7 @@ require "time"
|
|
|
8
8
|
require_relative "worker/version"
|
|
9
9
|
require_relative "protocol"
|
|
10
10
|
require_relative "errors"
|
|
11
|
+
require_relative "kafka"
|
|
11
12
|
require_relative "options"
|
|
12
13
|
require_relative "context"
|
|
13
14
|
require_relative "log_buffer"
|
|
@@ -16,6 +17,7 @@ require_relative "thread_pool"
|
|
|
16
17
|
require_relative "metrics"
|
|
17
18
|
require_relative "job"
|
|
18
19
|
require_relative "cancellation_listener"
|
|
20
|
+
require_relative "kafka_transport"
|
|
19
21
|
|
|
20
22
|
module Milvaion
|
|
21
23
|
# A Milvaion worker. Register jobs with #job, then call #run (blocking) or #start / #stop.
|
|
@@ -130,9 +132,12 @@ module Milvaion
|
|
|
130
132
|
end
|
|
131
133
|
end
|
|
132
134
|
|
|
133
|
-
# Connects, registers this instance and starts consuming. Raises if
|
|
135
|
+
# Connects, registers this instance and starts consuming. Raises if the broker or Redis cannot be
|
|
134
136
|
# reached within startup_timeout.
|
|
135
137
|
def start
|
|
138
|
+
# Before anything else, so a missing rdkafka gem fails with a clear message.
|
|
139
|
+
KafkaTransport.load_rdkafka! if options.kafka?
|
|
140
|
+
|
|
136
141
|
@state_mutex.synchronize do
|
|
137
142
|
raise Error, "Worker cannot be started from state #{@state}." unless @state == :idle
|
|
138
143
|
raise Error, "Add at least one job before starting the worker." if @jobs.empty?
|
|
@@ -142,6 +147,11 @@ module Milvaion
|
|
|
142
147
|
|
|
143
148
|
logger.info("Starting worker #{worker_id} (instance #{instance_id}) with #{@jobs.size} job type(s).")
|
|
144
149
|
|
|
150
|
+
if options.kafka?
|
|
151
|
+
@kafka = KafkaTransport.new(options, -> { register })
|
|
152
|
+
@publisher = @kafka
|
|
153
|
+
end
|
|
154
|
+
|
|
145
155
|
@logs = LogBuffer.new(->(batch) { send_logs(batch) }, logger).start
|
|
146
156
|
@processor = Processor.new(options, @jobs, @publisher, @logs, @cancellations)
|
|
147
157
|
@processor.on_count_changed = -> { request_immediate_heartbeat }
|
|
@@ -155,7 +165,11 @@ module Milvaion
|
|
|
155
165
|
logger.warn("Redis is disabled: jobs cannot be cancelled from the dashboard.")
|
|
156
166
|
end
|
|
157
167
|
|
|
158
|
-
|
|
168
|
+
if @kafka
|
|
169
|
+
@kafka.start(@processor, @pool)
|
|
170
|
+
else
|
|
171
|
+
connect
|
|
172
|
+
end
|
|
159
173
|
rescue Exception # rubocop:disable Lint/RescueException -- clean up on any failure, then re-raise
|
|
160
174
|
set_state(:stopped)
|
|
161
175
|
close
|
|
@@ -338,7 +352,7 @@ module Milvaion
|
|
|
338
352
|
end
|
|
339
353
|
|
|
340
354
|
def register
|
|
341
|
-
@publisher.publish("", Protocol::QUEUE_WORKER_REGISTRATION, JSON.generate(build_registration), persistent: true)
|
|
355
|
+
@publisher.publish("", Protocol::QUEUE_WORKER_REGISTRATION, JSON.generate(build_registration), persistent: true, key: instance_id)
|
|
342
356
|
send_heartbeat(false)
|
|
343
357
|
logger.info("Registered #{worker_id} (instance #{instance_id}).")
|
|
344
358
|
end
|
|
@@ -395,13 +409,14 @@ module Milvaion
|
|
|
395
409
|
"isStopping" => stopping
|
|
396
410
|
})
|
|
397
411
|
|
|
398
|
-
@publisher.publish("", Protocol::QUEUE_WORKER_HEARTBEAT, body, persistent: false)
|
|
412
|
+
@publisher.publish("", Protocol::QUEUE_WORKER_HEARTBEAT, body, persistent: false, key: instance_id)
|
|
399
413
|
rescue StandardError => e
|
|
400
414
|
logger.debug("Heartbeat failed; will retry on the next interval: #{e.message}")
|
|
401
415
|
end
|
|
402
416
|
|
|
403
417
|
def send_logs(batch)
|
|
404
|
-
|
|
418
|
+
first = batch.is_a?(Hash) ? Array(batch["logs"]).first : nil
|
|
419
|
+
@publisher.publish("", Protocol::QUEUE_WORKER_LOGS, JSON.generate(batch), persistent: true, key: first.is_a?(Hash) ? first["occurrenceId"] : nil)
|
|
405
420
|
end
|
|
406
421
|
|
|
407
422
|
def shutdown
|
|
@@ -414,6 +429,7 @@ module Milvaion
|
|
|
414
429
|
nil
|
|
415
430
|
end
|
|
416
431
|
@consumers.clear
|
|
432
|
+
@kafka&.stop_consuming
|
|
417
433
|
|
|
418
434
|
# Deliveries waiting for a pool thread have not started: give them straight back.
|
|
419
435
|
@pool.close.each do |delivery|
|
|
@@ -430,6 +446,9 @@ module Milvaion
|
|
|
430
446
|
@processor.wait_for_idle(10)
|
|
431
447
|
end
|
|
432
448
|
|
|
449
|
+
# Kafka: commit what has finished and leave the group.
|
|
450
|
+
@kafka&.drained
|
|
451
|
+
|
|
433
452
|
@heartbeat_mutex.synchronize do
|
|
434
453
|
@heartbeat_stopped = true
|
|
435
454
|
@heartbeat_changed.broadcast
|
|
@@ -453,6 +472,7 @@ module Milvaion
|
|
|
453
472
|
@listener&.stop
|
|
454
473
|
@logs&.close
|
|
455
474
|
@publisher.detach
|
|
475
|
+
@kafka&.close
|
|
456
476
|
@pool&.close
|
|
457
477
|
@pool&.join(5)
|
|
458
478
|
|
|
@@ -486,7 +506,8 @@ module Milvaion
|
|
|
486
506
|
end
|
|
487
507
|
end
|
|
488
508
|
|
|
489
|
-
|
|
509
|
+
# key is the Kafka record key; RabbitMQ has no use for it.
|
|
510
|
+
def publish(exchange, routing_key, body, persistent: true, correlation_id: nil, headers: nil, key: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
490
511
|
@mutex.synchronize do
|
|
491
512
|
channel = @channel
|
|
492
513
|
raise ConnectionError, "Not connected to RabbitMQ." if channel.nil? || !channel.open?
|
data/milvaion-worker.gemspec
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
Gem::Specification.new do |spec|
|
|
4
4
|
spec.name = "milvaion-worker"
|
|
5
5
|
# Kept equal to Milvaion::Worker::VERSION by a spec; the publish workflow checks it against the tag.
|
|
6
|
-
spec.version = "1.
|
|
6
|
+
spec.version = "1.1.0"
|
|
7
7
|
spec.authors = ["Milvasoft"]
|
|
8
8
|
spec.summary = "Ruby worker SDK for the Milvaion distributed job scheduler."
|
|
9
9
|
spec.description = "Run Milvaion jobs in Ruby: registration, heartbeats, live logs, retries with " \
|
|
10
10
|
"exponential backoff, dead lettering, timeouts, cancellation and graceful shutdown " \
|
|
11
|
-
"over RabbitMQ and Redis."
|
|
11
|
+
"over RabbitMQ (or Apache Kafka with the rdkafka gem) and Redis."
|
|
12
12
|
spec.homepage = "https://github.com/Milvasoft/milvaion"
|
|
13
13
|
spec.license = "Apache-2.0"
|
|
14
14
|
spec.required_ruby_version = ">= 3.1"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: milvaion-worker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Milvasoft
|
|
@@ -66,7 +66,7 @@ dependencies:
|
|
|
66
66
|
version: '6'
|
|
67
67
|
description: 'Run Milvaion jobs in Ruby: registration, heartbeats, live logs, retries
|
|
68
68
|
with exponential backoff, dead lettering, timeouts, cancellation and graceful shutdown
|
|
69
|
-
over RabbitMQ and Redis.'
|
|
69
|
+
over RabbitMQ (or Apache Kafka with the rdkafka gem) and Redis.'
|
|
70
70
|
email:
|
|
71
71
|
executables: []
|
|
72
72
|
extensions: []
|
|
@@ -79,6 +79,8 @@ files:
|
|
|
79
79
|
- lib/milvaion/context.rb
|
|
80
80
|
- lib/milvaion/errors.rb
|
|
81
81
|
- lib/milvaion/job.rb
|
|
82
|
+
- lib/milvaion/kafka.rb
|
|
83
|
+
- lib/milvaion/kafka_transport.rb
|
|
82
84
|
- lib/milvaion/log_buffer.rb
|
|
83
85
|
- lib/milvaion/metrics.rb
|
|
84
86
|
- lib/milvaion/options.rb
|