lolitra 0.2.0 → 0.2.1

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