who_can 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ *.log
6
+ .idea
7
+ rabbitmq
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --color
2
+ --require ./spec/support/logging_progress_bar_formatter.rb
3
+ --format Motionbox::LoggingProgressBarFormatter
4
+
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm 1.9.2-p290@whocan --create
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+ source "http://localhost:50000"
3
+
4
+ # Specify your gem's dependencies in whocan.gemspec
5
+ gemspec
6
+
7
+
8
+ # vim:ft=ruby
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,64 @@
1
+ require 'rubygems'
2
+ require 'amqp'
3
+ require 'state_machine'
4
+ require 'uuidtools'
5
+ require 'thread'
6
+ require 'monitor'
7
+ require 'logger'
8
+ require 'set'
9
+ require 'deferred'
10
+
11
+ %w[ core_ext/array/extract_options
12
+ core_ext/module/aliasing
13
+ core_ext/hash/reverse_merge
14
+ core_ext/hash/indifferent_access ].each do |p|
15
+
16
+ require "active_support/#{p}"
17
+ end
18
+
19
+ module WhoCan
20
+ DEFAULT_PING_EXCHANGE = 'who_can.default.fanout'
21
+ DEFAULT_CONNECT_URL = 'amqp://127.0.0.1'
22
+
23
+ def self.connect_url
24
+ @config ||= DEFAULT_CONNECT_URL
25
+ end
26
+
27
+ def self.connect_url=(config)
28
+ @config = config
29
+ end
30
+
31
+ def self.new(*a, &b)
32
+ WhoCan::Base.new(*a, &b)
33
+ end
34
+
35
+ def self.logger
36
+ @logger ||= Logger.new('/dev/null').tap { |l| l.level = Logger::FATAL }
37
+ end
38
+
39
+ def self.logger=(log)
40
+ @logger = log
41
+ end
42
+
43
+ class WhoCanError < StandardError; end
44
+ class DelayMustBeSetError < WhoCanError
45
+ def initialize
46
+ super("delay must be set for on_ping Response object")
47
+ end
48
+ end
49
+
50
+ class ChannelPoolError < WhoCanError; end
51
+ class PoolIsNotOpenException < ChannelPoolError; end
52
+ class TimeoutError < WhoCanError; end
53
+ end
54
+
55
+
56
+ require 'who_can/logging'
57
+ require 'who_can/base'
58
+ require 'who_can/pinger'
59
+ require 'who_can/responder'
60
+ require 'who_can/heartbeater'
61
+ require 'who_can/connection_wrapper'
62
+ require 'who_can/connection_manager'
63
+
64
+
@@ -0,0 +1,72 @@
1
+ module WhoCan
2
+ class Base
3
+ include Deferred::Accessors
4
+ include Logging
5
+
6
+ deferred_event :disconnection
7
+ attr_accessor :connection_opts
8
+
9
+ def initialize(connection_opts=nil)
10
+ @connection_opts = connection_opts
11
+ @connection = nil
12
+ if @connection_opts.is_a?(AMQP::Session)
13
+ @connection = @connection_opts
14
+ end
15
+ # logger.debug { "connection_opts: #{@connection_opts.inspect}" }
16
+ @responders = {}
17
+
18
+ end
19
+
20
+ def connection(&blk)
21
+ @connection ||= AMQP.connect(connection_opts || WhoCan.connect_url)
22
+ on_open(&blk) if blk
23
+ @connection
24
+ end
25
+ alias :connect! :connection
26
+
27
+ def on_open(&block)
28
+ connection.on_open(&block)
29
+ end
30
+
31
+ def start_monitoring!(opts = {})
32
+ @ekg = Heartbeater::EKG.new(connection)
33
+ @ekg.on_heartbeat_failure do
34
+ on_disconnection.succeed
35
+ close!
36
+ end
37
+ @ekg.start!
38
+ end
39
+
40
+ def new_pinger
41
+ WhoCan::Pinger.new(connection)
42
+ end
43
+
44
+ def new_responder(exchange_name)
45
+ WhoCan::Responder.new(connection, exchange_name)
46
+ end
47
+
48
+ def close!(&block)
49
+ unless @connection
50
+ block.call
51
+ return
52
+ end
53
+ cnx, @connection = @connection, nil
54
+ @channel = nil
55
+ on_disconnection(&block)
56
+ cnx.on_disconnection do
57
+ on_disconnection.succeed
58
+ end
59
+ cnx.close
60
+ on_disconnection
61
+ end
62
+
63
+ def clear_responder(ping_exchange_name)
64
+ if resp = @responders.delete(ping_exchange_name)
65
+ resp.close!
66
+ true
67
+ end
68
+ false
69
+ end
70
+ end
71
+ end
72
+
@@ -0,0 +1,268 @@
1
+ module WhoCan
2
+ # This class handles setting up the AMQP connection. You must start the
3
+ # EventMachine reactor before running anything in this class
4
+ #
5
+ # Keeps two connections open, a primary and secondary, and reacts to changes
6
+ # in their state. In the non-failure case, when a client calls #connection on
7
+ # us, they will receive the primary connection (once it's been established).
8
+ # If the primary is down, or the connection can't be established for some
9
+ # reason, then the secondary is returned.
10
+ #
11
+ # When a failover situation happens (i.e. the primary goes down) the
12
+ # on_failure callback will be fired. When that callback is fired, the
13
+ # users of this class are expected to clean up and wait until the
14
+ # on_recovery deferred is fired. The on_recovery deferred is fired when there
15
+ # is at least one open connection to a broker available.
16
+ #
17
+ # @note this class *will* automatically fail-back when the primary is
18
+ # re-established.
19
+ #
20
+ # TODO: determine how long to wait for connection to primary before failing
21
+ # over to secondary.
22
+ #
23
+ class ConnectionManager
24
+ include Deferred::Accessors
25
+ include Logging
26
+
27
+ DEFAULT_OPTIONS = {
28
+ :primary_grace_time => 5.0
29
+ }
30
+
31
+ deferred_event :open, :failure, :close
32
+
33
+ attr_reader :primary_opts, :secondary_opts, :primary, :secondary
34
+
35
+ attr_accessor :primary_grace_time
36
+
37
+ state_machine :state, :initial => :new do
38
+
39
+ event :start do
40
+ transition :new => :start_up
41
+ end
42
+
43
+ # when the secondary connects, it only matters to us if the primary
44
+ # hasn't been established yet or if it's in some transitional state
45
+ #
46
+ event :secondary_connected do
47
+ transition :start_up => :awaiting_primary, :unless => :primary_connected?
48
+ transition :degraded => :connected
49
+ transition :down => :failed_over
50
+ end
51
+
52
+ # fired when we're awaiting the primary to connect and it took too long
53
+ event :primary_wait_timeout do
54
+ transition :awaiting_primary => :failed_over
55
+ end
56
+
57
+ # when the primary connects we move into "connected" state and the
58
+ # clients can now start using the connection
59
+ #
60
+ event :primary_connected do
61
+ transition [:start_up, :down] => :degraded, :unless => :secondary_connected?
62
+ transition [:awaiting_primary, :failed_over] => :connected
63
+ end
64
+
65
+ event :secondary_failed do
66
+ # if we're in failed_over mode, transition to down
67
+ # if we're in connected, just ignore event
68
+ transition :connected => :degraded
69
+ transition :failed_over => :down
70
+ end
71
+
72
+ event :primary_failed do
73
+ transition :connected => :failed_over
74
+ transition :degraded => :down
75
+ end
76
+
77
+ event :close do
78
+ transition (all - [:new, :shutting_down, :closed]) => :shutting_down
79
+ transition :new => :closed
80
+ end
81
+
82
+ event :closed do
83
+ transition :shutting_down => :closed
84
+ end
85
+
86
+ # start trying to connect to the cluster
87
+ after_transition :new => :start_up, :do => [:do_connect_primary, :do_connect_secondary]
88
+
89
+ # the secondary has connected, start the timer to wait for the primary to connect
90
+ after_transition :start_up => :awaiting_primary, :do => :start_primary_timer
91
+
92
+ # the primary has not connected within the grace period, allow clients to connect
93
+ after_transition :awaiting_primary => :failed_over, :do => :enter_failed_over_state
94
+
95
+ # the primary has connected within the grace period, enter the connected state
96
+ after_transition :awaiting_primary => :connected, :do => :become_usable!
97
+
98
+ #make sure we are up as soon as possible - don't wait for the secondary
99
+ after_transition :start_up => :degraded, :do => :become_usable!
100
+
101
+ #primary has returned
102
+ after_transition :failed_over => :connected, :do => [:fire_failure_deferred, :become_usable!]
103
+
104
+ # the primary fails, secondary is available
105
+ after_transition :connected => :failed_over, :do => [:fire_failure_deferred, :enter_failed_over_state, :do_connect_primary]
106
+
107
+ #the two states we can be in when the secondary is going down
108
+ after_transition :failed_over => :down, :do => [:fire_failure_deferred, :do_connect_secondary]
109
+ after_transition :connected => :degraded, :do => :do_connect_secondary
110
+
111
+ #primary went down, and secondary was already down
112
+ after_transition :degraded => :down, :do => [:fire_failure_deferred, :do_connect_primary, :do_connect_secondary]
113
+
114
+ #we've been down, now we're coming back up
115
+ after_transition :down => :failed_over, :do => :enter_failed_over_state
116
+ after_transition :down => :degraded, :do => :become_usable!
117
+
118
+ # shutdown states
119
+ after_transition [:awaiting_primary, :degraded, :failed_over, :down, :connected] => :shutting_down, :do => :do_close
120
+
121
+ # we are really closed
122
+ after_transition :shutting_down => :closed, :do => :fire_closed_callbacks
123
+
124
+ # logging of state transitions
125
+ around_transition do |cw,block|
126
+ orig_state = cw.state
127
+ block.call
128
+ logger.debug { "CONNECTION MANAGER: moved from #{orig_state} to #{cw.state}" }
129
+ end
130
+
131
+ end
132
+
133
+
134
+ # @param [AMQP-URL,Hash] primary_cnx valid argument to AMQP.connect to be
135
+ # used as the primary connection.
136
+ #
137
+ # @param [AMQP-URL,Hash] secondary_cnx valid argument to AMQP.connect to be
138
+ # used as the secondary connection.
139
+ #
140
+ # @option opts [Float] :primary_grace_time (5.0) how long we should wait
141
+ # during startup for the primary to connect before giving up and just
142
+ # using the secondary connection
143
+ #
144
+ def initialize(primary_opts, secondary_opts, opts={})
145
+ opts = opts.with_indifferent_access.reverse_merge(DEFAULT_OPTIONS)
146
+
147
+ @primary_opts = primary_opts
148
+ @secondary_opts = secondary_opts
149
+
150
+ @primary_grace_time = opts[:primary_grace_time]
151
+
152
+ if @primary_opts.kind_of?(ConnectionWrapper)
153
+ @primary = @primary_opts
154
+ @primary.name ||= 'primary'
155
+ else
156
+ @primary = ConnectionWrapper.new(primary_opts, :name => 'primary')
157
+ end
158
+
159
+ if @secondary_opts.kind_of?(ConnectionWrapper)
160
+ @secondary = @secondary_opts
161
+ @secondary.name ||= 'secondary'
162
+ else
163
+ @secondary = ConnectionWrapper.new(secondary_opts, :name => 'secondary')
164
+ end
165
+
166
+ @active_connection = nil
167
+
168
+ super()
169
+ end
170
+
171
+ def start(*a, &b)
172
+ on_open(&b)
173
+ super(*a)
174
+ end
175
+
176
+ # connect to the broker, handle reconnections. barf if transition to
177
+ # :start_up state is not possible
178
+ def start!(&blk)
179
+ on_open(&b)
180
+ super(*a)
181
+ end
182
+
183
+ def close(*a, &b)
184
+ on_close(&b)
185
+ super(*a)
186
+ end
187
+
188
+ def close!(*a, &b)
189
+ on_close(&b)
190
+ super(*a)
191
+ end
192
+
193
+ def connection
194
+ @active_connection
195
+ end
196
+
197
+ def primary_connected?
198
+ primary and primary.connected?
199
+ end
200
+
201
+ def secondary_connected?
202
+ secondary and secondary.connected?
203
+ end
204
+
205
+ def usable?
206
+ failed_over? or degraded? or connected?
207
+ end
208
+
209
+ alias :useable? :usable?
210
+
211
+ protected
212
+ def do_connect_primary
213
+ @primary.on_failure { primary_failed }
214
+ @primary.on_open { primary_connected }
215
+ @primary.connect
216
+ end
217
+
218
+ def do_connect_secondary
219
+ @secondary.on_failure { secondary_failed }
220
+ @secondary.on_open { secondary_connected }
221
+ @secondary.connect
222
+ end
223
+
224
+ def do_close
225
+ logger.debug {"shutting down primary"}
226
+ @primary.close do
227
+ logger.debug {"shutting down secondary"}
228
+ @secondary.close do
229
+ logger.debug {"secondary shutdown, we're closed'"}
230
+ closed!
231
+ end
232
+ end
233
+ end
234
+
235
+ def start_primary_timer
236
+ @primary_timer ||= EM::Timer.new(@primary_grace_time) { primary_wait_timeout }
237
+ end
238
+
239
+ def clear_primary_timer!
240
+ if @primary_timer
241
+ @primary_timer.cancel
242
+ @primary_timer = nil
243
+ end
244
+ end
245
+
246
+ def fire_failure_deferred
247
+ reset_open_event
248
+ reset_failure_event.succeed
249
+ end
250
+
251
+ def fire_closed_callbacks
252
+ on_close.succeed
253
+ end
254
+
255
+ def enter_failed_over_state
256
+ clear_primary_timer!
257
+ @active_connection = @secondary
258
+ on_open.succeed
259
+ end
260
+
261
+ def become_usable!
262
+ clear_primary_timer!
263
+ @active_connection = @primary
264
+ on_open.succeed
265
+ end
266
+ end
267
+ end
268
+
@@ -0,0 +1,210 @@
1
+ module StateMachine
2
+ class StateMachine::Machine
3
+ include ::WhoCan::Logging
4
+ end
5
+ end
6
+
7
+ module WhoCan
8
+ # encapsulates the retry and failure detection logic. Used by ConnectionManager
9
+ class ConnectionWrapper
10
+ include Logging
11
+ include Deferred::Accessors
12
+
13
+ attr_reader :config, :amqp_session, :ekg
14
+
15
+ attr_accessor :name
16
+
17
+ # how long should we wait between reconnection attempts
18
+ attr_accessor :reconnect_delay, :shutdown_timeout
19
+
20
+ deferred_event :open, :failure, :close, :initial_connection_failure
21
+
22
+ state_machine :state, :initial => :new do
23
+ # moving from no-live-connection to attempting connection
24
+ after_transition [:new, :retrying] => :connecting, :do => :do_connection
25
+
26
+ # callback for handling "failure to connect to the broker"
27
+ after_transition :connecting => :retrying,
28
+ :do => :after_initial_connection_failure
29
+
30
+ # connection has been established
31
+ after_transition :connecting => :connected, :do => :after_connected
32
+
33
+ # if our connection failed in some way, be sure we shut down ekg
34
+ before_transition :connected => :retrying, :do => :shutdown_ekg
35
+
36
+ # connection fails in some way, hook up retry logic
37
+ after_transition [:connecting, :connected] => :retrying, :do => [:fire_failure_callbacks, :do_retry]
38
+
39
+ # close has been requested
40
+ after_transition any => :closing, :do => :do_close
41
+
42
+ # close has succeeded
43
+ after_transition [:closing, :new] => :closed, :do => :after_closed
44
+
45
+ # logging of state transitions
46
+ around_transition do |cw,block|
47
+ orig_state = cw.state
48
+ block.call
49
+ # logger.debug { "CONNECTION WRAPPER(#{cw.name}): moved from #{orig_state} to #{cw.state}" }
50
+ end
51
+
52
+ event :connect do
53
+ transition [:new, :retrying] => :connecting
54
+ end
55
+
56
+ event :connected do
57
+ transition :connecting => :connected
58
+ end
59
+
60
+ event :initial_connection_failure do
61
+ transition :connecting => :retrying
62
+ end
63
+
64
+ event :tcp_connection_failed do
65
+ transition :connected => :retrying
66
+ end
67
+
68
+ event :close do
69
+ transition [:connecting, :retrying, :connected] => :closing
70
+ transition :new => :closed
71
+ end
72
+
73
+ event :closed do
74
+ transition :closing => :closed
75
+ end
76
+ end
77
+
78
+
79
+ def initialize(config, opts={})
80
+ @config = config
81
+ @name = opts[:name]
82
+ @reconnect_delay = opts[:reconnect_delay] || 5
83
+ @shutdown_timeout = opts[:shutdown_timeout] || 2.0
84
+
85
+ @ekg = nil
86
+ @retry_count = 0
87
+ @amqp_session = nil
88
+ @on_initial_connection_failure_cbs = []
89
+ super()
90
+ end
91
+
92
+ # XXX: figure out how to do this in a cleaner fashion
93
+ def connect(*a, &b)
94
+ on_open(&b)
95
+ super(*a)
96
+ end
97
+
98
+ def connect!(*a, &b)
99
+ on_open(&b)
100
+ super(*a)
101
+ end
102
+
103
+ def close(*a, &b)
104
+ on_close(&b) # hook deferred into this event
105
+ super(*a)
106
+ end
107
+
108
+ def close!(*a, &b)
109
+ on_close(&b) # hook deferred into this event
110
+ super(*a)
111
+ end
112
+
113
+ protected
114
+ def do_connection
115
+ logger.info { "#{name} attempting connection to #{@config.inspect}" }
116
+
117
+ AMQP.connect(@config, :on_tcp_connection_failure => method(:initial_connection_failure!).to_proc) do |sess|
118
+ @ekg = Heartbeater::EKG.new(sess)
119
+
120
+ @ekg.on_heartbeat_failure { tcp_connection_failed }
121
+
122
+ @ekg.start!
123
+ @amqp_session = sess
124
+
125
+ connected
126
+ end
127
+ end
128
+
129
+ def do_close
130
+ @retry_timer.cancel if @retry_timer
131
+
132
+ logger.debug { "wrapper(#{name}): do_close called" }
133
+
134
+ if @amqp_session || @ekg
135
+ logger.debug { "wrapper(#{name}): closing ekg!" }
136
+ shutdown_ekg { closed }
137
+ else
138
+ closed
139
+ end
140
+ end
141
+
142
+ def fire_failure_callbacks
143
+ return if closing? or closed?
144
+ logger.debug { "wrapper(#{name}): fire_failure_callbacks" }
145
+ reset_open_event
146
+ reset_failure_event.succeed
147
+ end
148
+
149
+ def shutdown_ekg(&blk)
150
+ Deferred::Default.new.tap do |my_dfr|
151
+ if @ekg
152
+ amqp_session, @amqp_session = @amqp_session, nil
153
+
154
+ my_dfr.timeout(@shutdown_timeout) # we need to ensure that we will time out
155
+ my_dfr.ensure_that(&blk) if blk
156
+
157
+ if amqp_session
158
+ @ekg.on_shutdown.ensure_that do
159
+ logger.debug { "wrapper(#{name}): ekg shutdown completed" }
160
+
161
+ amqp_session.on_closed { my_dfr.succeed }
162
+
163
+ amqp_session.close
164
+ end
165
+ end
166
+
167
+ logger.debug { "wrapper(#{name}): shutting down ekg" }
168
+
169
+ @ekg.on_shutdown.errback do |e|
170
+ logger.debug { "wrapper(#{name}): ekg shutdown timed out!" }
171
+ end
172
+
173
+ @ekg.on_shutdown.timeout(@shutdown_timeout, TimeoutError) # ensure that ekg shutdown won't hang
174
+ @ekg.shutdown!
175
+ @ekg = nil
176
+ else
177
+ my_dfr.succeed
178
+ end
179
+ end
180
+ end
181
+
182
+ def after_connected
183
+ on_open.succeed
184
+ end
185
+
186
+ def after_closed
187
+ on_close.succeed
188
+ end
189
+
190
+ def after_initial_connection_failure
191
+ logger.warn { "wrapper(#{name}): connection to #{@config.inspect} failed, this is either a misconfiguration or the server is down" }
192
+ logger.warn { "wrapper(#{name}): retrying after a delay of #{reconnect_delay}s" }
193
+
194
+ reset_initial_connection_failure_event.succeed
195
+ end
196
+
197
+ def do_retry
198
+ return if closing? or closed?
199
+
200
+ logger.debug { "wrapper(#{name}): do_retry setting up reconnection timer" }
201
+
202
+ @retry_timer = EM::Timer.new(reconnect_delay) do
203
+ @retry_count += 1
204
+ logger.debug { "wrapper(#{name}): retry attempt: #{@retry_count}" }
205
+ connect!
206
+ end
207
+ end
208
+ end
209
+ end
210
+