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/worker.rb
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Worker Process
|
|
3
|
+
#
|
|
4
|
+
# Loads whatever environment you need running to manage and run jobs.
|
|
5
|
+
#
|
|
6
|
+
# A master process listens for UDP Packets and then delegates to a worker process
|
|
7
|
+
# The master keep track of how busy the worker processes are.
|
|
8
|
+
#
|
|
9
|
+
# Besides monitoring the child processes are forked by the master process. The master process
|
|
10
|
+
# can then use signals to trigger the worker to check for new work.
|
|
11
|
+
#
|
|
12
|
+
#
|
|
13
|
+
# pid = fork do
|
|
14
|
+
# worker = Worker.new(config,logger)
|
|
15
|
+
# signal our parent to let it know we're up and running
|
|
16
|
+
# worker.listen # waits for SIGUSR1 to check for work, and SIGUSR2 to exit
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# wait for child to signal us with it's startup status
|
|
20
|
+
#
|
|
21
|
+
# workers << pid
|
|
22
|
+
#
|
|
23
|
+
|
|
24
|
+
require 'thread'
|
|
25
|
+
require 'rubygems'
|
|
26
|
+
require 'mysql'
|
|
27
|
+
require 'active_record'
|
|
28
|
+
require 'jobs/client'
|
|
29
|
+
require 'jobs/runnable'
|
|
30
|
+
|
|
31
|
+
module Jobs
|
|
32
|
+
class Worker
|
|
33
|
+
include Jobs::Runnable
|
|
34
|
+
def initialize(config, runpath, config_path, logger, env)
|
|
35
|
+
@config = config
|
|
36
|
+
@runpath = runpath
|
|
37
|
+
@config_path = config_path
|
|
38
|
+
@logger = logger
|
|
39
|
+
@queue = Queue.new
|
|
40
|
+
@lock = Mutex.new
|
|
41
|
+
@sig_lock = Mutex.new
|
|
42
|
+
@thread = nil
|
|
43
|
+
@env = env
|
|
44
|
+
preload
|
|
45
|
+
|
|
46
|
+
unless defined?(Jobs::Initializer) and Jobs::Initializer.ready?
|
|
47
|
+
Jobs::Initializer.run! File.join(@runpath,@config['jobpath']), @config_path, @env
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
establish_connection
|
|
51
|
+
@threads = (@config['threads'] or 1).to_i
|
|
52
|
+
@pid = Process.pid
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def listen
|
|
56
|
+
@alive = true
|
|
57
|
+
@thread = Thread.new{ worker }
|
|
58
|
+
@count = 0
|
|
59
|
+
trap("USR1") { @sig_lock.synchronize {@count += 1} } # queue up some work
|
|
60
|
+
trap("USR2") { @alive = false; trap("USR2",'SIG_DFL') } # sent to stop normally
|
|
61
|
+
trap('TERM') { @alive = false; trap("TERM",'SIG_DFL') }
|
|
62
|
+
trap('INT') { @alive = false; trap("INT",'SIG_DFL') }
|
|
63
|
+
|
|
64
|
+
while @alive do
|
|
65
|
+
sleep 1 # 1 second resolution
|
|
66
|
+
count = 0
|
|
67
|
+
@sig_lock.synchronize{ count = @count; @count = 0 }
|
|
68
|
+
if count > 0
|
|
69
|
+
@logger.debug("[job worker #{@pid}]: processing #{count} jobs")
|
|
70
|
+
if count > @threads # we've queued up more then we can handle chunk it out and start chugging away
|
|
71
|
+
(count/@threads).times do
|
|
72
|
+
a = count - (count-@threads)
|
|
73
|
+
#@logger.debug("[job worker #{@pid}]: queueing #{a}")
|
|
74
|
+
@queue << a
|
|
75
|
+
count = (count-@threads)
|
|
76
|
+
end
|
|
77
|
+
if count > 0
|
|
78
|
+
#@logger.debug("[job worker #{@pid}]: queueing #{count}")
|
|
79
|
+
@queue << count
|
|
80
|
+
end
|
|
81
|
+
else
|
|
82
|
+
#@logger.debug("[job worker #{@pid}]: queueing #{count}")
|
|
83
|
+
@queue << count
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
@queue << nil
|
|
88
|
+
@logger.info "[job worker #{@pid}]: Joining with main thread"
|
|
89
|
+
@thread.join # wait for the worker
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
def worker
|
|
94
|
+
while @alive
|
|
95
|
+
count = 0
|
|
96
|
+
|
|
97
|
+
if @queue.empty?
|
|
98
|
+
@logger.debug "[job worker #{@pid}]: #{Process.pid} waiting..."
|
|
99
|
+
count = @queue.pop # sleep until we get some work
|
|
100
|
+
else
|
|
101
|
+
begin # pop as many as we can until we reach a max
|
|
102
|
+
while count < @threads do
|
|
103
|
+
value = @queue.pop(true)
|
|
104
|
+
if value.nil?
|
|
105
|
+
count = nil
|
|
106
|
+
break
|
|
107
|
+
end
|
|
108
|
+
count += value
|
|
109
|
+
end
|
|
110
|
+
rescue ThreadError => e
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
@logger.debug "[job worker #{@pid}]: #{@pid} awake with: #{count} suggested jobs"
|
|
115
|
+
|
|
116
|
+
break if count.nil? # if we get a nil count we're done
|
|
117
|
+
|
|
118
|
+
count = @threads if count == 0
|
|
119
|
+
|
|
120
|
+
jobs = []
|
|
121
|
+
|
|
122
|
+
begin
|
|
123
|
+
Jobs::Job.transaction do
|
|
124
|
+
|
|
125
|
+
conditions = sql_runnable_conditions(@config['jobs_included'], @config['jobs_excluded'])
|
|
126
|
+
jobs = Jobs::Job.find(:all, :conditions => conditions, :limit => count ) # only retrieve as many jobs as we were told about
|
|
127
|
+
|
|
128
|
+
# lock the jobs we're about to process here
|
|
129
|
+
@logger.debug "[job worker #{@pid}]: got #{jobs.size} to process out of #{count}"
|
|
130
|
+
jobs.each do|job|
|
|
131
|
+
job.locked = true
|
|
132
|
+
job.save!
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
reload!
|
|
137
|
+
|
|
138
|
+
if jobs.size > 2 and @threads > 1
|
|
139
|
+
threads = jobs.map do |job|
|
|
140
|
+
Thread.new(job) do|j|
|
|
141
|
+
process(j)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
threads.each{ |t| t.join }
|
|
145
|
+
else
|
|
146
|
+
jobs.each do |job|
|
|
147
|
+
process(job)
|
|
148
|
+
break unless @alive
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
ensure
|
|
152
|
+
Jobs::Job.transaction do
|
|
153
|
+
jobs.each do|job|
|
|
154
|
+
if job.locked
|
|
155
|
+
job.locked = false
|
|
156
|
+
job.save!
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
end
|
|
163
|
+
@logger.info "[job worker #{@pid}]: exiting work thread"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def process(job)
|
|
167
|
+
job.status = 'processing'
|
|
168
|
+
job.save!
|
|
169
|
+
task = job.instance(@logger, @lock)
|
|
170
|
+
if task
|
|
171
|
+
task.real_execute
|
|
172
|
+
else
|
|
173
|
+
# report no job defined error
|
|
174
|
+
end
|
|
175
|
+
rescue Object => e
|
|
176
|
+
@logger.error "[job worker #{@pid}]: #{e.message}\n#{e.backtrace.join("\n")}"
|
|
177
|
+
if job
|
|
178
|
+
job.details ||= ""
|
|
179
|
+
job.details << "#{e.message}\n#{e.backtrace.join("\n")}"
|
|
180
|
+
job.status = 'error'
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def preload
|
|
185
|
+
if @config['preload']
|
|
186
|
+
preload = @config['preload']
|
|
187
|
+
if preload.is_a?(Array)
|
|
188
|
+
preload.each { |f| require f }
|
|
189
|
+
else
|
|
190
|
+
require preload
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def establish_connection
|
|
196
|
+
|
|
197
|
+
@logger.info("[job worker #{@pid}]: establish connection environment with #{@config_path.inspect} and env: #{@env.inspect}")
|
|
198
|
+
@db = YAML.load_file(File.join(File.dirname(@config_path),'database.yml'))[@env]
|
|
199
|
+
ActiveRecord::Base.establish_connection @db
|
|
200
|
+
ActiveRecord::Base.logger = @logger
|
|
201
|
+
|
|
202
|
+
# load the jobs/job model
|
|
203
|
+
require 'jobs/job'
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def reload!
|
|
207
|
+
if defined?(RAILS_ENV) and RAILS_ENV != 'production'
|
|
208
|
+
# if this method is defined, it'll be called before each job is executed... during
|
|
209
|
+
# development this allows you to change your job code without restarting the job server...
|
|
210
|
+
# clear things out
|
|
211
|
+
|
|
212
|
+
ActiveRecord::Base.reset_subclasses if defined?(ActiveRecord)
|
|
213
|
+
ActiveSupport::Dependencies.clear if defined?(ActiveSupport)
|
|
214
|
+
ActiveRecord::Base.clear_reloadable_connections! if defined?(ActiveRecord)
|
|
215
|
+
|
|
216
|
+
# reload the environment... XXX: skipping the run_callbacks from reload_application in dispatcher.rb action_pack...
|
|
217
|
+
# this might cause the shit to hit the fan...
|
|
218
|
+
Routing::Routes.reload if defined?(Routing)
|
|
219
|
+
ActionController::Base.view_paths.reload! if defined?(ActionController)
|
|
220
|
+
ActionView::Helpers::AssetTagHelper::AssetTag::Cache.clear if defined?(ActionView)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
end
|
|
225
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require 'RMagick'
|
|
2
|
+
require 'jobs/client'
|
|
3
|
+
|
|
4
|
+
# require test models
|
|
5
|
+
$:.unshift File.join(File.dirname(__FILE__),'..','lib')
|
|
6
|
+
require 'image'
|
|
7
|
+
|
|
8
|
+
class ImageThumbJob < Jobs::Base
|
|
9
|
+
def execute
|
|
10
|
+
#@logger.debug("lookup record: #{@record.data[:id].inspect} #{Image.connection.inspect}")
|
|
11
|
+
# fetch the record to process
|
|
12
|
+
image_record = Image.find_by_id( @record.data[:id].to_i )
|
|
13
|
+
|
|
14
|
+
# read the image into memory
|
|
15
|
+
image = Magick::Image.read(image_record.path).first
|
|
16
|
+
|
|
17
|
+
# scale the image to the given dimensions
|
|
18
|
+
image.change_geometry!( @record.data[:size] ) { |cols,rows,img|
|
|
19
|
+
img.resize!(cols < 1 ? 1 : cols, rows < 1 ? 1 : rows)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# save the thumbnail, where we can find it later
|
|
23
|
+
image.write image_record.thumb_path
|
|
24
|
+
|
|
25
|
+
# save the record
|
|
26
|
+
image_record.save
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class CreateTestData < ActiveRecord::Migration
|
|
2
|
+
|
|
3
|
+
def self.up
|
|
4
|
+
create_table :messages, :force => true do |t|
|
|
5
|
+
t.string :subject
|
|
6
|
+
t.string :content
|
|
7
|
+
t.integer :image_id
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
create_table :images, :force => true do |t|
|
|
11
|
+
t.string :name
|
|
12
|
+
t.string :alt_text
|
|
13
|
+
t.string :path
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
end
|
data/tests/lib/image.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class Image < ActiveRecord::Base
|
|
2
|
+
include Jobs::Scheduler
|
|
3
|
+
|
|
4
|
+
has_one :job, :class_name => 'Jobs::Job', :as => :taskable, :dependent => :destroy
|
|
5
|
+
|
|
6
|
+
after_create :filter_image
|
|
7
|
+
|
|
8
|
+
def filter_image
|
|
9
|
+
# schedule the attached image to be scaled to 64x32
|
|
10
|
+
schedule(:image_thumb,{:id => self.id, :size => '64x32' })
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def thumb_path
|
|
14
|
+
extname = File.extname(self.path)
|
|
15
|
+
pathname = self.path.gsub(/#{extname}$/,'')
|
|
16
|
+
"#{pathname}-thumb#{extname}"
|
|
17
|
+
end
|
|
18
|
+
end
|
data/tests/sample.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
require 'test/unit'
|
|
2
|
+
require 'rubygems'
|
|
3
|
+
require 'active_record'
|
|
4
|
+
|
|
5
|
+
# load the jobs client environment
|
|
6
|
+
$:.unshift File.join(File.dirname(__FILE__),'..','lib')
|
|
7
|
+
require 'jobs/client'
|
|
8
|
+
|
|
9
|
+
# load test models
|
|
10
|
+
$:.unshift File.join(File.dirname(__FILE__),'lib')
|
|
11
|
+
require 'message'
|
|
12
|
+
require 'image'
|
|
13
|
+
|
|
14
|
+
Jobs::Initializer.test!
|
|
15
|
+
|
|
16
|
+
class TestServer < Test::Unit::TestCase
|
|
17
|
+
|
|
18
|
+
def setup
|
|
19
|
+
# ensure we have a connection established
|
|
20
|
+
if not ActiveRecord::Base.connected?
|
|
21
|
+
ActiveRecord::Base.establish_connection YAML.load_file(File.join(File.dirname(__FILE__),'..','config','database.yml'))['test']
|
|
22
|
+
ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__),'logs','test-db.log'))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
#
|
|
27
|
+
# test running a background thumbnail creation job
|
|
28
|
+
#
|
|
29
|
+
def test_single_image_thumb_job
|
|
30
|
+
timer = Time.now
|
|
31
|
+
# create new message record
|
|
32
|
+
image = Image.new :name => 'test',
|
|
33
|
+
:alt_text => 'sample',
|
|
34
|
+
:path => File.expand_path(File.join(File.dirname(__FILE__),'sample.png'))
|
|
35
|
+
|
|
36
|
+
# save the new record, should trigger the image
|
|
37
|
+
assert image.save
|
|
38
|
+
|
|
39
|
+
until image.job.status != 'processing' and image.job.status != 'pending'
|
|
40
|
+
sleep 1
|
|
41
|
+
image = Image.find_by_id(image.id)
|
|
42
|
+
puts "waiting for job to complete... '#{image.job.status}' and #{Jobs::Job.count(:conditions => ["status = 'complete'"])} completed"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
image = Image.find_by_id(image.id)
|
|
46
|
+
|
|
47
|
+
assert_equal 'complete', image.job.status
|
|
48
|
+
assert File.exist?(image.thumb_path)
|
|
49
|
+
dur = Time.now - timer
|
|
50
|
+
puts "Duration: #{image.job.updated_at.to_f - image.job.created_at.to_f} seconds and duration: #{image.job.duration}, real: #{dur}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_high_load_thumb_jobs
|
|
54
|
+
|
|
55
|
+
images = []
|
|
56
|
+
|
|
57
|
+
20.times do
|
|
58
|
+
# create new message record
|
|
59
|
+
image = Image.new :name => 'test',
|
|
60
|
+
:alt_text => 'sample',
|
|
61
|
+
:path => File.expand_path(File.join(File.dirname(__FILE__),'sample.png'))
|
|
62
|
+
|
|
63
|
+
# save the new record, should trigger the image
|
|
64
|
+
assert image.save
|
|
65
|
+
images << image
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
images.each do|image|
|
|
69
|
+
|
|
70
|
+
until image.job.status != 'processing' and image.job.status != 'pending'
|
|
71
|
+
sleep 1 # poll every second for updated job status
|
|
72
|
+
image = Image.find_by_id(image.id)
|
|
73
|
+
puts "waiting for job to complete... '#{image.job.status}' and #{Jobs::Job.count(:conditions => ["status = 'complete'"])} completed"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
assert_equal 'complete', image.job.status
|
|
77
|
+
assert File.exist?(image.thumb_path)
|
|
78
|
+
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test_file_search_job
|
|
83
|
+
puts "call test_file_search_job"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: taf2-rjqueue
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.4
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Todd A. Fisher
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
|
|
12
|
+
date: 2008-12-15 00:00:00 -08:00
|
|
13
|
+
default_executable:
|
|
14
|
+
dependencies: []
|
|
15
|
+
|
|
16
|
+
description: A Job Queue Server. Responses to UDP requests.
|
|
17
|
+
email: todd.fisher@gmail.com
|
|
18
|
+
executables:
|
|
19
|
+
- rjqueue
|
|
20
|
+
extensions: []
|
|
21
|
+
|
|
22
|
+
extra_rdoc_files: []
|
|
23
|
+
|
|
24
|
+
files:
|
|
25
|
+
- LICENSE
|
|
26
|
+
- README
|
|
27
|
+
- Rakefile
|
|
28
|
+
- bin/rjqueue
|
|
29
|
+
- config/database.yml
|
|
30
|
+
- config/jobs.yml
|
|
31
|
+
- lib/jobs/scheduler.rb
|
|
32
|
+
- lib/jobs/initializer.rb
|
|
33
|
+
- lib/jobs/config.rb
|
|
34
|
+
- lib/jobs/migrate.rb
|
|
35
|
+
- lib/jobs/worker.rb
|
|
36
|
+
- lib/jobs/runnable.rb
|
|
37
|
+
- lib/jobs/job.rb
|
|
38
|
+
- lib/jobs/server.rb
|
|
39
|
+
- lib/jobs/client.rb
|
|
40
|
+
- lib/jobs/base.rb
|
|
41
|
+
has_rdoc: true
|
|
42
|
+
homepage: http://idle-hacking.com/
|
|
43
|
+
post_install_message:
|
|
44
|
+
rdoc_options: []
|
|
45
|
+
|
|
46
|
+
require_paths:
|
|
47
|
+
- lib
|
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: "0"
|
|
53
|
+
version:
|
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: "0"
|
|
59
|
+
version:
|
|
60
|
+
requirements: []
|
|
61
|
+
|
|
62
|
+
rubyforge_project: rjqueue
|
|
63
|
+
rubygems_version: 1.2.0
|
|
64
|
+
signing_key:
|
|
65
|
+
specification_version: 2
|
|
66
|
+
summary: Ruby Job Queue
|
|
67
|
+
test_files:
|
|
68
|
+
- tests/sample.png
|
|
69
|
+
- tests/test_server.rb
|
|
70
|
+
- tests/jobs/simple_job.rb
|
|
71
|
+
- tests/jobs/image_thumb_job.rb
|
|
72
|
+
- tests/jobs/find_file_job.rb
|
|
73
|
+
- tests/lib/message.rb
|
|
74
|
+
- tests/lib/image.rb
|
|
75
|
+
- tests/lib/create_test_data.rb
|