uninterruptible 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9ffdc9d6870a2bf9ae5573fbf9cc5b0e8a4d3f86
4
+ data.tar.gz: 81019ac427f4af4e182c8cf367ce9c807cef97a6
5
+ SHA512:
6
+ metadata.gz: 163f49c6706ade360df9626e817c92af5e0c6276d6867a2dab5a6b5ab4582e5ff7e25209316a778add47b8bf45629e0abae3c02f170c47c6b98db2e5a7f55370
7
+ data.tar.gz: 7dd7aa37124bde1196b48f6884d62b1db6214c695ac1f8e4b7c1ff1832d0f015c03829c6b502705857be79b29f8f156bdf4d4b05c1f280fdd1c6513765839097
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.3.0
4
+ before_install: gem install bundler -v 1.11.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in uninterruptible.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Dan Wentworth
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # Uninterruptible
2
+
3
+ Uninterruptible gives you zero downtime restarts for your TCP servers with nearly zero effort. Sounds good? Read on...
4
+
5
+ Small socket servers are great, sometimes you need a quick and efficient way of moving data between servers (or even
6
+ processes on the same machine). Restarting these processes can be a bit hairy though, you either need to build your
7
+ clients smart enough to keep trying to connect, potentially backing up traffic or you just leave your server and
8
+ hope for the best.
9
+
10
+ You _know_ that you'll need to restart it one day and cross your fingers that you can kill the old one and start the
11
+ new one before anyone notices. Not ideal at all.
12
+
13
+ ![Just a quick switch](http://i.imgur.com/aFyJJM6.jpg)
14
+
15
+ Uninterruptible gives your socket server magic restarting powers. Send your running Uninterruptible server USR1 and
16
+ it will start a brand new copy of itself which will immediately start handling new requests while the old server stays
17
+ alive until all of it's active connections are complete.
18
+
19
+ ## Basic Usage
20
+
21
+ Add this line to your application's Gemfile:
22
+
23
+ ```ruby
24
+ gem 'uninterruptible'
25
+ ```
26
+
27
+ To build your server all you need to do is include `Uninterruptible::Server` and implement `handle_request`. Let's build
28
+ a simple echo server:
29
+
30
+ ```ruby
31
+ # echo_server.rb
32
+ class EchoServer
33
+ include Uninterruptible::Server
34
+
35
+ def handle_request(client_socket)
36
+ received_data = client_socket.gets
37
+ client_socket.puts(received_data)
38
+ end
39
+ end
40
+ ```
41
+
42
+ To turn this into a running server you only need to configure a port to listen on and the command used to start the
43
+ server and call `run`:
44
+
45
+ ```ruby
46
+ echo_server = EchoServer.new
47
+ echo_server.configure do |config|
48
+ config.bind_port = 6789
49
+ config.start_command = 'ruby echo_server.rb'
50
+ end
51
+ echo_server.run
52
+ ```
53
+
54
+ To restart the server just send `USR1`, a new server will start listening on your port, the old one will quit once it's
55
+ finished processing all of it's existing connections. To kill the server (allowing for all connections to finish) call
56
+ `TERM`.
57
+
58
+ ## Configuration Options
59
+
60
+ ```ruby
61
+ echo_server.configure do |config|
62
+ config.start_command = 'ruby echo_server.rb' # *Required* Command to execute to start a new server process
63
+ config.bind_port = 6789 # *Required* Port to listen on, falls back to ENV['PORT']
64
+ config.bind_address = '::' # Address to listen on
65
+ config.pidfile_path = 'tmp/pids/echoserver.pid' # Location to write a pidfile, falls back to ENV['PID_FILE']
66
+ config.log_path = 'log/echoserver.log' # Location to write logfile, defaults to STDOUT
67
+ config.log_level = Logger::INFO # Log writing severity, defaults to Logger::INFO
68
+ end
69
+ ```
70
+
71
+ ## The Magic
72
+
73
+ Upon receiving `USR1`, your server will spawn a new copy of itself and pass the file descriptor of the open socket to
74
+ the new server. The new server attaches itself to the file descriptor then sends a `TERM` signal to the original
75
+ process. The original server stops listening on the socket and shuts itself down once all ongoing requests have
76
+ completed.
77
+
78
+ ![Restart Flow](http://i.imgur.com/k8uNP55.png)
79
+
80
+ ## Concurrency
81
+
82
+ By default, Uninterruptible operates on a very simple one thread per connection concurrency model. If you'd like to use
83
+ something more advanced such as a threadpool or an event driven pattern you can define this in your server class.
84
+
85
+ By overriding `accept_connections` you can change how connections are accepted and handled. It is recommended that you
86
+ call `process_request` from this method and still implement `handle_request` to do the bulk of the work since
87
+ `process_request` tracks the number of active connections to the server.
88
+
89
+ If you wanted to implement a threadpool to process your requests you could do the following:
90
+
91
+ ```ruby
92
+ class EchoServer
93
+ # ...
94
+
95
+ def accept_connections
96
+ threads = 4.times.map do
97
+ Thread.new { worker_loop }
98
+ end
99
+
100
+ threads.each(&:join)
101
+ end
102
+
103
+ def worker_loop
104
+ loop do
105
+ client_socket = tcp_server.accept
106
+ process_request(client_socket)
107
+ end
108
+ end
109
+ end
110
+ ```
111
+
112
+ ## Contributing
113
+
114
+ Bug reports and pull requests are welcome on GitHub at https://github.com/darkphnx/uninterruptible.
115
+
116
+ ## License
117
+
118
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
119
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "uninterruptible"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,47 @@
1
+ module Uninterruptible
2
+ # Configuration parameters for an individual instance of a server.
3
+ #
4
+ # See {Server#configure} for usage instructions.
5
+ class Configuration
6
+ attr_writer :bind_port, :bind_address, :pidfile_path, :start_command, :log_path, :log_level
7
+
8
+ # Available TCP Port for the server to bind to (required). Falls back to environment variable PORT if set.
9
+ #
10
+ # @return [Integer] Port number to bind to
11
+ def bind_port
12
+ port = (@bind_port || ENV["PORT"])
13
+ raise ConfigurationError, "You must configure a bind_port" if port.nil?
14
+ port.to_i
15
+ end
16
+
17
+ # Address to bind the server to (defaults to +::+).
18
+ def bind_address
19
+ @bind_address || "::"
20
+ end
21
+
22
+ # Location to write the pid of the current server to. If blank pidfile will not be written. Falls back to
23
+ # environment variable PID_FILE if set.
24
+ def pidfile_path
25
+ @pidfile_path || ENV["PID_FILE"]
26
+ end
27
+
28
+ # Command that should be used to reexecute the server (required). Note: +bundle exec+ will be automatically added.
29
+ #
30
+ # @example
31
+ # rake app:run_server
32
+ def start_command
33
+ raise ConfigurationError, "You must configure a start_command" unless @start_command
34
+ @start_command
35
+ end
36
+
37
+ # Where should log output be written to? (defaults to STDOUT)
38
+ def log_path
39
+ @log_path || STDOUT
40
+ end
41
+
42
+ # Severity of entries written to the log, should be one of Logger::Severity (default Logger::INFO)
43
+ def log_level
44
+ @log_level || Logger::INFO
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,181 @@
1
+ require 'socket'
2
+ require 'logger'
3
+
4
+ module Uninterruptible
5
+ # The meat and potatoes of uninterruptible, include this in your server, configure it and override #handle_request.
6
+ #
7
+ # Calling #run will listen on the configured port and start a blocking server. Send that server signal USR1 to
8
+ # begin a hot-restart and TERM to start a graceful shutdown. Send TERM again for an immediate shutdown.
9
+ #
10
+ # @example
11
+ # class HelloServer
12
+ # include Uninterruptible::Server
13
+ #
14
+ # def handle_request(client_socket)
15
+ # name = client_socket.read
16
+ # client_socket.write("Hello #{name}!")
17
+ # end
18
+ # end
19
+ #
20
+ # To then use this server, call #configure on it to set the port and restart command, then call #run to start.
21
+ #
22
+ # @example
23
+ # hello_server = HelloServer.new
24
+ # hello_server.configure do |config|
25
+ # config.start_command = 'rake my_app:hello_server'
26
+ # config.port = 7000
27
+ # end
28
+ # hello_server.run
29
+ #
30
+ module Server
31
+ def self.included(base)
32
+ base.class_eval do
33
+ attr_reader :active_connections, :tcp_server, :mutex
34
+ end
35
+ end
36
+
37
+ # Configure the server, see {Uninterruptible::Configuration} for full options.
38
+ #
39
+ # @yield [Uninterruptible::Configuration] the current configuration for this server instance
40
+ #
41
+ # @return [Uninterruptible::Configuration] the current configuration (after yield)
42
+ def configure
43
+ yield server_configuration if block_given?
44
+ server_configuration
45
+ end
46
+
47
+ # Starts the server, this is a blocking operation. Bind to the address and port specified in the configuration,
48
+ # write the pidfile (if configured) and start accepting new connections for processing.
49
+ def run
50
+ @active_connections = 0
51
+ @mutex = Mutex.new
52
+
53
+ logger.info "Starting server on #{server_configuration.bind_address}:#{server_configuration.bind_port}"
54
+
55
+ establish_tcp_server
56
+ write_pidfile
57
+ setup_signal_traps
58
+ accept_connections
59
+ end
60
+
61
+ # @abstract Override this method to process incoming requests. Each request is handled in it's own thread.
62
+ # Socket will be automatically closed after completion.
63
+ #
64
+ # @param [TCPSocket] client_socket Incoming socket from the client
65
+ def handle_request(client_socket)
66
+ raise NotImplementedError
67
+ end
68
+
69
+ private
70
+
71
+ # Start a blocking loop which accepts new connections and hands them off to #process_request. Override this to
72
+ # use a different concurrency pattern, a thread per connection is the default.
73
+ def accept_connections
74
+ loop do
75
+ Thread.start(tcp_server.accept) do |client_socket|
76
+ logger.debug "Accepted connection from #{client_socket.peeraddr.last}"
77
+ process_request(client_socket)
78
+ end
79
+ end
80
+ end
81
+
82
+ # Keeps a track of the number of active connections and passes the client connection to #handle_request for the
83
+ # user to do with as they wish. Automatically closes a connection once #handle_request has completed.
84
+ #
85
+ # @param [TCPSocket] client_socket Incoming socket from the client connection
86
+ def process_request(client_socket)
87
+ mutex.synchronize { @active_connections += 1 }
88
+ begin
89
+ handle_request(client_socket)
90
+ ensure
91
+ client_socket.close
92
+ mutex.synchronize { @active_connections -= 1 }
93
+ end
94
+ end
95
+
96
+ # Listen (or reconnect) to the bind address and port specified in the config. If TCP_SERVER_FD is set in the env,
97
+ # reconnect to that file descriptor. Once @tcp_server is set, write the file descriptor ID to the env.
98
+ def establish_tcp_server
99
+ if ENV['TCP_SERVER_FD']
100
+ # If there's a file descriptor present, take over from a previous instance of this server and kill it off
101
+ logger.debug "Reconnecting to file descriptor..."
102
+ @tcp_server = TCPServer.for_fd(ENV['TCP_SERVER_FD'].to_i)
103
+ kill_parent
104
+ else
105
+ logger.debug "Opening new socket..."
106
+ @tcp_server = TCPServer.open(server_configuration.bind_address, server_configuration.bind_port)
107
+ end
108
+
109
+ @tcp_server.autoclose = false
110
+ @tcp_server.close_on_exec = false
111
+
112
+ ENV["TCP_SERVER_FD"] = @tcp_server.to_i.to_s
113
+ end
114
+
115
+ # Send a TERM signal to the parent process. This will be called by a newly spawned server if it has been started
116
+ # by another instance of this server.
117
+ def kill_parent
118
+ logger.debug "Killing parent process #{Process.ppid}"
119
+ Process.kill('TERM', Process.ppid)
120
+ end
121
+
122
+ # Write the current pid out to pidfile_path if configured
123
+ def write_pidfile
124
+ return unless server_configuration.pidfile_path
125
+
126
+ logger.debug "Writing pid to #{server_configuration.pidfile_path}"
127
+ File.write(server_configuration.pidfile_path, Process.pid.to_s)
128
+ end
129
+
130
+ # Catch TERM and USR1 signals which control the lifecycle of the server.
131
+ def setup_signal_traps
132
+ # On TERM begin a graceful shutdown, if a second TERM is received shutdown immediately with an exit code of 1
133
+ Signal.trap('TERM') do
134
+ Process.exit(1) if $shutdown
135
+
136
+ $shutdown = true
137
+ graceful_shutdown
138
+ end
139
+
140
+ # On USR1 begin a hot restart
141
+ Signal.trap('USR1') do
142
+ hot_restart
143
+ end
144
+ end
145
+
146
+ # Stop listening on tcp_server, wait until all active connections have finished processing and exit with 0.
147
+ def graceful_shutdown
148
+ tcp_server.close unless tcp_server.closed?
149
+
150
+ until active_connections.zero?
151
+ sleep 0.5
152
+ end
153
+
154
+ Process.exit(0)
155
+ end
156
+
157
+ # Start a new copy of this server, maintaining all current file descriptors and env.
158
+ def hot_restart
159
+ fork do
160
+ Dir.chdir(ENV['APP_ROOT']) if ENV['APP_ROOT']
161
+ ENV.delete('BUNDLE_GEMFILE') # Ensure a fresh bundle is used
162
+ exec("bundle exec --keep-file-descriptors #{server_configuration.start_command}", :close_others => false)
163
+ end
164
+ end
165
+
166
+ # The current configuration of this server
167
+ #
168
+ # @return [Uninterruptible::Configuration] Current or new configuration if unset.
169
+ def server_configuration
170
+ @server_configuration ||= Uninterruptible::Configuration.new
171
+ end
172
+
173
+ def logger
174
+ @logger ||= begin
175
+ log = Logger.new(server_configuration.log_path)
176
+ log.level = server_configuration.log_level
177
+ log
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,3 @@
1
+ module Uninterruptible
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,8 @@
1
+ require "uninterruptible/version"
2
+ require 'uninterruptible/configuration'
3
+ require 'uninterruptible/server'
4
+
5
+ # All of the interesting stuff is in Uninterruptible::Server
6
+ module Uninterruptible
7
+ class ConfigurationError < StandardError; end
8
+ end
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'uninterruptible/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "uninterruptible"
8
+ spec.version = Uninterruptible::VERSION
9
+ spec.authors = ["Dan Wentworth"]
10
+ spec.email = ["dan@atechmedia.com"]
11
+
12
+ spec.summary = "Zero-downtime restarts for your trivial TCP servers"
13
+ spec.description = "Uninterruptible gives your socket server magic restarting powers. Send your running "\
14
+ "Uninterruptible server USR1 and it will start a brand new copy of itself which will immediately start handling "\
15
+ "new requests while the old server stays alive until all of it's active connections are complete."
16
+ spec.homepage = "https://github.com/darkphnx/uninterruptible"
17
+ spec.license = "MIT"
18
+
19
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.11"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec", "~> 3.0"
27
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: uninterruptible
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Dan Wentworth
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-03-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.11'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.11'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ description: Uninterruptible gives your socket server magic restarting powers. Send
56
+ your running Uninterruptible server USR1 and it will start a brand new copy of itself
57
+ which will immediately start handling new requests while the old server stays alive
58
+ until all of it's active connections are complete.
59
+ email:
60
+ - dan@atechmedia.com
61
+ executables: []
62
+ extensions: []
63
+ extra_rdoc_files: []
64
+ files:
65
+ - ".rspec"
66
+ - ".travis.yml"
67
+ - Gemfile
68
+ - LICENSE.txt
69
+ - README.md
70
+ - Rakefile
71
+ - bin/console
72
+ - bin/setup
73
+ - lib/uninterruptible.rb
74
+ - lib/uninterruptible/configuration.rb
75
+ - lib/uninterruptible/server.rb
76
+ - lib/uninterruptible/version.rb
77
+ - uninterruptible.gemspec
78
+ homepage: https://github.com/darkphnx/uninterruptible
79
+ licenses:
80
+ - MIT
81
+ metadata: {}
82
+ post_install_message:
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubyforge_project:
98
+ rubygems_version: 2.5.1
99
+ signing_key:
100
+ specification_version: 4
101
+ summary: Zero-downtime restarts for your trivial TCP servers
102
+ test_files: []