lepus 0.0.1.beta2
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/.github/workflows/specs.yml +44 -0
- data/.gitignore +12 -0
- data/.rspec +1 -0
- data/.rubocop.yml +35 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +10 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +120 -0
- data/LICENSE.txt +21 -0
- data/README.md +213 -0
- data/Rakefile +4 -0
- data/bin/console +9 -0
- data/bin/setup +7 -0
- data/docker-compose.yml +8 -0
- data/exec/lepus +9 -0
- data/gemfiles/rails52.gemfile +5 -0
- data/gemfiles/rails52.gemfile.lock +242 -0
- data/gemfiles/rails61.gemfile +5 -0
- data/gemfiles/rails61.gemfile.lock +260 -0
- data/lepus.gemspec +53 -0
- data/lib/lepus/app_executor.rb +19 -0
- data/lib/lepus/cli.rb +27 -0
- data/lib/lepus/configuration.rb +90 -0
- data/lib/lepus/consumer.rb +177 -0
- data/lib/lepus/consumer_config.rb +149 -0
- data/lib/lepus/consumer_wrapper.rb +46 -0
- data/lib/lepus/lifecycle_hooks.rb +49 -0
- data/lib/lepus/message.rb +37 -0
- data/lib/lepus/middleware.rb +18 -0
- data/lib/lepus/middlewares/honeybadger.rb +23 -0
- data/lib/lepus/middlewares/json.rb +35 -0
- data/lib/lepus/middlewares/max_retry.rb +57 -0
- data/lib/lepus/primitive/string.rb +55 -0
- data/lib/lepus/process.rb +136 -0
- data/lib/lepus/process_registry.rb +37 -0
- data/lib/lepus/processes/base.rb +50 -0
- data/lib/lepus/processes/callbacks.rb +72 -0
- data/lib/lepus/processes/consumer.rb +113 -0
- data/lib/lepus/processes/interruptible.rb +38 -0
- data/lib/lepus/processes/procline.rb +11 -0
- data/lib/lepus/processes/registrable.rb +67 -0
- data/lib/lepus/processes/runnable.rb +102 -0
- data/lib/lepus/processes/supervised.rb +44 -0
- data/lib/lepus/processes.rb +6 -0
- data/lib/lepus/producer.rb +42 -0
- data/lib/lepus/rails/log_subscriber.rb +120 -0
- data/lib/lepus/rails/railtie.rb +31 -0
- data/lib/lepus/rails.rb +7 -0
- data/lib/lepus/supervisor/config.rb +45 -0
- data/lib/lepus/supervisor/maintenance.rb +35 -0
- data/lib/lepus/supervisor/pidfile.rb +61 -0
- data/lib/lepus/supervisor/pidfiled.rb +29 -0
- data/lib/lepus/supervisor/signals.rb +71 -0
- data/lib/lepus/supervisor.rb +204 -0
- data/lib/lepus/timer.rb +29 -0
- data/lib/lepus/version.rb +5 -0
- data/lib/lepus.rb +95 -0
- data/lib/puma/plugin/lepus.rb +74 -0
- metadata +290 -0
@@ -0,0 +1,177 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lepus
|
4
|
+
# The abstract base class for consumers processing messages from queues.
|
5
|
+
# @abstract Subclass and override {#work} to implement.
|
6
|
+
class Consumer
|
7
|
+
class << self
|
8
|
+
def abstract_class?
|
9
|
+
return @abstract_class == true if defined?(@abstract_class)
|
10
|
+
|
11
|
+
instance_variable_get(:@config).nil?
|
12
|
+
end
|
13
|
+
|
14
|
+
def abstract_class=(value)
|
15
|
+
@config = nil
|
16
|
+
@abstract_class = value
|
17
|
+
end
|
18
|
+
|
19
|
+
def inherited(subclass)
|
20
|
+
super
|
21
|
+
subclass.abstract_class = false
|
22
|
+
end
|
23
|
+
|
24
|
+
def config
|
25
|
+
return if @abstract_class == true
|
26
|
+
return @config if defined?(@config)
|
27
|
+
|
28
|
+
name = Primitive::String.new(to_s).underscore.split("/").last
|
29
|
+
@config = ConsumerConfig.new(queue: name, exchange: name)
|
30
|
+
end
|
31
|
+
|
32
|
+
# List of registered middlewares. Register new middlewares with {.use}.
|
33
|
+
# @return [Array<Lepus::Middleware>]
|
34
|
+
def middlewares
|
35
|
+
@middlewares ||= []
|
36
|
+
end
|
37
|
+
|
38
|
+
# Registers a new middleware by instantiating +middleware+ and passing it +opts+.
|
39
|
+
#
|
40
|
+
# @param [Symbol, Class<Lepus::Middleware>] middleware The middleware class to instantiate and register.
|
41
|
+
# @param [Hash] opts The options for instantiating the middleware.
|
42
|
+
def use(middleware, opts = {})
|
43
|
+
if middleware.is_a?(Symbol) || middleware.is_a?(String)
|
44
|
+
begin
|
45
|
+
require_relative "middlewares/#{middleware}"
|
46
|
+
class_name = Primitive::String.new(middleware.to_s).classify
|
47
|
+
class_name = "JSON" if class_name == "Json"
|
48
|
+
middleware = Lepus::Middlewares.const_get(class_name)
|
49
|
+
rescue LoadError, NameError
|
50
|
+
raise ArgumentError, "Middleware #{middleware} not found"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
middlewares << middleware.new(**opts)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Configures the consumer, setting queue, exchange and other options to be used by
|
58
|
+
# the add_consumer method.
|
59
|
+
#
|
60
|
+
# @param [Hash] opts The options to configure the consumer with.
|
61
|
+
# @option opts [String, Hash] :queue The name of the queue to consume from.
|
62
|
+
# @option opts [String, Hash] :exchange The name of the exchange the queue should be bound to.
|
63
|
+
# @option opts [Array] :routing_key The routing keys used for the queue binding.
|
64
|
+
# @option opts [Boolean, Hash] :retry_queue (false) Whether a retry queue should be provided.
|
65
|
+
# @option opts [Boolean, Hash] :error_queue (false) Whether an error queue should be provided.
|
66
|
+
def configure(opts = {})
|
67
|
+
@config = ConsumerConfig.new(opts)
|
68
|
+
yield(@config) if block_given?
|
69
|
+
@config
|
70
|
+
end
|
71
|
+
|
72
|
+
def descendants # :nodoc:
|
73
|
+
descendants = []
|
74
|
+
ObjectSpace.each_object(singleton_class) do |k|
|
75
|
+
descendants.unshift k unless k == self
|
76
|
+
end
|
77
|
+
descendants.uniq
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# The method that is called when a message from the queue is received.
|
82
|
+
# Keep in mind that the parameters received can be altered by middlewares!
|
83
|
+
#
|
84
|
+
# @param [Leupus::Message] message The message to process.
|
85
|
+
#
|
86
|
+
# @return [:ack, :reject, :requeue] A symbol denoting what should be done with the message.
|
87
|
+
def perform(message)
|
88
|
+
raise NotImplementedError
|
89
|
+
end
|
90
|
+
|
91
|
+
# Wraps #perform to add middlewares. This is being called by Lepus when a message is received for the consumer.
|
92
|
+
#
|
93
|
+
# @param [Bunny::DeliveryInfo] delivery_info The delivery info of the received message.
|
94
|
+
# @param [Bunny::MessageProperties] metadata The metadata of the received message.
|
95
|
+
# @param [String] payload The payload of the received message.
|
96
|
+
# @raise [InvalidConsumerReturnError] if you return something other than +:ack+, +:reject+ or +:requeue+ from {#perform}.
|
97
|
+
def process_delivery(delivery_info, metadata, payload)
|
98
|
+
message = Message.new(delivery_info, metadata, payload)
|
99
|
+
self
|
100
|
+
.class
|
101
|
+
.middlewares
|
102
|
+
.reverse
|
103
|
+
.reduce(work_proc) do |next_middleware, middleware|
|
104
|
+
nest_middleware(middleware, next_middleware)
|
105
|
+
end
|
106
|
+
.call(message)
|
107
|
+
rescue Lepus::InvalidConsumerReturnError
|
108
|
+
raise
|
109
|
+
rescue Exception => ex # rubocop:disable Lint/RescueException
|
110
|
+
# @TODO: add error handling
|
111
|
+
logger.error(ex)
|
112
|
+
|
113
|
+
reject!
|
114
|
+
end
|
115
|
+
|
116
|
+
protected
|
117
|
+
|
118
|
+
def logger
|
119
|
+
Lepus.logger
|
120
|
+
end
|
121
|
+
|
122
|
+
# Helper method to ack a message.
|
123
|
+
#
|
124
|
+
# @return [:ack]
|
125
|
+
def ack!
|
126
|
+
:ack
|
127
|
+
end
|
128
|
+
alias_method :ack, :ack!
|
129
|
+
|
130
|
+
# Helper method to reject a message.
|
131
|
+
#
|
132
|
+
# @return [:reject]
|
133
|
+
#
|
134
|
+
def reject!
|
135
|
+
:reject
|
136
|
+
end
|
137
|
+
alias_method :reject, :reject!
|
138
|
+
|
139
|
+
# Helper method to requeue a message.
|
140
|
+
#
|
141
|
+
# @return [:requeue]
|
142
|
+
def requeue!
|
143
|
+
:requeue
|
144
|
+
end
|
145
|
+
alias_method :requeue, :requeue!
|
146
|
+
|
147
|
+
# Helper method to nack a message.
|
148
|
+
#
|
149
|
+
# @return [:nack]
|
150
|
+
def nack!
|
151
|
+
:nack
|
152
|
+
end
|
153
|
+
alias_method :nack, :nack!
|
154
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
def work_proc
|
158
|
+
->(message) do
|
159
|
+
perform(message).tap do |result|
|
160
|
+
verify_result(result)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def nest_middleware(middleware, next_middleware)
|
166
|
+
->(message) do
|
167
|
+
middleware.call(message, next_middleware)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def verify_result(result)
|
172
|
+
return if %i[ack reject requeue nack].include?(result)
|
173
|
+
|
174
|
+
raise InvalidConsumerReturnError, result
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require "bunny"
|
2
|
+
|
3
|
+
module Lepus
|
4
|
+
# Parse the list of options for the consumer.
|
5
|
+
class ConsumerConfig
|
6
|
+
DEFAULT_EXCHANGE_OPTIONS = {
|
7
|
+
name: nil,
|
8
|
+
type: :topic, # The type of the exchange (:direct, :fanout, :topic or :headers).
|
9
|
+
durable: true
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
DEFAULT_QUEUE_OPTIONS = {
|
13
|
+
name: nil,
|
14
|
+
durable: true
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
DEFAULT_RETRY_QUEUE_OPTIONS = {
|
18
|
+
name: nil,
|
19
|
+
durable: true,
|
20
|
+
delay: 5000,
|
21
|
+
arguments: {}
|
22
|
+
}
|
23
|
+
|
24
|
+
DEFAULT_ERROR_QUEUE_OPTIONS = DEFAULT_QUEUE_OPTIONS
|
25
|
+
|
26
|
+
attr_reader :options
|
27
|
+
|
28
|
+
def initialize(options = {})
|
29
|
+
opts = HashUtil.deep_symbolize_keys(options)
|
30
|
+
|
31
|
+
@exchange_opts = DEFAULT_EXCHANGE_OPTIONS.merge(
|
32
|
+
declaration_config(opts.delete(:exchange))
|
33
|
+
)
|
34
|
+
@queue_opts = DEFAULT_QUEUE_OPTIONS.merge(
|
35
|
+
declaration_config(opts.delete(:queue))
|
36
|
+
)
|
37
|
+
if (value = opts.delete(:retry_queue))
|
38
|
+
@retry_queue_opts = DEFAULT_RETRY_QUEUE_OPTIONS.merge(
|
39
|
+
declaration_config(value)
|
40
|
+
)
|
41
|
+
end
|
42
|
+
if (value = opts.delete(:error_queue))
|
43
|
+
@error_queue_opts = DEFAULT_ERROR_QUEUE_OPTIONS.merge(
|
44
|
+
declaration_config(value)
|
45
|
+
)
|
46
|
+
end
|
47
|
+
@bind_opts = opts.delete(:bind) || {}
|
48
|
+
if (routing_key = opts.delete(:routing_key))
|
49
|
+
@bind_opts[:routing_key] ||= routing_key
|
50
|
+
end
|
51
|
+
@options = opts
|
52
|
+
end
|
53
|
+
|
54
|
+
def channel_args
|
55
|
+
@channel_opts.values_at(
|
56
|
+
:consumer_pool_size,
|
57
|
+
:consumer_pool_abort_on_exception,
|
58
|
+
:consumer_pool_shutdown_timeout
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
def exchange_args
|
63
|
+
[exchange_name, @exchange_opts.reject { |k, v| k == :name }]
|
64
|
+
end
|
65
|
+
|
66
|
+
def consumer_queue_args
|
67
|
+
opts = @queue_opts.reject { |k, v| k == :name }
|
68
|
+
return [queue_name, opts] unless retry_queue_args
|
69
|
+
|
70
|
+
opts[:arguments] ||= {}
|
71
|
+
opts[:arguments]["x-dead-letter-exchange"] = ""
|
72
|
+
opts[:arguments]["x-dead-letter-routing-key"] = retry_queue_name
|
73
|
+
|
74
|
+
[queue_name, opts]
|
75
|
+
end
|
76
|
+
|
77
|
+
def retry_queue_args
|
78
|
+
return unless @retry_queue_opts
|
79
|
+
|
80
|
+
delay = @retry_queue_opts[:delay]
|
81
|
+
args = (@retry_queue_opts[:arguments] || {}).merge(
|
82
|
+
"x-dead-letter-exchange" => "",
|
83
|
+
"x-dead-letter-routing-key" => queue_name,
|
84
|
+
"x-message-ttl" => delay
|
85
|
+
)
|
86
|
+
extra_keys = %i[name delay]
|
87
|
+
opts = @retry_queue_opts.reject { |k, v| extra_keys.include?(k) }
|
88
|
+
[retry_queue_name, opts.merge(arguments: args)]
|
89
|
+
end
|
90
|
+
|
91
|
+
def error_queue_args
|
92
|
+
return unless @error_queue_opts
|
93
|
+
|
94
|
+
name = @error_queue_opts[:name]
|
95
|
+
name ||= "#{queue_name}.error"
|
96
|
+
[name, @error_queue_opts.reject { |k, v| k == :name }]
|
97
|
+
end
|
98
|
+
|
99
|
+
def binds_args
|
100
|
+
arguments = @bind_opts.fetch(:arguments, {}).transform_keys(&:to_s)
|
101
|
+
opts = {}
|
102
|
+
opts[:arguments] = arguments unless arguments.empty?
|
103
|
+
if (routing_keys = @bind_opts[:routing_key]).is_a?(Array)
|
104
|
+
routing_keys.map { |key| opts.merge(routing_key: key) }
|
105
|
+
elsif (routing_key = @bind_opts[:routing_key])
|
106
|
+
[opts.merge(routing_key: routing_key)]
|
107
|
+
else
|
108
|
+
[opts]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
protected
|
113
|
+
|
114
|
+
def exchange_name
|
115
|
+
@exchange_opts[:name] || raise(InvalidConsumerConfigError, "Exchange name is required")
|
116
|
+
end
|
117
|
+
|
118
|
+
def queue_name
|
119
|
+
@queue_opts[:name] || raise(InvalidConsumerConfigError, "Queue name is required")
|
120
|
+
end
|
121
|
+
|
122
|
+
def retry_queue_name
|
123
|
+
name = @retry_queue_opts[:name]
|
124
|
+
name ||= "#{queue_name}.retry"
|
125
|
+
name
|
126
|
+
end
|
127
|
+
|
128
|
+
# Normalizes a declaration config (for exchanges and queues) into a configuration Hash.
|
129
|
+
#
|
130
|
+
# If the given `value` is a String, convert it to a Hash with the key `:name` and the value.
|
131
|
+
# If the given `value` is a Hash, leave it as is.
|
132
|
+
def declaration_config(value)
|
133
|
+
case value
|
134
|
+
when Hash then value
|
135
|
+
when String then {name: value}
|
136
|
+
when NilClass then {}
|
137
|
+
when TrueClass then {}
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
class HashUtil
|
142
|
+
def self.deep_symbolize_keys(hash)
|
143
|
+
hash.each_with_object({}) do |(k, v), memo|
|
144
|
+
memo[k.to_sym] = v.is_a?(Hash) ? deep_symbolize_keys(v) : v
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "bunny"
|
2
|
+
|
3
|
+
module Lepus
|
4
|
+
# Wraps the user-defined consumer to provide the expected interface to Bunny.
|
5
|
+
class ConsumerWrapper < Bunny::Consumer
|
6
|
+
# @param [Lepus::Consumer] consumer The user-defined consumer implementation derived from {Lepus::Consumer}.
|
7
|
+
# @param [Bunny::Channel] channel The channel used for the consumer.
|
8
|
+
# @param [Bunny::Queue] queue The queue the consumer is subscribed to.
|
9
|
+
# @param [String] consumer_tag A string identifying the consumer instance.
|
10
|
+
# @param [Hash] arguments Arguments that are passed on to +Bunny::Consumer.new+.
|
11
|
+
def initialize(consumer, channel, queue, consumer_tag, arguments = {})
|
12
|
+
@consumer = consumer
|
13
|
+
super(channel, queue, consumer_tag, false, false, arguments)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Called when a message is received from the subscribed queue.
|
17
|
+
#
|
18
|
+
# @param [Bunny::DeliveryInfo] delivery_info The delivery info of the received message.
|
19
|
+
# @param [Bunny::MessageProperties] metadata The metadata of the received message.
|
20
|
+
# @param [String] payload The payload of the received message.
|
21
|
+
def process_delivery(delivery_info, metadata, payload)
|
22
|
+
consumer
|
23
|
+
.process_delivery(delivery_info, metadata, payload)
|
24
|
+
.tap { |result| process_result(result, delivery_info.delivery_tag) }
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :consumer
|
30
|
+
|
31
|
+
def process_result(result, delivery_tag)
|
32
|
+
case result
|
33
|
+
when :ack
|
34
|
+
channel.ack(delivery_tag, false)
|
35
|
+
when :reject
|
36
|
+
channel.reject(delivery_tag)
|
37
|
+
when :requeue
|
38
|
+
channel.reject(delivery_tag, true)
|
39
|
+
when :nack
|
40
|
+
channel.nack(delivery_tag, false, true)
|
41
|
+
else
|
42
|
+
raise Lepus::InvalidConsumerReturnError, result
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lepus
|
4
|
+
# @TODO: Move after/before fork hooks to this module
|
5
|
+
module LifecycleHooks
|
6
|
+
def self.included(base)
|
7
|
+
base.extend ClassMethods
|
8
|
+
base.send :include, InstanceMethods
|
9
|
+
base.instance_variable_set(:@lifecycle_hooks, {start: [], stop: []})
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
attr_reader :lifecycle_hooks
|
14
|
+
|
15
|
+
def on_start(&block)
|
16
|
+
lifecycle_hooks[:start] << block
|
17
|
+
end
|
18
|
+
|
19
|
+
def on_stop(&block)
|
20
|
+
lifecycle_hooks[:stop] << block
|
21
|
+
end
|
22
|
+
|
23
|
+
def clear_hooks
|
24
|
+
lifecycle_hooks[:start] = []
|
25
|
+
lifecycle_hooks[:stop] = []
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module InstanceMethods
|
30
|
+
private
|
31
|
+
|
32
|
+
def run_start_hooks
|
33
|
+
run_hooks_for :start
|
34
|
+
end
|
35
|
+
|
36
|
+
def run_stop_hooks
|
37
|
+
run_hooks_for :stop
|
38
|
+
end
|
39
|
+
|
40
|
+
def run_hooks_for(event)
|
41
|
+
self.class.lifecycle_hooks.fetch(event, []).each do |block|
|
42
|
+
block.call
|
43
|
+
rescue Exception => exception # rubocop:disable Lint/RescueException
|
44
|
+
handle_thread_error(exception)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lepus
|
4
|
+
class Message
|
5
|
+
attr_reader :delivery_info, :metadata, :payload
|
6
|
+
|
7
|
+
def initialize(delivery_info, metadata, payload)
|
8
|
+
@delivery_info = delivery_info
|
9
|
+
@metadata = metadata
|
10
|
+
@payload = payload
|
11
|
+
end
|
12
|
+
|
13
|
+
def mutate(payload: nil, metadata: nil, delivery_info: nil)
|
14
|
+
self.class.new(
|
15
|
+
delivery_info || @delivery_info,
|
16
|
+
metadata || @metadata,
|
17
|
+
payload || @payload
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_h
|
22
|
+
{
|
23
|
+
delivery: delivery_info&.to_h,
|
24
|
+
metadata: metadata&.to_h,
|
25
|
+
payload: payload
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def eql?(other)
|
30
|
+
other.is_a?(self.class) &&
|
31
|
+
delivery_info == other.delivery_info &&
|
32
|
+
metadata == other.metadata &&
|
33
|
+
payload == other.payload
|
34
|
+
end
|
35
|
+
alias_method :==, :eql?
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lepus
|
4
|
+
# The abstract base class for middlewares.
|
5
|
+
# @abstract Subclass and override {#call} (and maybe +#initialize+) to implement.
|
6
|
+
class Middleware
|
7
|
+
def initialize(**)
|
8
|
+
end
|
9
|
+
|
10
|
+
# Invokes the middleware.
|
11
|
+
#
|
12
|
+
# @param [Lepus::Message] message The message to process.
|
13
|
+
# @param app The next middleware to call or the actual consumer instance.
|
14
|
+
def call(message, app)
|
15
|
+
raise NotImplementedError
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lepus
|
4
|
+
module Middlewares
|
5
|
+
# A middleware that automatically wraps {Lepus::Consumer#perform]} in an Honeybadger transaction.
|
6
|
+
class Honeybadger < Lepus::Middleware
|
7
|
+
# @param [Hash] opts The options for the middleware.
|
8
|
+
# @option opts [String] :class_name The name of the class you want to monitor.
|
9
|
+
def initialize(class_name:, **)
|
10
|
+
super
|
11
|
+
|
12
|
+
@class_name = class_name
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(message, app)
|
16
|
+
app.call(message)
|
17
|
+
rescue => err
|
18
|
+
::Honeybadger.notify(err, context: {class_name: @class_name})
|
19
|
+
raise err
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "multi_json"
|
4
|
+
|
5
|
+
module Lepus
|
6
|
+
module Middlewares
|
7
|
+
# A middleware that automatically parses your JSON payload.
|
8
|
+
class JSON < Lepus::Middleware
|
9
|
+
# @param [Hash] opts The options for the middleware.
|
10
|
+
# @option opts [Proc] :on_error (Proc.new { :reject }) A Proc to be called when an error occurs during processing.
|
11
|
+
# @option opts [Boolean] :symbolize_keys (false) Whether to symbolize the keys of your payload.
|
12
|
+
def initialize(**opts)
|
13
|
+
super
|
14
|
+
|
15
|
+
@on_error = opts.fetch(:on_error, proc { :reject })
|
16
|
+
@symbolize_keys = opts.fetch(:symbolize_keys, false)
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(message, app)
|
20
|
+
begin
|
21
|
+
parsed_payload =
|
22
|
+
MultiJson.load(message.payload, symbolize_keys: symbolize_keys)
|
23
|
+
rescue => e
|
24
|
+
return on_error.call(e)
|
25
|
+
end
|
26
|
+
|
27
|
+
app.call(message.mutate(payload: parsed_payload))
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :symbolize_keys, :on_error
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Lepus
|
2
|
+
module Middlewares
|
3
|
+
# A middleware that automatically puts messages on an error queue when the specified number of retries are exceeded.
|
4
|
+
class MaxRetry < Lepus::Middleware
|
5
|
+
include Lepus::AppExecutor
|
6
|
+
|
7
|
+
# @param [Hash] opts The options for the middleware.
|
8
|
+
# @option opts [Integer] :retries The number of retries before the message is sent to the error queue.
|
9
|
+
# @option opts [String] :error_queue The name of the queue where messages should be sent to when the max retries are reached.
|
10
|
+
def initialize(retries:, error_queue:)
|
11
|
+
super
|
12
|
+
|
13
|
+
@retries = retries
|
14
|
+
@error_queue = error_queue
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(message, app)
|
18
|
+
return handle_exceeded(message) if retries_exceeded?(message.metadata)
|
19
|
+
|
20
|
+
app.call(message)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :retries, :error_queue
|
26
|
+
|
27
|
+
def handle_exceeded(message)
|
28
|
+
payload = message.payload
|
29
|
+
payload = MultiJson.dump(payload) if payload.is_a?(Hash)
|
30
|
+
::Bunny::Exchange.default(message.delivery_info.channel).publish(
|
31
|
+
payload,
|
32
|
+
routing_key: error_queue
|
33
|
+
)
|
34
|
+
:ack
|
35
|
+
rescue => err
|
36
|
+
handle_thread_error(err)
|
37
|
+
end
|
38
|
+
|
39
|
+
def retries_exceeded?(metadata)
|
40
|
+
return false if metadata.headers.nil?
|
41
|
+
|
42
|
+
rejected_deaths =
|
43
|
+
metadata
|
44
|
+
.headers
|
45
|
+
.fetch("x-death", [])
|
46
|
+
.select { |death| death["reason"] == "rejected" }
|
47
|
+
|
48
|
+
return false unless rejected_deaths.any?
|
49
|
+
|
50
|
+
retry_count = rejected_deaths.map { |death| death["count"] }.compact.max
|
51
|
+
return false unless retry_count
|
52
|
+
|
53
|
+
retry_count > @retries
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "dry/inflector"
|
5
|
+
rescue LoadError
|
6
|
+
# noop
|
7
|
+
end
|
8
|
+
|
9
|
+
begin
|
10
|
+
require "active_support/inflector"
|
11
|
+
rescue LoadError
|
12
|
+
# noop
|
13
|
+
end
|
14
|
+
|
15
|
+
module Lepus::Primitive
|
16
|
+
class String < ::String
|
17
|
+
def classify
|
18
|
+
new_str = if defined?(Dry::Inflector)
|
19
|
+
Dry::Inflector.new.classify(self)
|
20
|
+
elsif defined?(ActiveSupport::Inflector)
|
21
|
+
ActiveSupport::Inflector.classify(self)
|
22
|
+
else
|
23
|
+
split("/").collect do |c|
|
24
|
+
c.split("_").collect(&:capitalize).join
|
25
|
+
end.join("::")
|
26
|
+
end
|
27
|
+
|
28
|
+
self.class.new(new_str)
|
29
|
+
end
|
30
|
+
|
31
|
+
def constantize
|
32
|
+
if defined?(Dry::Inflector)
|
33
|
+
Dry::Inflector.new.constantize(self)
|
34
|
+
elsif defined?(ActiveSupport::Inflector)
|
35
|
+
ActiveSupport::Inflector.constantize(self)
|
36
|
+
else
|
37
|
+
Object.const_get(self)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def underscore
|
42
|
+
new_str = sub(/^::/, "")
|
43
|
+
.gsub("::", "/")
|
44
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
45
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
46
|
+
.tr("-", "_")
|
47
|
+
.tr(".", "_")
|
48
|
+
.gsub(/\s/, "_")
|
49
|
+
.gsub(/__+/, "_")
|
50
|
+
.downcase
|
51
|
+
|
52
|
+
self.class.new(new_str)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|