gorgon 0.5.0.rc1 → 0.6.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. data/Gemfile.lock +2 -4
  2. data/gorgon.gemspec +0 -1
  3. data/lib/gorgon/amqp_service.rb +5 -5
  4. data/lib/gorgon/gem_command_handler.rb +2 -1
  5. data/lib/gorgon/listener.rb +7 -5
  6. data/lib/gorgon/originator_protocol.rb +1 -0
  7. data/lib/gorgon/version.rb +1 -1
  8. data/lib/gorgon/worker_manager.rb +5 -2
  9. data/lib/gorgon_amq-protocol/.gitignore +15 -0
  10. data/lib/gorgon_amq-protocol/.gitmodules +3 -0
  11. data/lib/gorgon_amq-protocol/.rspec +3 -0
  12. data/lib/gorgon_amq-protocol/.travis.yml +19 -0
  13. data/lib/gorgon_amq-protocol/lib/gorgon_amq/bit_set.rb +82 -0
  14. data/lib/gorgon_amq-protocol/lib/gorgon_amq/endianness.rb +15 -0
  15. data/lib/gorgon_amq-protocol/lib/gorgon_amq/int_allocator.rb +96 -0
  16. data/lib/gorgon_amq-protocol/lib/gorgon_amq/pack.rb +53 -0
  17. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol.rb +4 -0
  18. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/client.rb +2322 -0
  19. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/constants.rb +22 -0
  20. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/exceptions.rb +60 -0
  21. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/float_32bit.rb +14 -0
  22. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/frame.rb +210 -0
  23. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/table.rb +142 -0
  24. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/table_value_decoder.rb +190 -0
  25. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/table_value_encoder.rb +123 -0
  26. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/type_constants.rb +26 -0
  27. data/lib/gorgon_amq-protocol/lib/gorgon_amq/protocol/version.rb +5 -0
  28. data/lib/gorgon_amq-protocol/lib/gorgon_amq/settings.rb +114 -0
  29. data/lib/gorgon_amq-protocol/lib/gorgon_amq/uri.rb +37 -0
  30. data/lib/gorgon_bunny/lib/gorgon_amq/protocol/extensions.rb +16 -0
  31. data/lib/gorgon_bunny/lib/gorgon_bunny.rb +89 -0
  32. data/lib/gorgon_bunny/lib/gorgon_bunny/authentication/credentials_encoder.rb +55 -0
  33. data/lib/gorgon_bunny/lib/gorgon_bunny/authentication/external_mechanism_encoder.rb +27 -0
  34. data/lib/gorgon_bunny/lib/gorgon_bunny/authentication/plain_mechanism_encoder.rb +19 -0
  35. data/lib/gorgon_bunny/lib/gorgon_bunny/channel.rb +1875 -0
  36. data/lib/gorgon_bunny/lib/gorgon_bunny/channel_id_allocator.rb +80 -0
  37. data/lib/gorgon_bunny/lib/gorgon_bunny/compatibility.rb +24 -0
  38. data/lib/gorgon_bunny/lib/gorgon_bunny/concurrent/atomic_fixnum.rb +74 -0
  39. data/lib/gorgon_bunny/lib/gorgon_bunny/concurrent/condition.rb +66 -0
  40. data/lib/gorgon_bunny/lib/gorgon_bunny/concurrent/continuation_queue.rb +41 -0
  41. data/lib/gorgon_bunny/lib/gorgon_bunny/concurrent/linked_continuation_queue.rb +61 -0
  42. data/lib/gorgon_bunny/lib/gorgon_bunny/concurrent/synchronized_sorted_set.rb +56 -0
  43. data/lib/gorgon_bunny/lib/gorgon_bunny/consumer.rb +123 -0
  44. data/lib/gorgon_bunny/lib/gorgon_bunny/consumer_tag_generator.rb +23 -0
  45. data/lib/gorgon_bunny/lib/gorgon_bunny/consumer_work_pool.rb +94 -0
  46. data/lib/gorgon_bunny/lib/gorgon_bunny/delivery_info.rb +93 -0
  47. data/lib/gorgon_bunny/lib/gorgon_bunny/exceptions.rb +236 -0
  48. data/lib/gorgon_bunny/lib/gorgon_bunny/exchange.rb +271 -0
  49. data/lib/gorgon_bunny/lib/gorgon_bunny/framing.rb +56 -0
  50. data/lib/gorgon_bunny/lib/gorgon_bunny/heartbeat_sender.rb +70 -0
  51. data/lib/gorgon_bunny/lib/gorgon_bunny/message_properties.rb +119 -0
  52. data/lib/gorgon_bunny/lib/gorgon_bunny/queue.rb +387 -0
  53. data/lib/gorgon_bunny/lib/gorgon_bunny/reader_loop.rb +116 -0
  54. data/lib/gorgon_bunny/lib/gorgon_bunny/return_info.rb +74 -0
  55. data/lib/gorgon_bunny/lib/gorgon_bunny/session.rb +1044 -0
  56. data/lib/gorgon_bunny/lib/gorgon_bunny/socket.rb +83 -0
  57. data/lib/gorgon_bunny/lib/gorgon_bunny/ssl_socket.rb +57 -0
  58. data/lib/gorgon_bunny/lib/gorgon_bunny/system_timer.rb +20 -0
  59. data/lib/gorgon_bunny/lib/gorgon_bunny/test_kit.rb +27 -0
  60. data/lib/gorgon_bunny/lib/gorgon_bunny/timeout.rb +18 -0
  61. data/lib/gorgon_bunny/lib/gorgon_bunny/transport.rb +398 -0
  62. data/lib/gorgon_bunny/lib/gorgon_bunny/version.rb +6 -0
  63. data/lib/gorgon_bunny/lib/gorgon_bunny/versioned_delivery_tag.rb +28 -0
  64. data/spec/crash_reporter_spec.rb +1 -1
  65. data/spec/gem_command_handler_spec.rb +2 -2
  66. data/spec/listener_spec.rb +5 -5
  67. data/spec/worker_manager_spec.rb +3 -3
  68. metadata +56 -17
@@ -0,0 +1,387 @@
1
+ require "gorgon_bunny/compatibility"
2
+
3
+ module GorgonBunny
4
+ # Represents AMQP 0.9.1 queue.
5
+ #
6
+ # @see http://rubybunny.info/articles/queues.html Queues and Consumers guide
7
+ # @see http://rubybunny.info/articles/extensions.html RabbitMQ Extensions guide
8
+ class Queue
9
+
10
+ include GorgonBunny::Compatibility
11
+
12
+
13
+ #
14
+ # API
15
+ #
16
+
17
+ # @return [GorgonBunny::Channel] Channel this queue uses
18
+ attr_reader :channel
19
+ # @return [String] Queue name
20
+ attr_reader :name
21
+ # @return [Hash] Options this queue was created with
22
+ attr_reader :options
23
+
24
+ # @param [GorgonBunny::Channel] channel_or_connection Channel this queue will use. {GorgonBunny::Session} instances are supported only for
25
+ # backwards compatibility with 0.8.
26
+ # @param [String] name Queue name. Pass an empty string to make RabbitMQ generate a unique one.
27
+ # @param [Hash] opts Queue properties
28
+ #
29
+ # @option opts [Boolean] :durable (false) Should this queue be durable?
30
+ # @option opts [Boolean] :auto_delete (false) Should this queue be automatically deleted when the last consumer disconnects?
31
+ # @option opts [Boolean] :exclusive (false) Should this queue be exclusive (only can be used by this connection, removed when the connection is closed)?
32
+ # @option opts [Boolean] :arguments ({}) Additional optional arguments (typically used by RabbitMQ extensions and plugins)
33
+ #
34
+ # @see GorgonBunny::Channel#queue
35
+ # @see http://rubybunny.info/articles/queues.html Queues and Consumers guide
36
+ # @see http://rubybunny.info/articles/extensions.html RabbitMQ Extensions guide
37
+ # @api public
38
+ def initialize(channel_or_connection, name = GorgonAMQ::Protocol::EMPTY_STRING, opts = {})
39
+ # old GorgonBunny versions pass a connection here. In that case,
40
+ # we just use default channel from it. MK.
41
+ @channel = channel_from(channel_or_connection)
42
+ @name = name
43
+ @options = self.class.add_default_options(name, opts)
44
+ @consumers = Hash.new
45
+
46
+ @durable = @options[:durable]
47
+ @exclusive = @options[:exclusive]
48
+ @server_named = @name.empty?
49
+ @auto_delete = @options[:auto_delete]
50
+ @arguments = @options[:arguments]
51
+
52
+ @bindings = Array.new
53
+
54
+ @default_consumer = nil
55
+
56
+ declare! unless opts[:no_declare]
57
+
58
+ @channel.register_queue(self)
59
+ end
60
+
61
+ # @return [Boolean] true if this queue was declared as durable (will survive broker restart).
62
+ # @api public
63
+ # @see http://rubybunny.info/articles/queues.html Queues and Consumers guide
64
+ def durable?
65
+ @durable
66
+ end # durable?
67
+
68
+ # @return [Boolean] true if this queue was declared as exclusive (limited to just one consumer)
69
+ # @api public
70
+ # @see http://rubybunny.info/articles/queues.html Queues and Consumers guide
71
+ def exclusive?
72
+ @exclusive
73
+ end # exclusive?
74
+
75
+ # @return [Boolean] true if this queue was declared as automatically deleted (deleted as soon as last consumer unbinds).
76
+ # @api public
77
+ # @see http://rubybunny.info/articles/queues.html Queues and Consumers guide
78
+ def auto_delete?
79
+ @auto_delete
80
+ end # auto_delete?
81
+
82
+ # @return [Boolean] true if this queue was declared as server named.
83
+ # @api public
84
+ # @see http://rubybunny.info/articles/queues.html Queues and Consumers guide
85
+ def server_named?
86
+ @server_named
87
+ end # server_named?
88
+
89
+ # @return [Hash] Additional optional arguments (typically used by RabbitMQ extensions and plugins)
90
+ # @api public
91
+ def arguments
92
+ @arguments
93
+ end
94
+
95
+ # Binds queue to an exchange
96
+ #
97
+ # @param [GorgonBunny::Exchange,String] exchange Exchange to bind to
98
+ # @param [Hash] opts Binding properties
99
+ #
100
+ # @option opts [String] :routing_key Routing key
101
+ # @option opts [Hash] :arguments ({}) Additional optional binding arguments
102
+ #
103
+ # @see http://rubybunny.info/articles/queues.html Queues and Consumers guide
104
+ # @see http://rubybunny.info/articles/bindings.html Bindings guide
105
+ # @api public
106
+ def bind(exchange, opts = {})
107
+ @channel.queue_bind(@name, exchange, opts)
108
+
109
+ exchange_name = if exchange.respond_to?(:name)
110
+ exchange.name
111
+ else
112
+ exchange
113
+ end
114
+
115
+
116
+ # store bindings for automatic recovery. We need to be very careful to
117
+ # not cause an infinite rebinding loop here when we recover. MK.
118
+ binding = { :exchange => exchange_name, :routing_key => (opts[:routing_key] || opts[:key]), :arguments => opts[:arguments] }
119
+ @bindings.push(binding) unless @bindings.include?(binding)
120
+
121
+ self
122
+ end
123
+
124
+ # Unbinds queue from an exchange
125
+ #
126
+ # @param [GorgonBunny::Exchange,String] exchange Exchange to unbind from
127
+ # @param [Hash] opts Binding properties
128
+ #
129
+ # @option opts [String] :routing_key Routing key
130
+ # @option opts [Hash] :arguments ({}) Additional optional binding arguments
131
+ #
132
+ # @see http://rubybunny.info/articles/queues.html Queues and Consumers guide
133
+ # @see http://rubybunny.info/articles/bindings.html Bindings guide
134
+ # @api public
135
+ def unbind(exchange, opts = {})
136
+ @channel.queue_unbind(@name, exchange, opts)
137
+
138
+ exchange_name = if exchange.respond_to?(:name)
139
+ exchange.name
140
+ else
141
+ exchange
142
+ end
143
+
144
+
145
+ @bindings.delete_if { |b| b[:exchange] == exchange_name && b[:routing_key] == (opts[:routing_key] || opts[:key]) && b[:arguments] == opts[:arguments] }
146
+
147
+ self
148
+ end
149
+
150
+ # Adds a consumer to the queue (subscribes for message deliveries).
151
+ #
152
+ # @param [Hash] opts Options
153
+ #
154
+ # @option opts [Boolean] :manual_ack (false) Will this consumer use manual acknowledgements?
155
+ # @option opts [Boolean] :exclusive (false) Should this consumer be exclusive for this queue?
156
+ # @option opts [Boolean] :block (false) Should the call block calling thread?
157
+ # @option opts [#call] :on_cancellation Block to execute when this consumer is cancelled remotely (e.g. via the RabbitMQ Management plugin)
158
+ # @option opts [String] :consumer_tag Unique consumer identifier. It is usually recommended to let GorgonBunny generate it for you.
159
+ # @option opts [Hash] :arguments ({}) Additional (optional) arguments, typically used by RabbitMQ extensions
160
+ #
161
+ # @see http://rubybunny.info/articles/queues.html Queues and Consumers guide
162
+ # @api public
163
+ def subscribe(opts = {
164
+ :consumer_tag => @channel.generate_consumer_tag,
165
+ :ack => false,
166
+ :exclusive => false,
167
+ :block => false,
168
+ :on_cancellation => nil
169
+ }, &block)
170
+
171
+ ctag = opts.fetch(:consumer_tag, @channel.generate_consumer_tag)
172
+ consumer = Consumer.new(@channel,
173
+ self,
174
+ ctag,
175
+ !(opts[:ack] || opts[:manual_ack]),
176
+ opts[:exclusive],
177
+ opts[:arguments])
178
+
179
+ consumer.on_delivery(&block)
180
+ consumer.on_cancellation(&opts[:on_cancellation]) if opts[:on_cancellation]
181
+
182
+ @channel.basic_consume_with(consumer)
183
+ if opts[:block]
184
+ # joins current thread with the consumers pool, will block
185
+ # the current thread for as long as the consumer pool is active
186
+ @channel.work_pool.join
187
+ end
188
+
189
+ consumer
190
+ end
191
+
192
+ # Adds a consumer object to the queue (subscribes for message deliveries).
193
+ #
194
+ # @param [GorgonBunny::Consumer] consumer a {GorgonBunny::Consumer} subclass that implements consumer interface
195
+ # @param [Hash] opts Options
196
+ #
197
+ # @option opts [Boolean] block (false) Should the call block calling thread?
198
+ #
199
+ # @see http://rubybunny.info/articles/queues.html Queues and Consumers guide
200
+ # @api public
201
+ def subscribe_with(consumer, opts = {:block => false})
202
+ @channel.basic_consume_with(consumer)
203
+
204
+ @channel.work_pool.join if opts[:block]
205
+ consumer
206
+ end
207
+
208
+ # @param [Hash] opts Options
209
+ #
210
+ # @option opts [Boolean] :ack (false) Will the message be acknowledged manually?
211
+ #
212
+ # @return [Array] Triple of delivery info, message properties and message content.
213
+ # If the queue is empty, all three will be nils.
214
+ # @see http://rubybunny.info/articles/queues.html Queues and Consumers guide
215
+ # @see GorgonBunny::Queue#subscribe
216
+ # @api public
217
+ #
218
+ # @example
219
+ # conn = GorgonBunny.new
220
+ # conn.start
221
+ #
222
+ # ch = conn.create_channel
223
+ # q = ch.queue("test1")
224
+ # x = ch.default_exchange
225
+ # x.publish("Hello, everybody!", :routing_key => 'test1')
226
+ #
227
+ # delivery_info, properties, payload = q.pop
228
+ #
229
+ # puts "This is the message: " + payload + "\n\n"
230
+ # conn.close
231
+ def pop(opts = {:ack => false}, &block)
232
+ delivery_info, properties, content = @channel.basic_get(@name, opts)
233
+
234
+ if block
235
+ block.call(delivery_info, properties, content)
236
+ else
237
+ [delivery_info, properties, content]
238
+ end
239
+ end
240
+ alias get pop
241
+
242
+ # Version of {GorgonBunny::Queue#pop} that returns data in legacy format
243
+ # (as a hash).
244
+ # @return [Hash]
245
+ # @deprecated
246
+ def pop_as_hash(opts = {:ack => false}, &block)
247
+ delivery_info, properties, content = @channel.basic_get(@name, opts)
248
+
249
+ result = {:header => properties, :payload => content, :delivery_details => delivery_info}
250
+
251
+ if block
252
+ block.call(result)
253
+ else
254
+ result
255
+ end
256
+ end
257
+
258
+
259
+ # Publishes a message to the queue via default exchange. Takes the same arguments
260
+ # as {GorgonBunny::Exchange#publish}
261
+ #
262
+ # @see GorgonBunny::Exchange#publish
263
+ # @see GorgonBunny::Channel#default_exchange
264
+ # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
265
+ def publish(payload, opts = {})
266
+ @channel.default_exchange.publish(payload, opts.merge(:routing_key => @name))
267
+
268
+ self
269
+ end
270
+
271
+
272
+ # Deletes the queue
273
+ #
274
+ # @param [Hash] opts Options
275
+ #
276
+ # @option opts [Boolean] if_unused (false) Should this queue be deleted only if it has no consumers?
277
+ # @option opts [Boolean] if_empty (false) Should this queue be deleted only if it has no messages?
278
+ #
279
+ # @see http://rubybunny.info/articles/queues.html Queues and Consumers guide
280
+ # @api public
281
+ def delete(opts = {})
282
+ @channel.deregister_queue(self)
283
+ @channel.queue_delete(@name, opts)
284
+ end
285
+
286
+ # Purges a queue (removes all messages from it)
287
+ # @see http://rubybunny.info/articles/queues.html Queues and Consumers guide
288
+ # @api public
289
+ def purge(opts = {})
290
+ @channel.queue_purge(@name, opts)
291
+
292
+ self
293
+ end
294
+
295
+ # @return [Hash] A hash with information about the number of queue messages and consumers
296
+ # @see #message_count
297
+ # @see #consumer_count
298
+ def status
299
+ queue_declare_ok = @channel.queue_declare(@name, @options.merge(:passive => true))
300
+ {:message_count => queue_declare_ok.message_count,
301
+ :consumer_count => queue_declare_ok.consumer_count}
302
+ end
303
+
304
+ # @return [Integer] How many messages the queue has ready (e.g. not delivered but not unacknowledged)
305
+ def message_count
306
+ s = self.status
307
+ s[:message_count]
308
+ end
309
+
310
+ # @return [Integer] How many active consumers the queue has
311
+ def consumer_count
312
+ s = self.status
313
+ s[:consumer_count]
314
+ end
315
+
316
+ #
317
+ # Recovery
318
+ #
319
+
320
+ # @private
321
+ def recover_from_network_failure
322
+ if self.server_named?
323
+ old_name = @name.dup
324
+ @name = GorgonAMQ::Protocol::EMPTY_STRING
325
+
326
+ @channel.deregister_queue_named(old_name)
327
+ end
328
+
329
+ # TODO: inject and use logger
330
+ # puts "Recovering queue #{@name}"
331
+ begin
332
+ declare!
333
+
334
+ @channel.register_queue(self)
335
+ rescue Exception => e
336
+ # TODO: inject and use logger
337
+ puts "Caught #{e.inspect} while redeclaring and registering #{@name}!"
338
+ end
339
+ recover_bindings
340
+ end
341
+
342
+ # @private
343
+ def recover_bindings
344
+ @bindings.each do |b|
345
+ # TODO: inject and use logger
346
+ # puts "Recovering binding #{b.inspect}"
347
+ self.bind(b[:exchange], b)
348
+ end
349
+ end
350
+
351
+
352
+ #
353
+ # Implementation
354
+ #
355
+
356
+ # @private
357
+ def declare!
358
+ queue_declare_ok = @channel.queue_declare(@name, @options)
359
+ @name = queue_declare_ok.queue
360
+ end
361
+
362
+ protected
363
+
364
+ # @private
365
+ def self.add_default_options(name, opts, block)
366
+ { :queue => name, :nowait => (block.nil? && !name.empty?) }.merge(opts)
367
+ end
368
+
369
+ # @private
370
+ def self.add_default_options(name, opts)
371
+ # :nowait is always false for GorgonBunny
372
+ h = { :queue => name, :nowait => false }.merge(opts)
373
+
374
+ if name.empty?
375
+ {
376
+ :passive => false,
377
+ :durable => false,
378
+ :exclusive => false,
379
+ :auto_delete => false,
380
+ :arguments => nil
381
+ }.merge(h)
382
+ else
383
+ h
384
+ end
385
+ end
386
+ end
387
+ end
@@ -0,0 +1,116 @@
1
+ require "thread"
2
+
3
+ module GorgonBunny
4
+ # Network activity loop that reads and passes incoming AMQP 0.9.1 methods for
5
+ # processing. They are dispatched further down the line in GorgonBunny::Session and GorgonBunny::Channel.
6
+ # This loop uses a separate thread internally.
7
+ #
8
+ # This mimics the way RabbitMQ Java is designed quite closely.
9
+ # @private
10
+ class ReaderLoop
11
+
12
+ def initialize(transport, session, session_thread)
13
+ @transport = transport
14
+ @session = session
15
+ @session_thread = session_thread
16
+ @logger = @session.logger
17
+ end
18
+
19
+
20
+ def start
21
+ @thread = Thread.new(&method(:run_loop))
22
+ end
23
+
24
+ def resume
25
+ start
26
+ end
27
+
28
+
29
+ def run_loop
30
+ loop do
31
+ begin
32
+ break if @stopping || @network_is_down
33
+ run_once
34
+ rescue Errno::EBADF => ebadf
35
+ break if @stopping
36
+ # ignored, happens when we loop after the transport has already been closed
37
+ rescue GorgonAMQ::Protocol::EmptyResponseError, IOError, SystemCallError => e
38
+ break if @stopping
39
+ log_exception(e)
40
+
41
+ @network_is_down = true
42
+
43
+ if @session.automatically_recover?
44
+ @session.handle_network_failure(e)
45
+ else
46
+ @session_thread.raise(GorgonBunny::NetworkFailure.new("detected a network failure: #{e.message}", e))
47
+ end
48
+ rescue ShutdownSignal => _
49
+ break
50
+ rescue Exception => e
51
+ break if @stopping
52
+ log_exception(e)
53
+
54
+ @network_is_down = true
55
+ @session_thread.raise(GorgonBunny::NetworkFailure.new("caught an unexpected exception in the network loop: #{e.message}", e))
56
+ end
57
+ end
58
+
59
+ @stopped = true
60
+ end
61
+
62
+ def run_once
63
+ frame = @transport.read_next_frame
64
+ return if frame.is_a?(GorgonAMQ::Protocol::HeartbeatFrame)
65
+
66
+ if !frame.final? || frame.method_class.has_content?
67
+ header = @transport.read_next_frame
68
+ content = ''
69
+
70
+ if header.body_size > 0
71
+ loop do
72
+ body_frame = @transport.read_next_frame
73
+ content << body_frame.decode_payload
74
+
75
+ break if content.bytesize >= header.body_size
76
+ end
77
+ end
78
+
79
+ @session.handle_frameset(frame.channel, [frame.decode_payload, header.decode_payload, content])
80
+ else
81
+ @session.handle_frame(frame.channel, frame.decode_payload)
82
+ end
83
+ end
84
+
85
+ def stop
86
+ @stopping = true
87
+ end
88
+
89
+ def stopped?
90
+ @stopped
91
+ end
92
+
93
+ def raise(e)
94
+ @thread.raise(e) if @thread
95
+ end
96
+
97
+ def join
98
+ @thread.join if @thread
99
+ end
100
+
101
+ def kill
102
+ if @thread
103
+ @thread.kill
104
+ @thread.join
105
+ end
106
+ end
107
+
108
+ def log_exception(e)
109
+ @logger.error "Exception in the reader loop: #{e.class.name}: #{e.message}"
110
+ @logger.error "Backtrace: "
111
+ e.backtrace.each do |line|
112
+ @logger.error "\t#{line}"
113
+ end
114
+ end
115
+ end
116
+ end