jobby 0.2.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/jobby +2 -10
- data/lib/runner.rb +14 -18
- data/lib/server.rb +89 -34
- data/spec/server_spec.rb +28 -26
- metadata +5 -5
data/bin/jobby
CHANGED
@@ -22,7 +22,7 @@ require "#{File.dirname(__FILE__)}/../lib/server"
|
|
22
22
|
require "#{File.dirname(__FILE__)}/../lib/client"
|
23
23
|
require "#{File.dirname(__FILE__)}/../lib/runner"
|
24
24
|
|
25
|
-
$0 = "
|
25
|
+
$0 = "#{$0} #{ARGV.join(" ")}" # set the process name
|
26
26
|
|
27
27
|
options = {}
|
28
28
|
OptionParser.new do |opts|
|
@@ -38,7 +38,7 @@ OptionParser.new do |opts|
|
|
38
38
|
end
|
39
39
|
|
40
40
|
opts.on("--version", "Show version") do
|
41
|
-
puts "0.
|
41
|
+
puts "0.3.1"
|
42
42
|
exit
|
43
43
|
end
|
44
44
|
|
@@ -50,14 +50,6 @@ OptionParser.new do |opts|
|
|
50
50
|
options[:input] = input
|
51
51
|
end
|
52
52
|
|
53
|
-
opts.on("-f", "--flush", "Shutdown the server on the socket specified by --socket") do |flush|
|
54
|
-
options[:flush] = flush
|
55
|
-
end
|
56
|
-
|
57
|
-
opts.on("-w", "--wipe", "Shutdown the server and terminate the children immediately on the socket specified by --socket") do |wipe|
|
58
|
-
options[:wipe] = wipe
|
59
|
-
end
|
60
|
-
|
61
53
|
opts.separator ""
|
62
54
|
opts.separator "Server options:"
|
63
55
|
|
data/lib/runner.rb
CHANGED
@@ -24,11 +24,7 @@ module Jobby
|
|
24
24
|
|
25
25
|
def initialize(options = {})
|
26
26
|
@options = DEFAULT_OPTIONS.merge options
|
27
|
-
if @options[:
|
28
|
-
@options[:input] = "||JOBBY WIPE||"
|
29
|
-
elsif @options[:flush]
|
30
|
-
@options[:input] = "||JOBBY FLUSH||"
|
31
|
-
elsif @options[:input].nil?
|
27
|
+
if @options[:input].nil?
|
32
28
|
message "--input not supplied, reading from STDIN (use ctrl-d to end input)"
|
33
29
|
@options[:input] = $stdin.read
|
34
30
|
end
|
@@ -58,20 +54,12 @@ module Jobby
|
|
58
54
|
def run
|
59
55
|
change_process_ownership
|
60
56
|
begin
|
61
|
-
if @options[:flush] or @options[:wipe]
|
62
|
-
if @options[:wipe]
|
63
|
-
message "Stopping Jobby server and terminating forked children..."
|
64
|
-
else
|
65
|
-
message "Stopping Jobby server..."
|
66
|
-
end
|
67
|
-
end
|
68
57
|
run_client
|
69
58
|
rescue Errno::EACCES => exception
|
70
59
|
return error(exception.message)
|
71
60
|
rescue Errno::ENOENT, Errno::ECONNREFUSED
|
72
61
|
# Connect failed, fork and start the server process
|
73
62
|
message "There doesn't seem to be a server listening on #{@options[:socket]} - starting one..." if @options[:verbose]
|
74
|
-
exit if @options[:flush] or @options[:wipe]
|
75
63
|
fork do
|
76
64
|
begin
|
77
65
|
Jobby::Server.new(@options[:socket], @options[:max_child_processes], @options[:log], @options[:prerun_proc]).run(&get_proc_from_options)
|
@@ -79,11 +67,19 @@ module Jobby
|
|
79
67
|
return error(exception.message)
|
80
68
|
end
|
81
69
|
end
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
70
|
+
# give the server 30 seconds to start
|
71
|
+
@connected = false
|
72
|
+
60.times do
|
73
|
+
begin
|
74
|
+
run_client
|
75
|
+
@connected = true
|
76
|
+
break
|
77
|
+
rescue Errno::ECONNREFUSED, Errno::EACCES, Errno::ENOENT
|
78
|
+
sleep 0.5
|
79
|
+
end
|
80
|
+
end
|
81
|
+
unless @connected
|
82
|
+
error "Couldn't connect to the server process after 60 tries"
|
87
83
|
end
|
88
84
|
end
|
89
85
|
end
|
data/lib/server.rb
CHANGED
@@ -32,47 +32,51 @@ module Jobby
|
|
32
32
|
#
|
33
33
|
# ==Stopping the server
|
34
34
|
#
|
35
|
-
#
|
35
|
+
# There are two built-in ways of stopping the server:
|
36
36
|
#
|
37
|
-
#
|
38
|
-
#
|
37
|
+
# SIGUSR1 will stop the server accepting any more connections, but it
|
38
|
+
# will continue to fork if there are any requests in the queue.
|
39
|
+
# It will then wait for the children to exit before terminating.
|
39
40
|
#
|
40
|
-
#
|
41
|
-
#
|
41
|
+
# SIGTERM will stop the server forking any more children, kill 9 any
|
42
|
+
# existing children and terminate it.
|
42
43
|
#
|
43
44
|
# ==Log rotation
|
44
45
|
#
|
45
|
-
# The server can receive
|
46
|
+
# The server can receive SIGHUP as notification that the logfile has been
|
46
47
|
# rotated. This will close and re-open the handle to the log file. Since the
|
47
|
-
# server process forks to produce children, they too can handle
|
48
|
-
#
|
48
|
+
# server process forks to produce children, they too can handle SIGHUP on log
|
49
|
+
# rotation.
|
49
50
|
#
|
50
51
|
# To tell all Jobby processes that the log file has been rotated, use something
|
51
52
|
# like:
|
52
53
|
#
|
53
|
-
# % pkill -
|
54
|
+
# % pkill -HUP -f jobby
|
54
55
|
#
|
55
56
|
class Server
|
57
|
+
# The log parameter can be either a filepath or an IO object.
|
56
58
|
def initialize(socket_path, max_forked_processes, log, prerun = nil)
|
59
|
+
$0 = "jobbyd: #{socket_path}" # set the process name
|
60
|
+
@log = log.path rescue log
|
61
|
+
reopen_standard_streams
|
62
|
+
close_fds
|
63
|
+
start_logging
|
57
64
|
@socket_path = socket_path
|
58
65
|
@max_forked_processes = max_forked_processes.to_i
|
59
|
-
@log = log
|
60
66
|
@queue = Queue.new
|
61
|
-
|
67
|
+
setup_signal_handling
|
68
|
+
prerun.call(@logger) unless prerun.nil?
|
62
69
|
end
|
63
70
|
|
64
71
|
# Starts the server and listens for connections. The specified block is run in
|
65
72
|
# the child processes. When a connection is received, the input parameter is
|
66
73
|
# immediately added to the queue.
|
67
74
|
def run(&block)
|
68
|
-
|
75
|
+
try_to_connect_to_socket
|
69
76
|
unless block_given?
|
70
77
|
@logger.error "No block given, exiting"
|
71
78
|
terminate
|
72
79
|
end
|
73
|
-
if @prerun
|
74
|
-
@prerun.call(@logger)
|
75
|
-
end
|
76
80
|
start_forking_thread(block)
|
77
81
|
loop do
|
78
82
|
client = @socket.accept
|
@@ -80,20 +84,58 @@ module Jobby
|
|
80
84
|
while bytes = client.read(128)
|
81
85
|
input += bytes
|
82
86
|
end
|
83
|
-
if input == "||JOBBY FLUSH||"
|
84
|
-
terminate
|
85
|
-
elsif input == "||JOBBY WIPE||"
|
86
|
-
terminate_children
|
87
|
-
terminate
|
88
|
-
else
|
89
|
-
@queue << input
|
90
|
-
end
|
91
87
|
client.close
|
88
|
+
@queue << input
|
92
89
|
end
|
93
90
|
end
|
94
91
|
|
95
92
|
protected
|
96
93
|
|
94
|
+
# Reopens STDIN (/dev/null), STDOUT and STDERR (both @log).
|
95
|
+
def reopen_standard_streams
|
96
|
+
$stdin.reopen("/dev/null", "r")
|
97
|
+
# @log is either a string or an IO object
|
98
|
+
if @log.respond_to? :close
|
99
|
+
$stdout.reopen(@log)
|
100
|
+
$stderr.reopen(@log)
|
101
|
+
else
|
102
|
+
$stdout.reopen(@log, "w")
|
103
|
+
$stderr.reopen(@log, "w")
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# This closes all file descriptors for this process except STDIN, STDOUT
|
108
|
+
# and STDERR. This is because we might have inherited some FDs from the
|
109
|
+
# calling process, which we don't want.
|
110
|
+
def close_fds
|
111
|
+
Dir.entries("/proc/#{Process.pid}/fd/").each do |file|
|
112
|
+
unless file == '.' or file == '..' or file.to_i < 3
|
113
|
+
IO.new(file.to_i).close rescue nil
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Traps SIGHUP, SIGTERM and SIGUSR1 for log rotation, immediate shutdown
|
119
|
+
# and very pleasant shutdown.
|
120
|
+
def setup_signal_handling
|
121
|
+
Signal.trap("HUP") do
|
122
|
+
@logger.info "HUP signal received"
|
123
|
+
rotate_log
|
124
|
+
end
|
125
|
+
Signal.trap("TERM") do
|
126
|
+
@logger.info "TERM signal received"
|
127
|
+
@socket.close unless @socket.closed?
|
128
|
+
@queue.clear
|
129
|
+
terminate_children
|
130
|
+
terminate
|
131
|
+
end
|
132
|
+
Signal.trap("USR1") do
|
133
|
+
@logger.info "USR1 signal received"
|
134
|
+
wait_for_children_to_return
|
135
|
+
terminate
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
97
139
|
# Runs a thread to manage the forked processes. It will block, waiting for a
|
98
140
|
# child to finish if the maximum number of forked processes are already
|
99
141
|
# running. It will then, read from the queue and fork off a new process.
|
@@ -113,7 +155,14 @@ module Jobby
|
|
113
155
|
# fork and run code that performs the actual work
|
114
156
|
input = @queue.pop
|
115
157
|
@pids << fork do
|
116
|
-
|
158
|
+
@socket.close unless @socket.closed? # inherited from the Jobby::Server
|
159
|
+
# re-trap TERM to simply exit, since it is inherited from the Jobby::Server
|
160
|
+
Signal.trap("TERM") do
|
161
|
+
@logger.info "Terminating child process #{Process.pid}"
|
162
|
+
exit 0
|
163
|
+
end
|
164
|
+
Signal.trap("USR1") {}
|
165
|
+
$0 = "jobby: #{@socket_path}" # set the process name
|
117
166
|
@logger.info "Child process started (#{Process.pid})"
|
118
167
|
block.call(input, @logger)
|
119
168
|
exit 0
|
@@ -128,7 +177,7 @@ module Jobby
|
|
128
177
|
# Checks if a process is already listening on the socket. If not, removes the
|
129
178
|
# socket file (if it's there) and starts a server. Throws an Errno::EADDRINUSE
|
130
179
|
# exception if an existing server is detected.
|
131
|
-
def
|
180
|
+
def try_to_connect_to_socket
|
132
181
|
unless File.exists? @socket_path
|
133
182
|
connect_to_socket
|
134
183
|
else
|
@@ -143,7 +192,6 @@ module Jobby
|
|
143
192
|
connect_to_socket
|
144
193
|
end
|
145
194
|
end
|
146
|
-
start_logging
|
147
195
|
end
|
148
196
|
|
149
197
|
def connect_to_socket
|
@@ -155,9 +203,6 @@ module Jobby
|
|
155
203
|
def start_logging
|
156
204
|
@logger = Logger.new @log
|
157
205
|
@logger.info "Server started at #{Time.now}"
|
158
|
-
Signal.trap("USR1") do
|
159
|
-
rotate_log
|
160
|
-
end
|
161
206
|
end
|
162
207
|
|
163
208
|
def reap_child
|
@@ -165,16 +210,26 @@ module Jobby
|
|
165
210
|
end
|
166
211
|
|
167
212
|
def rotate_log
|
213
|
+
@logger.info "Rotating log file"
|
168
214
|
@logger.close
|
169
215
|
@logger = Logger.new @log
|
170
|
-
|
216
|
+
end
|
217
|
+
|
218
|
+
# Closes the socket and waits for any children to finish before
|
219
|
+
# terminating. New children that are already in the queue may be
|
220
|
+
# still be forked at this stage.
|
221
|
+
def wait_for_children_to_return
|
222
|
+
@socket.close
|
223
|
+
while @pids.length > 0
|
224
|
+
sleep 1
|
225
|
+
end
|
171
226
|
end
|
172
227
|
|
173
228
|
# Cleans up the server and exits the process with a return code 0.
|
174
229
|
def terminate
|
175
230
|
@queue.clear
|
176
|
-
@logger.info "
|
177
|
-
@socket.close
|
231
|
+
@logger.info "Terminating server #{Process.pid}"
|
232
|
+
@socket.close unless @socket.closed?
|
178
233
|
FileUtils.rm(@socket_path, :force => true)
|
179
234
|
exit! 0
|
180
235
|
end
|
@@ -184,10 +239,10 @@ module Jobby
|
|
184
239
|
# immediately, perhaps due to 'runaway' children.
|
185
240
|
def terminate_children
|
186
241
|
@queue.clear
|
187
|
-
@logger.info "
|
242
|
+
@logger.info "Terminating forked children"
|
188
243
|
@pids.each do |pid|
|
189
244
|
begin
|
190
|
-
Process.kill
|
245
|
+
Process.kill 15, pid
|
191
246
|
rescue Errno::ESRCH
|
192
247
|
end
|
193
248
|
end
|
data/spec/server_spec.rb
CHANGED
@@ -1,14 +1,17 @@
|
|
1
1
|
require "#{File.dirname(__FILE__)}/../lib/server"
|
2
2
|
require "#{File.dirname(__FILE__)}/../lib/client"
|
3
3
|
require 'fileutils'
|
4
|
-
$0 = "jobby spec"
|
5
|
-
|
6
4
|
|
7
5
|
# Due to the multi-process nature of these specs, there are a bunch of sleep calls
|
8
6
|
# around. This is, of course, pretty brittle but I can't think of a better way of
|
9
7
|
# handling it. If they start failing 'randomly', I would first start by increasing
|
10
8
|
# the sleep times.
|
11
9
|
|
10
|
+
class Jobby::Server
|
11
|
+
# Redefining STDIN, STDOUT and STDERR makes testing pretty savage
|
12
|
+
def reopen_standard_streams; end
|
13
|
+
end
|
14
|
+
|
12
15
|
describe Jobby::Server do
|
13
16
|
|
14
17
|
def run_server(socket, max_child_processes, log_filepath, prerun = nil, &block)
|
@@ -35,6 +38,7 @@ describe Jobby::Server do
|
|
35
38
|
end
|
36
39
|
|
37
40
|
before :each do
|
41
|
+
$0 = "jobby spec"
|
38
42
|
run_server(@socket, @max_child_processes, @log_filepath) {
|
39
43
|
File.open(@child_filepath, "a+") do |file|
|
40
44
|
file << "#{Process.pid}"
|
@@ -85,19 +89,19 @@ describe Jobby::Server do
|
|
85
89
|
sleep 1
|
86
90
|
io_filepath = File.expand_path("#{File.dirname(__FILE__)}/io_log_test.log")
|
87
91
|
FileUtils.rm io_filepath, :force => true
|
88
|
-
io = File.open(io_filepath, "
|
92
|
+
io = File.open(io_filepath, "w")
|
89
93
|
run_server(@socket, @max_child_processes, io) {}
|
90
94
|
terminate_server
|
91
95
|
sleep 0.5
|
92
|
-
File.readlines(io_filepath).length.should eql(
|
96
|
+
File.readlines(io_filepath).length.should eql(4)
|
93
97
|
FileUtils.rm io_filepath
|
94
98
|
end
|
95
99
|
|
96
|
-
it "should flush and reload the log file when it receieves the
|
100
|
+
it "should flush and reload the log file when it receieves the HUP signal" do
|
97
101
|
FileUtils.rm @log_filepath
|
98
|
-
Process.kill "
|
102
|
+
Process.kill "HUP", @server_pid
|
99
103
|
sleep 0.2
|
100
|
-
File.read(@log_filepath).should match(
|
104
|
+
File.read(@log_filepath).should match(/# Logfile created on/)
|
101
105
|
end
|
102
106
|
|
103
107
|
it "should not run if a block is not given" do
|
@@ -146,35 +150,23 @@ describe Jobby::Server do
|
|
146
150
|
File.readlines(@child_filepath).length.should eql(@max_child_processes + 2)
|
147
151
|
end
|
148
152
|
|
149
|
-
it "should receive a
|
153
|
+
it "should receive a USR1 signal then stop accepting connections and terminate after reaping all child PIDs" do
|
150
154
|
terminate_server
|
151
155
|
sleep 1
|
152
156
|
run_server(@socket, 1, @log_filepath) do
|
153
|
-
sleep
|
157
|
+
sleep 3
|
154
158
|
end
|
155
159
|
2.times do |i|
|
156
160
|
Jobby::Client.new(@socket) { |c| c.send("hiya") }
|
157
161
|
end
|
158
|
-
sleep
|
159
|
-
|
162
|
+
sleep 0.5
|
163
|
+
Process.kill "USR1", @server_pid
|
160
164
|
sleep 1.5
|
165
|
+
lambda { Jobby::Client.new(@socket) { |c| c.send("hello?") } }.should raise_error(Errno::ECONNREFUSED)
|
166
|
+
sleep 5
|
161
167
|
lambda { Jobby::Client.new(@socket) { |c| c.send("hello?") } }.should raise_error(Errno::ENOENT)
|
162
168
|
`pgrep -f 'jobby spec' | wc -l`.strip.should eql("2")
|
163
|
-
|
164
|
-
|
165
|
-
it "should receive a wipe command from the client and terminate, taking the children with it" do
|
166
|
-
terminate_server
|
167
|
-
run_server(@socket, 1, @log_filepath) do
|
168
|
-
sleep 2
|
169
|
-
end
|
170
|
-
2.times do |i|
|
171
|
-
Jobby::Client.new(@socket) { |c| c.send("hiya") }
|
172
|
-
end
|
173
|
-
sleep 1
|
174
|
-
Jobby::Client.new(@socket) { |c| c.send("||JOBBY WIPE||") }
|
175
|
-
sleep 2.5
|
176
|
-
lambda { Jobby::Client.new(@socket) { |c| c.send("hello?") } }.should raise_error(Errno::ENOENT)
|
177
|
-
`pgrep -f 'jobby spec'`.strip.should eql("#{Process.pid}")
|
169
|
+
`pgrep -f 'jobbyd spec' | wc -l`.strip.should eql("1")
|
178
170
|
end
|
179
171
|
|
180
172
|
it "should be able to run a Ruby file before any forking" do
|
@@ -191,4 +183,14 @@ describe Jobby::Server do
|
|
191
183
|
sleep 3
|
192
184
|
File.read(@child_filepath).should eql("preran OK")
|
193
185
|
end
|
186
|
+
|
187
|
+
it "close all file descriptors that might have been inherited from the calling process" do
|
188
|
+
terminate_server
|
189
|
+
f = File.open("spec/file_for_prerunning.rb", "r")
|
190
|
+
run_server(@socket, 1, @log_filepath) do
|
191
|
+
sleep 2
|
192
|
+
end
|
193
|
+
Dir.entries("/proc/#{@server_pid}/fd/").length.should eql(7)
|
194
|
+
f.close
|
195
|
+
end
|
194
196
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jobby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mark Somerville
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2008-
|
12
|
+
date: 2008-11-13 00:00:00 +00:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -23,12 +23,12 @@ extra_rdoc_files:
|
|
23
23
|
- README
|
24
24
|
files:
|
25
25
|
- bin/jobby
|
26
|
+
- lib/client.rb
|
26
27
|
- lib/runner.rb
|
27
28
|
- lib/server.rb
|
28
|
-
-
|
29
|
+
- spec/file_for_prerunning.rb
|
29
30
|
- spec/server_spec.rb
|
30
31
|
- spec/run_all.rb
|
31
|
-
- spec/file_for_prerunning.rb
|
32
32
|
- README
|
33
33
|
has_rdoc: true
|
34
34
|
homepage: http://mark.scottishclimbs.com/
|
@@ -52,7 +52,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
52
52
|
requirements: []
|
53
53
|
|
54
54
|
rubyforge_project: jobby
|
55
|
-
rubygems_version: 1.0
|
55
|
+
rubygems_version: 1.2.0
|
56
56
|
signing_key:
|
57
57
|
specification_version: 2
|
58
58
|
summary: Jobby is a small utility and library for managing running jobs in concurrent processes.
|