jobby 0.2.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/bin/jobby +2 -10
  2. data/lib/runner.rb +14 -18
  3. data/lib/server.rb +89 -34
  4. data/spec/server_spec.rb +28 -26
  5. 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 = "jobby" # set the process name
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.2.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
 
@@ -24,11 +24,7 @@ module Jobby
24
24
 
25
25
  def initialize(options = {})
26
26
  @options = DEFAULT_OPTIONS.merge options
27
- if @options[:wipe]
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
- sleep 2 # give the server time to start
83
- begin
84
- run_client
85
- rescue Errno::ECONNREFUSED
86
- return error("Couldn't connect to the server process")
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
@@ -32,47 +32,51 @@ module Jobby
32
32
  #
33
33
  # ==Stopping the server
34
34
  #
35
- # A client process can send one of two special strings to stop the server.
35
+ # There are two built-in ways of stopping the server:
36
36
  #
37
- # "||JOBBY FLUSH||" will stop the server forking any more children and shut
38
- # it down.
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
- # "||JOBBY WIPE||" will stop the server forking any more children, kill 9
41
- # any existing children and shut it down.
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 USR1 signals as notification that the logfile has been
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 USR1 signals on
48
- # log rotation.
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 -USR1 -f jobby
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
- @prerun = prerun
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
- connect_to_socket_and_start_logging
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
- $0 = "jobby: child" # set the process name
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 connect_to_socket_and_start_logging
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
- @logger.info "USR1 received, rotating log file"
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 "Flush received - terminating server"
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 "Wipe received - terminating forked children"
242
+ @logger.info "Terminating forked children"
188
243
  @pids.each do |pid|
189
244
  begin
190
- Process.kill 9, pid
245
+ Process.kill 15, pid
191
246
  rescue Errno::ESRCH
192
247
  end
193
248
  end
@@ -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, "a+")
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(1)
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 USR1 signal" do
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 "USR1", @server_pid
102
+ Process.kill "HUP", @server_pid
99
103
  sleep 0.2
100
- File.read(@log_filepath).should match(/USR1 received, rotating log file/)
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 flush command from the client and terminate while the children continue processing" do
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 2
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 1
159
- Jobby::Client.new(@socket) { |c| c.send("||JOBBY FLUSH||") }
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
- end
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.2.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-07-09 00:00:00 +01:00
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
- - lib/client.rb
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.1
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.