tengine_event 0.4.6
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +26 -0
- data/Gemfile.lock +64 -0
- data/README.rdoc +21 -0
- data/Rakefile +51 -0
- data/VERSION +1 -0
- data/bin/tengine_event_sucks +48 -0
- data/bin/tengine_fire +46 -0
- data/lib/tengine/event/model_notifiable.rb +49 -0
- data/lib/tengine/event/sender.rb +125 -0
- data/lib/tengine/event.rb +184 -0
- data/lib/tengine/mq/suite.rb +1102 -0
- data/lib/tengine/mq.rb +5 -0
- data/lib/tengine/null_logger.rb +10 -0
- data/lib/tengine_event.rb +13 -0
- data/spec/.gitignore +1 -0
- data/spec/mq_config.yml.example +13 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/tengine/event/model_notifiable_spec.rb +80 -0
- data/spec/tengine/event/sender_spec.rb +124 -0
- data/spec/tengine/event_spec.rb +397 -0
- data/spec/tengine/mq/connect_actually_spec.rb +38 -0
- data/spec/tengine/mq/suite_spec.rb +981 -0
- data/spec/tengine/null_logger_spec.rb +13 -0
- data/spec/tengine_spec.rb +17 -0
- data/tengine_event.gemspec +96 -0
- metadata +203 -0
@@ -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:
|