tengine_event 0.4.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1102 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'tengine/mq'
3
+
4
+ require 'active_support/version'
5
+ require 'active_support/core_ext/hash/deep_merge'
6
+ require 'tengine/support/core_ext/hash/compact'
7
+ require 'tengine/support/core_ext/hash/deep_dup'
8
+ require 'tengine/support/core_ext/hash/keys'
9
+ require 'tengine/support/core_ext/enumerable/each_next_tick'
10
+ require 'tengine/support/core_ext/enumerable/deep_freeze'
11
+ require 'tengine/support/core_ext/module/private_constant'
12
+ require 'amqp'
13
+ require 'amqp/extensions/rabbitmq'
14
+
15
+ class Tengine::Mq::Suite
16
+
17
+ #######
18
+ private
19
+ #######
20
+
21
+ PendingEvent = Struct.new :tag, :sender, :event, :opts, :retry, :block
22
+ private_constant :PendingEvent
23
+
24
+ # This is to accumulate a set of exceptions happend at a series of executions.
25
+ class ExceptionsContainer < RuntimeError
26
+ def initialize
27
+ super
28
+ @set = Array.new
29
+ end
30
+
31
+ # diagnostics
32
+ def message
33
+ msgs = @set.map {|i| i.message }.join "\n\t"
34
+ sprintf "multiple exceptions are reported.\n\t%s", msgs
35
+ end
36
+
37
+ def << e
38
+ @set << e
39
+ end
40
+
41
+ def raise
42
+ case @set.size
43
+ when 0
44
+ # no exceptions
45
+ when 1
46
+ # only one exception happened inside
47
+ Kernel.raise @set.first
48
+ else
49
+ # multiple.
50
+ super
51
+ end
52
+ end
53
+ end
54
+ private_constant :ExceptionsContainer
55
+
56
+ # Some (not all) of the descriptions below are quoted from the AMQP gem's yardoc.
57
+ #
58
+ # @param [Hash] cfg Tons of optional arguments can be specified and they are settings for
59
+ # connections, queues, and senders. But they are all optional, i.e. you are 100%
60
+ # safe to omit the whole. Specify only what you concern.
61
+ # @option cfg [Hash] :sender Configurations for message sender, see below.
62
+ # @option cfg [Hash] :connection Configurations for MQ connection, see below.
63
+ # @option cfg [Hash] :channel Configurations for MQ channel, see below.
64
+ # @option cfg [Hash] :exchange Configurations for MQ exchange, see below.
65
+ # @option cfg [Hash] :queue Configurations for MQ queue, see below.
66
+ # @option sender [Boolean] :keep_connection Whether a connection shall be shut down after transmitted a message, or not.
67
+ # If set, a sender can eventually shut your reactor down and the whole
68
+ # EventMachine loop can be abandoned.
69
+ # @option sender [Numeric] :retry_interval Seconds to wait before attempting to retransmit a message after failure. Zero
70
+ # means an immediate retry so watch out.
71
+ # @option sender [Integer] :retry_count Max count of retry attempts. Set zero here to stop sender from even think of
72
+ # retrying.
73
+ # @option connection [String] :user Authentication info.
74
+ # @option connection [String] :pass Authentication info.
75
+ # @option connection [String] :host Where to connect.
76
+ # @option connection [String] :port Where to connect.
77
+ # @option connection [String] :vhost The AMQP virtual host.
78
+ # @option connection [Numeric] :timeout Connection timeout in secs.
79
+ # @option connection [Boolean] :logging ?? Description TBD ??
80
+ # @option connection [Boolean] :insist ?? Description TBD ??
81
+ # @option connection [Numeric] :auto_reconnect_delay When set, a TCP session loss yields an automatic reconnect attempt, with this
82
+ # delay (in secs). Without it no reconnection attempts are made.
83
+ # @option channel [Numeric] :prefetch Specifies number of messages to prefetch. Learn more: AMQP's QoS features.
84
+ # @option channel [Boolean] :auto_recovery Turns on automatic network failure recovery mode for the channel. *Note* it is
85
+ # highly recommended that you leave this flag untouched (default enabled).
86
+ # Otherwise you have to have 100% control over how to recover your channel and
87
+ # its dependent queues/exchanges. That should be doable via #add_hook though.
88
+ # @option exchange [String] :name Explicit name to use, or empty (i.e. "") to let the broker allocate an
89
+ # appropriate name.
90
+ # @option exchange [Symbol] :type One of direct, fanout, topic, or headers.
91
+ # @option exchange [Hash] :publish Default options for publishing messages. See below.
92
+ # @option exchange [Boolean] :passive If set, the server will not create the exchange if it does not already
93
+ # exist. The client can use this to check whether an exchange exists without
94
+ # modifying the server state.
95
+ # @option exchange [Boolean] :durable If set when creating a new exchange, the exchange will be marked as
96
+ # durable. Durable exchanges and their bindings are recreated upon a server
97
+ # restart (information about them is persisted). Non-durable (transient)
98
+ # exchanges do not survive if/when a server restarts (information about them is
99
+ # stored exclusively in RAM).
100
+ # @option exchange [Boolean] :auto_delete If set, the exchange is deleted when all queues have finished using it. The
101
+ # server waits for a short period of time before determining the exchange is
102
+ # unused to give time to the client code to bind a queue to it.
103
+ # @option exchange [Boolean] :internal If set, the exchange may not be used directly by publishers, but only when
104
+ # bound to other exchanges. Internal exchanges are used to construct wiring that
105
+ # is not visible to applications. *This is a RabbitMQ-specific extension.*
106
+ # @option exchange [Boolean] :nowait If set, the server will not respond to the method. The client should not wait
107
+ # for a reply method. If the server could not complete the method it will raise
108
+ # a channel or connection exception.
109
+ # @option exchange [Boolean] :no_declare If set, exchange declaration command won't be sent to the broker. Allows to
110
+ # forcefully avoid declaration. We recommend that only experienced developers
111
+ # consider this option.
112
+ # @option exchange [String] :default_routing_key Default routing key that will be used by AMQP::Exchange#publish when no routing
113
+ # key is not passed explicitly.
114
+ # @option exchange [Hash] :arguments A hash of optional arguments with the declaration. Some brokers implement AMQP
115
+ # extensions using x-prefixed declaration arguments.
116
+ # @option publish [String] :routing_key Specifies message routing key. Routing key determines what queues messages are
117
+ # delivered to (exact routing algorithms vary between exchange types).
118
+ # @option publish [Boolean] :mandatory This flag tells the server how to react if the message cannot be routed to a
119
+ # queue. If message is mandatory, the server will return unroutable message back
120
+ # to the client with basic.return AMQP method. If message is not mandatory, the
121
+ # server silently drops the message.
122
+ # @option publish [Boolean] :immediate This flag tells the server how to react if the message cannot be routed to a
123
+ # queue consumer immediately. If this flag is set, the server will return an
124
+ # undeliverable message with a Return method. If this flag is zero, the server
125
+ # will queue the message, but with no guarantee that it will ever be consumed.
126
+ # @option publish [Boolean] :persistent When true, this message will be persisted to disk and remain in the queue until
127
+ # it is consumed. When false, the message is only kept in a transient store and
128
+ # will lost in case of server restart. When performance and latency are more
129
+ # important than durability, set :persistent => false. If durability is more
130
+ # important, set :persistent => true.
131
+ # @option publish [String] :content_type Content-type of message payload.
132
+ # @option queue [String] :name Explicit name to use, or empty (i.e. "") to let the broker allocate an
133
+ # appropriate name.
134
+ # @option queue [Hash] :subscribe Default options for subscribing. See below.
135
+ # @option queue [Boolean] :passive If set, the server will not create the queue if it does not already exist. The
136
+ # client can use this to check whether the queue exists without modifying the
137
+ # server state.
138
+ # @option queue [Boolean] :durable If set when creating a new queue, the queue will be marked as durable. Durable
139
+ # queues remain active when a server restarts. Non-durable queues (transient
140
+ # queues) are purged if/when a server restarts. Note that durable queues do not
141
+ # necessarily hold persistent messages, although it does not make sense to send
142
+ # persistent messages to a transient queue (though it is allowed).
143
+ # @option queue [Boolean] :exclusive Exclusive queues may only be consumed from by the current connection. Setting
144
+ # the 'exclusive' flag always implies 'auto-delete'. Only a single consumer is
145
+ # allowed to remove messages from the queue. The default is a shared
146
+ # queue. Multiple clients may consume messages from the queue.
147
+ # @option queue [Boolean] :auto_delete If set, the queue is deleted when all consumers have finished using it. Last
148
+ # consumer can be cancelled either explicitly or because its channel is
149
+ # closed. If there was no consumer ever on the queue, it won't be deleted.
150
+ # @option queue [Boolean] :nowait If set, the server will not respond to the method. The client should not wait
151
+ # for a reply method. If the server could not complete the method it will raise
152
+ # a channel or connection exception.
153
+ # @option queue [Hash] :arguments A hash of optional arguments with the declaration. Some brokers implement AMQP
154
+ # extensions using x-prefixed declaration arguments. For example, RabbitMQ
155
+ # recognizes x-message-ttl declaration arguments that defines TTL of messages in
156
+ # the queue.
157
+ # @option subscribe [Boolean] :ack If this field is set to false the server does not expect acknowledgments for
158
+ # messages. That is, when a message is delivered to the client the server
159
+ # automatically and silently acknowledges it on behalf of the client. This
160
+ # functionality increases performance but at the cost of reliability. Messages
161
+ # can get lost if a client dies before it can deliver them to the application.
162
+ # @option subscribe [Boolean] :nowait If set, the server will not respond to the method. The client should not wait
163
+ # for a reply method. If the server could not complete the method it will raise
164
+ # a channel or connection exception.
165
+ # @option subscribe [#call] :confirm If set, this proc will be called when the server confirms subscription to the
166
+ # queue with a basic.consume-ok message. Setting this option will automatically
167
+ # set :nowait => false. This is required for the server to send a confirmation.
168
+ # @option subscribe [Boolean] :exclusive Request exclusive consumer access, meaning only this consumer can access the
169
+ # queue. This is useful when you want a long-lived shared queue to be
170
+ # temporarily accessible by just one application (or thread, or process). If
171
+ # application exclusive consumer is part of crashes or loses network connection
172
+ # to the broker, channel is closed and exclusive consumer is thus cancelled.
173
+ def initialize cfg = Hash.new
174
+ @terminating = false
175
+ @mutex = Mutex.new
176
+ @condvar = ConditionVariable.new
177
+ @setting_up = Hash.new
178
+ @state = :uninitialized # see setup_handshake for possible values
179
+ @firing_queue = EM::Queue.new
180
+ @publishing_events = Array.new
181
+ @retrying_events = Hash.new
182
+ @pending_events = Hash.new
183
+ @hooks = Hash.new do |h, k| h.store k, Array.new end
184
+ @config = {
185
+ :sender => {
186
+ :keep_connection => false,
187
+ :retry_interval => 1, # in seconds
188
+ :retry_count => 30,
189
+ },
190
+ :connection => {
191
+ :user => 'guest',
192
+ :pass => 'guest',
193
+ :vhost => '/',
194
+ :logging => false,
195
+ :insist => false,
196
+ :host => 'localhost',
197
+ :port => 5672,
198
+ :auto_reconnect_delay => 1, # in seconds
199
+ },
200
+ :channel => {
201
+ :prefetch => 1,
202
+ :auto_recovery => true,
203
+ },
204
+ :exchange => {
205
+ :name => 'tengine_event_exchange',
206
+ :type => :direct,
207
+ :passive => false,
208
+ :durable => true,
209
+ :auto_delete => false,
210
+ :internal => false,
211
+ :nowait => false,
212
+ :publish => {
213
+ :content_type => "application/json", # RFC4627
214
+ :persistent => true,
215
+ },
216
+ },
217
+ :queue => {
218
+ :name => 'tengine_event_queue',
219
+ :passive => false,
220
+ :durable => true,
221
+ :auto_delete => false,
222
+ :exclusive => false,
223
+ :nowait => false,
224
+ :subscribe => {
225
+ :ack => true,
226
+ :nowait => false,
227
+ :confirm => nil,
228
+ },
229
+ },
230
+ }
231
+ @config.deep_merge! cfg.to_hash.deep_symbolize_keys.compact
232
+ @config.deep_freeze
233
+ install_default_hooks
234
+ end
235
+
236
+ ######
237
+ public
238
+ ######
239
+
240
+ attr_reader :config
241
+
242
+ # @yield [args] Given block is called when the hook condition met.
243
+ # @yieldparam [Array] args Any arguments passed to the callback are passed through.
244
+ # @param [Symbol] name Hook name to add
245
+ def add_hook name, &block
246
+ raise ArgumentError, "no block given" unless block_given?
247
+ synchronize do
248
+ @hooks[name.intern] << block
249
+ end
250
+ end
251
+
252
+ # @yield [header, payload] Given block is called every time a message was received by the queue.
253
+ # @yieldparam [AMQP::Header] header Message metadata.
254
+ # @yieldparam [String] payload Message entity.
255
+ # @param [Hash] cfg Subscription options
256
+ # @option opts [Boolean] :ack Whether the broker (not us) expects acknowledgements from our side. If this is true, you
257
+ # have to call header.ack somewhere inside the block, and unacknowledged messages are
258
+ # re-sent later. Otherwise you need not to call header.ack, and the server doesn't know
259
+ # your sudden death or packet loss or network problems.
260
+ # @option opts [Boolean] :nowait With this flag on, like other :nowait cases, the request is dealt silently. Don't get
261
+ # confused: the broker do not respond to the subscribe request, but does push messages to
262
+ # us. i.e. this flag only affects to :confirm optional argument.
263
+ # @option opts [#call] :confirm This is called when the broker replied your subscription. I have never seen this called
264
+ # twice. This argument assumes :nowait => false.
265
+ # @option opts [Boolean] :exclusive Request exclusive consumer access, meaning only this consumer can access the queue. When
266
+ # you experience a network problem, exclusive access is cancelled. Which itself is not a
267
+ # strange behaviour, but if you do a auto-recover the exclusivity might suddenly lost. So
268
+ # beware.
269
+ def subscribe cfg = Hash.new
270
+ raise ArgumentError, "no block given" unless block_given?
271
+ ensures :queue do |q|
272
+ opts = @config[:queue][:subscribe].merge cfg.compact
273
+ q.subscribe opts do |h, b|
274
+ yield h, b
275
+ end
276
+ end
277
+ end
278
+
279
+ # @yield [cok] Given block is called after it had successfully ubsubscribed from the
280
+ # broker.
281
+ # @yieldparam [AMQP::Protocol::Basic::CancelOK] cok Message metadata. *Note* can be nil when you set nowait: true.
282
+ # @param [Hash] cfg Subscription options
283
+ # @option opts [Boolean] :nowait With this flag on, like other :nowait cases, the request is dealt silently.
284
+ # The block is called anyway though.
285
+ def unsubscribe cfg = Hash.new
286
+ raise ArgumentError, "no block given" unless block_given?
287
+ synchronize do
288
+ if ivar? :queue and @queue.default_consumer
289
+ cfg[:nowait] = cfg.fetch :nowait, false
290
+ if cfg[:nowait]
291
+ @queue.unsubscribe cfg
292
+ yield nil
293
+ else
294
+ @queue.unsubscribe cfg do |cok|
295
+ yield cok
296
+ end
297
+ end
298
+ else
299
+ logger :warn, "unsubscribe called but not subscribed"
300
+ yield nil
301
+ end
302
+ end
303
+ end
304
+
305
+ # You don't have to understand it. Use Tengine::Event::Sender.
306
+ #
307
+ # @param [Tengine::Event::Sender] sender Event sender
308
+ # @param [Tengine::Event] event Event to submit
309
+ # @param [Hash] opts Options to pass to the publisher
310
+ # @param [#call] block Callback to be triggered *after* the transmission.
311
+ # @option opts [Boolean] :keep_connection Whether a connection shall be shut down after transmitted a message, or not. If
312
+ # set, a sender can eventually shut your reactor down and the whole EventMachine
313
+ # loop can be abondoned.
314
+ # @option opts [Numeric] :retry_interval Seconds to wait before attempting to retransmit a message after failure.
315
+ # @option opts [Numeric] :retry_count Max count of retry attempts.
316
+ def fire sender, event, opts, block
317
+ cfg = @config[:sender].merge opts.compact
318
+ e = PendingEvent.new 0, sender, event, cfg, 0, block
319
+ synchronize do
320
+ @pending_events[e] = true
321
+ case @state when :disconnected
322
+ # wait for next connection
323
+ @retrying_events[e] = [nil, Time.at(0)]
324
+ else
325
+ @firing_queue.push e # serialize
326
+ trigger_firing_thread if @firing_queue.size <= 1 # first kick
327
+ end
328
+ end
329
+ end
330
+
331
+ # stops the suite.
332
+ def stop
333
+ # べつに何も難しいことがしたいわけではなくて最終的にp0を呼べばいいんだけど、EMがいるかいないか、@connectionがいるかいないかの条件分
334
+ # けで無駄に長いメソッドになっている。
335
+
336
+ p0 = lambda do
337
+ EM.cancel_timer @reconnection_timer if ivar? :reconnection_timer
338
+ @retrying_events.each_value do |(idx, *)|
339
+ EM.stop_timer idx if idx
340
+ end
341
+ @retrying_events.clear
342
+ stop_firing_queue
343
+
344
+ @state = :uninitialized # この後またEM.run{ .. }されるかも
345
+ @setting_up.clear
346
+ @firing_queue = EM::Queue.new
347
+ @connection = nil
348
+ @channel = nil
349
+ @queue = nil
350
+ @exchange = nil
351
+ @reconnection_timer = nil
352
+ GC.start # 気休め
353
+
354
+ logger :info, "OK, stopped. Good bye."
355
+ if block_given? then
356
+ yield
357
+ else
358
+ EM.stop
359
+ end
360
+ end
361
+
362
+ p1 = lambda do
363
+ if ivar? :connection
364
+ @connection.disconnect do
365
+ synchronize do
366
+ p0.call
367
+ end
368
+ end
369
+ else
370
+ p0.call
371
+ end
372
+ end
373
+
374
+ p2 = lambda do
375
+ if ivar? :channel
376
+ @channel.close do
377
+ synchronize do
378
+ p1.call
379
+ end
380
+ end
381
+ else
382
+ p1.call
383
+ end
384
+ end
385
+
386
+ p3 = lambda do
387
+ if ivar? :queue and @queue.default_consumer
388
+ @queue.unsubscribe :nowait => false do
389
+ synchronize do
390
+ p2.call
391
+ end
392
+ end
393
+ else
394
+ p2.call
395
+ end
396
+ end
397
+
398
+ p4 = lambda do
399
+ synchronize do
400
+ logger :info, "finishing up, now sending remaining events."
401
+ @condvar.wait @mutex until @pending_events.empty?
402
+ end
403
+ end
404
+
405
+ p5 = lambda do |a|
406
+ synchronize do
407
+ p3.call
408
+ end
409
+ end
410
+
411
+ initiate_termination do
412
+ if EM.reactor_running?
413
+ EM.defer p4, p5
414
+ elsif block_given?
415
+ yield
416
+ end
417
+ end
418
+ end
419
+
420
+ # Declares that the application is now terminating this MQ connection. No reconnection / resend attempts are made any more. The
421
+ # connection (if any) is still open and you can push / pull using it, but by calling this method you hereby agree that no messages
422
+ # involving this suite are reliable any longer.
423
+ #
424
+ # Yields after the declaration.
425
+ def initiate_termination
426
+ @terminating = true # FIXME: should be mutex-protected
427
+ yield
428
+ end
429
+
430
+ def inspect
431
+ sprintf "#<%p:%#x %s cfg=%p ev=%p hook=%p>", self.class, self.object_id, @state, @config, @pending_events, @hooks
432
+ end
433
+
434
+ def pretty_print pp
435
+ pp.pp_object self
436
+ end
437
+
438
+ # used by pretty printer
439
+ def pretty_print_instance_variables
440
+ %w[@state @config @pending_events @hooks]
441
+ end
442
+
443
+ #######
444
+
445
+ # @deprecated Do not use it.
446
+ def connection; deprecated :connection end
447
+
448
+ # @deprecated Do not use it.
449
+ def channel; deprecated :channel end
450
+
451
+ # @deprecated Do not use it.
452
+ def exchange; deprecated :exchange end
453
+
454
+ # @deprecated Do not use it.
455
+ def queue; deprecated :queue end
456
+
457
+ #######
458
+
459
+ # @api private
460
+ def pending_events
461
+ synchronize do
462
+ @pending_events.keys.select {|i| yield i }.map {|i| i.event }
463
+ end
464
+ end
465
+
466
+ # @api private
467
+ def pending_events_for sender
468
+ pending_events do |i|
469
+ i.sender == sender
470
+ end
471
+ end
472
+
473
+ # @api private
474
+ def self.pending? event
475
+ e = ObjectSpace.each_object self
476
+ e.any? do |obj|
477
+ not obj.pending_events do |i| i.event == event end.empty?
478
+ end
479
+ end
480
+
481
+ #######
482
+ private
483
+ #######
484
+
485
+ # A thin Tengine.logger wrapper. As this gem can be used without a logger, we have to work around it.
486
+ # @param [Symbol] lv One of debug, info, warn, error, fatal, or Logger constants.
487
+ # @param [String] fmt printf format
488
+ # @param [Array] argv printf variadic arguments
489
+ def logger lv, fmt, *argv
490
+ msg = sprintf fmt, *argv
491
+ if defined? Tengine.logger
492
+ Tengine.logger.send lv, msg
493
+ else
494
+ STDERR.puts msg
495
+ end
496
+ end
497
+
498
+ # Wanted to avoid recursive mutex deadlocking, so this convenient method. But beware, recursive locking situation is in fact a bad
499
+ # habit (if not a bug), and we pay a considerable cost to avoid them here. This method is far from being lightweight especially when
500
+ # recursive locking happens.
501
+ def synchronize
502
+ begin
503
+ @mutex.lock
504
+ rescue ThreadError => e
505
+ # A deadlock was detected, which means of course, we have the lock.
506
+ bt = e.backtrace.join "\n\tfrom "
507
+ logger :debug, "%s\n\tfrom %s", e, bt
508
+ ensure
509
+ begin
510
+ return yield
511
+ ensure
512
+ begin
513
+ @mutex.unlock
514
+ rescue ThreadError => e
515
+ # @mutex might magically be unlocked... For instance, the execution context might have been splitted from inside of the
516
+ # yielded block. That case, the context who reached here do not own @mutex so should not unlock.
517
+ bt = e.backtrace.join "\n\tfrom "
518
+ logger :debug, "%s\n¥tfrom %s\n", e, bt
519
+ end
520
+ end
521
+ end
522
+ end
523
+
524
+ # suppress warning on debug mode
525
+ def ivar? name
526
+ vid = "@#{name}"
527
+ instance_variable_defined?(vid) && instance_variable_get(vid)
528
+ end
529
+
530
+ # misc also
531
+ def rehash_them_all
532
+ instance_variables.each do |i|
533
+ obj = instance_variable_get i
534
+ case obj when Hash then
535
+ obj.rehash unless obj.frozen?
536
+ end
537
+ end
538
+ end
539
+
540
+ #######
541
+
542
+ # Generates a callback according to klass and mid
543
+ # @param [String] klass callback category
544
+ # @param [Symbol] mid callback ID
545
+ # @return [Proc] a callback.
546
+ def callback_entity klass, mid
547
+ lambda do |*argv|
548
+ exceptions = ExceptionsContainer.new
549
+ begin
550
+ @hooks[:everything].reverse_each do |proc|
551
+ begin
552
+ proc.yield klass, mid, argv
553
+ rescue Exception => e
554
+ exceptions << e
555
+ end
556
+ end
557
+
558
+ @hooks[:"#{klass}.#{mid}"].reverse_each do |proc|;
559
+ begin
560
+ proc.yield(*argv)
561
+ rescue Exception => e
562
+ exceptions << e
563
+ end
564
+ end
565
+ ensure
566
+ exceptions.raise
567
+ end
568
+ end
569
+ end
570
+
571
+ def deprecated klass
572
+ # このメソッドは警告を表示する。デバッグ用。
573
+ synchronize do
574
+ obj = ivar? klass
575
+ if obj
576
+ logger :debug, "Deprecation warning. Method %s called from %s", klass, caller[3]
577
+ else
578
+ raise RuntimeError, "found a timing issue. please report to @shyouhei with a reproducible sample code."
579
+ end
580
+ return obj
581
+ end
582
+ end
583
+
584
+ # @yields [obj] yields generated object
585
+ def ensures klass
586
+ raise "eventmachine's reactor needed" unless EM.reactor_running?
587
+ # このメソッドはEM.deferでklassの初期化を待つ。EM.deferだから戻り値を使ってはいけない。引数のブロックは、klassが初期化されたことが確
588
+ # 認された後にcallされる。
589
+ p1 = lambda do
590
+ synchronize do
591
+ unless ivar? klass
592
+ setups klass unless @setting_up[klass]
593
+ @condvar.wait @mutex until ivar? klass
594
+ end
595
+ end
596
+ end
597
+ p2 = lambda do |a|
598
+ obj = ivar? klass
599
+ yield obj if block_given?
600
+ end
601
+ EM.defer p1, p2
602
+ end
603
+
604
+ def generate_connection cb
605
+ cfg = cb.merge @config[:connection] do |k, v1, v2| v2 end
606
+ AMQP.connect cfg do |conn|
607
+ synchronize do
608
+ @state = :connected
609
+ end
610
+ yield conn
611
+ end
612
+ rescue AMQP::TCPConnectionFailed
613
+ # on_tcp_connection_failrueは指定しているのだけれどそれでもこの例外はあがってくるのだろうか? よくわからない
614
+ # いちおう同じことをさせておく
615
+ cfg[:on_tcp_connection_failrue].yield cfg
616
+ end
617
+
618
+ def generate_channel *;
619
+ cfg = @config[:channel]
620
+ ensures :connection do |conn|
621
+ id = AMQP::Channel.next_channel_id
622
+ AMQP::Channel.new conn, id, cfg do |ch|
623
+ yield ch
624
+ end
625
+ end
626
+ end
627
+
628
+ def generate_queue *;
629
+ cfg = @config[:queue].dup
630
+ name = cfg.delete :name
631
+ ensures :exchange do |xchg|
632
+ if cfg[:nowait]
633
+ que = @channel.queue name, cfg
634
+ que.bind xchg
635
+ yield que
636
+ else
637
+ @channel.queue name, cfg do |que|
638
+ que.bind xchg, :nowait => false do
639
+ yield que
640
+ end
641
+ end
642
+ end
643
+ end
644
+ end
645
+
646
+ def generate_exchange *;
647
+ cfg = @config[:exchange].dup
648
+ name = cfg.delete :name
649
+ type = cfg.delete :type
650
+ cfg.delete :publish # not needed here
651
+ ensures :channel do |ch|
652
+ if cfg[:nowait]
653
+ xchg = AMQP::Exchange.new ch, type.intern, name, cfg
654
+ yield xchg
655
+ else
656
+ AMQP::Exchange.new ch, type.intern, name, cfg do |xchg|
657
+ yield xchg
658
+ end
659
+ end
660
+ end
661
+ end
662
+
663
+ def hooks_basic
664
+ %w[
665
+ before_recovery
666
+ after_recovery
667
+ on_connection_interruption
668
+ ]
669
+ end
670
+ alias hooks_queue hooks_basic
671
+ alias hooks_exchange hooks_queue
672
+
673
+ def hooks_channel
674
+ hooks_basic + %w[on_error]
675
+ end
676
+
677
+ def hooks_connection
678
+ hooks_channel + %w[
679
+ on_closed
680
+ on_possible_authentication_failure
681
+ on_tcp_connection_failure
682
+ on_tcp_connection_loss
683
+ ]
684
+ end
685
+
686
+ @@is_under_rspec = \
687
+ begin
688
+ RSpec
689
+ rescue NameError
690
+ false
691
+ end
692
+
693
+ def setups klass
694
+ # このメソッドはvidの初期化を実際に行う。mutexは確保されている前提である。
695
+ @setting_up[klass] = true
696
+ mids = send "hooks_#{klass}"
697
+ callbacks = mids.inject Hash.new do |r, x|
698
+ y = x.intern
699
+ cb = callback_entity klass, y
700
+ r.update y => cb
701
+ end
702
+
703
+ send "generate_#{klass}", callbacks do |obj|
704
+ callbacks.each_pair do |k, v|
705
+ if @@is_under_rspec
706
+ begin
707
+ obj.send k, &v
708
+ rescue RSpec::Mocks::MockExpectationError
709
+ # objはmock objectかも...
710
+ end
711
+ else
712
+ obj.send k, &v
713
+ end
714
+ end
715
+
716
+ unless @@is_under_rspec
717
+ # rspec ではないとき(テスト以外)はundefしておく
718
+ # 本当はテスト時もundefしたいが...
719
+ eigen = class << obj; self; end
720
+ eigen.send :undef_method, *mids
721
+ end
722
+
723
+ synchronize do
724
+ instance_variable_set "@#{klass}", obj
725
+ @condvar.broadcast
726
+ end
727
+ end
728
+ end
729
+
730
+ #######
731
+
732
+ def ensures_handshake
733
+ raise ArgumentError, "no block given" unless block_given?
734
+ raise "eventmachine's reactor needed" unless EM.reactor_running?
735
+ case @state when :established, :unsupported
736
+ EM.next_tick do yield end
737
+ else
738
+ # 2段階のEM.deferを行っている。まず初段で@channelの初期化をキックして、channel -> connectionと依存関係をたぐってコネクションを確立
739
+ # する。channelを成立させるところまでの待ち合わせが第一段。次に、生成した@channelを用いてpublisher confirmationのハンドシェイクを
740
+ # キックして、これが確立するのを待つのが第二段。
741
+ logger :info, "waiting for MQ to be set up (now %s)...", @state
742
+
743
+ d4 = lambda do |a|
744
+ yield
745
+ end
746
+ d3 = lambda do
747
+ synchronize do
748
+ unless ensures_handshake_internal
749
+ setups_handshake unless @setting_up[:handshake]
750
+ # @condvar.wait @mutex until ensures_handshake_internal
751
+ @mutex.sleep 0.1 until ensures_handshake_internal
752
+ end
753
+ end
754
+ end
755
+ d2 = lambda do |a|
756
+ EM.defer d3, d4
757
+ end
758
+ d1 = lambda do
759
+ synchronize do
760
+ ensures :channel
761
+ @condvar.wait @mutex until ivar? :channel
762
+ end
763
+ end
764
+ d0 = lambda do
765
+ EM.defer d1, d2
766
+ end
767
+ d0.call
768
+ end
769
+ end
770
+
771
+ def ensures_handshake_internal
772
+ case @state when :established, :unsupported
773
+ true
774
+ else
775
+ false
776
+ end
777
+ end
778
+
779
+ def setups_handshake
780
+ @setting_up[:handshake] = true
781
+ # possible values of @state:
782
+ #
783
+ # :uninitialized --- not connected yet
784
+ # :disconnected --- connection lost and still not recovered
785
+ # :connected --- AMQP session established (no handshake yet)
786
+ # :handshaking --- handshake in progress, not established yet
787
+ # :established --- proper handshake was made
788
+ # :unsupported --- peer rejected handshake, but the connection itself is OK.
789
+ cap = @connection.server_capabilities
790
+ if cap and cap["publisher_confirms"] then
791
+ @state = :handshaking
792
+ @channel.confirm_select do
793
+ # this is in next EM loop...
794
+ synchronize do
795
+ reinvoke_retry_timers unless @retrying_events.empty?
796
+ @channel.on_ack do |ack|
797
+ # this is in another EM loop...
798
+ consume_basic_ack ack
799
+ end
800
+ @channel.on_nack do |ack|
801
+ # this is in yet another EM loop...
802
+ consume_basic_nack ack
803
+ end
804
+ @tag = 0
805
+ @state = :established
806
+ @setting_up.delete :handshake
807
+ @condvar.broadcast
808
+ end
809
+ end
810
+ else
811
+ logger :warn, <<-end
812
+
813
+ The message queue broker you are connecting lacks Publisher [BEWARE!]
814
+ Confirmation capability, so we cannot make sure your events are in [BEWARE!]
815
+ fact reaching to one of the Tengine cores. We strongly recommend [BEWARE!]
816
+ you to use a relatively recent version of RabbitMQ. [BEWARE!]
817
+
818
+ end
819
+ @state = :unsupported
820
+ @setting_up.delete :handshake
821
+ @condvar.broadcast
822
+ end
823
+ end
824
+
825
+ def consume_basic_ack ack
826
+ synchronize do
827
+ f = @publishing_events.empty?
828
+ n = ack.delivery_tag
829
+ ok = []
830
+ ng = []
831
+ if ack.multiple
832
+ ok, @publishing_events = @publishing_events.partition {|i| i.tag <= n }
833
+ else
834
+ ng, @publishing_events = @publishing_events.partition {|i| i.tag < n }
835
+ if @publishing_events.empty?
836
+ # the packet in quesion is lost?
837
+ elsif ev = @publishing_events.shift
838
+ ok = [ev]
839
+ end
840
+ end
841
+ f ||= !ng.empty?
842
+ ng.each_next_tick do |e|
843
+ synchronize do
844
+ # NGなので再送
845
+ e.retry += 1
846
+ rehash_them_all
847
+ @firing_queue.push e
848
+ end
849
+ end
850
+ ok.each_next_tick do |e|
851
+ # OK, ブロックを評価
852
+ e.block.call if e.block
853
+ end
854
+ unless ok.empty?
855
+ rehash_them_all
856
+ ok.each do |e|
857
+ # ただしく停止させるために上のnext_tickではなくここで
858
+ @retrying_events.delete e
859
+ @pending_events.delete e
860
+ end
861
+ @condvar.broadcast
862
+ f = ok.inject f do |r, e| r | e.opts[:keep_connection] end
863
+ end
864
+ # 帰ってきたackが最後のackで、もう待ちがなくて、かつackに対応するイベントがすべてkeep_connection: falseで送信されていた場合、もう
865
+ # このリアクターは止めていいい。ngが空でなければ@pending_eventsには何か入っている。
866
+ stop if f == false and @pending_events.empty?
867
+ end
868
+ end
869
+
870
+ def consume_basic_nack nack
871
+ # nackされたら(再送するから)止まっちゃだめ。なので逆にフローはシンプル。
872
+ synchronize do
873
+ n = nack.delivery_tag
874
+ ng = []
875
+ if ack.multiple
876
+ ng, @publishing_events = @publishing_events.partition {|i| i.tag <= n }
877
+ else
878
+ ng, @publishing_events = @publishing_events.partition {|i| i.tag < n }
879
+ if @publishing_events.empty?
880
+ # the packet in quesion is lost?
881
+ elsif ev = @publishing_events.shift
882
+ ng = [ev]
883
+ end
884
+ end
885
+ ng.each_next_tick do |e|
886
+ synchronize do
887
+ e.retry += 1
888
+ rehash_them_all
889
+ @firing_queue.push e
890
+ end
891
+ end
892
+ end
893
+ end
894
+
895
+ #######
896
+
897
+ def revoke_retry_timers
898
+ synchronize do
899
+ if @state != :disconnected
900
+ @state = :disconnected
901
+ @retrying_events.each_value do |(idx, *)|
902
+ EM.stop_timer idx if idx
903
+ end
904
+ # all unacknowledged events are hereby considered LOST
905
+ t0 = Time.at 0
906
+ @publishing_events.each do |e|
907
+ @retrying_events[e] = [nil, t0]
908
+ end
909
+ end
910
+ end
911
+ end
912
+
913
+ def reinvoke_retry_timers
914
+ synchronize do
915
+ @retrying_events.each_pair.to_a.each_next_tick do |i, (j, k)|
916
+ u = (k + (i.opts[:retry_interval] || 0)) - Time.now
917
+ if u < 0
918
+ # retry interval passed, just send it again
919
+ @firing_queue.push i
920
+ else
921
+ # need to re-add timer (no repeat)
922
+ EM.add_timer u do @firing_queue.push i end
923
+ end
924
+ end
925
+ @retrying_events.clear
926
+ trigger_firing_thread
927
+ end
928
+ end
929
+
930
+ def install_default_hooks
931
+ add_hook :everything do |klass, mid, *argv|
932
+ logger :debug, "AMQP event callback: %s.%s", klass, mid#, argv
933
+ end
934
+
935
+ add_hook :'connection.before_recovery' do |conn|
936
+ EM.cancel_timer @reconnection_timer if ivar? :reconnection_timer
937
+ end
938
+
939
+ add_hook :'connection.on_tcp_connection_loss' do |conn|
940
+ # Wow! AMQP::Session#reconnect is brain-damaged that it cannot be cancelled!
941
+ # conn.reconnect false, auto_reconnect_delay.to_i if auto_reconnect_delay and not @terminating
942
+
943
+ ard = @config[:connection][:auto_reconnect_delay]
944
+ host = @config[:connection][:host]
945
+ port = @config[:connection][:port]
946
+ if ard and not @terminating and conn.closed?
947
+ conn.instance_eval { @reconnecting = true; reset }
948
+ @reconnection_timer = EM.add_timer ard do
949
+ EM.reconnect host, port, conn
950
+ @reconnection_timer = nil
951
+ end
952
+ end
953
+ end
954
+
955
+ add_hook :'connection.on_tcp_connection_failure' do |setting|
956
+ @mutex.synchronize do
957
+ case @state when :uninitialized then
958
+ # 最初の接続に失敗した場合。https://www.pivotaltracker.com/story/show/18317933
959
+ raise "It seems the MQ broker is missing (misconfiguration?)"
960
+ else
961
+ logger :error, "It seems the MQ broker is missing."
962
+ end
963
+ end
964
+ end
965
+
966
+ add_hook :'channel.on_connection_interruption' do |ch|
967
+ revoke_retry_timers
968
+ end
969
+
970
+ add_hook :'channel.after_recovery' do |ch|
971
+ # AMAZING that an AMQP::Channel instance deletes a once-registered callbacks!
972
+ # see: amq/client/async/channel.rb, search for "def reset_state!"
973
+ hooks_channel.inject(Hash.new) {|r, x|
974
+ r.update x.intern => callback_entity(:channel, x.intern)
975
+ }.each_pair {|k, v|
976
+ ch.send(k, &v)
977
+ }
978
+
979
+ ch.prefetch @config[:channel][:prefetch] do
980
+ @setting_up.delete :handshake # re-initialize
981
+ reinvoke_retry_timers
982
+ @condvar.broadcast
983
+ end
984
+ end
985
+
986
+ add_hook :'connection.after_recovery' do |conn|
987
+ synchronize do
988
+ @state = :connected
989
+ end
990
+ end
991
+ end
992
+
993
+ #######
994
+
995
+ def stop_firing_queue
996
+ # beautiful...
997
+ @firing_queue.instance_eval do
998
+ @items.clear
999
+ @popq.clear
1000
+ end
1001
+ end
1002
+
1003
+ def gencb
1004
+ @gencb ||= lambda do |ev|
1005
+ case @state when :unsupported, :established
1006
+ fire_internal ev
1007
+ @firing_queue.pop(&gencb)
1008
+ else
1009
+ # disconnectedとか。
1010
+ # 無視?
1011
+ @firing_queue.push ev
1012
+ end
1013
+ end
1014
+ end
1015
+
1016
+ def trigger_firing_thread
1017
+ # inside mutex
1018
+ # event already pushed
1019
+ ensures_handshake do
1020
+ ensures :exchange do
1021
+ synchronize do
1022
+ @firing_queue.pop(&gencb)
1023
+ end
1024
+ end
1025
+ end
1026
+ end
1027
+
1028
+ def fire_internal ev
1029
+ publish ev
1030
+ rescue Exception => ex
1031
+ # exchange.publish はたとえば RuntimeError を raise したりするようだ
1032
+ publish_failed ev, ex
1033
+ else
1034
+ published ev
1035
+ end
1036
+
1037
+ def publish ev
1038
+ @exchange.publish ev.event.to_json, @config[:exchange][:publish]
1039
+ end
1040
+
1041
+ def publish_failed ev, ex
1042
+ if resendable_p ev
1043
+ idx = EM.add_timer ev.opts[:retry_interval] do
1044
+ synchronize do
1045
+ ev.retry += 1
1046
+ @firing_queue.push ev
1047
+ end
1048
+ end
1049
+ @retrying_events[ev] = [idx, Time.now]
1050
+ else
1051
+ # inside mutex lock
1052
+ rehash_them_all
1053
+ @retrying_events.delete ev
1054
+ @pending_events.delete ev
1055
+ @publishing_events.reject! {|i| i == ev }
1056
+ @condvar.broadcast
1057
+ logger :fatal, "SEND FAILED: EVENT LOST %p", ev.event
1058
+ stop unless ev.opts[:keep_connection] # 送信失敗かつコネクション維持しないということはここで停止すべき
1059
+ end
1060
+ end
1061
+
1062
+ def resendable_p ev
1063
+ return false if @terminating and not @connection
1064
+ return false if @terminating and not @connection.connected?
1065
+ return ev.retry < ev.opts[:retry_count]
1066
+ end
1067
+
1068
+ def published ev
1069
+ case @state
1070
+ when :unsupported then
1071
+ # ackなし、next_tickをもって送信終了と見なす
1072
+ EM.next_tick do
1073
+ synchronize do
1074
+ rehash_them_all
1075
+ @retrying_events.delete ev
1076
+ @pending_events.delete ev
1077
+ @publishing_events.reject! {|i| i == ev }
1078
+ @condvar.broadcast
1079
+ ev.block.call if ev.block
1080
+ stop unless ev.opts[:keep_connection]
1081
+ end
1082
+ end
1083
+ when :established then
1084
+ # ackあり、ackを待つ
1085
+ @tag += 1
1086
+ ev.tag = @tag
1087
+ rehash_them_all
1088
+ @publishing_events.push ev
1089
+ end
1090
+ end
1091
+ end
1092
+
1093
+ #
1094
+ # Local Variables:
1095
+ # mode: ruby
1096
+ # coding: utf-8-unix
1097
+ # indent-tabs-mode: nil
1098
+ # tab-width: 4
1099
+ # ruby-indent-level: 2
1100
+ # fill-column: 135
1101
+ # default-justification: full
1102
+ # End: