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 +7 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +119 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/uninterruptible/configuration.rb +47 -0
- data/lib/uninterruptible/server.rb +181 -0
- data/lib/uninterruptible/version.rb +3 -0
- data/lib/uninterruptible.rb +8 -0
- data/uninterruptible.gemspec +27 -0
- metadata +102 -0
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
data/.travis.yml
ADDED
data/Gemfile
ADDED
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
|
+

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

|
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
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,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,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: []
|