mongojob 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,5 @@
1
+ # This is the project's main Rakefile. All tasks are bundled in the tasks/ directory.
2
+
3
+ require File.expand_path(File.join(File.dirname(__FILE__), "lib", "mongojob"))
4
+
5
+ Dir['tasks/*.rb'].each { |task| load(task) }
data/bin/mongojob-cli ADDED
File without changes
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
4
+
5
+ require 'mongojob'
6
+
7
+ options = MongoJob::Deamon.parse_options
8
+ worker = MongoJob::Deamon.new(options)
9
+ worker.run
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
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
4
+
5
+ require 'mongojob'
6
+
7
+ options = MongoJob::Worker.parse_options
8
+ worker = MongoJob::Worker.new(options)
9
+ worker.run
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,17 @@
1
+ module MongoJob
2
+ class Deamon
3
+
4
+ extend Mixins::FiberRunner::ClassMethods
5
+ include Mixins::FiberRunner::InstanceMethods
6
+
7
+
8
+
9
+ # Runs the worker
10
+ def run
11
+ EM.run do
12
+ run_defined_tasks
13
+ end
14
+ end
15
+
16
+ end
17
+ end
@@ -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
@@ -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,13 @@
1
+ module MongoJob
2
+ module Mixins
3
+ module Document
4
+ def connection
5
+ MongoJob.connection
6
+ end
7
+
8
+ def database_name
9
+ MongoJob.database_name
10
+ end
11
+ end
12
+ end
13
+ 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