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