ElmerFudd 0.0.26 → 0.0.27
Sign up to get free protection for your applications and to get access to all the features.
- 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
|