rack-nackmode 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +23 -0
- data/lib/rack/nack_mode.rb +72 -4
- metadata +3 -3
data/README.markdown
CHANGED
@@ -60,4 +60,27 @@ The `use` statement to initialise the middleware takes the following options:
|
|
60
60
|
balancer it's going down before it can safely do so. Defaults to 3, which
|
61
61
|
matches e.g. haproxy's default for how many failed checks it needs before
|
62
62
|
marking a backend as down.
|
63
|
+
* `:healthcheck_timeout` – how long (in seconds) the app should wait for
|
64
|
+
the first health check request. This is to avoid the app refusing to shut
|
65
|
+
down if the load balancer is misconfigured (or absent); if it waits this
|
66
|
+
long without seeing a single health check, it will simply shut down. Should
|
67
|
+
be significantly longer than your load balancer's health check interval.
|
68
|
+
Defaults to 15 seconds, which is conservatively longer than
|
69
|
+
haproxy's default interval.
|
63
70
|
* `:logger` – middleware will log progress to this object if supplied.
|
71
|
+
|
72
|
+
## Testing
|
73
|
+
|
74
|
+
The RSpec specs cover most of the functionality:
|
75
|
+
|
76
|
+
$ bundle exec rspec
|
77
|
+
|
78
|
+
### Integration testing
|
79
|
+
|
80
|
+
To really verify this works, we need to set up two instances of an app using
|
81
|
+
this middleware behind a load balancer, and fire requests at the load balancer
|
82
|
+
while taking down one of the instances.
|
83
|
+
|
84
|
+
$ bundle exec kitchen test
|
85
|
+
|
86
|
+
You'll need [Vagrant](http://www.vagrantup.com/) installed.
|
data/lib/rack/nack_mode.rb
CHANGED
@@ -38,6 +38,11 @@ module Rack
|
|
38
38
|
# marking a backend as down.
|
39
39
|
DEFAULT_NACKS_BEFORE_SHUTDOWN = 3
|
40
40
|
|
41
|
+
# Default time (in seconds) during shutdown to wait for the first
|
42
|
+
# healthcheck request before concluding that the healthcheck is missing or
|
43
|
+
# misconfigured, and shutting down anyway.
|
44
|
+
DEFAULT_HEALTHCHECK_TIMEOUT = 15
|
45
|
+
|
41
46
|
def initialize(app, options = {})
|
42
47
|
@app = app
|
43
48
|
|
@@ -45,6 +50,7 @@ module Rack
|
|
45
50
|
:healthy_if,
|
46
51
|
:sick_if,
|
47
52
|
:nacks_before_shutdown,
|
53
|
+
:healthcheck_timeout,
|
48
54
|
:logger
|
49
55
|
@path = options[:path] || '/admin'
|
50
56
|
@health_callback = if options[:healthy_if] && options[:sick_if]
|
@@ -58,6 +64,7 @@ module Rack
|
|
58
64
|
end
|
59
65
|
@nacks_before_shutdown = options[:nacks_before_shutdown] || DEFAULT_NACKS_BEFORE_SHUTDOWN
|
60
66
|
raise ArgumentError, ":nacks_before_shutdown must be at least 1" unless @nacks_before_shutdown >= 1
|
67
|
+
@healthcheck_timeout = options[:healthcheck_timeout] || DEFAULT_HEALTHCHECK_TIMEOUT
|
61
68
|
@logger = options[:logger]
|
62
69
|
|
63
70
|
yield self if block_given?
|
@@ -65,6 +72,7 @@ module Rack
|
|
65
72
|
|
66
73
|
def call(env)
|
67
74
|
if health_check?(env)
|
75
|
+
clear_healthcheck_timeout
|
68
76
|
health_check_response(env)
|
69
77
|
else
|
70
78
|
@app.call(env)
|
@@ -74,9 +82,26 @@ module Rack
|
|
74
82
|
def shutdown(&block)
|
75
83
|
info "Shutting down after NACKing #@nacks_before_shutdown health checks"
|
76
84
|
@shutdown_callback = block
|
85
|
+
|
86
|
+
install_healthcheck_timeout { do_shutdown }
|
87
|
+
|
88
|
+
nil
|
77
89
|
end
|
78
90
|
|
79
91
|
private
|
92
|
+
def install_healthcheck_timeout
|
93
|
+
@healthcheck_timer = Timer.new(@healthcheck_timeout) do
|
94
|
+
warn "Gave up waiting for a health check after #{@healthcheck_timeout}s; bailing out."
|
95
|
+
yield
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def clear_healthcheck_timeout
|
100
|
+
return unless @healthcheck_timer
|
101
|
+
@healthcheck_timer.cancel
|
102
|
+
@healthcheck_timer = nil
|
103
|
+
end
|
104
|
+
|
80
105
|
def health_check?(env)
|
81
106
|
env['PATH_INFO'] == @path && env['REQUEST_METHOD'] == 'GET'
|
82
107
|
end
|
@@ -87,12 +112,10 @@ module Rack
|
|
87
112
|
if @nacks_before_shutdown <= 0
|
88
113
|
if defined?(EM)
|
89
114
|
EM.next_tick do
|
90
|
-
|
91
|
-
@shutdown_callback.call
|
115
|
+
do_shutdown
|
92
116
|
end
|
93
117
|
else
|
94
|
-
|
95
|
-
@shutdown_callback.call
|
118
|
+
do_shutdown
|
96
119
|
end
|
97
120
|
else
|
98
121
|
info "Waiting for #@nacks_before_shutdown more health checks"
|
@@ -109,6 +132,11 @@ module Rack
|
|
109
132
|
@shutdown_callback
|
110
133
|
end
|
111
134
|
|
135
|
+
def do_shutdown
|
136
|
+
info 'Shutting down'
|
137
|
+
@shutdown_callback.call
|
138
|
+
end
|
139
|
+
|
112
140
|
def healthy?
|
113
141
|
@health_callback.call
|
114
142
|
end
|
@@ -125,5 +153,45 @@ module Rack
|
|
125
153
|
def info(*args)
|
126
154
|
@logger.info(*args) if @logger
|
127
155
|
end
|
156
|
+
|
157
|
+
def warn(*args)
|
158
|
+
@logger.warn(*args) if @logger
|
159
|
+
end
|
160
|
+
|
161
|
+
module Timer
|
162
|
+
def self.new(timeout)
|
163
|
+
if defined?(EM)
|
164
|
+
EMTimer.new(timeout) { yield }
|
165
|
+
else
|
166
|
+
ThreadTimer.new(timeout) { yield }
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
class EMTimer
|
171
|
+
def initialize(timeout)
|
172
|
+
@timer = EM.add_timer(timeout) { yield }
|
173
|
+
end
|
174
|
+
|
175
|
+
def cancel
|
176
|
+
EM.cancel_timer(@timer)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
class ThreadTimer
|
181
|
+
def initialize(timeout)
|
182
|
+
@thread = Thread.new do
|
183
|
+
waited = sleep(timeout)
|
184
|
+
# if we woke up early, waited < timeout
|
185
|
+
if waited >= timeout
|
186
|
+
yield
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def cancel
|
192
|
+
@thread.run # will wake up early from sleep
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
128
196
|
end
|
129
197
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-nackmode
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-06-04 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|
@@ -135,7 +135,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
135
135
|
version: '0'
|
136
136
|
requirements: []
|
137
137
|
rubyforge_project:
|
138
|
-
rubygems_version: 1.8.
|
138
|
+
rubygems_version: 1.8.23
|
139
139
|
signing_key:
|
140
140
|
specification_version: 3
|
141
141
|
summary: Middleware for zero-downtime maintenance behind a load balancer
|