raemon 0.1.2 → 0.2.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.
- data/VERSION +1 -1
- data/examples/beanstalk.rb +5 -7
- data/examples/evented.rb +9 -8
- data/examples/queue.rb +14 -0
- data/examples/sampled/config/environment.rb +2 -0
- data/examples/sampled/lib/sampled.rb +2 -5
- data/examples/sampled/log/.keep +0 -0
- data/examples/test.rb +15 -8
- data/lib/raemon/master.rb +381 -46
- data/lib/raemon/server.rb +14 -9
- data/lib/raemon/util.rb +23 -0
- data/lib/raemon/worker.rb +18 -38
- data/lib/raemon.rb +8 -6
- data/raemon.gemspec +78 -0
- metadata +17 -7
- data/examples/sampled/test/example_test.rb +0 -11
- data/examples/sampled/test/test_helper.rb +0 -7
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
data/examples/beanstalk.rb
CHANGED
@@ -17,13 +17,10 @@ class JobWorker
|
|
17
17
|
logger.info "=> Stopping worker #{Process.pid}"
|
18
18
|
|
19
19
|
@beanstalk.close
|
20
|
-
exit
|
21
20
|
end
|
22
21
|
|
23
|
-
def
|
22
|
+
def run
|
24
23
|
loop do
|
25
|
-
stop if shutting_down?
|
26
|
-
|
27
24
|
begin
|
28
25
|
job = @beanstalk.reserve(2)
|
29
26
|
rescue Beanstalk::TimedOut
|
@@ -35,17 +32,18 @@ class JobWorker
|
|
35
32
|
# process job here ...
|
36
33
|
job.delete
|
37
34
|
end
|
35
|
+
|
36
|
+
heartbeat!
|
38
37
|
end
|
39
38
|
end
|
40
|
-
|
41
39
|
end
|
42
40
|
|
43
41
|
ROOT_DIR = File.expand_path('~')
|
44
42
|
|
45
|
-
# Raemon::Master.
|
43
|
+
# Raemon::Master.start 3, JobWorker, {
|
46
44
|
# :detach => true,
|
47
45
|
# :logger => Logger.new("#{ROOT_DIR}/beanstalk.log"),
|
48
46
|
# :pid_file => "#{ROOT_DIR}/beanstalk.pid"
|
49
47
|
# }
|
50
48
|
|
51
|
-
Raemon::Master.
|
49
|
+
Raemon::Master.start 3, JobWorker
|
data/examples/evented.rb
CHANGED
@@ -15,11 +15,10 @@ class EventedJobWorker
|
|
15
15
|
logger.info "=> Stopping worker #{Process.pid}"
|
16
16
|
|
17
17
|
EM.stop_event_loop if EM.reactor_running?
|
18
|
-
exit
|
19
18
|
end
|
20
19
|
|
21
|
-
def
|
22
|
-
|
20
|
+
def run
|
21
|
+
EM.run do
|
23
22
|
@queue = EMJack::Connection.new
|
24
23
|
|
25
24
|
@queue.each_job(5) do |job|
|
@@ -27,21 +26,23 @@ class EventedJobWorker
|
|
27
26
|
# process(job)
|
28
27
|
@queue.delete(job)
|
29
28
|
|
30
|
-
|
29
|
+
heartbeat!
|
31
30
|
end
|
32
31
|
|
33
32
|
@queue.on_error do |error|
|
34
33
|
case error
|
35
34
|
when :timed_out
|
36
|
-
# We use the reserve timeout
|
37
|
-
|
35
|
+
# We use the reserve timeout for the heartbeat
|
36
|
+
heartbeat!
|
37
|
+
|
38
|
+
# Note: this will only run once.. we need to call the @queue.each_job
|
39
|
+
# again .. maybe put it in a block
|
38
40
|
else
|
39
41
|
logger.error error.to_s
|
40
42
|
end
|
41
43
|
end
|
42
44
|
end
|
43
45
|
end
|
44
|
-
|
45
46
|
end
|
46
47
|
|
47
|
-
Raemon::Master.
|
48
|
+
Raemon::Master.start 2, EventedJobWorker
|
data/examples/queue.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# Use the code below to put some jobs onto the queue to test beanstalk.rb
|
2
|
+
# and evented.rb examples
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'beanstalk-client'
|
6
|
+
|
7
|
+
@queue = Beanstalk::Pool.new(['localhost:11300'])
|
8
|
+
|
9
|
+
num_jobs = 20
|
10
|
+
|
11
|
+
(0...num_jobs).each do |id|
|
12
|
+
job = "Job#{id}"
|
13
|
+
@queue.put job
|
14
|
+
end
|
@@ -4,18 +4,15 @@ module Sampled
|
|
4
4
|
include Raemon::Worker
|
5
5
|
|
6
6
|
def start
|
7
|
-
logger.info "
|
7
|
+
logger.info "=> Starting worker #{Process.pid}"
|
8
8
|
end
|
9
9
|
|
10
10
|
def stop
|
11
11
|
logger.info "=> Stopping worker #{Process.pid}"
|
12
|
-
exit
|
13
12
|
end
|
14
13
|
|
15
|
-
def
|
14
|
+
def run
|
16
15
|
loop do
|
17
|
-
stop if shutting_down?
|
18
|
-
|
19
16
|
logger.warn "I'm executing .. #{Process.ppid}:#{Process.pid}"
|
20
17
|
sleep 2
|
21
18
|
end
|
File without changes
|
data/examples/test.rb
CHANGED
@@ -12,18 +12,25 @@ class Test
|
|
12
12
|
|
13
13
|
def stop
|
14
14
|
logger.info "=> Stopping worker #{Process.pid}"
|
15
|
-
exit
|
16
15
|
end
|
17
16
|
|
18
|
-
def
|
19
|
-
|
20
|
-
stop if shutting_down?
|
17
|
+
def run
|
18
|
+
x = 0
|
21
19
|
|
22
|
-
|
23
|
-
|
20
|
+
loop do
|
21
|
+
logger.warn "I'm executing #{x}.. #{Process.ppid}:#{Process.pid}"
|
22
|
+
|
23
|
+
if x < 3
|
24
|
+
sleep 2
|
25
|
+
else
|
26
|
+
sleep 6
|
27
|
+
end
|
28
|
+
|
29
|
+
heartbeat!
|
30
|
+
|
31
|
+
x += 1
|
24
32
|
end
|
25
33
|
end
|
26
|
-
|
27
34
|
end
|
28
35
|
|
29
|
-
Raemon::Master.
|
36
|
+
Raemon::Master.start 2, Test, :timeout => 5
|
data/lib/raemon/master.rb
CHANGED
@@ -1,78 +1,413 @@
|
|
1
1
|
module Raemon
|
2
2
|
class Master
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
WORKERS = {}
|
4
|
+
|
5
|
+
SELF_PIPE = []
|
6
|
+
|
7
|
+
SIG_QUEUE = []
|
8
|
+
|
9
|
+
CHUNK_SIZE = (16*1024)
|
6
10
|
|
7
|
-
|
8
|
-
|
9
|
-
|
11
|
+
attr_accessor :name, :num_workers, :worker_klass,
|
12
|
+
:master_pid, :pid_file,
|
13
|
+
:logger, :timeout, :memory_limit
|
14
|
+
|
15
|
+
def self.start(num_workers, worker_klass, options={})
|
16
|
+
master = new(options)
|
17
|
+
master.start(num_workers, worker_klass)
|
10
18
|
end
|
11
19
|
|
12
|
-
def self.
|
13
|
-
|
14
|
-
|
15
|
-
|
20
|
+
def self.stop(options={})
|
21
|
+
pid_file = options[:pid_file]
|
22
|
+
pid = options[:pid] || (File.read(pid_file).to_i rescue 0)
|
23
|
+
Process.kill('QUIT', pid) if pid > 0
|
16
24
|
rescue Errno::ESRCH
|
17
25
|
end
|
18
26
|
|
19
|
-
def initialize(
|
20
|
-
@detach =
|
21
|
-
@
|
22
|
-
@pid_file =
|
23
|
-
@
|
27
|
+
def initialize(options={})
|
28
|
+
@detach = options[:detach] || false
|
29
|
+
@name = options[:name] || 'Raemon'
|
30
|
+
@pid_file = options[:pid_file]
|
31
|
+
@logger = options[:logger] || Logger.new(STDOUT)
|
32
|
+
@timeout = options[:timeout] || 180 # 3 minutes
|
33
|
+
@memory_limit = options[:memory_limit] # in MB
|
24
34
|
|
25
35
|
daemonize if @detach
|
26
36
|
end
|
27
37
|
|
28
|
-
def
|
29
|
-
logger.info "=> Starting
|
30
|
-
|
38
|
+
def start(num_workers, worker_klass)
|
39
|
+
logger.info "=> Starting #{name} with #{num_workers} worker(s)"
|
40
|
+
|
41
|
+
@master_pid = $$
|
42
|
+
@num_workers = num_workers
|
31
43
|
@worker_klass = worker_klass
|
32
44
|
|
33
|
-
# Check if the worker implements our
|
45
|
+
# Check if the worker implements our interface
|
34
46
|
if !worker_klass.include?(Raemon::Worker)
|
35
47
|
logger.error "** Invalid Raemon worker"
|
48
|
+
logger.close
|
36
49
|
exit
|
37
50
|
end
|
38
51
|
|
39
|
-
#
|
40
|
-
|
41
|
-
|
42
|
-
# Setup signals for the master process
|
43
|
-
setup_signals
|
44
|
-
|
45
|
-
# Wait for all the workers
|
46
|
-
Process.waitall
|
47
|
-
|
48
|
-
logger.close
|
52
|
+
# Start the master loop which spawns and monitors workers
|
53
|
+
master_loop!
|
49
54
|
end
|
50
55
|
|
51
|
-
|
52
|
-
|
56
|
+
# Terminates all workers, but does not exit master process
|
57
|
+
def stop(graceful=true)
|
58
|
+
kill_each_worker(graceful ? :QUIT : :TERM)
|
59
|
+
timeleft = timeout
|
60
|
+
step = 0.2
|
61
|
+
reap_all_workers
|
62
|
+
until WORKERS.empty?
|
63
|
+
sleep(step)
|
64
|
+
reap_all_workers
|
65
|
+
(timeleft -= step) > 0 and next
|
66
|
+
kill_each_worker(:KILL)
|
67
|
+
end
|
53
68
|
end
|
54
69
|
|
55
|
-
def
|
56
|
-
|
70
|
+
def worker_heartbeat!(worker)
|
71
|
+
return if timeout <= 0
|
72
|
+
@last_pulse ||= 0
|
57
73
|
|
58
|
-
|
74
|
+
begin
|
75
|
+
if Time.now.to_i > @last_pulse + (timeout/2)
|
76
|
+
# Pulse (our lifeline to the master process)
|
77
|
+
worker.pulse.chmod(@last_pulse = Time.now.to_i)
|
78
|
+
|
79
|
+
# Make sure master is still around otherwise exit
|
80
|
+
master_pid == Process.ppid or return
|
81
|
+
end
|
82
|
+
rescue => ex
|
83
|
+
if worker.pulse
|
84
|
+
logger.error "Unhandled listen loop exception #{ex.inspect}."
|
85
|
+
logger.error ex.backtrace.join("\n")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
59
89
|
|
60
|
-
|
61
|
-
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
# list of signals we care about and trap in master.
|
94
|
+
QUEUE_SIGS = [ :WINCH, :QUIT, :INT, :TERM, :USR1, :USR2, :HUP, :TTIN, :TTOU ]
|
95
|
+
|
96
|
+
|
97
|
+
# monitors children and receives signals forever
|
98
|
+
# (or until a termination signal is sent). This handles signals
|
99
|
+
# one-at-a-time time and we'll happily drop signals in case somebody
|
100
|
+
# is signalling us too often.
|
101
|
+
def master_loop!
|
102
|
+
# this pipe is used to wake us up from select(2) in #join when signals
|
103
|
+
# are trapped. See trap_deferred
|
104
|
+
init_self_pipe!
|
105
|
+
respawn = true
|
106
|
+
|
107
|
+
QUEUE_SIGS.each { |sig| trap_deferred(sig) }
|
108
|
+
trap(:CHLD) { |sig_nr| awaken_master }
|
109
|
+
|
110
|
+
process_name 'master'
|
111
|
+
logger.info "master process ready"
|
112
|
+
|
113
|
+
# Spawn workers for the first time
|
114
|
+
maintain_worker_count
|
115
|
+
|
116
|
+
begin
|
117
|
+
loop do
|
118
|
+
monitor_memory_usage
|
119
|
+
reap_all_workers
|
62
120
|
|
63
|
-
|
64
|
-
|
65
|
-
|
121
|
+
case SIG_QUEUE.shift
|
122
|
+
when nil
|
123
|
+
murder_lazy_workers
|
124
|
+
maintain_worker_count if respawn
|
125
|
+
master_sleep
|
126
|
+
when :QUIT # graceful shutdown
|
127
|
+
break
|
128
|
+
when :TERM, :INT # immediate shutdown
|
129
|
+
stop(false)
|
130
|
+
break
|
131
|
+
when :USR1
|
132
|
+
kill_each_worker(:USR1)
|
133
|
+
when :USR2
|
134
|
+
kill_each_worker(:USR2)
|
135
|
+
when :WINCH
|
136
|
+
if Process.ppid == 1 || Process.getpgrp != $$
|
137
|
+
respawn = false
|
138
|
+
logger.info "gracefully stopping all workers"
|
139
|
+
kill_each_worker(:QUIT)
|
140
|
+
else
|
141
|
+
logger.info "SIGWINCH ignored because we're not daemonized"
|
142
|
+
end
|
143
|
+
when :TTIN
|
144
|
+
@num_workers += 1
|
145
|
+
when :TTOU
|
146
|
+
@num_workers -= 1 if @num_workers > 0
|
147
|
+
when :HUP
|
148
|
+
# TODO: should restart the workers, but a :QUIT could stall
|
149
|
+
# respawn = true
|
150
|
+
# kill_each_worker(:QUIT)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
rescue Errno::EINTR
|
154
|
+
retry
|
155
|
+
rescue => ex
|
156
|
+
logger.error "Unhandled master loop exception #{ex.inspect}."
|
157
|
+
logger.error ex.backtrace.join("\n")
|
158
|
+
retry
|
159
|
+
end
|
160
|
+
|
161
|
+
# Gracefully shutdown all workers on our way out
|
162
|
+
stop
|
163
|
+
logger.info "master complete"
|
164
|
+
|
165
|
+
# Close resources
|
166
|
+
unlink_pid_safe(pid_file) if pid_file
|
167
|
+
logger.close
|
168
|
+
end
|
169
|
+
|
170
|
+
|
171
|
+
# defer a signal for later processing in #master_loop! (master process)
|
172
|
+
def trap_deferred(signal)
|
173
|
+
trap(signal) do |sig_nr|
|
174
|
+
if SIG_QUEUE.size < 5
|
175
|
+
SIG_QUEUE << signal
|
176
|
+
awaken_master
|
177
|
+
else
|
178
|
+
logger.error "ignoring SIG#{signal}, queue=#{SIG_QUEUE.inspect}"
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
66
182
|
|
67
|
-
|
68
|
-
|
183
|
+
# wait for a signal hander to wake us up and then consume the pipe
|
184
|
+
# Wake up every second anyways to run murder_lazy_workers
|
185
|
+
def master_sleep
|
186
|
+
begin
|
187
|
+
ready = IO.select([SELF_PIPE.first], nil, nil, 1) or return
|
188
|
+
ready.first && ready.first.first or return
|
189
|
+
loop { SELF_PIPE.first.read_nonblock(CHUNK_SIZE) }
|
190
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
191
|
+
end
|
192
|
+
end
|
69
193
|
|
70
|
-
|
71
|
-
|
194
|
+
def awaken_master
|
195
|
+
begin
|
196
|
+
SELF_PIPE.last.write_nonblock('.') # wakeup master process from select
|
197
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
198
|
+
# pipe is full, master should wake up anyways
|
199
|
+
retry
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# reaps all unreaped workers
|
204
|
+
def reap_all_workers
|
205
|
+
begin
|
206
|
+
loop do
|
207
|
+
wpid, status = Process.waitpid2(-1, Process::WNOHANG)
|
208
|
+
wpid or break
|
209
|
+
worker = WORKERS.delete(wpid) and worker.pulse.close rescue nil
|
210
|
+
logger.info "reaped #{status.inspect} " \
|
211
|
+
"worker=#{worker.id rescue 'unknown'}"
|
212
|
+
end
|
213
|
+
rescue Errno::ECHILD
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# forcibly terminate all workers that haven't checked in in timeout
|
218
|
+
# seconds. The timeout is implemented using an unlinked File
|
219
|
+
# shared between the parent process and each worker. The worker
|
220
|
+
# runs File#chmod to modify the ctime of the File. If the ctime
|
221
|
+
# is stale for >timeout seconds, then we'll kill the corresponding
|
222
|
+
# worker.
|
223
|
+
def murder_lazy_workers
|
224
|
+
return if timeout <= 0
|
225
|
+
|
226
|
+
diff = stat = nil
|
227
|
+
|
228
|
+
WORKERS.dup.each_pair do |wpid, worker|
|
229
|
+
begin
|
230
|
+
stat = worker.pulse.stat
|
231
|
+
rescue => ex
|
232
|
+
logger.warn "worker=#{worker.id} PID:#{wpid} stat error: #{ex.inspect}"
|
233
|
+
kill_worker(:QUIT, wpid)
|
234
|
+
next
|
235
|
+
end
|
236
|
+
stat.mode == 0100000 and next
|
237
|
+
(diff = (Time.now - stat.ctime)) <= timeout and next
|
238
|
+
logger.error "worker=#{worker.id} PID:#{wpid} timeout " \
|
239
|
+
"(#{diff}s > #{timeout}s), killing"
|
240
|
+
kill_worker(:KILL, wpid) # take no prisoners for timeout violations
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# Spawn workers, and initalize new workers if some are
|
245
|
+
# no longer running
|
246
|
+
def spawn_workers
|
247
|
+
(0...num_workers).each do |id|
|
248
|
+
WORKERS.values.include?(id) and next
|
249
|
+
worker = worker_klass.new(self, id, Raemon::Util.tmpio)
|
250
|
+
|
251
|
+
# Fork the worker processes wrapped in the worker loop
|
252
|
+
WORKERS[fork { worker_loop!(worker) }] = worker
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def maintain_worker_count
|
257
|
+
off = num_workers - WORKERS.size
|
258
|
+
|
259
|
+
if off.zero?
|
260
|
+
return
|
261
|
+
elsif off == num_workers
|
262
|
+
# None of the workers are running, lets be gentle
|
263
|
+
@spawn_attempts ||= 0
|
264
|
+
sleep 1 if @spawn_attempts > 1
|
265
|
+
if timeout > 0 && @spawn_attempts > timeout
|
266
|
+
# We couldn't get the workers going after timeout
|
267
|
+
# seconds, so let's assume this will never work
|
268
|
+
logger.error "Unable to spawn workers after #{@spawn_attempts} attempts"
|
269
|
+
master_quit
|
270
|
+
return
|
271
|
+
end
|
272
|
+
@spawn_attempts += 1
|
273
|
+
else
|
274
|
+
@spawn_attempts = nil
|
275
|
+
end
|
276
|
+
|
277
|
+
return spawn_workers if off > 0
|
278
|
+
|
279
|
+
WORKERS.dup.each_pair do |wpid, worker|
|
280
|
+
worker.id >= num_workers && kill_worker(:QUIT, wpid) rescue nil
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
# gets rid of stuff the worker has no business keeping track of
|
285
|
+
# to free some resources and drops all sig handlers.
|
286
|
+
# traps for USR1, USR2, and HUP may be set in the after_fork Proc
|
287
|
+
# by the user.
|
288
|
+
def init_worker_process(worker)
|
289
|
+
QUEUE_SIGS.each { |sig| trap(sig, nil) }
|
290
|
+
trap(:CHLD, 'DEFAULT')
|
291
|
+
SIG_QUEUE.clear
|
292
|
+
process_name "worker[#{worker.id}]"
|
293
|
+
|
294
|
+
init_self_pipe!
|
295
|
+
WORKERS.values.each { |other_worker| other_worker.pulse.close rescue nil }
|
296
|
+
WORKERS.clear
|
297
|
+
|
298
|
+
worker.pulse.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
299
|
+
@timeout /= 2.0 # halve it for select
|
300
|
+
end
|
72
301
|
|
73
|
-
|
74
|
-
|
75
|
-
|
302
|
+
# runs inside each forked worker, this sits around and waits
|
303
|
+
# for connections and doesn't die until the parent dies (or is
|
304
|
+
# given a INT, QUIT, or TERM signal)
|
305
|
+
def worker_loop!(worker)
|
306
|
+
init_worker_process(worker)
|
307
|
+
|
308
|
+
# Graceful shutdown
|
309
|
+
trap(:QUIT) do
|
310
|
+
worker.stop if worker.respond_to?(:stop)
|
311
|
+
exit!(0)
|
312
|
+
end
|
313
|
+
|
314
|
+
# Immediate termination
|
315
|
+
[:TERM, :INT].each { |sig| trap(sig) { exit!(0) } }
|
316
|
+
|
317
|
+
# Worker start
|
318
|
+
logger.info "worker=#{worker.id} ready"
|
319
|
+
worker.start if worker.respond_to?(:start)
|
320
|
+
|
321
|
+
# Worker run loop
|
322
|
+
worker.run
|
323
|
+
end
|
324
|
+
|
325
|
+
# delivers a signal to a worker and fails gracefully if the worker
|
326
|
+
# is no longer running.
|
327
|
+
def kill_worker(signal, wpid)
|
328
|
+
begin
|
329
|
+
Process.kill(signal, wpid)
|
330
|
+
rescue Errno::ESRCH
|
331
|
+
worker = WORKERS.delete(wpid) and worker.pulse.close rescue nil
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
# delivers a signal to each worker
|
336
|
+
def kill_each_worker(signal)
|
337
|
+
WORKERS.keys.each { |wpid| kill_worker(signal, wpid) }
|
338
|
+
end
|
339
|
+
|
340
|
+
# Make the master quit
|
341
|
+
def master_quit
|
342
|
+
SIG_QUEUE << :QUIT
|
343
|
+
end
|
344
|
+
|
345
|
+
# unlinks a PID file at given +path+ if it contains the current PID
|
346
|
+
# still potentially racy without locking the directory (which is
|
347
|
+
# non-portable and may interact badly with other programs), but the
|
348
|
+
# window for hitting the race condition is small
|
349
|
+
def unlink_pid_safe(path)
|
350
|
+
(File.read(path).to_i == $$ and File.unlink(path)) rescue nil
|
351
|
+
end
|
352
|
+
|
353
|
+
# returns a PID if a given path contains a non-stale PID file,
|
354
|
+
# nil otherwise.
|
355
|
+
def valid_pid?(path)
|
356
|
+
wpid = File.read(path).to_i
|
357
|
+
wpid <= 0 and return
|
358
|
+
begin
|
359
|
+
Process.kill(0, wpid) # send null signal to check if its alive
|
360
|
+
return wpid
|
361
|
+
rescue Errno::ESRCH
|
362
|
+
# don't unlink stale pid files, racy without non-portable locking...
|
363
|
+
end
|
364
|
+
rescue Errno::ENOENT
|
365
|
+
end
|
366
|
+
|
367
|
+
def process_name(tag)
|
368
|
+
$0 = "#{name} #{tag}"
|
369
|
+
end
|
370
|
+
|
371
|
+
def init_self_pipe!
|
372
|
+
SELF_PIPE.each { |io| io.close rescue nil }
|
373
|
+
SELF_PIPE.replace(IO.pipe)
|
374
|
+
SELF_PIPE.each { |io| io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
|
375
|
+
end
|
376
|
+
|
377
|
+
def daemonize
|
378
|
+
exit if Process.fork
|
379
|
+
|
380
|
+
Process.setsid
|
381
|
+
|
382
|
+
Dir.chdir '/'
|
383
|
+
File.umask 0000
|
384
|
+
|
385
|
+
STDIN.reopen '/dev/null'
|
386
|
+
STDOUT.reopen '/dev/null', 'a'
|
387
|
+
STDERR.reopen '/dev/null', 'a'
|
388
|
+
|
389
|
+
File.open(pid_file, 'w') { |f| f.puts(Process.pid) } if pid_file
|
390
|
+
end
|
391
|
+
|
392
|
+
# Check memory usage every 60 seconds if a memory limit is enforced
|
393
|
+
def monitor_memory_usage
|
394
|
+
return if memory_limit.nil?
|
395
|
+
@last_memory_chk ||= 0
|
396
|
+
|
397
|
+
if @last_memory_chk + 60 < Time.now.to_i
|
398
|
+
@last_memory_chk = Time.now.to_i
|
399
|
+
WORKERS.dup.each_pair do |wpid, worker|
|
400
|
+
if memory_usage(wpid) > (memory_limit*1024)
|
401
|
+
logger.warn "memory limit (#{memory_limit}MB) reached by worker=#{worker.id}"
|
402
|
+
kill_worker(:QUIT, wpid)
|
403
|
+
end
|
404
|
+
end
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
def memory_usage(pid)
|
409
|
+
`ps -o rss= -p #{pid}`.to_i
|
410
|
+
end
|
76
411
|
|
77
412
|
end
|
78
413
|
end
|
data/lib/raemon/server.rb
CHANGED
@@ -16,7 +16,7 @@ module Raemon
|
|
16
16
|
|
17
17
|
# Check if the server is already running
|
18
18
|
if running?
|
19
|
-
|
19
|
+
$stderr.puts "Error: #{server_name} is already running."
|
20
20
|
exit
|
21
21
|
end
|
22
22
|
|
@@ -25,15 +25,18 @@ module Raemon
|
|
25
25
|
|
26
26
|
worker_klass = instance_eval(config.worker_klass)
|
27
27
|
|
28
|
-
Raemon::Master.
|
29
|
-
:
|
30
|
-
:
|
31
|
-
:
|
28
|
+
Raemon::Master.start config.num_workers, worker_klass, {
|
29
|
+
:name => server_name,
|
30
|
+
:pid_file => pid_file,
|
31
|
+
:detach => config.detach,
|
32
|
+
:logger => config.logger,
|
33
|
+
:timeout => config.timeout,
|
34
|
+
:memory_limit => config.memory_limit
|
32
35
|
}
|
33
36
|
end
|
34
37
|
|
35
38
|
def shutdown!
|
36
|
-
Raemon::Master.
|
39
|
+
Raemon::Master.stop :pid_file => pid_file
|
37
40
|
end
|
38
41
|
|
39
42
|
def console!
|
@@ -101,13 +104,15 @@ module Raemon
|
|
101
104
|
end
|
102
105
|
|
103
106
|
def running?
|
104
|
-
|
105
|
-
|
107
|
+
pid = File.read(pid_file).to_i rescue 0
|
108
|
+
Process.kill(0, pid) if pid > 0
|
109
|
+
rescue Errno::ESRCH
|
106
110
|
end
|
107
111
|
end
|
108
112
|
|
109
113
|
class Configuration
|
110
|
-
ATTRIBUTES = [ :name, :detach, :
|
114
|
+
ATTRIBUTES = [ :name, :detach, :num_workers, :worker_klass,
|
115
|
+
:log_level, :logger, :timeout, :memory_limit ]
|
111
116
|
|
112
117
|
attr_accessor *ATTRIBUTES
|
113
118
|
|
data/lib/raemon/util.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module Raemon
|
2
|
+
class Util
|
3
|
+
class << self
|
4
|
+
|
5
|
+
# Creates and returns a new File object. The File is unlinked
|
6
|
+
# immediately, switched to binary mode, and userspace output
|
7
|
+
# buffering is disabled. Method pulled from Unicorn library.
|
8
|
+
def tmpio
|
9
|
+
begin
|
10
|
+
fp = File.open("#{Dir::tmpdir}/#{rand}",
|
11
|
+
File::RDWR | File::CREAT | File::EXCL, 0600)
|
12
|
+
rescue Errno::EEXIST
|
13
|
+
retry
|
14
|
+
end
|
15
|
+
File.unlink(fp.path)
|
16
|
+
fp.binmode
|
17
|
+
fp.sync = true
|
18
|
+
fp
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/raemon/worker.rb
CHANGED
@@ -2,52 +2,32 @@ module Raemon
|
|
2
2
|
module Worker
|
3
3
|
|
4
4
|
def self.included(base)
|
5
|
-
base.send :extend, ClassMethods
|
6
5
|
base.send :include, InstanceMethods
|
7
6
|
end
|
8
7
|
|
9
|
-
module ClassMethods
|
10
|
-
def start!(master=nil)
|
11
|
-
child_pid = Process.fork do
|
12
|
-
# Child process
|
13
|
-
worker = new(master)
|
14
|
-
worker.execute
|
15
|
-
end
|
16
|
-
|
17
|
-
# Parent returns the worker's pid
|
18
|
-
return child_pid
|
19
|
-
end
|
20
|
-
|
21
|
-
def stop!(worker_pid)
|
22
|
-
Process.kill('QUIT', worker_pid) rescue nil
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
8
|
module InstanceMethods
|
27
|
-
attr_reader :logger
|
9
|
+
attr_reader :master, :logger, :id, :pid, :pulse
|
28
10
|
|
29
|
-
def initialize(master
|
11
|
+
def initialize(master, id, pulse)
|
30
12
|
@master = master
|
31
|
-
@logger = master.logger
|
32
|
-
|
33
|
-
|
34
|
-
|
13
|
+
@logger = master.logger
|
14
|
+
@id = id
|
15
|
+
@pid = $$
|
16
|
+
@pulse = pulse
|
35
17
|
end
|
36
|
-
|
37
|
-
def
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
trap('TERM', force_quit_block)
|
48
|
-
trap('INT') {} # Reset INT signal handler
|
18
|
+
|
19
|
+
def ==(other_id)
|
20
|
+
@id == other_id
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
raise "Abstract method"
|
25
|
+
end
|
26
|
+
|
27
|
+
def heartbeat!
|
28
|
+
master.worker_heartbeat!(self)
|
49
29
|
end
|
50
30
|
end
|
51
|
-
|
31
|
+
|
52
32
|
end
|
53
33
|
end
|
data/lib/raemon.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
-
require '
|
1
|
+
require 'fcntl'
|
2
|
+
require 'tmpdir'
|
2
3
|
require 'logger'
|
3
4
|
|
4
5
|
module Raemon
|
5
|
-
VERSION = '0.
|
6
|
+
VERSION = '0.2.0'
|
7
|
+
|
8
|
+
autoload :Master, 'raemon/master'
|
9
|
+
autoload :Worker, 'raemon/worker'
|
10
|
+
autoload :Server, 'raemon/server'
|
11
|
+
autoload :Util, 'raemon/util'
|
6
12
|
end
|
7
|
-
|
8
|
-
require 'raemon/server'
|
9
|
-
require 'raemon/master'
|
10
|
-
require 'raemon/worker'
|
data/raemon.gemspec
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{raemon}
|
8
|
+
s.version = "0.2.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Peter Kieltyka"]
|
12
|
+
s.date = %q{2010-03-15}
|
13
|
+
s.description = %q{Raemon is a Ruby framework for building UNIX daemons.}
|
14
|
+
s.email = %q{peter.kieltyka@nulayer.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".gitignore",
|
21
|
+
"LICENSE",
|
22
|
+
"README",
|
23
|
+
"Rakefile",
|
24
|
+
"VERSION",
|
25
|
+
"examples/beanstalk.rb",
|
26
|
+
"examples/evented.rb",
|
27
|
+
"examples/queue.rb",
|
28
|
+
"examples/sampled/bin/sampled",
|
29
|
+
"examples/sampled/config/boot.rb",
|
30
|
+
"examples/sampled/config/environment.rb",
|
31
|
+
"examples/sampled/config/environments/development.rb",
|
32
|
+
"examples/sampled/config/environments/production.rb",
|
33
|
+
"examples/sampled/config/environments/test.rb",
|
34
|
+
"examples/sampled/config/initializers/settings.rb",
|
35
|
+
"examples/sampled/config/settings.yml",
|
36
|
+
"examples/sampled/lib/sampled.rb",
|
37
|
+
"examples/sampled/log/.keep",
|
38
|
+
"examples/sampled/test/.keep",
|
39
|
+
"examples/sampled/tmp/pids/.keep",
|
40
|
+
"examples/sampled/vendor/.keep",
|
41
|
+
"examples/test.rb",
|
42
|
+
"lib/raemon.rb",
|
43
|
+
"lib/raemon/master.rb",
|
44
|
+
"lib/raemon/server.rb",
|
45
|
+
"lib/raemon/util.rb",
|
46
|
+
"lib/raemon/worker.rb",
|
47
|
+
"raemon.gemspec"
|
48
|
+
]
|
49
|
+
s.homepage = %q{http://github.com/pkieltyka/raemon}
|
50
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
51
|
+
s.require_paths = ["lib"]
|
52
|
+
s.rubygems_version = %q{1.3.6}
|
53
|
+
s.summary = %q{Raemon is a Ruby framework for building UNIX daemons.}
|
54
|
+
s.test_files = [
|
55
|
+
"examples/beanstalk.rb",
|
56
|
+
"examples/evented.rb",
|
57
|
+
"examples/queue.rb",
|
58
|
+
"examples/sampled/config/boot.rb",
|
59
|
+
"examples/sampled/config/environment.rb",
|
60
|
+
"examples/sampled/config/environments/development.rb",
|
61
|
+
"examples/sampled/config/environments/production.rb",
|
62
|
+
"examples/sampled/config/environments/test.rb",
|
63
|
+
"examples/sampled/config/initializers/settings.rb",
|
64
|
+
"examples/sampled/lib/sampled.rb",
|
65
|
+
"examples/test.rb"
|
66
|
+
]
|
67
|
+
|
68
|
+
if s.respond_to? :specification_version then
|
69
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
70
|
+
s.specification_version = 3
|
71
|
+
|
72
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
73
|
+
else
|
74
|
+
end
|
75
|
+
else
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
metadata
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: raemon
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 2
|
8
|
+
- 0
|
9
|
+
version: 0.2.0
|
5
10
|
platform: ruby
|
6
11
|
authors:
|
7
12
|
- Peter Kieltyka
|
@@ -9,7 +14,7 @@ autorequire:
|
|
9
14
|
bindir: bin
|
10
15
|
cert_chain: []
|
11
16
|
|
12
|
-
date: 2010-
|
17
|
+
date: 2010-03-15 00:00:00 -04:00
|
13
18
|
default_executable:
|
14
19
|
dependencies: []
|
15
20
|
|
@@ -30,6 +35,7 @@ files:
|
|
30
35
|
- VERSION
|
31
36
|
- examples/beanstalk.rb
|
32
37
|
- examples/evented.rb
|
38
|
+
- examples/queue.rb
|
33
39
|
- examples/sampled/bin/sampled
|
34
40
|
- examples/sampled/config/boot.rb
|
35
41
|
- examples/sampled/config/environment.rb
|
@@ -39,6 +45,7 @@ files:
|
|
39
45
|
- examples/sampled/config/initializers/settings.rb
|
40
46
|
- examples/sampled/config/settings.yml
|
41
47
|
- examples/sampled/lib/sampled.rb
|
48
|
+
- examples/sampled/log/.keep
|
42
49
|
- examples/sampled/test/.keep
|
43
50
|
- examples/sampled/tmp/pids/.keep
|
44
51
|
- examples/sampled/vendor/.keep
|
@@ -46,7 +53,9 @@ files:
|
|
46
53
|
- lib/raemon.rb
|
47
54
|
- lib/raemon/master.rb
|
48
55
|
- lib/raemon/server.rb
|
56
|
+
- lib/raemon/util.rb
|
49
57
|
- lib/raemon/worker.rb
|
58
|
+
- raemon.gemspec
|
50
59
|
has_rdoc: true
|
51
60
|
homepage: http://github.com/pkieltyka/raemon
|
52
61
|
licenses: []
|
@@ -60,24 +69,27 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
60
69
|
requirements:
|
61
70
|
- - ">="
|
62
71
|
- !ruby/object:Gem::Version
|
72
|
+
segments:
|
73
|
+
- 0
|
63
74
|
version: "0"
|
64
|
-
version:
|
65
75
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
66
76
|
requirements:
|
67
77
|
- - ">="
|
68
78
|
- !ruby/object:Gem::Version
|
79
|
+
segments:
|
80
|
+
- 0
|
69
81
|
version: "0"
|
70
|
-
version:
|
71
82
|
requirements: []
|
72
83
|
|
73
84
|
rubyforge_project:
|
74
|
-
rubygems_version: 1.3.
|
85
|
+
rubygems_version: 1.3.6
|
75
86
|
signing_key:
|
76
87
|
specification_version: 3
|
77
88
|
summary: Raemon is a Ruby framework for building UNIX daemons.
|
78
89
|
test_files:
|
79
90
|
- examples/beanstalk.rb
|
80
91
|
- examples/evented.rb
|
92
|
+
- examples/queue.rb
|
81
93
|
- examples/sampled/config/boot.rb
|
82
94
|
- examples/sampled/config/environment.rb
|
83
95
|
- examples/sampled/config/environments/development.rb
|
@@ -85,6 +97,4 @@ test_files:
|
|
85
97
|
- examples/sampled/config/environments/test.rb
|
86
98
|
- examples/sampled/config/initializers/settings.rb
|
87
99
|
- examples/sampled/lib/sampled.rb
|
88
|
-
- examples/sampled/test/example_test.rb
|
89
|
-
- examples/sampled/test/test_helper.rb
|
90
100
|
- examples/test.rb
|