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.
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: