taf2-rjqueue 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,10 @@
1
+ class FindFileJob < Jobs::Base
2
+ # simple test to find a file
3
+
4
+ def execute
5
+ File.open("#{File.dirname(__FILE__)}/output", "w") do|f|
6
+ f << call("find #{File.dirname(__FILE__)} -name find_file_job.rb").inspect
7
+ end
8
+ end
9
+
10
+ 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,6 @@
1
+
2
+ class SimpleJob < Jobs::Base
3
+ def execute
4
+ sleep 1 # one second
5
+ end
6
+ 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
@@ -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
@@ -0,0 +1,3 @@
1
+ class Message < ActiveRecord::Base
2
+ belongs_to :image
3
+ 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