sleepier 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|