lolitra 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +31 -4
- data/lib/lolitra/handler_base.rb +61 -97
- data/lib/lolitra/rabbitmq_bus.rb +235 -0
- data/lib/lolitra/version.rb +1 -1
- metadata +4 -3
data/README.md
CHANGED
@@ -4,7 +4,7 @@ Amqp and Faye event bus
|
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
7
|
-
Add this line to your
|
7
|
+
Add this line to your Gemfile:
|
8
8
|
|
9
9
|
gem 'lolitra'
|
10
10
|
|
@@ -20,14 +20,13 @@ Or install it yourself as:
|
|
20
20
|
|
21
21
|
Create an event bus on initializers
|
22
22
|
|
23
|
-
Lolitra::
|
23
|
+
Lolitra::RabbitmqBus.new(
|
24
24
|
:exchange => "exchangetest",
|
25
|
-
:queue_prefix => "my_prefix_",
|
26
25
|
:host => "127.0.0.1",
|
27
26
|
:port => 5672,
|
28
27
|
:user => "guest",
|
29
28
|
:pass => "guest",
|
30
|
-
:pull_subscribers => [DevicesHandler
|
29
|
+
:pull_subscribers => [DevicesHandler]
|
31
30
|
)
|
32
31
|
|
33
32
|
Create messages
|
@@ -68,6 +67,34 @@ Create a message handler
|
|
68
67
|
|
69
68
|
end
|
70
69
|
|
70
|
+
*Rabbitmq*
|
71
|
+
Lolitra generates a deadletter exchange and queues to handle dead letters and will be aware about connections issues, reconnecting on every failure.
|
72
|
+
|
73
|
+
*Deadletter manual handling*
|
74
|
+
With lolitra you can recover dead letter message with irb or rails console.
|
75
|
+
|
76
|
+
```
|
77
|
+
Lolitra::subscribers
|
78
|
+
```
|
79
|
+
will return the available handlers
|
80
|
+
|
81
|
+
```
|
82
|
+
Lolitra::process_deadletters(DevicesHandler)
|
83
|
+
```
|
84
|
+
will process all dead letter from DevicesHandler until found an exception
|
85
|
+
Fail recover deadletter will reenqueue to dead letter queue
|
86
|
+
|
87
|
+
```
|
88
|
+
Lolitra::remove_next_deadletter(DeviceHandler)
|
89
|
+
```
|
90
|
+
Will remove next deadletter without doing anything
|
91
|
+
|
92
|
+
```
|
93
|
+
Lolitra::purge_deadletters(DeviceHandler)
|
94
|
+
```
|
95
|
+
Will remove all deadletters without doing anything
|
96
|
+
|
97
|
+
|
71
98
|
## Contributing
|
72
99
|
|
73
100
|
1. Fork it
|
data/lib/lolitra/handler_base.rb
CHANGED
@@ -3,6 +3,8 @@ require 'log4r'
|
|
3
3
|
require 'amqp'
|
4
4
|
require 'amqp/utilities/event_loop_helper'
|
5
5
|
require 'json'
|
6
|
+
require 'fileutils'
|
7
|
+
require_relative 'rabbitmq_bus'
|
6
8
|
|
7
9
|
module Lolitra
|
8
10
|
include Log4r
|
@@ -27,6 +29,32 @@ module Lolitra
|
|
27
29
|
Lolitra::MessageHandlerManager.publish(message)
|
28
30
|
end
|
29
31
|
|
32
|
+
def self.unsubscribe(handler_class, &block)
|
33
|
+
Lolitra::MessageHandlerManager.unsubscribe(handler_class, &block)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.disconnect(&block)
|
37
|
+
Lolitra::MessageHandlerManager.disconnect(&block)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.subscribers
|
41
|
+
Lolitra::MessageHandlerManager.instance.subscribers.collect do |subscriber|
|
42
|
+
subscriber.name
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.process_deadletters(subscriber)
|
47
|
+
Lolitra::MessageHandlerManager.instance.process_deadletters(subscriber)
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.remove_next_deadletter(subscriber)
|
51
|
+
Lolitra::MessageHandlerManager.instance.remove_next_deadletter(subscriber)
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.purge_deadletters(subscriber)
|
55
|
+
Lolitra::MessageHandlerManager.instance.purge_deadletters(subscriber)
|
56
|
+
end
|
57
|
+
|
30
58
|
module MessageHandler
|
31
59
|
module Helpers
|
32
60
|
def self.underscore(arg)
|
@@ -65,11 +93,7 @@ module Lolitra
|
|
65
93
|
end
|
66
94
|
|
67
95
|
def handle(message)
|
68
|
-
|
69
|
-
get_handler(message).handle(message)
|
70
|
-
rescue => e
|
71
|
-
Lolitra::log_exception(e)
|
72
|
-
end
|
96
|
+
get_handler(message).handle(message)
|
73
97
|
end
|
74
98
|
|
75
99
|
def publish(message)
|
@@ -142,6 +166,7 @@ module Lolitra
|
|
142
166
|
include Singleton
|
143
167
|
|
144
168
|
attr_accessor :bus
|
169
|
+
attr_accessor :subscribers
|
145
170
|
|
146
171
|
def self.bus=(new_bus)
|
147
172
|
instance.bus = new_bus
|
@@ -155,20 +180,36 @@ module Lolitra
|
|
155
180
|
instance.register_subscriber(handler_class)
|
156
181
|
end
|
157
182
|
|
183
|
+
def subscribers
|
184
|
+
@subscribers ||= []
|
185
|
+
end
|
186
|
+
|
187
|
+
def process_deadletters(handler_class)
|
188
|
+
bus.process_deadletters(handler_class)
|
189
|
+
end
|
190
|
+
|
191
|
+
def purge_deadletters(handler_class)
|
192
|
+
bus.purge_deadletters(handler_class)
|
193
|
+
end
|
194
|
+
|
195
|
+
def remove_next_deadletter(handler_class)
|
196
|
+
bus.remove_next_deadletter(handler_class)
|
197
|
+
end
|
198
|
+
|
158
199
|
def register_subscriber(handler_class)
|
200
|
+
subscribers << handler_class
|
159
201
|
handler_class.handle_messages.each do |message_class|
|
160
202
|
bus.subscribe(message_class, handler_class)
|
161
203
|
end
|
162
204
|
end
|
163
205
|
|
164
|
-
def self.
|
165
|
-
instance.
|
206
|
+
def self.unsubscribe(handler_class, &block)
|
207
|
+
instance.unsubscribe(handler_class, &block)
|
166
208
|
end
|
167
|
-
|
168
|
-
def
|
169
|
-
|
170
|
-
|
171
|
-
end
|
209
|
+
|
210
|
+
def unsubscribe(handler_class, &block)
|
211
|
+
Lolitra::logger.info("Unsubscribing #{handler_class}")
|
212
|
+
bus.unsubscribe(handler_class, &block)
|
172
213
|
end
|
173
214
|
|
174
215
|
def self.publish(message)
|
@@ -180,6 +221,14 @@ module Lolitra
|
|
180
221
|
Lolitra::logger.debug("#{message.marshall}")
|
181
222
|
bus.publish(message)
|
182
223
|
end
|
224
|
+
|
225
|
+
def self.disconnect(&block)
|
226
|
+
instance.disconnect(&block)
|
227
|
+
end
|
228
|
+
|
229
|
+
def disconnect(&block)
|
230
|
+
bus.disconnect(&block)
|
231
|
+
end
|
183
232
|
end
|
184
233
|
|
185
234
|
module Message
|
@@ -241,89 +290,4 @@ module Lolitra
|
|
241
290
|
@socketClient.publish(message.class.message_key, message.marshall)
|
242
291
|
end
|
243
292
|
end
|
244
|
-
|
245
|
-
class AmqpBus
|
246
|
-
attr_accessor :queue_prefix
|
247
|
-
attr_accessor :connection
|
248
|
-
attr_accessor :exchange
|
249
|
-
|
250
|
-
def initialize(hash = {})
|
251
|
-
Lolitra::MessageHandlerManager.bus = self
|
252
|
-
|
253
|
-
@channels = {}
|
254
|
-
@params = hash.reject { |key, value| !value }
|
255
|
-
raise "no :exchange specified" unless hash[:exchange]
|
256
|
-
|
257
|
-
self.queue_prefix = hash[:queue_prefix]||""
|
258
|
-
AMQP::Utilities::EventLoopHelper.run do
|
259
|
-
self.connection = AMQP.start(@params) do |connection|
|
260
|
-
channel = create_channel(connection) do |channel|
|
261
|
-
begin
|
262
|
-
self.exchange = channel.topic(@params[:exchange], :durable => true)
|
263
|
-
|
264
|
-
@params[:pull_subscribers].each do |handler|
|
265
|
-
Lolitra::MessageHandlerManager.register_pull_subscriber(handler)
|
266
|
-
end
|
267
|
-
rescue => e
|
268
|
-
Lolitra::logger.debug("error")
|
269
|
-
Lolitra::log_exception(e)
|
270
|
-
end
|
271
|
-
end
|
272
|
-
end
|
273
|
-
end
|
274
|
-
end
|
275
|
-
|
276
|
-
def subscribe(message_class, handler_class)
|
277
|
-
create_queue(message_class, handler_class, {:exclusive => true, :durable => false}, "")
|
278
|
-
end
|
279
|
-
|
280
|
-
def pull_subscribe(message_class, handler_class)
|
281
|
-
create_queue(message_class, handler_class, {:durable => true})
|
282
|
-
end
|
283
|
-
|
284
|
-
def publish(message)
|
285
|
-
#TODO: if exchange channel is closed doesn't log anything
|
286
|
-
self.exchange.publish(message.marshall, :routing_key => message.class.message_key, :timestamp => Time.now.to_i)
|
287
|
-
end
|
288
|
-
|
289
|
-
private
|
290
|
-
def create_channel(connection, &block)
|
291
|
-
channel = AMQP::Channel.new(connection) do
|
292
|
-
channel.on_error do |channel, close|
|
293
|
-
Lolitra::logger.error("Channel error: #{channel}")
|
294
|
-
Lolitra::logger.error(close)
|
295
|
-
end
|
296
|
-
block.call(channel)
|
297
|
-
end
|
298
|
-
channel
|
299
|
-
end
|
300
|
-
|
301
|
-
def create_queue(message_class, handler_class, options)
|
302
|
-
begin
|
303
|
-
queue_name = queue_prefix + MessageHandler::Helpers.underscore(handler_class.name)
|
304
|
-
|
305
|
-
create_channel(self.connection) do |channel|
|
306
|
-
channel.queue(queue_name, options).bind(self.exchange, :routing_key => message_class.message_key)
|
307
|
-
channel.close
|
308
|
-
end
|
309
|
-
|
310
|
-
if !@channels[queue_name] #Only one subscriber by queue_name
|
311
|
-
@channels[queue_name] = create_channel(self.connection) do |channel|
|
312
|
-
channel.prefetch(1).queue(queue_name, options).subscribe do |info, payload|
|
313
|
-
begin
|
314
|
-
Lolitra::logger.debug("Message recived: #{info.routing_key}")
|
315
|
-
Lolitra::logger.debug("#{payload}")
|
316
|
-
message_class_tmp = handler_class.handlers[info.routing_key][0]
|
317
|
-
handler_class.handle(message_class_tmp.unmarshall(payload))
|
318
|
-
rescue => e
|
319
|
-
Lolitra::log_exception(e)
|
320
|
-
end
|
321
|
-
end
|
322
|
-
end
|
323
|
-
end
|
324
|
-
rescue => e
|
325
|
-
Lolitra::log_exception(e)
|
326
|
-
end
|
327
|
-
end
|
328
|
-
end
|
329
293
|
end
|
@@ -0,0 +1,235 @@
|
|
1
|
+
module Lolitra
|
2
|
+
class RabbitmqBus
|
3
|
+
attr_accessor :connection
|
4
|
+
attr_accessor :exchange
|
5
|
+
attr_accessor :exchange_dead_letter
|
6
|
+
attr_accessor :options
|
7
|
+
attr_accessor :subscribers
|
8
|
+
|
9
|
+
SUBSCRIBE_OPTIONS = {:durable => true}
|
10
|
+
|
11
|
+
def initialize(hash = {})
|
12
|
+
Lolitra::MessageHandlerManager.bus = self
|
13
|
+
|
14
|
+
self.options = {
|
15
|
+
:queue_prefix => "",
|
16
|
+
:queue_suffix => "",
|
17
|
+
:exchange_dead_suffix => ".dead",
|
18
|
+
:exchange_dead_params => {},
|
19
|
+
:queue_params => {},
|
20
|
+
:queue_dead_suffix => ".dead",
|
21
|
+
:queue_dead_params => {},
|
22
|
+
:no_consume => false,
|
23
|
+
}.merge(hash.delete(:options) || {})
|
24
|
+
|
25
|
+
self.options[:queue_params][:arguments] = {} unless self.options[:queue_params][:arguments]
|
26
|
+
|
27
|
+
self.options[:queue_params][:arguments] = {
|
28
|
+
"x-dead-letter-exchange" => "#{hash[:exchange]}#{@options[:exchange_dead_suffix]}"
|
29
|
+
}.merge(self.options[:queue_params][:arguments])
|
30
|
+
|
31
|
+
@channels = {}
|
32
|
+
@params = hash.reject { |key, value| !value }
|
33
|
+
raise "no :exchange specified" unless hash[:exchange]
|
34
|
+
|
35
|
+
AMQP::Utilities::EventLoopHelper.run do
|
36
|
+
self.connection = AMQP.start(@params) do |connection|
|
37
|
+
Lolitra::logger.info("Connected to rabbitmq.")
|
38
|
+
channel = create_channel(connection) do |channel|
|
39
|
+
begin
|
40
|
+
self.exchange = channel.topic(@params[:exchange], :durable => true)
|
41
|
+
self.exchange_dead_letter = channel.topic("#{@params[:exchange]}#{@options[:exchange_dead_suffix]}", :durable => true)
|
42
|
+
|
43
|
+
@params[:subscribers].each do |handler|
|
44
|
+
Lolitra::MessageHandlerManager.register_subscriber(handler)
|
45
|
+
end
|
46
|
+
rescue => e
|
47
|
+
Lolitra::log_exception(e)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
self.connection.on_tcp_connection_loss do |connection, settings|
|
52
|
+
# reconnect in 10 seconds, without enforcement
|
53
|
+
Lolitra::logger.info("Connection loss. Trying to reconnect in 10 secs if needed.")
|
54
|
+
connection.reconnect(false, 10)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def disconnect(&block)
|
60
|
+
self.connection.close(&block)
|
61
|
+
end
|
62
|
+
|
63
|
+
def subscribe(message_class, handler_class)
|
64
|
+
create_queue(message_class, handler_class, SUBSCRIBE_OPTIONS)
|
65
|
+
end
|
66
|
+
|
67
|
+
def publish(message)
|
68
|
+
#TODO: if exchange channel is closed doesn't log anything
|
69
|
+
self.exchange.publish(message.marshall, :routing_key => message.class.message_key, :timestamp => Time.now.to_i)
|
70
|
+
end
|
71
|
+
|
72
|
+
def unsubscribe(handler_class, &block)
|
73
|
+
queue_name = generate_queue_name(handler_class)
|
74
|
+
begin
|
75
|
+
create_channel(self.connection) do |channel|
|
76
|
+
queue = channel.queue(queue_name, SUBSCRIBE_OPTIONS) do |queue|
|
77
|
+
begin
|
78
|
+
queue.delete
|
79
|
+
block.call(handler_class, true)
|
80
|
+
rescue => e
|
81
|
+
Lolitra::log_exception(e)
|
82
|
+
block.call(handler_class, false)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
rescue => e
|
87
|
+
Lolitra::log_exception(e)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def process_deadletters(handler_class)
|
92
|
+
queue_name_dead = generate_queue_name_dead(handler_class)
|
93
|
+
options = SUBSCRIBE_OPTIONS
|
94
|
+
create_channel(self.connection) do |channel|
|
95
|
+
begin
|
96
|
+
channel.queue(queue_name_dead, options.merge(@options[:queue_dead_params])) do |queue|
|
97
|
+
recursive_pop(channel, queue, handler_class)
|
98
|
+
end
|
99
|
+
rescue => e
|
100
|
+
Lolitra::log_exception(e)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
true
|
104
|
+
end
|
105
|
+
|
106
|
+
def purge_deadletters(handler_class)
|
107
|
+
queue_name_dead = generate_queue_name_dead(handler_class)
|
108
|
+
options = SUBSCRIBE_OPTIONS
|
109
|
+
create_channel(self.connection) do |channel|
|
110
|
+
begin
|
111
|
+
channel.queue(queue_name_dead, options.merge(@options[:queue_dead_params])) do |queue|
|
112
|
+
purge_queue(queue)
|
113
|
+
end
|
114
|
+
rescue => e
|
115
|
+
Lolitra::log_exception(e)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
true
|
119
|
+
end
|
120
|
+
|
121
|
+
def remove_next_deadletter(handler_class)
|
122
|
+
queue_name_dead = generate_queue_name_dead(handler_class)
|
123
|
+
options = SUBSCRIBE_OPTIONS
|
124
|
+
create_channel(self.connection) do |channel|
|
125
|
+
begin
|
126
|
+
channel.queue(queue_name_dead, options.merge(@options[:queue_dead_params])) do |queue|
|
127
|
+
queue.pop
|
128
|
+
end
|
129
|
+
rescue => e
|
130
|
+
Lolitra::log_exception(e)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
true
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
def purge_queue(queue)
|
138
|
+
queue.pop do |info, payload|
|
139
|
+
if (payload)
|
140
|
+
purge_queue(queue)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def recursive_pop(channel, queue, handler_class)
|
146
|
+
queue.pop(:ack => true) do |info, payload|
|
147
|
+
if payload
|
148
|
+
Lolitra::logger.info("Routing key: #{info.routing_key}")
|
149
|
+
Lolitra::logger.info("Payload: #{payload}")
|
150
|
+
begin
|
151
|
+
message_class_tmp = handler_class.handlers[info.routing_key][0]
|
152
|
+
handler_class.handle(message_class_tmp.unmarshall(payload))
|
153
|
+
info.ack
|
154
|
+
recursive_pop(channel, queue, handler_class)
|
155
|
+
rescue => e
|
156
|
+
channel.reject(info.delivery_tag, true)
|
157
|
+
Lolitra::log_exception(e)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def publish_payload(routing_key, payload)
|
164
|
+
self.exchange.publish(payload, :routing_key => routing_key, :timestamp => Time.now.to_i)
|
165
|
+
end
|
166
|
+
|
167
|
+
def create_channel(connection, &block)
|
168
|
+
channel = AMQP::Channel.new(connection, :auto_recovery => true) do
|
169
|
+
channel.on_error do |channel, close|
|
170
|
+
Lolitra::logger.error("Channel error: #{channel}")
|
171
|
+
Lolitra::logger.error(close)
|
172
|
+
end
|
173
|
+
block.call(channel)
|
174
|
+
end
|
175
|
+
channel
|
176
|
+
end
|
177
|
+
|
178
|
+
def generate_queue_name(handler_class)
|
179
|
+
"#{@options[:queue_prefix]}#{MessageHandler::Helpers.underscore(handler_class.name)}#{@options[:queue_suffix]}"
|
180
|
+
end
|
181
|
+
|
182
|
+
def generate_queue_name_dead(handler_class)
|
183
|
+
"#{generate_queue_name(handler_class)}#{@options[:queue_dead_suffix]}"
|
184
|
+
end
|
185
|
+
|
186
|
+
def create_queue(message_class, handler_class, options)
|
187
|
+
begin
|
188
|
+
queue_name = generate_queue_name(handler_class)
|
189
|
+
queue_name_dead = generate_queue_name_dead(handler_class)
|
190
|
+
|
191
|
+
create_channel(self.connection) do |channel|
|
192
|
+
begin
|
193
|
+
channel.queue(queue_name, options.merge(@options[:queue_params])).bind(self.exchange, :routing_key => message_class.message_key)
|
194
|
+
channel.queue(queue_name_dead, options.merge(@options[:queue_dead_params])).bind(self.exchange_dead_letter, :routing_key => message_class.message_key)
|
195
|
+
channel.close
|
196
|
+
rescue => e
|
197
|
+
Lolitra::log_exception(e)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
if !@options[:no_consume] && !@channels[queue_name] #Only one subscriber by queue_name
|
202
|
+
@channels[queue_name] = subscribe_to_messages(queue_name, options, handler_class)
|
203
|
+
end
|
204
|
+
rescue => e
|
205
|
+
Lolitra::log_exception(e)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def subscribe_to_messages(queue_name, options, handler_class)
|
210
|
+
create_channel(self.connection) do |channel|
|
211
|
+
channel.prefetch(1).queue(queue_name, options.merge(@options[:queue_params])).subscribe(:ack => true) do |info, payload|
|
212
|
+
begin
|
213
|
+
Lolitra::logger.debug("Message recived: #{info.routing_key}")
|
214
|
+
Lolitra::logger.debug("#{payload}")
|
215
|
+
message_class_tmp = handler_class.handlers[info.routing_key][0]
|
216
|
+
handler_class.handle(message_class_tmp.unmarshall(payload))
|
217
|
+
info.ack
|
218
|
+
rescue => e
|
219
|
+
channel.reject(info.delivery_tag, false)
|
220
|
+
Lolitra::log_exception(e)
|
221
|
+
mark_deadletter
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def remove_mark_deadletter
|
228
|
+
FileUtils.rm("#{Dir.pwd}/tmp/deadletter")
|
229
|
+
end
|
230
|
+
|
231
|
+
def mark_deadletter
|
232
|
+
FileUtils.touch("#{Dir.pwd}/tmp/deadletter")
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
data/lib/lolitra/version.rb
CHANGED
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 2
|
8
|
-
-
|
9
|
-
version: 0.2.
|
8
|
+
- 1
|
9
|
+
version: 0.2.1
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Hugo Freire
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2015-02-
|
17
|
+
date: 2015-02-26 00:00:00 +01:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -102,6 +102,7 @@ files:
|
|
102
102
|
- Rakefile
|
103
103
|
- lib/lolitra.rb
|
104
104
|
- lib/lolitra/handler_base.rb
|
105
|
+
- lib/lolitra/rabbitmq_bus.rb
|
105
106
|
- lib/lolitra/version.rb
|
106
107
|
- lolitra.gemspec
|
107
108
|
- spec/lolitra_spec.rb
|