uninterruptible 1.0.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.
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: []