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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fd6ca4c19eec0cfe4f1c67893e228e34de2738ff
4
- data.tar.gz: ea6138eb02264f866a9953d3e931245393d6dcdd
3
+ metadata.gz: e2843148a1580faa5bb73307c9284a57bfb2e764
4
+ data.tar.gz: 72a3ed699a197649d2b2d396af5bb9c59dae17d2
5
5
  SHA512:
6
- metadata.gz: 218fc0908e3aeed38a67737fedccaad4282faf858a2883773ff86a793172d663756f0275957d5b392482016fd128d02f726d8171b507c5060cb52328cfc67b17
7
- data.tar.gz: 0c71524cae5acb9e7e5995ab08863e50b50b05f1053f9124631d270f678ad9d819bc067f7f6763d0bad95fb52d0ed3616243c76e761354ede2ad30bfb2f511ba
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 "ElmerFudd/version"
2
- require "bunny"
3
- require "thread"
1
+ require 'ElmerFudd/version'
2
+ require 'bunny'
3
+ require 'thread'
4
+ require 'json'
4
5
 
5
6
  module ElmerFudd
6
- class Publisher
7
- def initialize(connection, uuid_service: -> { rand.to_s })
8
- @connection = connection
9
- @uuid_service = uuid_service
10
- @topic_x = {}
11
- end
12
-
13
- def notify(topic_exchange, routing_key, payload)
14
- @topic_x[topic_exchange] ||= channel.topic(topic_exchange)
15
- @topic_x[topic_exchange].publish payload.to_s, routing_key: routing_key
16
- nil
17
- end
18
-
19
- def cast(queue_name, 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
- mutex = Mutex.new
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,9 @@
1
+ module ElmerFudd
2
+ class DiscardReturnValueFilter
3
+ extend Filter
4
+ def self.call(env, message, filters)
5
+ call_next(env, message, filters)
6
+ nil
7
+ end
8
+ end
9
+ 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,12 @@
1
+ module ElmerFudd
2
+ module Filter
3
+ def call_next(env, message, filters)
4
+ next_filter, *remainder = filters
5
+ if remainder.empty?
6
+ next_filter.call(env, message)
7
+ else
8
+ next_filter.call(env, message, remainder)
9
+ end
10
+ end
11
+ end
12
+ 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
@@ -1,3 +1,3 @@
1
1
  module ElmerFudd
2
- VERSION = "0.0.26"
2
+ VERSION = "0.0.27"
3
3
  end
@@ -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.26
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-04-15 00:00:00.000000000 Z
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.6
126
+ rubygems_version: 2.4.7
113
127
  signing_key:
114
128
  specification_version: 4
115
129
  summary: RabbitMQ in OTP way