jobby 0.2.0 → 0.3.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.
- 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.
|