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 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