sleepier 0.0.1
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/lib/sleepier.rb +276 -0
- metadata +47 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 99a72d5a7f90f43a4cd9c157dbf8720911d7c338
|
4
|
+
data.tar.gz: a3a63fd4975ef42afca26ce82430e921da8a4e3f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 007ba397c02da92bbcc529b92e667994e842848d65227d64320c2c281030f34dd563bcd9db7f4255cccedb466770e4558557a5789bc69458edc1181a5612607d
|
7
|
+
data.tar.gz: aadd93b923261cb610768d6a94c509e430cb167b21130d0f229bb1db9ec4b7ed1deba990068f80c4faf8cf4aa6635916b8239514fd0b63c4a93a133c34f6c4af
|
data/lib/sleepier.rb
ADDED
@@ -0,0 +1,276 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'logger'
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
# Sleepier is a Process Management tool in the style of a supervisor. It most similar to the Erlang supervisor behaviour.
|
6
|
+
#
|
7
|
+
# The basic usage of Sleepier is:
|
8
|
+
#
|
9
|
+
# 1. Create an `Array` of `Sleepier::ChildSpec` objects
|
10
|
+
# 2. Initialize a `Sleepier::Supervisor` object with the array of `Sleepier::ChildSpec` objects
|
11
|
+
# 3. Create a new `Thread` and call `monitor` on the supervisor object within the thread
|
12
|
+
# 4. Call `start` on the supervisor
|
13
|
+
#
|
14
|
+
# Note that `start` will return as soon as the processes are started, and does not wait for them to finish.
|
15
|
+
#
|
16
|
+
# Features:
|
17
|
+
#
|
18
|
+
# - Starting and stopping processes
|
19
|
+
# - Rapid termination handling
|
20
|
+
# - Several process shutdown strategies
|
21
|
+
# - Different process lifecycles
|
22
|
+
# - Pluggable logging
|
23
|
+
module Sleepier
|
24
|
+
# The different styles which can be used to manage restarts
|
25
|
+
#
|
26
|
+
# - :permanent - Always restart the process, except when it has been restarted more than `max_restart_count` times in `max_restart_window` seconds
|
27
|
+
# - :temporary - Never restart the process
|
28
|
+
# - :transient - Only restart the process if it failed and hasn't been restarted more than `max_restart_count` times in `max_restart_window` seconds
|
29
|
+
VALID_RESTART_OPTIONS = [:permanent, :temporary, :transient]
|
30
|
+
|
31
|
+
# How to shutdown the process
|
32
|
+
#
|
33
|
+
# - :brutal_kill - Terminate immediately, without giving it a chance to terminate gracefully. Equivalent to a kill -9 on Linux
|
34
|
+
# - :timeout - Attempt to terminate gracefully, but after `shutdown_timeout` seconds, brutally kill
|
35
|
+
# - :infinity - Terminate gracefully, even if it takes forever. USE WITH CAUTION! THIS CAN RESULT IN NEVER-ENDING PROCESSES
|
36
|
+
VALID_SHUTDOWN_OPTIONS = [:brutal_kill, :timeout, :infinity]
|
37
|
+
|
38
|
+
@@logger = Logger.new(STDOUT)
|
39
|
+
|
40
|
+
# Logger used by sleepier functionality
|
41
|
+
def self.logger
|
42
|
+
@@logger
|
43
|
+
end
|
44
|
+
|
45
|
+
# Configure the sleepier logger to another Ruby Logger-style logger
|
46
|
+
#
|
47
|
+
# @param logger [Logger] The new logger to use
|
48
|
+
def self.logger=(logger)
|
49
|
+
@@logger = logger
|
50
|
+
end
|
51
|
+
|
52
|
+
# Specifies the properties of the child process to be launched
|
53
|
+
class ChildSpec < Object
|
54
|
+
|
55
|
+
attr_accessor :child_id, :start_func, :args, :restart, :shutdown, :pid, :terminating, :shutdown_timeout
|
56
|
+
|
57
|
+
# Create a new `ChildSpec`
|
58
|
+
#
|
59
|
+
# @param child_id Unique id associated with this specification. This can be used to terminate the process
|
60
|
+
# @param start_func The function run by the process
|
61
|
+
# @param args [Array] The list of arguments passed to the function. If there are none, pass it an empty `Array`
|
62
|
+
# @param restart [VALID_RESTART_OPTIONS] One of the `VALID_RESTART_OPTIONS` that determines how to handle restarts
|
63
|
+
# @param shutdown [VALID_SHUTDOWN_OPTIONS] One of the `VALID_SHUTDOWN_OPTIONS` that determines how to shutdown the process
|
64
|
+
# @param is_supervisor [true, false] Is the child a supervisor itself?! Potentially useful for trees of supervisors
|
65
|
+
# @param shutdown_timeout [int] If `shutdown` is `:timeout`, this is how long to wait before brutally killing the process
|
66
|
+
def initialize(child_id, start_func, args, restart, shutdown, is_supervisor=false, shutdown_timeout=0)
|
67
|
+
@child_id = child_id
|
68
|
+
@start_func = start_func
|
69
|
+
@args = args
|
70
|
+
@restart = restart
|
71
|
+
@shutdown = shutdown
|
72
|
+
@is_supervisor = is_supervisor
|
73
|
+
@shutdown_timeout = shutdown_timeout
|
74
|
+
@pid = nil
|
75
|
+
|
76
|
+
# Array of timestamps of restarts. Used for checking restarts
|
77
|
+
@failures = Array.new
|
78
|
+
|
79
|
+
# Used for
|
80
|
+
@terminating = false
|
81
|
+
end
|
82
|
+
|
83
|
+
def supervisor?
|
84
|
+
@is_supervisor
|
85
|
+
end
|
86
|
+
|
87
|
+
def child?
|
88
|
+
!@is_supervisor
|
89
|
+
end
|
90
|
+
|
91
|
+
# Called by the supervisor to check whether the process should be restarted. Checks whether the process has been restarted
|
92
|
+
# more than `max_restart_count` times in `max_restart_window` seconds, and whether the `shutdown` type even
|
93
|
+
# allows restarts
|
94
|
+
#
|
95
|
+
# @return [true, false]
|
96
|
+
def should_restart?(status_code, max_restart_count, max_restart_window)
|
97
|
+
if self.too_many_restarts?(max_restart_count, max_restart_window)
|
98
|
+
false
|
99
|
+
elsif self.allows_restart?(status_code) && !@terminating
|
100
|
+
true
|
101
|
+
else
|
102
|
+
false
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def allows_restart?(status_code)
|
107
|
+
case self.restart
|
108
|
+
when :permanent
|
109
|
+
true
|
110
|
+
when :temporary
|
111
|
+
false
|
112
|
+
when :transient
|
113
|
+
if status_code == 0
|
114
|
+
false
|
115
|
+
else
|
116
|
+
true
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Used to notify the child spec that it has been restarted, and when. This allows tracking of
|
122
|
+
# how many recent restarts the child spec has had.
|
123
|
+
def restarted
|
124
|
+
@failures << Time.now.to_i
|
125
|
+
end
|
126
|
+
|
127
|
+
def too_many_restarts?(max_restart_count, max_restart_window)
|
128
|
+
max_restart_count <= self.restarts_within_window(max_restart_window)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Counts how many restarts have happened within the last `max_restart_window` seconds, and
|
132
|
+
# clears out old failures.
|
133
|
+
def restarts_within_window(max_restart_window)
|
134
|
+
failures_within_window = 0
|
135
|
+
now = Time.now.to_i
|
136
|
+
new_failures = Array.new
|
137
|
+
|
138
|
+
@failures.each do |f|
|
139
|
+
if now - f < max_restart_window
|
140
|
+
failures_within_window += 1
|
141
|
+
new_failures << f
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Update the failures array to only include things currently within the window
|
146
|
+
@failures = new_failures
|
147
|
+
|
148
|
+
# Return the current number of failures within the window
|
149
|
+
failures_within_window
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# `Sleepier::Supervisor` manages a set of `Sleepier::ChildSpec` objects according to the guidance passed to it via the constructor.
|
154
|
+
class Supervisor < Object
|
155
|
+
# @todo implement strategies other than :one_for_one
|
156
|
+
#
|
157
|
+
# Determines how to handle restarts for all processes supervised
|
158
|
+
#
|
159
|
+
# - :one_for_one - Only restart the process that failed if one process terminates
|
160
|
+
# - :one_for_all - Restart all processes if one process terminates
|
161
|
+
# - :rest_for_one - Restart all processes after the process, in the order they started, that terminates as well as the process that terminated
|
162
|
+
VALID_RESTART_STRATEGIES = [:one_for_one, :one_for_all, :rest_for_one, :simple_one_for_one]
|
163
|
+
|
164
|
+
# Create the supervisor. Does *not* start it.
|
165
|
+
#
|
166
|
+
# @param child_specs [Sleepier::ChildSpec] What processes to start and monitor
|
167
|
+
# @param restart_strategy [VALID_RESTART_STRATEGIES] Managing how restarts are handled for the group of processes
|
168
|
+
# @param max_restart_count [int] How many times within `max_restart_window` a process can restart.
|
169
|
+
# @param max_restart_window [int] A moving window in seconds during which a process may terminate `max_restart_count` times before the supervisor gives up.
|
170
|
+
def initialize(child_specs, restart_strategy, max_restart_count=3, max_restart_window=5)
|
171
|
+
@child_specs = Hash.new
|
172
|
+
child_specs.each {|child_spec| @child_specs[child_spec.child_id] = child_spec}
|
173
|
+
|
174
|
+
@max_restart_count = max_restart_count
|
175
|
+
@max_restart_window = max_restart_window
|
176
|
+
|
177
|
+
if VALID_RESTART_STRATEGIES.include?(restart_strategy)
|
178
|
+
@restart_strategy = restart_strategy
|
179
|
+
else
|
180
|
+
raise Exception.new('Invalid restart strategy')
|
181
|
+
end
|
182
|
+
|
183
|
+
@started = false
|
184
|
+
end
|
185
|
+
|
186
|
+
# Watches for processes to terminate. Sends the pid and status returned by the process to `handle_finished_process`
|
187
|
+
#
|
188
|
+
# @note This may be called before calling start to minimize chances of a process terminating before monitoring starts
|
189
|
+
def monitor
|
190
|
+
while true
|
191
|
+
begin
|
192
|
+
pid, status = Process.wait2
|
193
|
+
rescue Errno::ECHILD
|
194
|
+
if @started
|
195
|
+
Sleepier.logger.warn("No children, exiting")
|
196
|
+
break
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
self.handle_finished_process(pid, status)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# Start all the child processes
|
205
|
+
def start
|
206
|
+
@child_specs.each do |child_id, child_spec|
|
207
|
+
self.start_process(child_id)
|
208
|
+
end
|
209
|
+
@started = true
|
210
|
+
end
|
211
|
+
|
212
|
+
# Add a new child process and start it
|
213
|
+
#
|
214
|
+
# @param child_spec [Sleepier::ChildSpec] spec to use and start
|
215
|
+
def start_new_child(child_spec)
|
216
|
+
@child_specs[child_spec.child_id] = child_spec
|
217
|
+
self.start_process(child_spec.child_id)
|
218
|
+
end
|
219
|
+
|
220
|
+
# Starts termination of a process. This does *not* wait for the process to finish.
|
221
|
+
#
|
222
|
+
# @param child_id Which child to terminate
|
223
|
+
#
|
224
|
+
# @todo Add a callback for when the process finishes here
|
225
|
+
def terminate_child(child_id)
|
226
|
+
child_spec = @child_specs[child_id]
|
227
|
+
child_spec.terminating = true
|
228
|
+
|
229
|
+
case child_spec.shutdown
|
230
|
+
when :brutal_kill
|
231
|
+
Process.kill("KILL", child_spec.pid)
|
232
|
+
when :timeout
|
233
|
+
Process.kill("TERM", child_spec.pid)
|
234
|
+
|
235
|
+
Thread.new do
|
236
|
+
sleep(child_spec.shutdown_timeout)
|
237
|
+
Process.kill("KILL", child_spec.pid)
|
238
|
+
end
|
239
|
+
when :infinity
|
240
|
+
Process.kill("TERM", child_spec.pid)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# Internal function that handles a process finishing
|
245
|
+
#
|
246
|
+
# @param pid [int] The pid of the finished process, used to find the right child process
|
247
|
+
# @param status [int] Status code. 0 is normal, anything else is abnormal termination
|
248
|
+
#
|
249
|
+
# @return [true,false] Returns true if the process should have been restarted and was, false otherwise
|
250
|
+
def handle_finished_process(pid, status)
|
251
|
+
@child_specs.each do |child_id, child_spec|
|
252
|
+
if child_spec.pid == pid
|
253
|
+
if child_spec.should_restart?(status, @max_restart_count, @max_restart_window)
|
254
|
+
child_spec.restarted
|
255
|
+
self.start_process(child_id)
|
256
|
+
return true
|
257
|
+
else
|
258
|
+
Sleepier.logger.info("#{child_spec.restart.to_s.capitalize} child #{child_spec.child_id} finished. Will not be restarted")
|
259
|
+
return false
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# Internal function used by `start_new_child` and `start`
|
266
|
+
def start_process(child_id)
|
267
|
+
child_spec = @child_specs[child_id]
|
268
|
+
pid = Process.fork do
|
269
|
+
child_spec.start_func.call(*(child_spec.args))
|
270
|
+
end
|
271
|
+
|
272
|
+
child_spec.pid = pid
|
273
|
+
Sleepier.logger.info("Started #{child_spec.child_id} with pid #{pid}")
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
metadata
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sleepier
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Patrick Dignan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-09-13 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Sleepily managing processes
|
14
|
+
email: dignan.patrick@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/sleepier.rb
|
20
|
+
homepage: http://rubygems.org/gems/sleepier
|
21
|
+
licenses:
|
22
|
+
- MIT
|
23
|
+
metadata: {}
|
24
|
+
post_install_message:
|
25
|
+
rdoc_options: []
|
26
|
+
require_paths:
|
27
|
+
- lib
|
28
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
requirements:
|
39
|
+
- yard
|
40
|
+
- redcarpet
|
41
|
+
rubyforge_project:
|
42
|
+
rubygems_version: 2.1.10
|
43
|
+
signing_key:
|
44
|
+
specification_version: 4
|
45
|
+
summary: Simple process monitoring
|
46
|
+
test_files: []
|
47
|
+
has_rdoc:
|