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 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: