ElmerFudd 0.0.26 → 0.0.27
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 +5 -0
- data/lib/ElmerFudd.rb +24 -358
- data/lib/ElmerFudd/active_record_connection_pool_filter.rb +21 -0
- data/lib/ElmerFudd/airbrake_filter.rb +17 -0
- data/lib/ElmerFudd/direct_handler.rb +42 -0
- data/lib/ElmerFudd/discard_return_value_filter.rb +9 -0
- data/lib/ElmerFudd/drop_failed_filter.rb +27 -0
- data/lib/ElmerFudd/filter.rb +12 -0
- data/lib/ElmerFudd/json_filter.rb +11 -0
- data/lib/ElmerFudd/json_publisher.rb +15 -0
- data/lib/ElmerFudd/publisher.rb +74 -0
- data/lib/ElmerFudd/redirect_failed_filter.rb +22 -0
- data/lib/ElmerFudd/retry_filter.rb +27 -0
- data/lib/ElmerFudd/rpc_handler.rb +14 -0
- data/lib/ElmerFudd/topic_handler.rb +8 -0
- data/lib/ElmerFudd/version.rb +1 -1
- data/lib/ElmerFudd/worker.rb +85 -0
- metadata +17 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e2843148a1580faa5bb73307c9284a57bfb2e764
|
4
|
+
data.tar.gz: 72a3ed699a197649d2b2d396af5bb9c59dae17d2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c4c6386d99be4549b7f48bc05dd734f8919cf11034cd836b657acf5a3df5cb7bc3f230bbdd8afd46d7b85e7b7d78b08177af8f210c5235973b9e3176c033ffae
|
7
|
+
data.tar.gz: 359f11e19af4c4be2925dd962fa7dde6d3244baedd0f7fec94d56a398db93110382a228b3d9fa5d96be3c71c0b75b69aaf76b056b4489bb4aaedb5150a4c0425
|
data/README.md
CHANGED
@@ -116,3 +116,8 @@ end
|
|
116
116
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
117
117
|
4. Push to the branch (`git push origin my-new-feature`)
|
118
118
|
5. Create new Pull Request
|
119
|
+
|
120
|
+
** Credits
|
121
|
+
- [Artur Roszczyk](https://github.com/sevos)
|
122
|
+
- [Andrzej Sliwa](https://github.com/andrzejsliwa)
|
123
|
+
- [Andrey Parubets](https://github.com/parubets)
|
data/lib/ElmerFudd.rb
CHANGED
@@ -1,361 +1,27 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
1
|
+
require 'ElmerFudd/version'
|
2
|
+
require 'bunny'
|
3
|
+
require 'thread'
|
4
|
+
require 'json'
|
4
5
|
|
5
6
|
module ElmerFudd
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
resource = ConditionVariable.new
|
27
|
-
correlation_id = @uuid_service.call
|
28
|
-
consumer_tag = @uuid_service.call
|
29
|
-
response = nil
|
30
|
-
|
31
|
-
Timeout.timeout(timeout) do
|
32
|
-
rpc_reply_queue.subscribe(manual_ack: false, block: false, consumer_tag: consumer_tag) do |delivery_info, properties, payload|
|
33
|
-
if properties[:correlation_id] == correlation_id
|
34
|
-
response = payload
|
35
|
-
mutex.synchronize { resource.signal }
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
x.publish(payload.to_s, routing_key: queue_name, reply_to: rpc_reply_queue.name,
|
40
|
-
correlation_id: correlation_id)
|
41
|
-
|
42
|
-
mutex.synchronize { resource.wait(mutex) unless response }
|
43
|
-
response
|
44
|
-
end
|
45
|
-
ensure
|
46
|
-
reply_channel.consumers[consumer_tag].cancel
|
47
|
-
end
|
48
|
-
|
49
|
-
private
|
50
|
-
|
51
|
-
def connection
|
52
|
-
@connection.tap do |c|
|
53
|
-
c.start unless c.connected?
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
def x
|
58
|
-
@x ||= channel.default_exchange
|
59
|
-
end
|
60
|
-
|
61
|
-
def channel
|
62
|
-
@channel ||= connection.create_channel
|
63
|
-
end
|
64
|
-
|
65
|
-
def reply_channel
|
66
|
-
@reply_channel ||= connection.create_channel
|
67
|
-
end
|
68
|
-
|
69
|
-
def rpc_reply_queue
|
70
|
-
@rpc_reply_queue ||= reply_channel.queue("", exclusive: true)
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
class JsonPublisher < Publisher
|
75
|
-
def notify(topic_exchange, routing_key, payload)
|
76
|
-
super(topic_exchange, routing_key, payload.to_json)
|
77
|
-
end
|
78
|
-
|
79
|
-
def cast(queue_name, payload)
|
80
|
-
super(queue_name, payload.to_json)
|
81
|
-
end
|
82
|
-
|
83
|
-
def call(queue_name, payload, **kwargs)
|
84
|
-
JSON.parse(super(queue_name, payload.to_json, **kwargs))
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
class Worker
|
89
|
-
Message = Struct.new(:delivery_info, :properties, :payload, :route)
|
90
|
-
Env = Struct.new(:channel, :logger, :worker_class)
|
91
|
-
Route = Struct.new(:exchange_name, :routing_keys, :queue_name)
|
92
|
-
|
93
|
-
def self.handlers
|
94
|
-
@handlers ||= []
|
95
|
-
end
|
96
|
-
|
97
|
-
def self.Route(queue_name, exchange_and_routing_keys = {"" => queue_name})
|
98
|
-
exchange, routing_keys = exchange_and_routing_keys.first
|
99
|
-
Route.new(exchange, routing_keys, queue_name)
|
100
|
-
end
|
101
|
-
|
102
|
-
def self.default_filters(*filters)
|
103
|
-
@filters = filters
|
104
|
-
end
|
105
|
-
|
106
|
-
def self.handle_event(route, filters: [], handler: nil, &block)
|
107
|
-
handlers << TopicHandler.new(route, handler || block, (@filters + filters + [DiscardReturnValueFilter]).uniq)
|
108
|
-
end
|
109
|
-
|
110
|
-
def self.handle_cast(route, filters: [], handler: nil, &block)
|
111
|
-
handlers << DirectHandler.new(route, handler || block, (@filters + filters + [DiscardReturnValueFilter]).uniq)
|
112
|
-
end
|
113
|
-
|
114
|
-
def self.handle_call(route, filters: [], handler: nil, &block)
|
115
|
-
handlers << RpcHandler.new(route, handler || block, (@filters + filters).uniq)
|
116
|
-
end
|
117
|
-
|
118
|
-
# Helper allowing to use any method taking hash as a handler
|
119
|
-
# def example(text:, **_)
|
120
|
-
# puts text
|
121
|
-
# end
|
122
|
-
# # then in worker
|
123
|
-
# handle_cast(...
|
124
|
-
# handler: payload_as_kwargs(method(:example)))
|
125
|
-
# Thanks to usage of **_ in arguments list it will accept
|
126
|
-
# any payload contaning 'text' key. Skipping **_ will require
|
127
|
-
# listing all payload keys in argument list
|
128
|
-
def self.payload_as_kwargs(handler, only: nil)
|
129
|
-
lambda do |_env, message|
|
130
|
-
symbolized_payload = message.payload.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
|
131
|
-
symbolized_payload = symbolized_payload.select { |k,v| Array(only).include?(k) } if only
|
132
|
-
handler.call(symbolized_payload)
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
|
-
def initialize(connection, concurrency: 1, logger: Logger.new($stdout))
|
137
|
-
@connection = connection
|
138
|
-
@concurrency = concurrency
|
139
|
-
@logger = logger
|
140
|
-
end
|
141
|
-
|
142
|
-
def start
|
143
|
-
self.class.handlers.each do |handler|
|
144
|
-
handler.queue(env).subscribe(manual_ack: true, block: false) do |delivery_info, properties, payload|
|
145
|
-
message = Message.new(delivery_info, properties, payload, handler.route)
|
146
|
-
begin
|
147
|
-
handler.call(env, message)
|
148
|
-
env.channel.acknowledge(message.delivery_info.delivery_tag)
|
149
|
-
rescue Exception => e
|
150
|
-
env.logger.fatal("Worker blocked: %s, %s:" % [e.class, e.message])
|
151
|
-
e.backtrace.each { |l| env.logger.fatal(l) }
|
152
|
-
end
|
153
|
-
end
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
private
|
158
|
-
|
159
|
-
def env
|
160
|
-
@env ||= Env.new(channel, @logger, self.class)
|
161
|
-
end
|
162
|
-
|
163
|
-
def connection
|
164
|
-
@connection.tap { |c| c.start unless c.connected? }
|
165
|
-
end
|
166
|
-
|
167
|
-
def channel
|
168
|
-
@channel ||= connection.create_channel.tap { |c| c.prefetch(@concurrency) }
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
|
-
module Filter
|
173
|
-
def call_next(env, message, filters)
|
174
|
-
next_filter, *remainder = filters
|
175
|
-
if remainder.empty?
|
176
|
-
next_filter.call(env, message)
|
177
|
-
else
|
178
|
-
next_filter.call(env, message, remainder)
|
179
|
-
end
|
180
|
-
end
|
181
|
-
end
|
182
|
-
|
183
|
-
class DirectHandler
|
184
|
-
include Filter
|
185
|
-
attr_reader :route
|
186
|
-
|
187
|
-
def initialize(route, callback, filters)
|
188
|
-
@route = route
|
189
|
-
@callback = callback
|
190
|
-
@filters = filters
|
191
|
-
end
|
192
|
-
|
193
|
-
def queue(env)
|
194
|
-
env.channel.queue(@route.queue_name, durable: true, exclusive: is_exclusive_queue).tap do |queue|
|
195
|
-
unless @route.exchange_name == ""
|
196
|
-
Array(@route.routing_keys).each do |routing_key|
|
197
|
-
queue.bind(exchange(env), routing_key: routing_key)
|
198
|
-
end
|
199
|
-
end
|
200
|
-
end
|
201
|
-
end
|
202
|
-
|
203
|
-
def exchange(env)
|
204
|
-
env.channel.direct(@route.exchange_name)
|
205
|
-
end
|
206
|
-
|
207
|
-
def call(env, message)
|
208
|
-
call_next(env, message, @filters + [@callback])
|
209
|
-
end
|
210
|
-
|
211
|
-
private
|
212
|
-
|
213
|
-
def is_exclusive_queue
|
214
|
-
@route.queue_name == ''
|
215
|
-
end
|
216
|
-
end
|
217
|
-
|
218
|
-
class TopicHandler < DirectHandler
|
219
|
-
def exchange(env)
|
220
|
-
env.channel.topic(@route.exchange_name, durable: false, internal: false, autodelete: false)
|
221
|
-
end
|
222
|
-
end
|
223
|
-
|
224
|
-
class RpcHandler < DirectHandler
|
225
|
-
def call(env, message)
|
226
|
-
reply(env, message, super)
|
227
|
-
end
|
228
|
-
|
229
|
-
def reply(env, original_message, response)
|
230
|
-
exchange(env).publish(response.to_s, routing_key: original_message.properties.reply_to,
|
231
|
-
correlation_id: original_message.properties.correlation_id)
|
232
|
-
end
|
233
|
-
end
|
234
|
-
|
235
|
-
class JsonFilter
|
236
|
-
extend Filter
|
237
|
-
def self.call(env, message, filters)
|
238
|
-
message.payload = JSON.parse(message.payload)
|
239
|
-
{result: call_next(env, message, filters)}.to_json
|
240
|
-
rescue JSON::ParserError
|
241
|
-
env.logger.error "Ignoring invalid JSON: #{message.payload}"
|
242
|
-
end
|
243
|
-
end
|
244
|
-
|
245
|
-
class DropFailedFilter
|
246
|
-
include Filter
|
247
|
-
|
248
|
-
def self.call(env, message, filters)
|
249
|
-
new.call(env, message, filters)
|
250
|
-
end
|
251
|
-
|
252
|
-
def initialize(exception: Exception,
|
253
|
-
exception_message_matches: /.*/)
|
254
|
-
@exception = exception
|
255
|
-
@exception_message_matches = exception_message_matches
|
256
|
-
end
|
257
|
-
|
258
|
-
def call(env, message, filters)
|
259
|
-
call_next(env, message, filters)
|
260
|
-
rescue @exception => e
|
261
|
-
if e.message =~ @exception_message_matches
|
262
|
-
env.logger.info "Ignoring failed payload: #{message.payload}"
|
263
|
-
env.logger.debug "#{e.class}: #{e.message}"
|
264
|
-
e.backtrace.each { |l| env.logger.debug(l) }
|
265
|
-
else
|
266
|
-
raise
|
267
|
-
end
|
268
|
-
end
|
269
|
-
end
|
270
|
-
|
271
|
-
class AirbrakeFilter
|
272
|
-
extend Filter
|
273
|
-
def self.call(env, message, filters)
|
274
|
-
call_next(env, message, filters)
|
275
|
-
rescue Exception => e
|
276
|
-
Airbrake.notify(e, parameters: {
|
277
|
-
payload: message.payload,
|
278
|
-
queue: message.route.queue_name,
|
279
|
-
exchange_name: message.route.exchange_name,
|
280
|
-
routing_key: message.delivery_info.routing_key,
|
281
|
-
matched_routing_key: message.route.routing_keys
|
282
|
-
})
|
283
|
-
raise
|
284
|
-
end
|
285
|
-
end
|
286
|
-
|
287
|
-
class ActiveRecordConnectionPoolFilter
|
288
|
-
extend Filter
|
289
|
-
def self.call(env, message, filters)
|
290
|
-
retry_num = 0
|
291
|
-
begin
|
292
|
-
ActiveRecord::Base.connection_pool.with_connection do
|
293
|
-
call_next(env, message, filters)
|
294
|
-
end
|
295
|
-
rescue ActiveRecord::ConnectionTimeoutError
|
296
|
-
retry_num += 1
|
297
|
-
if retry_num <= 5
|
298
|
-
sleep 1
|
299
|
-
retry
|
300
|
-
else
|
301
|
-
raise
|
302
|
-
end
|
303
|
-
end
|
304
|
-
end
|
305
|
-
end
|
306
|
-
|
307
|
-
class DiscardReturnValueFilter
|
308
|
-
extend Filter
|
309
|
-
def self.call(env, message, filters)
|
310
|
-
call_next(env, message, filters)
|
311
|
-
nil
|
312
|
-
end
|
313
|
-
end
|
314
|
-
|
315
|
-
class RedirectFailedFilter
|
316
|
-
include Filter
|
317
|
-
def initialize(producer, error_queue, exception: Exception,
|
318
|
-
exception_message_matches: /.*/)
|
319
|
-
@producer = producer
|
320
|
-
@error_queue = error_queue
|
321
|
-
@exception = exception
|
322
|
-
@exception_message_matches = exception_message_matches
|
323
|
-
end
|
324
|
-
|
325
|
-
def call(env, message, filters)
|
326
|
-
call_next(env, message, filters)
|
327
|
-
rescue @exception => e
|
328
|
-
if e.message =~ @exception_message_matches
|
329
|
-
@producer.cast @error_queue, message.payload
|
330
|
-
else
|
331
|
-
raise
|
332
|
-
end
|
333
|
-
end
|
334
|
-
end
|
335
|
-
|
336
|
-
class RetryFilter
|
337
|
-
include Filter
|
338
|
-
|
339
|
-
def initialize(times, exception: Exception,
|
340
|
-
exception_message_matches: /.*/)
|
341
|
-
@times = times
|
342
|
-
@exception = exception
|
343
|
-
@exception_message_matches = exception_message_matches
|
344
|
-
end
|
345
|
-
|
346
|
-
def call(env, message, filters)
|
347
|
-
retry_num = 0
|
348
|
-
begin
|
349
|
-
call_next(env, message, filters)
|
350
|
-
rescue @exception => e
|
351
|
-
if e.message =~ @exception_message_matches && retry_num < @times
|
352
|
-
retry_num += 1
|
353
|
-
sleep Math.log(retry_num, 2)
|
354
|
-
retry
|
355
|
-
else
|
356
|
-
raise
|
357
|
-
end
|
358
|
-
end
|
359
|
-
end
|
360
|
-
end
|
7
|
+
require 'ElmerFudd/publisher'
|
8
|
+
require 'ElmerFudd/json_publisher'
|
9
|
+
|
10
|
+
require 'ElmerFudd/filter'
|
11
|
+
require 'ElmerFudd/direct_handler'
|
12
|
+
require 'ElmerFudd/topic_handler'
|
13
|
+
require 'ElmerFudd/rpc_handler'
|
14
|
+
if defined?(Rspec)
|
15
|
+
require 'ElmerFudd/rspec'
|
16
|
+
end
|
17
|
+
require 'ElmerFudd/worker'
|
18
|
+
|
19
|
+
|
20
|
+
require 'ElmerFudd/active_record_connection_pool_filter'
|
21
|
+
require 'ElmerFudd/airbrake_filter'
|
22
|
+
require 'ElmerFudd/discard_return_value_filter'
|
23
|
+
require 'ElmerFudd/drop_failed_filter'
|
24
|
+
require 'ElmerFudd/json_filter'
|
25
|
+
require 'ElmerFudd/redirect_failed_filter'
|
26
|
+
require 'ElmerFudd/retry_filter'
|
361
27
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ElmerFudd
|
2
|
+
class ActiveRecordConnectionPoolFilter
|
3
|
+
extend Filter
|
4
|
+
def self.call(env, message, filters)
|
5
|
+
retry_num = 0
|
6
|
+
begin
|
7
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
8
|
+
call_next(env, message, filters)
|
9
|
+
end
|
10
|
+
rescue ActiveRecord::ConnectionTimeoutError
|
11
|
+
retry_num += 1
|
12
|
+
if retry_num <= 5
|
13
|
+
sleep 1
|
14
|
+
retry
|
15
|
+
else
|
16
|
+
raise
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module ElmerFudd
|
2
|
+
class AirbrakeFilter
|
3
|
+
extend Filter
|
4
|
+
def self.call(env, message, filters)
|
5
|
+
call_next(env, message, filters)
|
6
|
+
rescue Exception => e
|
7
|
+
Airbrake.notify(e, parameters: {
|
8
|
+
payload: message.payload,
|
9
|
+
queue: message.route.queue_name,
|
10
|
+
exchange_name: message.route.exchange_name,
|
11
|
+
routing_key: message.delivery_info.routing_key,
|
12
|
+
matched_routing_key: message.route.routing_keys
|
13
|
+
})
|
14
|
+
raise
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module ElmerFudd
|
2
|
+
class DirectHandler
|
3
|
+
include Filter
|
4
|
+
attr_reader :route
|
5
|
+
|
6
|
+
def initialize(route, callback, filters)
|
7
|
+
@route = route
|
8
|
+
@callback = callback
|
9
|
+
@filters = filters
|
10
|
+
end
|
11
|
+
|
12
|
+
def queue(env)
|
13
|
+
env.channel.queue(@route.queue_name, durable: true, exclusive: is_exclusive_queue).tap do |queue|
|
14
|
+
unless @route.exchange_name == ""
|
15
|
+
Array(@route.routing_keys).each do |routing_key|
|
16
|
+
queue.bind(exchange(env), routing_key: routing_key)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def exchange(env)
|
23
|
+
env.logger.debug "ElmerFudd Handler.exchange queue_name: #{@route.queue_name}, exchange_name: #{@route.exchange_name}, filters: #{filters_names}"
|
24
|
+
env.channel.direct(@route.exchange_name)
|
25
|
+
end
|
26
|
+
|
27
|
+
def call(env, message)
|
28
|
+
env.logger.debug "ElmerFudd DirectHandler.call queue_name: #{@route.queue_name}, exchange_name: #{@route.exchange_name}, filters: #{filters_names}, message: #{message.payload}"
|
29
|
+
call_next(env, message, @filters + [@callback])
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def filters_names
|
35
|
+
@filters.map { |f| f.respond_to?(:name) ? f.name : f.class.name }
|
36
|
+
end
|
37
|
+
|
38
|
+
def is_exclusive_queue
|
39
|
+
@route.queue_name == ''
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module ElmerFudd
|
2
|
+
class DropFailedFilter
|
3
|
+
include Filter
|
4
|
+
|
5
|
+
def self.call(env, message, filters)
|
6
|
+
new.call(env, message, filters)
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(exception: Exception,
|
10
|
+
exception_message_matches: /.*/)
|
11
|
+
@exception = exception
|
12
|
+
@exception_message_matches = exception_message_matches
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(env, message, filters)
|
16
|
+
call_next(env, message, filters)
|
17
|
+
rescue @exception => e
|
18
|
+
if e.message =~ @exception_message_matches
|
19
|
+
env.logger.info "Ignoring failed payload: #{message.payload}"
|
20
|
+
env.logger.debug "#{e.class}: #{e.message}"
|
21
|
+
e.backtrace.each { |l| env.logger.debug(l) }
|
22
|
+
else
|
23
|
+
raise
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module ElmerFudd
|
2
|
+
class JsonFilter
|
3
|
+
extend Filter
|
4
|
+
def self.call(env, message, filters)
|
5
|
+
message.payload = JSON.parse(message.payload)
|
6
|
+
{result: call_next(env, message, filters)}.to_json
|
7
|
+
rescue JSON::ParserError
|
8
|
+
env.logger.error "Ignoring invalid JSON: #{message.payload}"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ElmerFudd
|
2
|
+
class JsonPublisher < Publisher
|
3
|
+
def notify(topic_exchange, routing_key, payload)
|
4
|
+
super(topic_exchange, routing_key, payload.to_json)
|
5
|
+
end
|
6
|
+
|
7
|
+
def cast(queue_name, payload)
|
8
|
+
super(queue_name, payload.to_json)
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(queue_name, payload, **kwargs)
|
12
|
+
JSON.parse(super(queue_name, payload.to_json, **kwargs))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module ElmerFudd
|
2
|
+
class Publisher
|
3
|
+
def initialize(connection, uuid_service: -> { rand.to_s }, logger: Logger.new($stdout))
|
4
|
+
puts "LOGGGER #{logger}"
|
5
|
+
@connection = connection
|
6
|
+
@logger = logger
|
7
|
+
@uuid_service = uuid_service
|
8
|
+
@topic_x = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def notify(topic_exchange, routing_key, payload)
|
12
|
+
@logger.debug "ElmerFudd: NOTIFY - topic_exchange: #{topic_exchange}, routing_key: #{routing_key}, payload: #{payload}"
|
13
|
+
@topic_x[topic_exchange] ||= channel.topic(topic_exchange)
|
14
|
+
@topic_x[topic_exchange].publish payload.to_s, routing_key: routing_key
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def cast(queue_name, payload)
|
19
|
+
@logger.debug "ElmerFudd: CAST - queue_name: #{queue_name}, payload: #{payload}"
|
20
|
+
x.publish(payload.to_s, routing_key: queue_name)
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def call(queue_name, payload, timeout: 10)
|
25
|
+
@logger.debug "ElmerFudd: CALL - queue_name: #{queue_name}, payload: #{payload}, timeout: #{timeout}"
|
26
|
+
mutex = Mutex.new
|
27
|
+
resource = ConditionVariable.new
|
28
|
+
correlation_id = @uuid_service.call
|
29
|
+
consumer_tag = @uuid_service.call
|
30
|
+
response = nil
|
31
|
+
|
32
|
+
Timeout.timeout(timeout) do
|
33
|
+
rpc_reply_queue.subscribe(manual_ack: false, block: false, consumer_tag: consumer_tag) do |delivery_info, properties, payload|
|
34
|
+
if properties[:correlation_id] == correlation_id
|
35
|
+
response = payload
|
36
|
+
mutex.synchronize { resource.signal }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
x.publish(payload.to_s, routing_key: queue_name, reply_to: rpc_reply_queue.name,
|
41
|
+
correlation_id: correlation_id)
|
42
|
+
|
43
|
+
mutex.synchronize { resource.wait(mutex) unless response }
|
44
|
+
response
|
45
|
+
end
|
46
|
+
ensure
|
47
|
+
reply_channel.consumers[consumer_tag].cancel
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def connection
|
53
|
+
@connection.tap do |c|
|
54
|
+
c.start unless c.connected?
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def x
|
59
|
+
@x ||= channel.default_exchange
|
60
|
+
end
|
61
|
+
|
62
|
+
def channel
|
63
|
+
@channel ||= connection.create_channel
|
64
|
+
end
|
65
|
+
|
66
|
+
def reply_channel
|
67
|
+
@reply_channel ||= connection.create_channel
|
68
|
+
end
|
69
|
+
|
70
|
+
def rpc_reply_queue
|
71
|
+
@rpc_reply_queue ||= reply_channel.queue("", exclusive: true)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ElmerFudd
|
2
|
+
class RedirectFailedFilter
|
3
|
+
include Filter
|
4
|
+
def initialize(producer, error_queue, exception: Exception,
|
5
|
+
exception_message_matches: /.*/)
|
6
|
+
@producer = producer
|
7
|
+
@error_queue = error_queue
|
8
|
+
@exception = exception
|
9
|
+
@exception_message_matches = exception_message_matches
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env, message, filters)
|
13
|
+
call_next(env, message, filters)
|
14
|
+
rescue @exception => e
|
15
|
+
if e.message =~ @exception_message_matches
|
16
|
+
@producer.cast @error_queue, message.payload
|
17
|
+
else
|
18
|
+
raise
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module ElmerFudd
|
2
|
+
class RetryFilter
|
3
|
+
include Filter
|
4
|
+
|
5
|
+
def initialize(times, exception: Exception,
|
6
|
+
exception_message_matches: /.*/)
|
7
|
+
@times = times
|
8
|
+
@exception = exception
|
9
|
+
@exception_message_matches = exception_message_matches
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env, message, filters)
|
13
|
+
retry_num = 0
|
14
|
+
begin
|
15
|
+
call_next(env, message, filters)
|
16
|
+
rescue @exception => e
|
17
|
+
if e.message =~ @exception_message_matches && retry_num < @times
|
18
|
+
retry_num += 1
|
19
|
+
sleep Math.log(retry_num, 2)
|
20
|
+
retry
|
21
|
+
else
|
22
|
+
raise
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module ElmerFudd
|
2
|
+
class RpcHandler < DirectHandler
|
3
|
+
def call(env, message)
|
4
|
+
env.logger.debug "ElmerFudd RpcHandler.call queue_name: #{@route.queue_name}, exchange_name: #{@route.exchange_name}, filters: #{@filters.map(&:name)}, message: #{message.payload}"
|
5
|
+
|
6
|
+
reply(env, message, super)
|
7
|
+
end
|
8
|
+
|
9
|
+
def reply(env, original_message, response)
|
10
|
+
exchange(env).publish(response.to_s, routing_key: original_message.properties.reply_to,
|
11
|
+
correlation_id: original_message.properties.correlation_id)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
module ElmerFudd
|
2
|
+
class TopicHandler < DirectHandler
|
3
|
+
def exchange(env)
|
4
|
+
env.logger.debug "ElmerFudd TopicHandler.exchange queue_name: #{@route.queue_name}, exchange_name: #{@route.exchange_name}, filters: #{filters_names}"
|
5
|
+
env.channel.topic(@route.exchange_name, durable: false, internal: false, autodelete: false)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
data/lib/ElmerFudd/version.rb
CHANGED
@@ -0,0 +1,85 @@
|
|
1
|
+
module ElmerFudd
|
2
|
+
class Worker
|
3
|
+
Message = Struct.new(:delivery_info, :properties, :payload, :route)
|
4
|
+
Env = Struct.new(:channel, :logger, :worker_class)
|
5
|
+
Route = Struct.new(:exchange_name, :routing_keys, :queue_name)
|
6
|
+
|
7
|
+
def self.handlers
|
8
|
+
@handlers ||= []
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.Route(queue_name, exchange_and_routing_keys = {"" => queue_name})
|
12
|
+
exchange, routing_keys = exchange_and_routing_keys.first
|
13
|
+
Route.new(exchange, routing_keys, queue_name)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.default_filters(*filters)
|
17
|
+
@filters = filters
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.handle_event(route, filters: [], handler: nil, &block)
|
21
|
+
handlers << TopicHandler.new(route, handler || block, (@filters + filters + [DiscardReturnValueFilter]).uniq)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.handle_cast(route, filters: [], handler: nil, &block)
|
25
|
+
handlers << DirectHandler.new(route, handler || block, (@filters + filters + [DiscardReturnValueFilter]).uniq)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.handle_call(route, filters: [], handler: nil, &block)
|
29
|
+
handlers << RpcHandler.new(route, handler || block, (@filters + filters).uniq)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Helper allowing to use any method taking hash as a handler
|
33
|
+
# def example(text:, **_)
|
34
|
+
# puts text
|
35
|
+
# end
|
36
|
+
# # then in worker
|
37
|
+
# handle_cast(...
|
38
|
+
# handler: payload_as_kwargs(method(:example)))
|
39
|
+
# Thanks to usage of **_ in arguments list it will accept
|
40
|
+
# any payload contaning 'text' key. Skipping **_ will require
|
41
|
+
# listing all payload keys in argument list
|
42
|
+
def self.payload_as_kwargs(handler, only: nil)
|
43
|
+
lambda do |_env, message|
|
44
|
+
symbolized_payload = message.payload.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
|
45
|
+
sybolized_payload = symbolized_payload.select { |k,v| Array(only).include?(k) } if only
|
46
|
+
handler.call(symbolized_payload)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def initialize(connection, concurrency: 1, logger: Logger.new($stdout))
|
51
|
+
@connection = connection
|
52
|
+
@concurrency = concurrency
|
53
|
+
@logger = logger
|
54
|
+
end
|
55
|
+
|
56
|
+
def start
|
57
|
+
self.class.handlers.each do |handler|
|
58
|
+
handler.queue(env).subscribe(manual_ack: true, block: false) do |delivery_info, properties, payload|
|
59
|
+
message = Message.new(delivery_info, properties, payload, handler.route)
|
60
|
+
begin
|
61
|
+
handler.call(env, message)
|
62
|
+
env.channel.acknowledge(message.delivery_info.delivery_tag)
|
63
|
+
rescue Exception => e
|
64
|
+
env.logger.fatal("Worker blocked: %s, %s:" % [e.class, e.message])
|
65
|
+
e.backtrace.each { |l| env.logger.fatal(l) }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def env
|
74
|
+
@env ||= Env.new(channel, @logger, self.class)
|
75
|
+
end
|
76
|
+
|
77
|
+
def connection
|
78
|
+
@connection.tap { |c| c.start unless c.connected? }
|
79
|
+
end
|
80
|
+
|
81
|
+
def channel
|
82
|
+
@channel ||= connection.create_channel.tap { |c| c.prefetch(@concurrency) }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ElmerFudd
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.27
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrzej Sliwa
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-
|
12
|
+
date: 2015-05-18 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bunny
|
@@ -86,8 +86,22 @@ files:
|
|
86
86
|
- Rakefile
|
87
87
|
- elmer-fudd.jpg
|
88
88
|
- lib/ElmerFudd.rb
|
89
|
+
- lib/ElmerFudd/active_record_connection_pool_filter.rb
|
90
|
+
- lib/ElmerFudd/airbrake_filter.rb
|
91
|
+
- lib/ElmerFudd/direct_handler.rb
|
92
|
+
- lib/ElmerFudd/discard_return_value_filter.rb
|
93
|
+
- lib/ElmerFudd/drop_failed_filter.rb
|
94
|
+
- lib/ElmerFudd/filter.rb
|
95
|
+
- lib/ElmerFudd/json_filter.rb
|
96
|
+
- lib/ElmerFudd/json_publisher.rb
|
97
|
+
- lib/ElmerFudd/publisher.rb
|
98
|
+
- lib/ElmerFudd/redirect_failed_filter.rb
|
99
|
+
- lib/ElmerFudd/retry_filter.rb
|
100
|
+
- lib/ElmerFudd/rpc_handler.rb
|
89
101
|
- lib/ElmerFudd/rspec.rb
|
102
|
+
- lib/ElmerFudd/topic_handler.rb
|
90
103
|
- lib/ElmerFudd/version.rb
|
104
|
+
- lib/ElmerFudd/worker.rb
|
91
105
|
- spec/spec_helper.rb
|
92
106
|
homepage: https://github.com/bonusboxme/ElmerFudd
|
93
107
|
licenses:
|
@@ -109,7 +123,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
109
123
|
version: '0'
|
110
124
|
requirements: []
|
111
125
|
rubyforge_project:
|
112
|
-
rubygems_version: 2.4.
|
126
|
+
rubygems_version: 2.4.7
|
113
127
|
signing_key:
|
114
128
|
specification_version: 4
|
115
129
|
summary: RabbitMQ in OTP way
|