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 CHANGED
@@ -4,7 +4,7 @@ Amqp and Faye event bus
4
4
 
5
5
  ## Installation
6
6
 
7
- Add this line to your application's Gemfile:
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::AmqpBus.new(
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, FoldersHandler, UsersHandler]
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
@@ -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
- begin
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.register_pull_subscriber(handler_class)
165
- instance.register_pull_subscriber(handler_class)
206
+ def self.unsubscribe(handler_class, &block)
207
+ instance.unsubscribe(handler_class, &block)
166
208
  end
167
-
168
- def register_pull_subscriber(handler_class)
169
- handler_class.handle_messages.each do |message_class|
170
- bus.pull_subscribe(message_class, handler_class)
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
@@ -1,3 +1,3 @@
1
1
  module Lolitra
2
- VERSION = "0.2.0"
2
+ VERSION = "0.2.1"
3
3
  end
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 2
8
- - 0
9
- version: 0.2.0
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-20 00:00:00 +01:00
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