rack-nackmode 0.1.0 → 0.1.1
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.
- 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
|