sleepier 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/sleepier.rb +276 -0
  3. 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: