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.
Files changed (3) hide show
  1. data/README.markdown +23 -0
  2. data/lib/rack/nack_mode.rb +72 -4
  3. metadata +3 -3
@@ -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.
@@ -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
- info 'Shutting down'
91
- @shutdown_callback.call
115
+ do_shutdown
92
116
  end
93
117
  else
94
- info 'Shutting down'
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.0
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-05-12 00:00:00.000000000 Z
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.19
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