mongojob 0.0.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.
- data/LICENSE +20 -0
- data/README.md +29 -0
- data/Rakefile +5 -0
- data/bin/mongojob-cli +0 -0
- data/bin/mongojob-deamon +9 -0
- data/bin/mongojob-web +15 -0
- data/bin/mongojob-worker +9 -0
- data/lib/mongojob.rb +69 -0
- data/lib/mongojob/deamon.rb +17 -0
- data/lib/mongojob/helpers.rb +32 -0
- data/lib/mongojob/job.rb +63 -0
- data/lib/mongojob/mixins/document.rb +13 -0
- data/lib/mongojob/mixins/fiber_runner.rb +51 -0
- data/lib/mongojob/model/job.rb +121 -0
- data/lib/mongojob/model/queue.rb +16 -0
- data/lib/mongojob/model/worker.rb +35 -0
- data/lib/mongojob/version.rb +3 -0
- data/lib/mongojob/web.rb +80 -0
- data/lib/mongojob/web/helpers.rb +46 -0
- data/lib/mongojob/worker.rb +370 -0
- data/lib/mongojob/worker_helpers.rb +5 -0
- data/spec/mongojob/job_spec.rb +32 -0
- data/spec/mongojob/model/job_spec.rb +61 -0
- data/spec/mongojob/worker_spec.rb +101 -0
- data/spec/mongojob_spec.rb +58 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +47 -0
- data/tasks/spec.rb +16 -0
- metadata +167 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Michal Frackowiak
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# MongoJob
|
2
|
+
|
3
|
+
MongoJob is a job queuing system inspired by [Resque](http://github.com/defunkt/resque), and using [MongoDB](http://mongodb.org) as a backend.
|
4
|
+
|
5
|
+
MongoJob is specifically designed to handle both long and short-term jobs.
|
6
|
+
|
7
|
+
# Current features and status
|
8
|
+
|
9
|
+
- Persistent, database-backed queues and jobs
|
10
|
+
- Worker based on EventMachine
|
11
|
+
- Multiple ways to invoke jobs by the worker: process forking, fiber (for non-blocking jobs), blocking (in-line)
|
12
|
+
- Pinging and reporting - workers report status every few seconds
|
13
|
+
- Jobs with status - jobs can report progress and set custom status
|
14
|
+
- Web interface with current workers, jobs etc.
|
15
|
+
|
16
|
+
## Still TODO
|
17
|
+
|
18
|
+
- MongoJob-deamon that monitors workers and jobs, kills timed-out ones
|
19
|
+
- Job rescheduling upon failure
|
20
|
+
- Cron-like job scheduling
|
21
|
+
- More complete web interface
|
22
|
+
- Command-line interface
|
23
|
+
- Documentation
|
24
|
+
|
25
|
+
# Warning
|
26
|
+
|
27
|
+
The library is in early stage of development. If you are looking for a robust job scheduling system, I bet [Resque](http://github.com/defunkt/resque) is much more stable now and I highly recommend it to anyone.
|
28
|
+
|
29
|
+
|
data/Rakefile
ADDED
data/bin/mongojob-cli
ADDED
File without changes
|
data/bin/mongojob-deamon
ADDED
data/bin/mongojob-web
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
|
4
|
+
|
5
|
+
require 'vegas'
|
6
|
+
require 'mongojob/web'
|
7
|
+
|
8
|
+
Vegas::Runner.new(MongoJob::Web, 'mongojob-web') do |runner, opts, app|
|
9
|
+
opts.on('-h HOST', "--host HOST", "set the MongoDB host") {|host|
|
10
|
+
MongoJob.host = host
|
11
|
+
}
|
12
|
+
opts.on('-d DATABASE_NAME', "--database-name DATABASE_NAME", "set the MongoDB database name") {|database_name|
|
13
|
+
MongoJob.database_name = database_name
|
14
|
+
}
|
15
|
+
end
|
data/bin/mongojob-worker
ADDED
data/lib/mongojob.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
require "mongo_mapper"
|
2
|
+
|
3
|
+
$: << File.dirname(__FILE__)
|
4
|
+
MJ_ROOT = File.expand_path(File.join(File.dirname(__FILE__),'..'))
|
5
|
+
|
6
|
+
require "mongojob/helpers"
|
7
|
+
require "mongojob/version"
|
8
|
+
require "mongojob/mixins/document"
|
9
|
+
require "mongojob/mixins/fiber_runner"
|
10
|
+
require "mongojob/worker"
|
11
|
+
require "mongojob/deamon"
|
12
|
+
require "mongojob/job"
|
13
|
+
|
14
|
+
module MongoJob
|
15
|
+
|
16
|
+
def self.host=(host)
|
17
|
+
@host = host
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.host
|
21
|
+
@host || 'localhost'
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.connection
|
25
|
+
split = host.split ':'
|
26
|
+
@connection ||= Mongo::Connection.new(split[0], (split[1] || 27017).to_i)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.database_name=(database_name)
|
30
|
+
@database_name = database_name
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.database_name
|
34
|
+
@database_name
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.enqueue(klass, options = {})
|
38
|
+
queue_name = klass.is_a?(Class) ? queue_from_class(klass) : klass.to_s
|
39
|
+
raise "Given class does not return any queue name" unless queue_name
|
40
|
+
job = Model::Job.create({
|
41
|
+
klass: klass.is_a?(Class) ? klass.to_s : nil,
|
42
|
+
options: options,
|
43
|
+
queue_name: queue_name
|
44
|
+
})
|
45
|
+
job.id.to_s
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.reserve(queue_name, worker_id)
|
49
|
+
Model::Job.reserve(queue_name, worker_id)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.find_job(job_id)
|
53
|
+
Model::Job.find job_id
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.dequeue(job_id)
|
57
|
+
Model::Job.delete(job_id)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Given a class, try to extrapolate an appropriate queue based on a
|
61
|
+
# class instance variable or `queue` method.
|
62
|
+
def self.queue_from_class(klass)
|
63
|
+
klass.instance_variable_get(:@queue) ||
|
64
|
+
(klass.respond_to?(:queue) and klass.queue)
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
Dir[::File.join(::File.dirname(__FILE__), 'mongojob/model/*.rb')].each { |model| require(model) }
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module MongoJob
|
2
|
+
module Helpers
|
3
|
+
|
4
|
+
# Given a word with dashes, returns a camel cased version of it.
|
5
|
+
#
|
6
|
+
# classify('job-name') # => 'JobName'
|
7
|
+
def classify(dashed_word)
|
8
|
+
dashed_word.split('-').each { |part| part[0] = part[0].chr.upcase }.join
|
9
|
+
end
|
10
|
+
|
11
|
+
# Given a camel cased word, returns the constant it represents
|
12
|
+
#
|
13
|
+
# constantize('JobName') # => JobName
|
14
|
+
def constantize(camel_cased_word)
|
15
|
+
camel_cased_word = camel_cased_word.to_s
|
16
|
+
|
17
|
+
if camel_cased_word.include?('-')
|
18
|
+
camel_cased_word = classify(camel_cased_word)
|
19
|
+
end
|
20
|
+
|
21
|
+
names = camel_cased_word.split('::')
|
22
|
+
names.shift if names.empty? || names.first.empty?
|
23
|
+
|
24
|
+
constant = Object
|
25
|
+
names.each do |name|
|
26
|
+
constant = constant.const_get(name) || constant.const_missing(name)
|
27
|
+
end
|
28
|
+
constant
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
data/lib/mongojob/job.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
module MongoJob
|
2
|
+
# You should extend this class to handle jobs
|
3
|
+
class Job
|
4
|
+
|
5
|
+
attr_accessor :log
|
6
|
+
|
7
|
+
def self.threading threading
|
8
|
+
@threading = threading
|
9
|
+
end
|
10
|
+
def self.fork?
|
11
|
+
@threading == :fork
|
12
|
+
end
|
13
|
+
def self.fiber?
|
14
|
+
@threading == :fiber
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.queue(queue = nil)
|
18
|
+
@queue ||= queue
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(job_object, logger = nil)
|
22
|
+
@job = job_object
|
23
|
+
@log = logger || Logger.new(STDOUT)
|
24
|
+
end
|
25
|
+
|
26
|
+
def options
|
27
|
+
@job.options
|
28
|
+
end
|
29
|
+
|
30
|
+
def id
|
31
|
+
@job.id
|
32
|
+
end
|
33
|
+
|
34
|
+
# Please implement this method to perform any actual work.
|
35
|
+
def perform
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
# Convenience methods
|
40
|
+
|
41
|
+
# Set the status of the job for the current itteration. <tt>num</tt> and
|
42
|
+
# <tt>total</tt> are passed to the status as well as any messages.
|
43
|
+
#
|
44
|
+
# Usage:
|
45
|
+
# at(0.29) - at 29%
|
46
|
+
# at(0.29,1.0) - at 29%
|
47
|
+
# at(29,100) - at 29%
|
48
|
+
# at(2,7) - at 2 of 7
|
49
|
+
# at(2,7, {status: {foo: 'bar'}}) - at 2 of 7, and set custom_status
|
50
|
+
def at *args
|
51
|
+
@job.at *args
|
52
|
+
end
|
53
|
+
|
54
|
+
# Set custom status for the job. Accepts a hash as a parameter.
|
55
|
+
def update_status status
|
56
|
+
@job.set({
|
57
|
+
custom_status: status,
|
58
|
+
pinged_at: Time.now
|
59
|
+
})
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module MongoJob
|
2
|
+
module Mixins
|
3
|
+
module FiberRunner
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
def task method_name, period, *args
|
7
|
+
@tasks ||= []
|
8
|
+
@tasks << {method_name: method_name, period: period, args: args}
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module InstanceMethods
|
13
|
+
|
14
|
+
def run_em_fiber period, &blk
|
15
|
+
# log.info "Starting tick #{method_name} with period #{period} seconds"
|
16
|
+
Fiber.new do
|
17
|
+
loop do
|
18
|
+
f = Fiber.current
|
19
|
+
begin
|
20
|
+
# log.debug "Running method #{method_name}"
|
21
|
+
blk.call
|
22
|
+
rescue Exception => e
|
23
|
+
# do something
|
24
|
+
# log.error "Caught exception when running #{method_name}"
|
25
|
+
# log.error e
|
26
|
+
p e
|
27
|
+
end
|
28
|
+
EM.add_timer period do
|
29
|
+
f.resume
|
30
|
+
end
|
31
|
+
Fiber.yield
|
32
|
+
end
|
33
|
+
end.resume
|
34
|
+
end
|
35
|
+
|
36
|
+
def run_defined_tasks
|
37
|
+
tasks = nil
|
38
|
+
self.class.class_eval do
|
39
|
+
tasks = @tasks || []
|
40
|
+
end
|
41
|
+
tasks.each do |task|
|
42
|
+
run_em_fiber task[:period] do
|
43
|
+
self.send task[:method_name], *task[:args]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module MongoJob
|
2
|
+
module Model
|
3
|
+
class Job
|
4
|
+
include MongoMapper::Document
|
5
|
+
extend MongoJob::Mixins::Document
|
6
|
+
|
7
|
+
extend Helpers
|
8
|
+
include Helpers
|
9
|
+
|
10
|
+
key :queue_name, String
|
11
|
+
key :options, Hash
|
12
|
+
key :klass, String
|
13
|
+
|
14
|
+
key :worker_id, String
|
15
|
+
key :status, String, default: 'queued' # one of 'queued', 'working', 'done', 'failed'
|
16
|
+
key :error # if failed
|
17
|
+
|
18
|
+
key :progress, Hash # :at of :total
|
19
|
+
key :custom_status, Hash # Any custom status set by the worker
|
20
|
+
key :pinged_at, Time # Should be updated frequently by the worker
|
21
|
+
|
22
|
+
key :release_at, Time
|
23
|
+
key :started_at, Time
|
24
|
+
key :completed_at, Time
|
25
|
+
timestamps!
|
26
|
+
|
27
|
+
belongs_to :queue, class_name: 'MongoJob::Model::Queue', foreign_key: :queue_name
|
28
|
+
belongs_to :worker, class_name: 'MongoJob::Model::Worker', foreign_key: :worker_id
|
29
|
+
|
30
|
+
before_create :setup_queue
|
31
|
+
before_create :set_release_at
|
32
|
+
|
33
|
+
# Make sure the queue exists for a given queue name. The usual way to create a job is to provide a queue_name
|
34
|
+
# without caring about the unmet reference, so we need to fix it here.
|
35
|
+
def setup_queue
|
36
|
+
queue = Queue.find self.queue_name
|
37
|
+
unless queue
|
38
|
+
queue = Queue.create _id: self.queue_name
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def set_release_at
|
43
|
+
self.release_at ||= Time.now
|
44
|
+
end
|
45
|
+
|
46
|
+
def job_class
|
47
|
+
@job_class ||= constantize klass
|
48
|
+
end
|
49
|
+
|
50
|
+
def job_object
|
51
|
+
@job_object ||= job_class.new(self)
|
52
|
+
end
|
53
|
+
|
54
|
+
def fail error
|
55
|
+
error_text = error.is_a?(Exception) ? "#{error.message}\n\n#{error.backtrace}" : error.to_s
|
56
|
+
self.set({
|
57
|
+
status: 'failed',
|
58
|
+
error: error_text
|
59
|
+
})
|
60
|
+
reload
|
61
|
+
end
|
62
|
+
|
63
|
+
def complete
|
64
|
+
set({
|
65
|
+
status: 'done',
|
66
|
+
completed_at: Time.now
|
67
|
+
})
|
68
|
+
end
|
69
|
+
|
70
|
+
# Usage:
|
71
|
+
# at(0.29) - at 29%
|
72
|
+
# at(0.29,1.0) - at 29%
|
73
|
+
# at(29,100) - at 29%
|
74
|
+
# at(2,7) - at 2 of 7
|
75
|
+
# at(2,7, {status: {foo: 'bar'}}) - at 2 of 7, and set custom_status
|
76
|
+
def at *args
|
77
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
78
|
+
num = args[0]
|
79
|
+
total = args[1] || 1.0
|
80
|
+
custom_status = options[:status]
|
81
|
+
data = { pinged_at: Time.now }
|
82
|
+
data[:progress] = {
|
83
|
+
at: num,
|
84
|
+
total: total
|
85
|
+
} if num
|
86
|
+
data[:custom_status] = custom_status if custom_status
|
87
|
+
set data
|
88
|
+
reload
|
89
|
+
|
90
|
+
# TODO: stop the job if cancelled, e.g. by raising an exception.
|
91
|
+
end
|
92
|
+
|
93
|
+
def percent_done
|
94
|
+
progress['at'].to_f/ progress['total'].to_f if progress['at']
|
95
|
+
end
|
96
|
+
|
97
|
+
# Pop the first unassigned job from the queue
|
98
|
+
def self.reserve(queue_name, worker_id)
|
99
|
+
begin
|
100
|
+
job = self.first conditions: {
|
101
|
+
queue_name: queue_name,
|
102
|
+
status: 'queued'
|
103
|
+
}, order: 'release_at'
|
104
|
+
return nil unless job
|
105
|
+
if job
|
106
|
+
# Might be free. Update it with the new status
|
107
|
+
self.set({id: job.id, status: 'queued'}, {status: 'working', worker_id: worker_id, started_at: Time.now})
|
108
|
+
end
|
109
|
+
job.reload
|
110
|
+
end while job.worker_id != worker_id
|
111
|
+
job
|
112
|
+
end
|
113
|
+
|
114
|
+
def ping
|
115
|
+
set pinged_at: Time.now
|
116
|
+
reload
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|