rack-nackmode 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +63 -0
- data/lib/rack-nackmode.rb +1 -0
- data/lib/rack/nack_mode.rb +129 -0
- metadata +143 -0
data/README.markdown
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# Rack::NackMode
|
2
|
+
|
3
|
+
Middleware enabling zero-downtime maintenance behind a load balancer.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
`Rack::NackMode` adds a health check endpoint to your app that enables it to
|
8
|
+
communicate to a load balancer its ability (or lack thereof) to serve requests.
|
9
|
+
It supports a "NACK Mode" protocol, so that when your app wants to shut down, it
|
10
|
+
makes sure the load balancer knows to stop sending it requests before doing so.
|
11
|
+
It does this by responding to the health check request with a <em>n</em>egative
|
12
|
+
<em>ack</em>nowledgement (NACK) until the load balancer marks it as down. The app can
|
13
|
+
(and should) continue to serve requests until that point.
|
14
|
+
|
15
|
+
To make this work, your app needs to inform the middleware when it wants to shut
|
16
|
+
down, and the middleware will call back when it's safe to do so.
|
17
|
+
|
18
|
+
## Usage
|
19
|
+
|
20
|
+
Basic example:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
class MyApp < Sinatra::Base
|
24
|
+
use Rack::NackMode do |health_check|
|
25
|
+
# store the middleware instance for calling #shutdown below
|
26
|
+
@health_check = health_check
|
27
|
+
end
|
28
|
+
|
29
|
+
class << self
|
30
|
+
def shutdown
|
31
|
+
if @health_check # see note below
|
32
|
+
@health_check.shutdown { exit 0 }
|
33
|
+
else
|
34
|
+
exit 0
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
N.B. because Rack waits to initialise middleware until it receives an HTTP
|
42
|
+
request, it's possible to shut down before the middleware is initialised.
|
43
|
+
That's unlikely to be a problem, because having not received any HTTP requests,
|
44
|
+
we've obviously not received any *health check* requests either, meaning the
|
45
|
+
load balancer should already believe we're down: so it should be safe to
|
46
|
+
shutdown immediately, as in the above example.
|
47
|
+
|
48
|
+
### Configuration
|
49
|
+
|
50
|
+
The `use` statement to initialise the middleware takes the following options:
|
51
|
+
|
52
|
+
* `:path` – path for the health check endpoint (default `/admin`)
|
53
|
+
* Customising whether the health check reports healthy or sick (except while
|
54
|
+
shutting down, when it will always report sick):
|
55
|
+
* `:healthy_if` – callback that should return `true` if your app is
|
56
|
+
able to serve requests, and `false` otherwise.
|
57
|
+
* `:sick_if` – callback that should return `false` if your app is
|
58
|
+
able to serve requests, and `true` otherwise.
|
59
|
+
* `:nacks_before_shutdown` – how many times the app should tell the load
|
60
|
+
balancer it's going down before it can safely do so. Defaults to 3, which
|
61
|
+
matches e.g. haproxy's default for how many failed checks it needs before
|
62
|
+
marking a backend as down.
|
63
|
+
* `:logger` – middleware will log progress to this object if supplied.
|
@@ -0,0 +1 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'rack', 'nack_mode'))
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'active_support/core_ext/hash/keys'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
# Middleware that communicates impending shutdown to a load balancer via
|
5
|
+
# NACKing (negative acking) health checks. Your app needs to inform the
|
6
|
+
# middleware when it wants to shut down, and the middleware will call back
|
7
|
+
# when it's safe to do so.
|
8
|
+
#
|
9
|
+
# Responds to health checks on /admin (configurable via :path option).
|
10
|
+
#
|
11
|
+
# Basic usage:
|
12
|
+
# class MyApp < Sinatra::Base
|
13
|
+
# use Rack::NackMode do |health_check|
|
14
|
+
# # store the middleware instance for calling #shutdown below
|
15
|
+
# @health_check = health_check
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# class << self
|
19
|
+
# def shutdown
|
20
|
+
# if @health_check
|
21
|
+
# @health_check.shutdown { exit 0 }
|
22
|
+
# else
|
23
|
+
# exit 0
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# N.B. because Rack waits to initialise middleware until it receives an HTTP
|
30
|
+
# request, it's possible to shut down before the middleware is initialised.
|
31
|
+
# That's unlikely to be a problem, because having not received any HTTP
|
32
|
+
# requests, we've obviously not received any *health check* requests either,
|
33
|
+
# meaning the load balancer should already believe we're down: so it should
|
34
|
+
# be safe to shutdown immediately, as in the above example.
|
35
|
+
class NackMode
|
36
|
+
# Default number of health checks we NACK before shutting down. This
|
37
|
+
# matches e.g. haproxy's default for how many failed checks it needs before
|
38
|
+
# marking a backend as down.
|
39
|
+
DEFAULT_NACKS_BEFORE_SHUTDOWN = 3
|
40
|
+
|
41
|
+
def initialize(app, options = {})
|
42
|
+
@app = app
|
43
|
+
|
44
|
+
options.assert_valid_keys :path,
|
45
|
+
:healthy_if,
|
46
|
+
:sick_if,
|
47
|
+
:nacks_before_shutdown,
|
48
|
+
:logger
|
49
|
+
@path = options[:path] || '/admin'
|
50
|
+
@health_callback = if options[:healthy_if] && options[:sick_if]
|
51
|
+
raise ArgumentError, 'Please specify either :healthy_if or :sick_if, not both'
|
52
|
+
elsif healthy_if = options[:healthy_if]
|
53
|
+
healthy_if
|
54
|
+
elsif sick_if = options[:sick_if]
|
55
|
+
lambda { !sick_if.call }
|
56
|
+
else
|
57
|
+
lambda { true }
|
58
|
+
end
|
59
|
+
@nacks_before_shutdown = options[:nacks_before_shutdown] || DEFAULT_NACKS_BEFORE_SHUTDOWN
|
60
|
+
raise ArgumentError, ":nacks_before_shutdown must be at least 1" unless @nacks_before_shutdown >= 1
|
61
|
+
@logger = options[:logger]
|
62
|
+
|
63
|
+
yield self if block_given?
|
64
|
+
end
|
65
|
+
|
66
|
+
def call(env)
|
67
|
+
if health_check?(env)
|
68
|
+
health_check_response(env)
|
69
|
+
else
|
70
|
+
@app.call(env)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def shutdown(&block)
|
75
|
+
info "Shutting down after NACKing #@nacks_before_shutdown health checks"
|
76
|
+
@shutdown_callback = block
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
def health_check?(env)
|
81
|
+
env['PATH_INFO'] == @path && env['REQUEST_METHOD'] == 'GET'
|
82
|
+
end
|
83
|
+
|
84
|
+
def health_check_response(env)
|
85
|
+
if shutting_down?
|
86
|
+
@nacks_before_shutdown -= 1
|
87
|
+
if @nacks_before_shutdown <= 0
|
88
|
+
if defined?(EM)
|
89
|
+
EM.next_tick do
|
90
|
+
info 'Shutting down'
|
91
|
+
@shutdown_callback.call
|
92
|
+
end
|
93
|
+
else
|
94
|
+
info 'Shutting down'
|
95
|
+
@shutdown_callback.call
|
96
|
+
end
|
97
|
+
else
|
98
|
+
info "Waiting for #@nacks_before_shutdown more health checks"
|
99
|
+
end
|
100
|
+
respond_sick
|
101
|
+
elsif healthy?
|
102
|
+
respond_healthy
|
103
|
+
else
|
104
|
+
respond_sick
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def shutting_down?
|
109
|
+
@shutdown_callback
|
110
|
+
end
|
111
|
+
|
112
|
+
def healthy?
|
113
|
+
@health_callback.call
|
114
|
+
end
|
115
|
+
|
116
|
+
def respond_healthy
|
117
|
+
[200, {}, ['GOOD']]
|
118
|
+
end
|
119
|
+
|
120
|
+
def respond_sick
|
121
|
+
info 'Telling load balancer we are sick'
|
122
|
+
[503, {}, ['BAD']]
|
123
|
+
end
|
124
|
+
|
125
|
+
def info(*args)
|
126
|
+
@logger.info(*args) if @logger
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
metadata
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-nackmode
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Sam Stokes
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-05-12 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activesupport
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: em-spec-helpers
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rack-test
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: sinatra
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
description: ! 'Middleware that communicates impending shutdown to a load balancer
|
95
|
+
via NACKing
|
96
|
+
|
97
|
+
(negative acking) health checks. Provided you have at least two load-balanced
|
98
|
+
|
99
|
+
instances, this allows you to shut down or restart an instance without dropping
|
100
|
+
|
101
|
+
any requests.
|
102
|
+
|
103
|
+
|
104
|
+
Your app needs to inform the middleware when it wants to shut down, and the
|
105
|
+
|
106
|
+
middleware will call back when it''s safe to do so.
|
107
|
+
|
108
|
+
'
|
109
|
+
email:
|
110
|
+
- sam@rapportive.com
|
111
|
+
executables: []
|
112
|
+
extensions: []
|
113
|
+
extra_rdoc_files: []
|
114
|
+
files:
|
115
|
+
- lib/rack/nack_mode.rb
|
116
|
+
- lib/rack-nackmode.rb
|
117
|
+
- README.markdown
|
118
|
+
homepage: http://github.com/rapportive-oss/rack-nackmode
|
119
|
+
licenses: []
|
120
|
+
post_install_message:
|
121
|
+
rdoc_options: []
|
122
|
+
require_paths:
|
123
|
+
- lib
|
124
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
125
|
+
none: false
|
126
|
+
requirements:
|
127
|
+
- - ! '>='
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '0'
|
130
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
131
|
+
none: false
|
132
|
+
requirements:
|
133
|
+
- - ! '>='
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '0'
|
136
|
+
requirements: []
|
137
|
+
rubyforge_project:
|
138
|
+
rubygems_version: 1.8.19
|
139
|
+
signing_key:
|
140
|
+
specification_version: 3
|
141
|
+
summary: Middleware for zero-downtime maintenance behind a load balancer
|
142
|
+
test_files: []
|
143
|
+
has_rdoc:
|