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