rack-nackmode 0.1.0
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 +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:
|