taf2-rjqueue 0.1.4
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/LICENSE +471 -0
- data/README +7 -0
- data/Rakefile +80 -0
- data/bin/rjqueue +59 -0
- data/config/database.yml +7 -0
- data/config/jobs.yml +13 -0
- data/lib/jobs/base.rb +58 -0
- data/lib/jobs/client.rb +4 -0
- data/lib/jobs/config.rb +13 -0
- data/lib/jobs/initializer.rb +39 -0
- data/lib/jobs/job.rb +40 -0
- data/lib/jobs/migrate.rb +19 -0
- data/lib/jobs/runnable.rb +26 -0
- data/lib/jobs/scheduler.rb +41 -0
- data/lib/jobs/server.rb +382 -0
- data/lib/jobs/worker.rb +225 -0
- data/tests/jobs/find_file_job.rb +10 -0
- data/tests/jobs/image_thumb_job.rb +28 -0
- data/tests/jobs/simple_job.rb +6 -0
- data/tests/lib/create_test_data.rb +17 -0
- data/tests/lib/image.rb +18 -0
- data/tests/lib/message.rb +3 -0
- data/tests/sample.png +0 -0
- data/tests/test_server.rb +86 -0
- metadata +75 -0
data/lib/jobs/base.rb
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Jobs
|
|
2
|
+
class Base
|
|
3
|
+
attr_reader :record, :logger, :lock
|
|
4
|
+
|
|
5
|
+
def initialize(job_record, logger, lock)
|
|
6
|
+
@record = job_record
|
|
7
|
+
@logger = logger
|
|
8
|
+
@lock = lock
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def execute
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# really run the execute method
|
|
15
|
+
def real_execute
|
|
16
|
+
timer = Time.now
|
|
17
|
+
|
|
18
|
+
res = execute
|
|
19
|
+
|
|
20
|
+
@record.duration = Time.now - timer
|
|
21
|
+
|
|
22
|
+
@record.status = res ? "complete" : "error"
|
|
23
|
+
|
|
24
|
+
rescue Object => e
|
|
25
|
+
# add error details to the record
|
|
26
|
+
@record.details ||= ""
|
|
27
|
+
@record.details << "#{e.message}\n#{e.backtrace.join("\n")}"
|
|
28
|
+
|
|
29
|
+
# log the error
|
|
30
|
+
@logger.error "#{e.message}\n#{e.backtrace.join("\n")}"
|
|
31
|
+
@record.status = "error"
|
|
32
|
+
ensure
|
|
33
|
+
# it's no longer processing, so we must have landed here by error
|
|
34
|
+
@record.status = 'error' if @record.status == 'processing'
|
|
35
|
+
@record.locked = false
|
|
36
|
+
@record.save
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
protected
|
|
40
|
+
|
|
41
|
+
#
|
|
42
|
+
# call a process: capture the command output and status code
|
|
43
|
+
# returns [status, output]
|
|
44
|
+
def call(cmd)
|
|
45
|
+
rd, wr = IO.pipe
|
|
46
|
+
pid = fork do
|
|
47
|
+
$stdout.reopen wr
|
|
48
|
+
$stderr.reopen wr
|
|
49
|
+
$stdin.reopen rd
|
|
50
|
+
exec cmd
|
|
51
|
+
end
|
|
52
|
+
wr.close
|
|
53
|
+
pid, status = Process.wait2(pid)
|
|
54
|
+
[status, rd.read]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/jobs/client.rb
ADDED
data/lib/jobs/config.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
require 'active_record'
|
|
3
|
+
|
|
4
|
+
module Jobs
|
|
5
|
+
class Initializer
|
|
6
|
+
def self.run!(job_root_path,job_config_path,env)
|
|
7
|
+
::ActiveRecord::Base.send(:include,Jobs::Scheduler)
|
|
8
|
+
# define the Jobs::Keys constant, e.g. RAILS_ROOT/config/jobs.yml
|
|
9
|
+
eval %{::Jobs::Keys=HashWithIndifferentAccess.new(YAML.load_file(job_config_path)[env])}
|
|
10
|
+
# define the jobs root e.g. RAILS_ROOT/apps/jobs
|
|
11
|
+
eval %{::Jobs::Root=job_root_path}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.ready?
|
|
15
|
+
defined?(::Jobs::Root) and defined?(::Jobs::Keys)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.rails!
|
|
19
|
+
run! "#{RAILS_ROOT}/app/jobs", "#{RAILS_ROOT}/config/jobs.yml", RAILS_ENV
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Configure client to be selective about which job servers to send work
|
|
23
|
+
#
|
|
24
|
+
# Jobs::Initializer.config do|cfg|
|
|
25
|
+
#
|
|
26
|
+
# cfg[:sphinx] = [ {:host => '192.168.1.102', :port => 4321} ]
|
|
27
|
+
# cfg[:video_encoder] = [ {:host => '192.168.1.102', :port => 4321} ]
|
|
28
|
+
#
|
|
29
|
+
def self.config
|
|
30
|
+
config = {}
|
|
31
|
+
yield config
|
|
32
|
+
eval %{::Jobs::HostMap=config}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.test!
|
|
36
|
+
run! File.join(File.dirname(__FILE__),'..','..','tests','jobs'), File.join(File.dirname(__FILE__),'..','..','config','jobs.yml'), 'test'
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/jobs/job.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Jobs
|
|
2
|
+
class Job < ActiveRecord::Base
|
|
3
|
+
serialize :data
|
|
4
|
+
belongs_to :taskable, :polymorphic => true
|
|
5
|
+
|
|
6
|
+
#
|
|
7
|
+
# check if the job is currently being processed
|
|
8
|
+
#
|
|
9
|
+
# e.g. status == 'processing' or status == 'pending'
|
|
10
|
+
#
|
|
11
|
+
def busy?
|
|
12
|
+
self.status != 'processing' and self.status != 'pending'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def instance(logger_instance, lock)
|
|
16
|
+
load("#{Jobs::Root}/#{name}_job.rb")
|
|
17
|
+
klass = "#{name}_job".camelize.constantize
|
|
18
|
+
klass.new(self,logger_instance, lock)
|
|
19
|
+
rescue Object => e
|
|
20
|
+
logger.error "#{e.message}\n#{e.backtrace.join("\n")}"
|
|
21
|
+
return nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def retry
|
|
25
|
+
return false if locked
|
|
26
|
+
self.status = 'pending'
|
|
27
|
+
signal
|
|
28
|
+
save
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
LockedError = Class.new(::StandardError)
|
|
32
|
+
|
|
33
|
+
def retry!
|
|
34
|
+
raise LockedError if locked
|
|
35
|
+
self.status = 'pending'
|
|
36
|
+
signal
|
|
37
|
+
save!
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/jobs/migrate.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
class CreateJobs < ActiveRecord::Migration
|
|
2
|
+
def self.up
|
|
3
|
+
create_table :jobs, :force => true do |t|
|
|
4
|
+
t.string :name, :null => false
|
|
5
|
+
t.text :data
|
|
6
|
+
t.string :status
|
|
7
|
+
t.datetime :created_at
|
|
8
|
+
t.datetime :updated_at
|
|
9
|
+
t.integer :duration
|
|
10
|
+
t.integer :taskable_id
|
|
11
|
+
t.string :taskable_type
|
|
12
|
+
t.text :details
|
|
13
|
+
t.boolean :locked, :default => false, :null => false
|
|
14
|
+
t.integer :attempts, :default => 0, :null => false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
add_index :jobs, [:status, :locked]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Jobs
|
|
2
|
+
module Runnable
|
|
3
|
+
#
|
|
4
|
+
# returns a sql query condition given the included and excluded
|
|
5
|
+
# jobs for the given process. e.g. jobs.name IN ('job1','job2') and jobs.name NOT IN ('jobs3','jobs5')
|
|
6
|
+
# would allow only job1 and job2 to run on the server, but also deny jobs3 and jobs5
|
|
7
|
+
# in this way the typical use would be to either use the deny or use the allow, but never both together...
|
|
8
|
+
# however the following will handle both...
|
|
9
|
+
#
|
|
10
|
+
def sql_runnable_conditions(allowed,denied)
|
|
11
|
+
conditions = "status='pending' and locked=0 "
|
|
12
|
+
if allowed and !allowed.empty? and denied and !denied.empty? # both have something
|
|
13
|
+
includes = allowed.map{|a| "'#{a}'" }.join(',')
|
|
14
|
+
conditions << %Q( and name IN (#{includes}) )
|
|
15
|
+
excludes = denied.map{|d| "'#{d}'" }.join(',')
|
|
16
|
+
conditions << %Q( and name not IN (#{excludes}) )
|
|
17
|
+
elsif allowed and !allowed.empty? and denied.nil? # only allowed
|
|
18
|
+
includes = allowed.map{|a| "'#{a}'" }.join(',')
|
|
19
|
+
conditions << %Q( and name IN (#{includes}) )
|
|
20
|
+
elsif denied and !denied.empty? and allowed.nil? # only denied
|
|
21
|
+
excludes = denied.map{|d| "'#{d}'" }.join(',')
|
|
22
|
+
conditions << %Q( and name not IN (#{excludes}) )
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require 'socket'
|
|
2
|
+
require 'yaml'
|
|
3
|
+
require 'active_record'
|
|
4
|
+
require 'jobs/job'
|
|
5
|
+
require 'jobs/config'
|
|
6
|
+
|
|
7
|
+
module Jobs
|
|
8
|
+
|
|
9
|
+
module Scheduler
|
|
10
|
+
def schedule(job_name, options={})
|
|
11
|
+
threadable = options.delete(:threadable)
|
|
12
|
+
|
|
13
|
+
job = Jobs::Job.new(:name => job_name.to_s.strip,
|
|
14
|
+
:data => options,
|
|
15
|
+
:taskable_id => self.id,
|
|
16
|
+
:taskable_type => self.class.to_s,
|
|
17
|
+
:status => "pending")
|
|
18
|
+
job.save!
|
|
19
|
+
|
|
20
|
+
signal(job_name,threadable)
|
|
21
|
+
|
|
22
|
+
job.id
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
#
|
|
26
|
+
# send a signal to wake job queue
|
|
27
|
+
#
|
|
28
|
+
# threadable: provides a hint that a threaded worker may be used to run the job
|
|
29
|
+
#
|
|
30
|
+
def signal(job_name=nil,threadable=nil)
|
|
31
|
+
socket = UDPSocket.open
|
|
32
|
+
#socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)
|
|
33
|
+
hosts = ::Jobs::Config.host_for_job(job_name)
|
|
34
|
+
if hosts.is_a?(Array)
|
|
35
|
+
else
|
|
36
|
+
socket.send(threadable ? 't' : 's', 0, hosts[:host], hosts[:port] )
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
end
|
data/lib/jobs/server.rb
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Listen for UDP packets
|
|
3
|
+
#
|
|
4
|
+
# Start up X worker processes, communicates with worker processes by sending USR1 signal
|
|
5
|
+
# and USR2 to kill the process
|
|
6
|
+
#
|
|
7
|
+
require 'yaml'
|
|
8
|
+
require 'socket'
|
|
9
|
+
require 'jobs/runnable'
|
|
10
|
+
|
|
11
|
+
module Jobs
|
|
12
|
+
|
|
13
|
+
class Server
|
|
14
|
+
include Jobs::Runnable
|
|
15
|
+
|
|
16
|
+
def initialize(runpath, config_path, env)
|
|
17
|
+
@runpath = runpath
|
|
18
|
+
@config_path = File.expand_path(config_path)
|
|
19
|
+
@env = env
|
|
20
|
+
@workers = []
|
|
21
|
+
@next_worker = 0
|
|
22
|
+
load_config
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def migrate
|
|
26
|
+
enable_logger
|
|
27
|
+
if @config['preload']
|
|
28
|
+
preload = @config['preload']
|
|
29
|
+
if preload.is_a?(Array)
|
|
30
|
+
preload.each { |f| require f }
|
|
31
|
+
else
|
|
32
|
+
require preload
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
require 'rubygems'
|
|
37
|
+
require 'active_record'
|
|
38
|
+
|
|
39
|
+
@logger.info("[job worker #{@pid}]: establish connection environment with #{@config_path.inspect} and env: #{@env.inspect}")
|
|
40
|
+
@db = YAML.load_file(File.join(File.dirname(@config_path),'database.yml'))[@env]
|
|
41
|
+
ActiveRecord::Base.establish_connection @db
|
|
42
|
+
ActiveRecord::Base.logger = @logger
|
|
43
|
+
#ActiveRecord::Base.logger = Logger.new( "#{@logfile}-db.log" )
|
|
44
|
+
#ActiveRecord::Base.logger = Logger.new( "/dev/null" )
|
|
45
|
+
|
|
46
|
+
# load the jobs/job model
|
|
47
|
+
require 'jobs/job'
|
|
48
|
+
require 'jobs/migrate'
|
|
49
|
+
CreateJobs.up
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def kill
|
|
53
|
+
load_config
|
|
54
|
+
if File.exist?(@pid_file)
|
|
55
|
+
system("kill #{File.read(@pid_file)}")
|
|
56
|
+
else
|
|
57
|
+
puts "Job Queue Pid File not found! Try ps -ef | grep rjqueue"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def run(daemonize=true)
|
|
62
|
+
|
|
63
|
+
@daemonize = daemonize
|
|
64
|
+
if @daemonize
|
|
65
|
+
@child_up = false
|
|
66
|
+
|
|
67
|
+
# use this to determine when all workers are started and the child process is ready to listen for events
|
|
68
|
+
trap("USR1"){ @child_up = true }
|
|
69
|
+
|
|
70
|
+
if File.exist?(@pid_file)
|
|
71
|
+
STDERR.puts "Pid file for job already exists: #{@pid_file}"
|
|
72
|
+
exit 1
|
|
73
|
+
end
|
|
74
|
+
# daemonize, create a pipe to send status to the parent process, after the child has successfully started or failed
|
|
75
|
+
rd, wr = IO.pipe
|
|
76
|
+
|
|
77
|
+
@parent_pid = Process.pid
|
|
78
|
+
|
|
79
|
+
# wait for child process to start running before exiting parent process
|
|
80
|
+
fork do
|
|
81
|
+
rd.close
|
|
82
|
+
Process.setsid
|
|
83
|
+
fork do
|
|
84
|
+
begin
|
|
85
|
+
Process.setsid
|
|
86
|
+
File.open(@pid_file, 'wb') {|f| f << Process.pid}
|
|
87
|
+
Dir.chdir('/')
|
|
88
|
+
File.umask 0000
|
|
89
|
+
STDIN.reopen "/dev/null"
|
|
90
|
+
STDOUT.reopen "/dev/null", "a"
|
|
91
|
+
STDERR.reopen STDOUT
|
|
92
|
+
|
|
93
|
+
# XXX: change back to the runpath... this means if the runpath is removed
|
|
94
|
+
# say by a cap deploy... the daemon will likely die.
|
|
95
|
+
Dir.chdir(@runpath)
|
|
96
|
+
|
|
97
|
+
startup
|
|
98
|
+
|
|
99
|
+
@logger.info "Job process active"
|
|
100
|
+
|
|
101
|
+
wr.write "Listening on udp://#{@host}:#{@port}\n"
|
|
102
|
+
wr.flush
|
|
103
|
+
wr.close # signal to our parent we're up and running, this lets the parent exit
|
|
104
|
+
run_loop
|
|
105
|
+
rescue => e
|
|
106
|
+
if wr.closed?
|
|
107
|
+
@logger.error "#{e.message} #{e.backtrace.join("\n")}"
|
|
108
|
+
else
|
|
109
|
+
wr.write e.message
|
|
110
|
+
wr.write e.backtrace.join("\n")
|
|
111
|
+
wr.write "\n"
|
|
112
|
+
wr.write "ERROR!"
|
|
113
|
+
wr.flush
|
|
114
|
+
wr.close
|
|
115
|
+
end
|
|
116
|
+
ensure
|
|
117
|
+
cleanup
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
wr.close
|
|
121
|
+
end
|
|
122
|
+
wr.close
|
|
123
|
+
output = rd.read
|
|
124
|
+
puts output
|
|
125
|
+
rd.close
|
|
126
|
+
|
|
127
|
+
w = 0
|
|
128
|
+
puts "Waiting for child workers to start..."
|
|
129
|
+
while( !@child_up and w < 20 ) do
|
|
130
|
+
sleep 5
|
|
131
|
+
w+= 1
|
|
132
|
+
end
|
|
133
|
+
if @child_up
|
|
134
|
+
puts "Workers alive"
|
|
135
|
+
else
|
|
136
|
+
puts "Check error log, workers may not be started, or you may be starting so many workers it has taken longer then 10 seconds to start them all. In either event checking the log for details would be the first place to look."
|
|
137
|
+
@parent_pid = nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
exit(1) if output.match(/ERROR/i)
|
|
141
|
+
else
|
|
142
|
+
startup
|
|
143
|
+
run_loop
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
def check_count
|
|
150
|
+
count = 0
|
|
151
|
+
|
|
152
|
+
query = "select count(id) from jobs where #{sql_runnable_conditions(@config['jobs_included'], @config['jobs_excluded'])}"
|
|
153
|
+
@logger.debug("timeout check: #{query.inspect}")
|
|
154
|
+
|
|
155
|
+
res = @conn.query(query)
|
|
156
|
+
|
|
157
|
+
count = res.fetch_row[0].to_i
|
|
158
|
+
count
|
|
159
|
+
rescue Mysql::Error => e
|
|
160
|
+
@logger.error("[jobqueue]: #{e.message}\n#{e.backtrace.join("\n")}")
|
|
161
|
+
count = 1
|
|
162
|
+
db_connect!
|
|
163
|
+
count
|
|
164
|
+
ensure
|
|
165
|
+
res.free if res
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def db_connect!
|
|
169
|
+
# connect to the MySQL server
|
|
170
|
+
dbconf = YAML.load_file(File.join(File.dirname(@config_path),'database.yml'))[@env]
|
|
171
|
+
@conn = Mysql.real_connect(dbconf['host'], dbconf['username'], dbconf['password'], dbconf['database'], (dbconf['port'] or 3306) )
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def run_loop
|
|
175
|
+
|
|
176
|
+
@sleep_time = @wait_time
|
|
177
|
+
|
|
178
|
+
@config['workers'].times {|i| start_worker(i) }
|
|
179
|
+
|
|
180
|
+
unless defined?(Jobs::Initializer) and Jobs::Initializer.ready?
|
|
181
|
+
require 'rubygems'
|
|
182
|
+
require 'jobs/client'
|
|
183
|
+
Jobs::Initializer.run! File.join(@runpath,@config['jobpath']), @config_path, @env
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# job queue master process needs to be aware of the number of jobs pending on timeout
|
|
187
|
+
require 'rubygems'
|
|
188
|
+
require 'mysql'
|
|
189
|
+
|
|
190
|
+
db_connect!
|
|
191
|
+
|
|
192
|
+
begin
|
|
193
|
+
count = check_count
|
|
194
|
+
signal_work(count)
|
|
195
|
+
|
|
196
|
+
begin
|
|
197
|
+
Process.kill("USR1", @parent_pid) if !@parent_pid.nil?
|
|
198
|
+
rescue => e
|
|
199
|
+
@logger.error "#{e.message}\n#{e.backtrace.join("\n")}"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
while(@running) do
|
|
203
|
+
count = 0
|
|
204
|
+
begin
|
|
205
|
+
if IO.select([@sock],[],[],@sleep_time)
|
|
206
|
+
while( (msg = @sock.read_nonblock(1)) ) do # each message is 1 byte ['s','t']
|
|
207
|
+
count += 1 # a new message, increment the request count
|
|
208
|
+
end
|
|
209
|
+
else
|
|
210
|
+
count = check_count
|
|
211
|
+
end
|
|
212
|
+
rescue Errno::EAGAIN => e
|
|
213
|
+
# there is more information pending, but we don't have it hear yet... not sure if this condition happens with UDP
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
if count > 0
|
|
217
|
+
@logger.info("[jobqueue]: received #{count} events")
|
|
218
|
+
# signal workers, distribute work load evenly to each worker
|
|
219
|
+
signal_work(count)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
end
|
|
223
|
+
rescue Object => e
|
|
224
|
+
if @running
|
|
225
|
+
@logger.error "[jobqueue] #{e.message}\n#{e.backtrace.join("\n")}"
|
|
226
|
+
sleep 0.1 # throttle incase things get crazy
|
|
227
|
+
retry
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
@logger.info "Stopping workers"
|
|
232
|
+
@workers.each do|worker|
|
|
233
|
+
stop_worker(worker)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def stop_worker(pid)
|
|
239
|
+
@logger.info "Stopping worker: #{pid}"
|
|
240
|
+
Process.kill('USR2', pid)
|
|
241
|
+
Process.waitpid2(pid,0)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def start_worker(worker_id)
|
|
245
|
+
config = @config
|
|
246
|
+
config_path = @config_path
|
|
247
|
+
logger = @logger
|
|
248
|
+
env = @env
|
|
249
|
+
|
|
250
|
+
rd, wr = IO.pipe
|
|
251
|
+
# startup workers
|
|
252
|
+
@logger.info "starting worker"
|
|
253
|
+
@workers << fork do
|
|
254
|
+
begin
|
|
255
|
+
rd.close
|
|
256
|
+
require 'jobs/worker'
|
|
257
|
+
worker = Jobs::Worker.new(config,@runpath, config_path,logger,env)
|
|
258
|
+
@logger.info "created worker: #{Process.pid}"
|
|
259
|
+
# drop a worker pid
|
|
260
|
+
File.open(@pid_file.gsub(/\.pid$/,"-#{worker_id}.pid"),'wb') {|f| f << Process.pid }
|
|
261
|
+
# signal parent
|
|
262
|
+
wr.write "up"
|
|
263
|
+
wr.close
|
|
264
|
+
worker.listen
|
|
265
|
+
# cleanup pid
|
|
266
|
+
File.unlink @pid_file.gsub(/\.pid$/,"-#{worker_id}.pid")
|
|
267
|
+
rescue Object => e
|
|
268
|
+
msg = "#{e.message}\n#{e.backtrace.join("\n")}"
|
|
269
|
+
if wr.closed?
|
|
270
|
+
@logger.error msg
|
|
271
|
+
else
|
|
272
|
+
wr.write msg
|
|
273
|
+
wr.close
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
wr.close
|
|
278
|
+
@logger.info "waiting for worker to reply"
|
|
279
|
+
msg = rd.read
|
|
280
|
+
rd.close
|
|
281
|
+
@logger.info "worker: #{msg.inspect}"
|
|
282
|
+
raise "Failed to start worker: #{msg.inspect}" if msg != 'up'
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# tell a non-busy worker there is new work
|
|
286
|
+
def signal_work(count) # count is unused
|
|
287
|
+
return if count.nil? or count == 0
|
|
288
|
+
count.times do
|
|
289
|
+
Process.kill( 'USR1', @workers[@next_worker] )
|
|
290
|
+
@next_worker += 1 # simple round robin...
|
|
291
|
+
@next_worker = 0 if @next_worker >= @workers.size
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def startup
|
|
296
|
+
enable_logger
|
|
297
|
+
|
|
298
|
+
# open sock connection
|
|
299
|
+
@sock = bind_socket
|
|
300
|
+
|
|
301
|
+
@logger.info "Job process active"
|
|
302
|
+
@logger.info "Listening on udp://#{@host}:#{@port}\n"
|
|
303
|
+
|
|
304
|
+
@running = true
|
|
305
|
+
|
|
306
|
+
trap('TERM') { shutdown('TERM') }
|
|
307
|
+
trap('INT') { shutdown('INT') }
|
|
308
|
+
trap('HUP') { @logger.info 'ignore HUP' }
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def shutdown(sig)
|
|
312
|
+
@logger.info "trap #{sig}"
|
|
313
|
+
@running = false # toggle the run state
|
|
314
|
+
trap(sig,"SIG_DFL") # turn the signal handler off
|
|
315
|
+
Process.kill("USR2", Process.pid) # resend the signal to trigger and exception in IO.select
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def cleanup
|
|
319
|
+
@logger.info "[jobqueue] Stopping: #{@pid_file.inspect}"
|
|
320
|
+
if File.exist?(@pid_file)
|
|
321
|
+
File.unlink(@pid_file)
|
|
322
|
+
end
|
|
323
|
+
@conn.close if @conn
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def bind_socket
|
|
327
|
+
# listen to udp packets
|
|
328
|
+
sock = UDPSocket.open
|
|
329
|
+
sock.bind(@host, @port)
|
|
330
|
+
sock
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# load the server configuration, and intialize configuration instance variables
|
|
334
|
+
def load_config
|
|
335
|
+
@config = YAML.load_file(@config_path)
|
|
336
|
+
@config = @config[@env]
|
|
337
|
+
if @config['runpath']
|
|
338
|
+
runpath = @config['runpath']
|
|
339
|
+
if !runpath.match(/^\//)
|
|
340
|
+
@runpath = File.expand_path(runpath)
|
|
341
|
+
else
|
|
342
|
+
@runpath = runpath
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
@pid_file = @config['pidfile'] or File.join(@runpath,'log','jobs.pid')
|
|
347
|
+
|
|
348
|
+
if @pid_file and !@pid_file.match(/^\//)
|
|
349
|
+
# make pidfile path absolute
|
|
350
|
+
@pid_file = File.join(@runpath,File.dirname(@pid_file), File.basename(@pid_file))
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# store some common config keys
|
|
354
|
+
@wait_time = @config['wait_time'] || 10
|
|
355
|
+
@port = @config['port'] || 4321
|
|
356
|
+
@host = @config['host'] || '127.0.0.1'
|
|
357
|
+
@threads = @config['threads'] || 10
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# setup logging
|
|
361
|
+
def enable_logger
|
|
362
|
+
require 'logger'
|
|
363
|
+
if @config['logfile']
|
|
364
|
+
@logfile = @config['logfile']
|
|
365
|
+
if @daemonize
|
|
366
|
+
if !@logfile.match(/^\//)
|
|
367
|
+
@logfile = File.join(@runpath, @logfile)
|
|
368
|
+
end
|
|
369
|
+
@logger = Logger.new( @logfile )
|
|
370
|
+
else
|
|
371
|
+
@logger = Logger.new(STDOUT)
|
|
372
|
+
end
|
|
373
|
+
else
|
|
374
|
+
@logger = Logger.new( '/dev/null' )
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
@logger.level = Logger::ERROR if @env == 'production'
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
end
|