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