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 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` &ndash; 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` &ndash; callback that should return `true` if your app is
56
+ able to serve requests, and `false` otherwise.
57
+ * `:sick_if` &ndash; callback that should return `false` if your app is
58
+ able to serve requests, and `true` otherwise.
59
+ * `:nacks_before_shutdown` &ndash; 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` &ndash; 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: