lepus 0.0.1.beta2 → 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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/linter.yml +21 -0
  3. data/.github/workflows/specs.yml +93 -13
  4. data/.gitignore +2 -0
  5. data/.rubocop.yml +10 -0
  6. data/.tool-versions +1 -1
  7. data/Gemfile +7 -0
  8. data/Gemfile.lock +36 -9
  9. data/Makefile +19 -0
  10. data/README.md +562 -7
  11. data/bin/setup +5 -2
  12. data/config.ru +14 -0
  13. data/docker-compose.yml +5 -3
  14. data/docs/README.md +80 -0
  15. data/docs/cli.md +108 -0
  16. data/docs/configuration.md +171 -0
  17. data/docs/consumers.md +168 -0
  18. data/docs/getting-started.md +136 -0
  19. data/docs/images/lepus-web.png +0 -0
  20. data/docs/middleware.md +240 -0
  21. data/docs/producers.md +173 -0
  22. data/docs/prometheus.md +112 -0
  23. data/docs/rails.md +161 -0
  24. data/docs/supervisor.md +112 -0
  25. data/docs/testing.md +141 -0
  26. data/docs/web.md +85 -0
  27. data/examples/grafana-dashboard.json +450 -0
  28. data/gemfiles/Gemfile.rails-5.2 +7 -0
  29. data/gemfiles/{rails52.gemfile.lock → Gemfile.rails-5.2.lock} +102 -69
  30. data/gemfiles/Gemfile.rails-6.1 +7 -0
  31. data/gemfiles/{rails61.gemfile.lock → Gemfile.rails-6.1.lock} +113 -79
  32. data/gemfiles/{rails52.gemfile → Gemfile.rails-7.2} +1 -1
  33. data/gemfiles/Gemfile.rails-7.2.lock +321 -0
  34. data/gemfiles/{rails61.gemfile → Gemfile.rails-8.0} +1 -1
  35. data/gemfiles/Gemfile.rails-8.0.lock +322 -0
  36. data/lepus.gemspec +7 -1
  37. data/lib/lepus/cli.rb +35 -4
  38. data/lib/lepus/configuration.rb +107 -0
  39. data/lib/lepus/connection_pool.rb +135 -0
  40. data/lib/lepus/consumer.rb +59 -41
  41. data/lib/lepus/consumers/config.rb +183 -0
  42. data/lib/lepus/consumers/handler.rb +56 -0
  43. data/lib/lepus/consumers/middleware_chain.rb +22 -0
  44. data/lib/lepus/consumers/middlewares/exception_logger.rb +27 -0
  45. data/lib/lepus/consumers/middlewares/honeybadger.rb +33 -0
  46. data/lib/lepus/consumers/middlewares/json.rb +37 -0
  47. data/lib/lepus/consumers/middlewares/max_retry.rb +83 -0
  48. data/lib/lepus/consumers/middlewares/unique.rb +65 -0
  49. data/lib/lepus/consumers/stats.rb +70 -0
  50. data/lib/lepus/consumers/stats_registry.rb +29 -0
  51. data/lib/lepus/consumers/worker.rb +141 -0
  52. data/lib/lepus/consumers/worker_factory.rb +124 -0
  53. data/lib/lepus/consumers.rb +6 -0
  54. data/lib/lepus/message/delivery_info.rb +72 -0
  55. data/lib/lepus/message/metadata.rb +99 -0
  56. data/lib/lepus/message.rb +88 -5
  57. data/lib/lepus/middleware_chain.rb +83 -0
  58. data/lib/lepus/primitive/hash.rb +29 -0
  59. data/lib/lepus/process.rb +24 -24
  60. data/lib/lepus/process_registry/backend.rb +49 -0
  61. data/lib/lepus/process_registry/file_backend.rb +108 -0
  62. data/lib/lepus/process_registry/message_builder.rb +72 -0
  63. data/lib/lepus/process_registry/rabbitmq_backend.rb +153 -0
  64. data/lib/lepus/process_registry.rb +56 -23
  65. data/lib/lepus/processes/base.rb +0 -5
  66. data/lib/lepus/processes/callbacks.rb +3 -0
  67. data/lib/lepus/processes/interruptible.rb +4 -8
  68. data/lib/lepus/processes/procline.rb +1 -1
  69. data/lib/lepus/processes/registrable.rb +1 -1
  70. data/lib/lepus/processes/runnable.rb +1 -1
  71. data/lib/lepus/processes.rb +15 -0
  72. data/lib/lepus/producer.rb +141 -30
  73. data/lib/lepus/producers/config.rb +46 -0
  74. data/lib/lepus/producers/definition.rb +48 -0
  75. data/lib/lepus/producers/hooks.rb +170 -0
  76. data/lib/lepus/producers/middleware_chain.rb +22 -0
  77. data/lib/lepus/producers/middlewares/correlation_id.rb +37 -0
  78. data/lib/lepus/producers/middlewares/header.rb +47 -0
  79. data/lib/lepus/producers/middlewares/instrumentation.rb +30 -0
  80. data/lib/lepus/producers/middlewares/json.rb +47 -0
  81. data/lib/lepus/producers/middlewares/unique.rb +67 -0
  82. data/lib/lepus/producers.rb +7 -0
  83. data/lib/lepus/prometheus/collector.rb +149 -0
  84. data/lib/lepus/prometheus/instrumentation.rb +168 -0
  85. data/lib/lepus/prometheus.rb +48 -0
  86. data/lib/lepus/publisher.rb +67 -0
  87. data/lib/lepus/supervisor/children_pipes.rb +25 -0
  88. data/lib/lepus/supervisor/lifecycle_hooks.rb +50 -0
  89. data/lib/lepus/supervisor/pidfiled.rb +1 -1
  90. data/lib/lepus/supervisor/registry_cleaner.rb +22 -0
  91. data/lib/lepus/supervisor.rb +129 -25
  92. data/lib/lepus/testing/exchange.rb +95 -0
  93. data/lib/lepus/testing/message_builder.rb +177 -0
  94. data/lib/lepus/testing/rspec_matchers.rb +258 -0
  95. data/lib/lepus/testing.rb +210 -0
  96. data/lib/lepus/unique.rb +18 -0
  97. data/lib/lepus/version.rb +1 -1
  98. data/lib/lepus/web/aggregator.rb +154 -0
  99. data/lib/lepus/web/api.rb +132 -0
  100. data/lib/lepus/web/app.rb +37 -0
  101. data/lib/lepus/web/management_api.rb +192 -0
  102. data/lib/lepus/web/respond_with.rb +28 -0
  103. data/lib/lepus/web.rb +238 -0
  104. data/lib/lepus.rb +39 -28
  105. data/test_offline.html +189 -0
  106. data/web/assets/css/styles.css +635 -0
  107. data/web/assets/js/app.js +6 -0
  108. data/web/assets/js/bootstrap.js +20 -0
  109. data/web/assets/js/controllers/connection_controller.js +44 -0
  110. data/web/assets/js/controllers/dashboard_controller.js +499 -0
  111. data/web/assets/js/controllers/queue_controller.js +17 -0
  112. data/web/assets/js/controllers/theme_controller.js +31 -0
  113. data/web/assets/js/offline-manager.js +233 -0
  114. data/web/assets/js/service-worker-manager.js +65 -0
  115. data/web/index.html +159 -0
  116. data/web/sw.js +144 -0
  117. metadata +177 -18
  118. data/lib/lepus/consumer_config.rb +0 -149
  119. data/lib/lepus/consumer_wrapper.rb +0 -46
  120. data/lib/lepus/lifecycle_hooks.rb +0 -49
  121. data/lib/lepus/middlewares/honeybadger.rb +0 -23
  122. data/lib/lepus/middlewares/json.rb +0 -35
  123. data/lib/lepus/middlewares/max_retry.rb +0 -57
  124. data/lib/lepus/processes/consumer.rb +0 -113
  125. data/lib/lepus/supervisor/config.rb +0 -45
@@ -0,0 +1,56 @@
1
+ require "bunny"
2
+
3
+ module Lepus
4
+ module Consumers
5
+ # Wraps the user-defined consumer to provide the expected interface to Bunny.
6
+ class Handler < Bunny::Consumer
7
+ # @param [Lepus::Consumer] consumer_class The user-defined consumer implementation derived from {Lepus::Consumer}.
8
+ # @param [Bunny::Channel] channel The channel used for the consumer.
9
+ # @param [Bunny::Queue] queue The queue the consumer is subscribed to.
10
+ # @param [String] consumer_tag A string identifying the consumer instance.
11
+ # @param [Hash] arguments Arguments that are passed on to +Bunny::Consumer.new+.
12
+ def initialize(consumer_class, channel, queue, consumer_tag, arguments = {})
13
+ @consumer_class = consumer_class
14
+ super(channel, queue, consumer_tag, _no_ack = false, _exclusive = false, arguments)
15
+ end
16
+
17
+ # Called when a message is received from the subscribed queue.
18
+ #
19
+ # @param [Bunny::DeliveryInfo] delivery_info The delivery info of the received message.
20
+ # @param [Bunny::MessageProperties] metadata The metadata of the received message.
21
+ # @param [String] payload The payload of the received message.
22
+ def process_delivery(delivery_info, metadata, payload)
23
+ consumer
24
+ .process_delivery(delivery_info, metadata, payload)
25
+ .tap do |result|
26
+ process_result(result, delivery_info.delivery_tag)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def consumer
33
+ @consumer ||= begin
34
+ instance = @consumer_class.new
35
+ instance.instance_variable_set(:@_handler_channel, channel)
36
+ instance
37
+ end
38
+ end
39
+
40
+ def process_result(result, delivery_tag)
41
+ case result
42
+ when :ack
43
+ channel.ack(delivery_tag, false)
44
+ when :reject
45
+ channel.reject(delivery_tag)
46
+ when :requeue
47
+ channel.reject(delivery_tag, true)
48
+ when :nack
49
+ channel.nack(delivery_tag, false, true)
50
+ else
51
+ raise Lepus::InvalidConsumerReturnError, result
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lepus
4
+ module Consumers
5
+ # Manages middleware registration and execution for consumers.
6
+ # Middlewares can modify the message (payload, headers, routing_key, etc.)
7
+ # before it is processed by the consumer.
8
+ class MiddlewareChain < Lepus::MiddlewareChain
9
+ private
10
+
11
+ def load_middleware(name, opts)
12
+ require_relative "middlewares/#{name}"
13
+ class_name = Primitive::String.new(name.to_s).classify
14
+ class_name = "JSON" if class_name == "Json"
15
+ klass = Lepus::Consumers::Middlewares.const_get(class_name)
16
+ klass.new(**opts)
17
+ rescue LoadError, NameError => e
18
+ raise ArgumentError, "Consumer middleware '#{name}' not found: #{e.message}"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lepus
4
+ module Consumers
5
+ module Middlewares
6
+ # A middleware that logs exceptions raised by downstream middleware/consumers.
7
+ # Default logger is Lepus.logger.
8
+ class ExceptionLogger < Lepus::Middleware
9
+ # @param [Hash] opts The options for the middleware.
10
+ # @option opts [Logger] :logger The logger to use. Defaults to Lepus.logger.
11
+ def initialize(logger: Lepus.logger, **)
12
+ super
13
+
14
+ @logger = logger
15
+ end
16
+
17
+ def call(message, app)
18
+ app.call(message)
19
+ rescue => err
20
+ # Log error message; let outer layers decide how to handle the exception
21
+ @logger.error(err.message)
22
+ raise err
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lepus
4
+ module Consumers
5
+ module Middlewares
6
+ # A middleware that automatically wraps {Lepus::Consumer#perform]} in an Honeybadger transaction.
7
+ class Honeybadger < Lepus::Middleware
8
+ # @param [Hash] opts The options for the middleware.
9
+ # @option opts [String] :class_name The name of the class you want to monitor.
10
+ def initialize(class_name: nil, **)
11
+ super
12
+
13
+ @class_name = class_name
14
+ end
15
+
16
+ def call(message, app)
17
+ app.call(message)
18
+ rescue => err
19
+ context = build_context(message)
20
+ ::Honeybadger.notify(err, context: context)
21
+ raise err
22
+ end
23
+
24
+ private
25
+
26
+ def build_context(message)
27
+ class_name = @class_name || message.consumer_class&.name
28
+ class_name ? {class_name: class_name} : {}
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "multi_json"
4
+
5
+ module Lepus
6
+ module Consumers
7
+ module Middlewares
8
+ # A middleware that automatically parses your JSON payload.
9
+ class JSON < Lepus::Middleware
10
+ # @param [Hash] opts The options for the middleware.
11
+ # @option opts [Proc] :on_error (Proc.new { :reject }) A Proc to be called when an error occurs during processing.
12
+ # @option opts [Boolean] :symbolize_keys (false) Whether to symbolize the keys of your payload.
13
+ def initialize(**opts)
14
+ super
15
+
16
+ @on_error = opts.fetch(:on_error, proc { :reject })
17
+ @symbolize_keys = opts.fetch(:symbolize_keys, false)
18
+ end
19
+
20
+ def call(message, app)
21
+ begin
22
+ parsed_payload =
23
+ MultiJson.load(message.payload, symbolize_keys: symbolize_keys)
24
+ rescue => e
25
+ return on_error.call(e)
26
+ end
27
+
28
+ app.call(message.mutate(payload: parsed_payload))
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :symbolize_keys, :on_error
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lepus
4
+ module Consumers
5
+ module Middlewares
6
+ # A middleware that automatically puts messages on an error queue when the specified number of retries are exceeded.
7
+ class MaxRetry < Lepus::Middleware
8
+ include Lepus::AppExecutor
9
+
10
+ # @param [Hash] opts The options for the middleware.
11
+ # @option opts [Integer] :retries The number of retries before the message is sent to the error queue.
12
+ # @option opts [String] :error_queue The name of the queue where messages should be sent to when the max retries are reached.
13
+ # If not provided, will fallback to the consumer's configured error queue name.
14
+ def initialize(retries:, error_queue: nil)
15
+ super
16
+
17
+ @retries = retries
18
+ @error_queue = error_queue
19
+ end
20
+
21
+ def call(message, app)
22
+ return handle_exceeded(message) if retries_exceeded?(message.metadata)
23
+
24
+ app.call(message)
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :retries
30
+
31
+ def handle_exceeded(message)
32
+ payload = message.payload
33
+ payload = MultiJson.dump(payload) if payload.is_a?(Hash)
34
+ ::Bunny::Exchange.default(message.channel).publish(
35
+ payload,
36
+ routing_key: error_queue_name(message)
37
+ )
38
+ :ack
39
+ rescue Lepus::InvalidConsumerConfigError => err
40
+ raise err
41
+ rescue => err
42
+ handle_thread_error(err)
43
+ end
44
+
45
+ def error_queue_name(message)
46
+ return @error_queue if @error_queue
47
+
48
+ default_error_queue_name(message)
49
+ end
50
+
51
+ def default_error_queue_name(message)
52
+ unless message.consumer_class&.config
53
+ raise Lepus::InvalidConsumerConfigError, "Error queue name is required and consumer class is not available"
54
+ end
55
+
56
+ config = message.consumer_class.config
57
+ unless config.error_queue_name
58
+ raise Lepus::InvalidConsumerConfigError, "Error queue name is required. Configure error_queue in consumer config or provide error_queue option to middleware"
59
+ end
60
+
61
+ config.error_queue_name
62
+ end
63
+
64
+ def retries_exceeded?(metadata)
65
+ return false if metadata.headers.nil?
66
+
67
+ rejected_deaths =
68
+ metadata
69
+ .headers
70
+ .fetch("x-death", [])
71
+ .select { |death| death["reason"] == "rejected" }
72
+
73
+ return false unless rejected_deaths.any?
74
+
75
+ retry_count = rejected_deaths.map { |death| death["count"] }.compact.max
76
+ return false unless retry_count
77
+
78
+ retry_count > @retries
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lepus
4
+ module Consumers
5
+ module Middlewares
6
+ # A middleware that releases deduplication locks after message processing.
7
+ #
8
+ # Works in tandem with Lepus::Producers::Middlewares::Unique. The producer
9
+ # acquires a lock and embeds the lock info in message headers. This consumer
10
+ # middleware reads those headers and releases the lock based on the configured
11
+ # +release_on+ conditions.
12
+ #
13
+ # @example Release on ack (default)
14
+ # use :unique
15
+ #
16
+ # @example Release on ack or reject
17
+ # use :unique, release_on: [:ack, :reject]
18
+ #
19
+ # @example Release on error (e.g., dead-letter scenarios)
20
+ # use :unique, release_on: [:ack, :error]
21
+ class Unique < Lepus::Middleware
22
+ HEADER_LOCK_KEY = "x-dedupe-lock-key"
23
+ HEADER_LOCK_ID = "x-dedupe-lock-id"
24
+ HEADER_LOCK_TTL = "x-dedupe-lock-ttl"
25
+
26
+ # @param release_on [Array<Symbol>] Conditions that trigger lock release.
27
+ # Valid values: +:ack+, +:reject+, +:requeue+, +:nack+, +:error+.
28
+ # Defaults to +[:ack]+.
29
+ def initialize(release_on: [:ack])
30
+ super()
31
+ @release_on = Array(release_on)
32
+ end
33
+
34
+ def call(message, app)
35
+ result = app.call(message)
36
+
37
+ if @release_on.include?(result)
38
+ release_lock(message)
39
+ end
40
+
41
+ result
42
+ rescue
43
+ release_lock(message) if @release_on.include?(:error)
44
+ raise
45
+ end
46
+
47
+ private
48
+
49
+ def release_lock(message)
50
+ headers = message.metadata&.headers
51
+ return unless headers
52
+
53
+ lock_key = headers[HEADER_LOCK_KEY]
54
+ lock_id = headers[HEADER_LOCK_ID]
55
+ return unless lock_key && lock_id
56
+
57
+ lock_opts = {}
58
+ lock_opts[:ttl] = headers[HEADER_LOCK_TTL] if headers[HEADER_LOCK_TTL]
59
+ lock = DeDupe::Lock.new(lock_key: lock_key, lock_id: lock_id, **lock_opts)
60
+ lock.release
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+
5
+ module Lepus
6
+ module Consumers
7
+ # Thread-safe per-consumer-class statistics tracker.
8
+ # Uses atomic counters to track processed/rejected/errored message counts.
9
+ class Stats
10
+ attr_reader :consumer_class
11
+
12
+ def initialize(consumer_class)
13
+ @consumer_class = consumer_class
14
+ @processed = Concurrent::AtomicFixnum.new(0)
15
+ @rejected = Concurrent::AtomicFixnum.new(0)
16
+ @errored = Concurrent::AtomicFixnum.new(0)
17
+ end
18
+
19
+ def record_processed
20
+ @processed.increment
21
+ end
22
+
23
+ def record_rejected
24
+ @rejected.increment
25
+ end
26
+
27
+ def record_errored
28
+ @errored.increment
29
+ end
30
+
31
+ def processed
32
+ @processed.value
33
+ end
34
+
35
+ def rejected
36
+ @rejected.value
37
+ end
38
+
39
+ def errored
40
+ @errored.value
41
+ end
42
+
43
+ def to_h
44
+ config = consumer_class.config
45
+ {
46
+ class_name: consumer_class.name,
47
+ exchange: config.exchange_name,
48
+ queue: config.queue_name,
49
+ route: extract_route(config),
50
+ threads: config.worker_threads,
51
+ processed: @processed.value,
52
+ rejected: @rejected.value,
53
+ errored: @errored.value
54
+ }
55
+ end
56
+
57
+ private
58
+
59
+ def extract_route(config)
60
+ binds = config.binds_args
61
+ return nil if binds.empty?
62
+
63
+ keys = binds.filter_map { |b| b[:routing_key] }
64
+ return nil if keys.empty?
65
+
66
+ (keys.length == 1) ? keys.first : keys
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+
5
+ module Lepus
6
+ module Consumers
7
+ # Per-worker registry of consumer stats.
8
+ # Uses Concurrent::Map for thread-safe lazy initialization.
9
+ class StatsRegistry
10
+ def initialize
11
+ @stats = Concurrent::Map.new
12
+ end
13
+
14
+ def for(consumer_class)
15
+ @stats.compute_if_absent(consumer_class.name) do
16
+ Stats.new(consumer_class)
17
+ end
18
+ end
19
+
20
+ def all
21
+ @stats.values.map(&:to_h)
22
+ end
23
+
24
+ def connection_count
25
+ @stats.size
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Lepus
6
+ module Consumers
7
+ class Worker < Processes::Base
8
+ include Processes::Runnable
9
+
10
+ extend Forwardable
11
+
12
+ def_delegators :definer, :name, :consumers
13
+
14
+ attr_reader :definer
15
+
16
+ def initialize(definer, **options)
17
+ @definer = definer
18
+
19
+ super(**options)
20
+ end
21
+
22
+ def metadata
23
+ super.merge(name: name, consumers: consumers.map(&:to_s))
24
+ end
25
+
26
+ def before_fork
27
+ consumers.each do |consumer_class|
28
+ next unless consumer_class.respond_to?(:before_fork, true)
29
+
30
+ warn "[DEPRECATION] #{consumer_class}.before_fork is deprecated and will be removed in Lepus 0.0.1. Use the process-level before_fork hook instead."
31
+
32
+ consumer_class.send(:before_fork)
33
+ end
34
+
35
+ definer.run_process_callbacks(:before_fork)
36
+ end
37
+
38
+ def after_fork
39
+ consumers.each do |consumer_class|
40
+ next unless consumer_class.respond_to?(:after_fork, true)
41
+
42
+ warn "[DEPRECATION] #{consumer_class}.after_fork is deprecated and will be removed in Lepus 0.0.1. Use the process-level after_fork hook instead."
43
+
44
+ consumer_class.send(:after_fork)
45
+ end
46
+
47
+ definer.run_process_callbacks(:after_fork)
48
+ end
49
+
50
+ private
51
+
52
+ SLEEP_INTERVAL = 5
53
+
54
+ def run
55
+ wrap_in_app_executor do
56
+ setup_consumers!
57
+ rescue Bunny::TCPConnectionFailed, Bunny::PossibleAuthenticationFailureError, Bunny::PreconditionFailed => e
58
+ raise Lepus::ShutdownError.new(e.message)
59
+ rescue Lepus::InvalidConsumerConfigError => e
60
+ raise Lepus::ShutdownError.new(e.message)
61
+ end
62
+
63
+ loop do
64
+ break if shutting_down?
65
+
66
+ wrap_in_app_executor do
67
+ interruptible_sleep(SLEEP_INTERVAL)
68
+ end
69
+ end
70
+ ensure
71
+ Lepus.instrument(:shutdown_process, process: self) do
72
+ run_process_callbacks(:shutdown) { shutdown }
73
+ end
74
+ end
75
+
76
+ def shutdown
77
+ @subscriptions.to_a.each(&:cancel)
78
+ @connection_pool&.shutdown
79
+
80
+ super
81
+ end
82
+
83
+ def set_procline
84
+ procline "#{kind.downcase}-#{name}: #{consumers.size} consumers"
85
+ end
86
+
87
+ def setup_consumers!
88
+ @subscriptions = consumers.flat_map do |consumer_class|
89
+ consumer_config = consumer_class.config
90
+
91
+ Array.new(consumer_config.worker_threads) do |n|
92
+ connection_pool.with_connection do |bunny|
93
+ channel = bunny.create_channel(*consumer_config.channel_args)
94
+ channel.basic_qos(consumer_config.prefetch_count) if consumer_config.prefetch_count
95
+ channel.on_uncaught_exception do |error|
96
+ handle_thread_error(error)
97
+ end
98
+
99
+ exchange = channel.exchange(consumer_config.exchange_name, **consumer_config.exchange_options)
100
+
101
+ if (args = consumer_config.retry_queue_args)
102
+ _retry_queue = channel.queue(*args)
103
+ end
104
+ if (args = consumer_config.error_queue_args)
105
+ _error_queue = channel.queue(*args)
106
+ end
107
+
108
+ main_queue = channel.queue(*consumer_config.consumer_queue_args)
109
+ consumer_config.binds_args.each do |opts|
110
+ main_queue.bind(exchange, **opts)
111
+ end
112
+
113
+ consumer_handler = build_handler(consumer_class, channel, main_queue, "#{consumer_class.name}-#{n + 1}")
114
+
115
+ consumer_handler.on_delivery do |delivery_info, metadata, payload|
116
+ consumer_handler.process_delivery(delivery_info, metadata, payload)
117
+ end
118
+ main_queue.subscribe_with(consumer_handler)
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ # Factory method for creating consumer handlers.
125
+ # Overridden by Lepus::Web to attach stats tracking.
126
+ def build_handler(consumer_class, channel, queue, tag)
127
+ Lepus::Consumers::Handler.new(consumer_class, channel, queue, tag)
128
+ end
129
+
130
+ def connection_pool
131
+ return @connection_pool if defined?(@connection_pool)
132
+
133
+ @connection_pool = Lepus::ConnectionPool.new(
134
+ size: definer.pool_size,
135
+ timeout: definer.pool_timeout,
136
+ suffix: definer.name
137
+ )
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lepus
4
+ module Consumers
5
+ # This is a configuration object for defining process-level settings
6
+ # It holds settings such as connection pool size, timeouts, and alive thresholds and
7
+ # more importantly, the list of consumers that should be run in this process.
8
+ #
9
+ # Note that this class only holds configuration data related the process and does not handle
10
+ # the actual process management or consumer execution. Consumer has its own configuration for
11
+ # AMPQ settings, queue names, etc.
12
+ class WorkerFactory
13
+ DEFAULT_NAME = "default"
14
+ DEFAULT_POOL_SIZE = 1
15
+ DEFAULT_POOL_TIMEOUT = 5.0
16
+
17
+ class << self
18
+ def [](name)
19
+ @instances ||= Concurrent::Map.new
20
+ @instances[name.to_s] ||= new(name)
21
+ end
22
+
23
+ def default
24
+ self[DEFAULT_NAME]
25
+ end
26
+
27
+ def exists?(name)
28
+ return false unless @instances
29
+
30
+ @instances.key?(name.to_s)
31
+ end
32
+
33
+ # Create an immutable copy of the process configuration with the specified consumers.
34
+ # @param name [String, Symbol] the name of the process configuration to use.
35
+ # @param consumers [Array<Lepus::Consumer>] the list of consumer classes to be run in this process.
36
+ # @return [Lepus::Consumers::WorkerFactory] the immutable process configuration.
37
+ def immutate_with(name, consumers: [])
38
+ definer = self[name].dup
39
+ definer.freeze_with(consumers)
40
+ definer
41
+ end
42
+
43
+ private
44
+
45
+ # This method is primarily for testing purposes to reset the instances map.
46
+ def clear_all
47
+ @instances = Concurrent::Map.new
48
+ end
49
+ end
50
+
51
+ # @return [String] the unique name for this process configuration. Default is "default".
52
+ attr_reader :name
53
+
54
+ # @return [Array<Lepus::Consumer>] the list of consumer classes to be run in this process.
55
+ attr_reader :consumers
56
+
57
+ # @return [Integer] the size of the connection pool for this process. Default is 1.
58
+ attr_accessor :pool_size
59
+
60
+ # @return [Integer] the timeout in seconds to wait for a connection from the pool. Default is 5 seconds.
61
+ attr_accessor :pool_timeout
62
+
63
+ # You probably want to use .[] or .default to get an instance instead of calling new directly.
64
+ def initialize(name)
65
+ @name = name.to_s
66
+ @pool_size = DEFAULT_POOL_SIZE
67
+ @pool_timeout = DEFAULT_POOL_TIMEOUT
68
+ @consumers = []
69
+ @callbacks = {before_fork: [], after_fork: []}
70
+ end
71
+
72
+ # Assign multiple attributes at once from a hash of options.
73
+ # @raise [ArgumentError] if an unknown attribute is provided.
74
+ # @return [void]
75
+ def assign(options = {})
76
+ options.each do |key, value|
77
+ raise ArgumentError, "Unknown attribute #{key}" unless respond_to?(:"#{key}=")
78
+
79
+ public_send(:"#{key}=", value)
80
+ end
81
+ end
82
+
83
+ # Freeze this configuration instance and set the consumers that will run in this process.
84
+ # @param consumers [Array<Lepus::Consumer>] the list of consumer classes to be run in this process.
85
+ # @return [void]
86
+ def freeze_with(consumers)
87
+ @consumers = Array(consumers).map do |consumer|
88
+ unless consumer <= Lepus::Consumer
89
+ raise ArgumentError, "#{consumer} is not a subclass of Lepus::Consumer"
90
+ end
91
+
92
+ consumer
93
+ end.uniq.freeze
94
+ @callbacks = @callbacks.transform_values(&:freeze)
95
+
96
+ freeze
97
+ end
98
+
99
+ # Instantiate a new Lepus::Consumers::Worker based on this configuration.
100
+ # @return [Lepus::Consumers::Worker] a new instance of Lepus::Consumers::Worker configured with this definition.
101
+ def instantiate_process
102
+ Lepus::Consumers::Worker.new(self)
103
+ end
104
+
105
+ def before_fork(&block)
106
+ callbacks[:before_fork] << block if block
107
+ end
108
+
109
+ def after_fork(&block)
110
+ callbacks[:after_fork] << block if block
111
+ end
112
+
113
+ def run_process_callbacks(type)
114
+ return unless callbacks[type]
115
+
116
+ callbacks[type].each { |callback| callback.call }
117
+ end
118
+
119
+ private
120
+
121
+ attr_reader :callbacks
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lepus
4
+ module Consumers
5
+ end
6
+ end