jobit 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/.gitignore +19 -0
- data/.rspec +1 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.md +1 -0
- data/Rakefile +8 -0
- data/app/controllers/jobit/jobs_controller.rb +19 -0
- data/config/routes.rb +3 -0
- data/init.rb +2 -0
- data/jobit.gemspec +20 -0
- data/lib/jobit.rb +8 -0
- data/lib/jobit/command.rb +88 -0
- data/lib/jobit/engine.rb +5 -0
- data/lib/jobit/job.rb +102 -0
- data/lib/jobit/jobby.rb +152 -0
- data/lib/jobit/jobs/commands.rb +64 -0
- data/lib/jobit/jobs/statuses.rb +53 -0
- data/lib/jobit/railtie.rb +11 -0
- data/lib/jobit/storage.rb +123 -0
- data/lib/jobit/version.rb +3 -0
- data/lib/jobit/worker.rb +49 -0
- data/lib/tasks/jobit.rake +1 -0
- data/lib/tasks/tasks.rb +20 -0
- data/spec/jobit/storage_spec.rb +93 -0
- data/spec/jobit_spec.rb +236 -0
- data/spec/spec_helper.rb +1 -0
- metadata +97 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Victor Yunevich
|
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 PURPOa AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SaALL 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 @@
|
|
1
|
+
nothing yet
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module Jobit
|
2
|
+
class JobsController < ActionController::Base
|
3
|
+
|
4
|
+
def index
|
5
|
+
job = Jobit::Job.find_by_name(params[:id])
|
6
|
+
|
7
|
+
if job.nil?
|
8
|
+
json_response = {:status => 'not_found'}
|
9
|
+
else
|
10
|
+
json_response = job.session
|
11
|
+
if job.status == 'complete'
|
12
|
+
job.destroy if job.status == "complete"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
render :json => json_response
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/config/routes.rb
ADDED
data/init.rb
ADDED
data/jobit.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/jobit/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Victor Yunevich"]
|
6
|
+
gem.email = ["v.t.g.m.b.x@gmail.com"]
|
7
|
+
gem.description = %q{Process background jobs in queue.}
|
8
|
+
gem.summary = %q{Background jobs processing}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.name = "jobit"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Jobit::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency "daemons"
|
19
|
+
gem.add_development_dependency "rspec"
|
20
|
+
end
|
data/lib/jobit.rb
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
require "jobit/version"
|
2
|
+
require File.dirname(__FILE__) + '/jobit/storage'
|
3
|
+
require File.dirname(__FILE__) + '/jobit/jobby'
|
4
|
+
require File.dirname(__FILE__) + '/jobit/job'
|
5
|
+
require File.dirname(__FILE__) + '/jobit/worker'
|
6
|
+
require File.dirname(__FILE__) + '/jobit/engine' if defined?(Rails)
|
7
|
+
require File.dirname(__FILE__) + '/jobit/railtie' if defined?(Rails)
|
8
|
+
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'daemons'
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
module Jobit
|
6
|
+
class Command
|
7
|
+
attr_accessor :worker_count
|
8
|
+
|
9
|
+
def initialize(args)
|
10
|
+
@files_to_reopen = []
|
11
|
+
@options = {
|
12
|
+
:quiet => true,
|
13
|
+
:pid_dir => "#{Rails.root}/tmp/pids"
|
14
|
+
}
|
15
|
+
|
16
|
+
@worker_count = 1
|
17
|
+
@monitor = false
|
18
|
+
|
19
|
+
opts = OptionParser.new do |opts|
|
20
|
+
opts.banner = "Usage: #{File.basename($0)} [options] start|stop|restart|run"
|
21
|
+
|
22
|
+
opts.on('-h', '--help', 'Show this message') do
|
23
|
+
puts opts
|
24
|
+
exit 1
|
25
|
+
end
|
26
|
+
opts.on('-n', '--number_of_workers=workers', "Number of unique workers to spawn") do |worker_count|
|
27
|
+
@worker_count = worker_count.to_i rescue 1
|
28
|
+
end
|
29
|
+
end
|
30
|
+
@args = opts.parse!(args)
|
31
|
+
end
|
32
|
+
|
33
|
+
def daemonize
|
34
|
+
#Jobit::Worker.backend.before_fork
|
35
|
+
|
36
|
+
ObjectSpace.each_object(File) do |file|
|
37
|
+
@files_to_reopen << file unless file.closed?
|
38
|
+
end
|
39
|
+
|
40
|
+
dir = @options[:pid_dir]
|
41
|
+
Dir.mkdir(dir) unless File.exists?(dir)
|
42
|
+
|
43
|
+
if @worker_count > 1 && @options[:identifier]
|
44
|
+
raise ArgumentError, 'Cannot specify both --number-of-workers and --identifier'
|
45
|
+
elsif @worker_count == 1 && @options[:identifier]
|
46
|
+
process_name = "jobit.#{@options[:identifier]}"
|
47
|
+
run_process(process_name, dir)
|
48
|
+
else
|
49
|
+
worker_count.times do |worker_index|
|
50
|
+
process_name = worker_count == 1 ? "jobit" : "jobit.#{worker_index}"
|
51
|
+
run_process(process_name, dir)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def run_process(process_name, dir)
|
57
|
+
Daemons.run_proc(process_name, :dir => dir, :dir_mode => :normal, :monitor => @monitor, :ARGV => @args) do |*args|
|
58
|
+
$0 = File.join(@options[:prefix], process_name) if @options[:prefix]
|
59
|
+
run process_name
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def run(worker_name = nil)
|
64
|
+
Dir.chdir(Rails.root)
|
65
|
+
|
66
|
+
# Re-open file handles
|
67
|
+
@files_to_reopen.each do |file|
|
68
|
+
begin
|
69
|
+
file.reopen file.path, "a+"
|
70
|
+
file.sync = true
|
71
|
+
rescue ::Exception
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
#Delayed::Worker.logger = Logger.new(File.join(Rails.root, 'log', 'delayed_job.log'))
|
76
|
+
#Delayed::Worker.backend.after_fork
|
77
|
+
|
78
|
+
worker = Jobit::Worker.new(@options)
|
79
|
+
#worker.name_prefix = "#{worker_name} "
|
80
|
+
worker.start
|
81
|
+
rescue => e
|
82
|
+
Rails.logger.fatal e
|
83
|
+
STDERR.puts e.message
|
84
|
+
exit 1
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
data/lib/jobit/engine.rb
ADDED
data/lib/jobit/job.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'benchmark'
|
3
|
+
|
4
|
+
module Jobit
|
5
|
+
|
6
|
+
class Job
|
7
|
+
|
8
|
+
def self.jobs_path
|
9
|
+
if defined?(Rails)
|
10
|
+
"#{Rails.root.to_s}/tmp/jobit"
|
11
|
+
else
|
12
|
+
File.dirname(__FILE__) + "/../../tmp/jobit"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.worker_name
|
17
|
+
"host:#{Socket.gethostname} pid:#{Process.pid}" rescue "pid:#{Process.pid}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.destroy_all
|
21
|
+
Storage.destroy_all
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.destroy_failed
|
25
|
+
failed_jobs = Storage.where({:status => 'failed'})
|
26
|
+
for job in failed_jobs
|
27
|
+
job.destroy
|
28
|
+
end
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.logger
|
33
|
+
if defined?(Rails)
|
34
|
+
Logger.new("#{Rails.root.to_s}/log/jobit.log", shift_age = 7, shift_size = 1048576)
|
35
|
+
else
|
36
|
+
Logger.new(File.dirname(__FILE__) + "/../../log/jobit.log")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Jobit::Job.all
|
41
|
+
# returns array of all jobs
|
42
|
+
def self.all
|
43
|
+
Storage.all
|
44
|
+
end
|
45
|
+
|
46
|
+
# Jobit::Job.find(11111.111)
|
47
|
+
# returns job or nil
|
48
|
+
def self.find(id)
|
49
|
+
Storage.find(id)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Jobit::Job.find_by_name('name')
|
53
|
+
# returns job or nil
|
54
|
+
def self.find_by_name(name)
|
55
|
+
Storage.find_by_name(name)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Jobit::Job.where({:name => 'name'})
|
59
|
+
# returns array of found jobs
|
60
|
+
def self.where(search)
|
61
|
+
Storage.where(search)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Jobit::Job.add(name, object, *args) {{options}}
|
65
|
+
# options:
|
66
|
+
# :priority => the job priority (Integer)
|
67
|
+
# :run_after => run job after some time from now ex: :run_after => 4.hours
|
68
|
+
# :schedule => run job at some time. ex: :schedule_at => "16:00"
|
69
|
+
# :repeat => how many times to repeat the job (Integer)
|
70
|
+
# :repeat_delay => delay in seconds before next repeat
|
71
|
+
def self.add(name, object, *args, &block)
|
72
|
+
unless JobitItems.method_defined?(object)
|
73
|
+
raise ArgumentError, "Can't add job #{object}. It's not defined in jobs."
|
74
|
+
end
|
75
|
+
|
76
|
+
options = {}
|
77
|
+
options = yield if block_given?
|
78
|
+
|
79
|
+
options[:name] = name
|
80
|
+
options[:object] = object
|
81
|
+
options[:args] = Marshal.dump(args)
|
82
|
+
|
83
|
+
Jobby.new(nil, options)
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.work_off(num = 100)
|
87
|
+
complete, failed, reissued = 0, 0, 0
|
88
|
+
num.times do
|
89
|
+
jobs = self.where({:status => 'new'})
|
90
|
+
break unless jobs.size > 0
|
91
|
+
res = jobs.first.run_job
|
92
|
+
complete += res[0]
|
93
|
+
failed += res[1]
|
94
|
+
reissued += res[2]
|
95
|
+
break if $exit
|
96
|
+
end
|
97
|
+
[complete, failed, reissued]
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
data/lib/jobit/jobby.rb
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/jobs/statuses'
|
2
|
+
require File.dirname(__FILE__) + '/jobs/commands'
|
3
|
+
|
4
|
+
module Jobit
|
5
|
+
class Jobby < Struct.new(
|
6
|
+
:id, :name, :object, :args, :message, :progress, :error, :priority, :run_at,
|
7
|
+
:schedule, :repeat, :repeat_delay, :created_at, :started_at, :stopped_at,
|
8
|
+
:locked_by, :locked_at, :tries, :status, :keep, :failed_at
|
9
|
+
)
|
10
|
+
|
11
|
+
MAX_ATTEMPTS = 25
|
12
|
+
MAX_RUN_TIME = 4 # hours
|
13
|
+
|
14
|
+
include Jobit::Jobs::Statuses
|
15
|
+
include Jobit::Jobs::Commands
|
16
|
+
|
17
|
+
def initialize(job = nil, options = {})
|
18
|
+
if job.nil?
|
19
|
+
create_new
|
20
|
+
if options
|
21
|
+
for key, val in options
|
22
|
+
self[key] = val
|
23
|
+
end
|
24
|
+
end
|
25
|
+
self.id = "#{priority}.#{id}"
|
26
|
+
Jobit::Storage.create(id, self)
|
27
|
+
else
|
28
|
+
job.each_with_index do |val, index|
|
29
|
+
self[index] = val
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def destroy
|
35
|
+
return unless id
|
36
|
+
Jobit::Storage.destroy(id)
|
37
|
+
clear
|
38
|
+
end
|
39
|
+
|
40
|
+
def process_job(args)
|
41
|
+
begin
|
42
|
+
logger.info "* [JOB] aquiring lock on #{name}"
|
43
|
+
runtime = Benchmark.realtime do
|
44
|
+
set_status 'running'
|
45
|
+
set_start_time
|
46
|
+
self.send(object,*args)
|
47
|
+
set_status 'complete'
|
48
|
+
set_stop_time
|
49
|
+
end
|
50
|
+
logger.info "* [JOB] #{name} completed after %.4f" % runtime
|
51
|
+
rescue Exception => e
|
52
|
+
log_exception(e)
|
53
|
+
msg = "#{e.message}\n\n#{e.backtrace.join("\n")}"
|
54
|
+
set_error(msg)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def run_job(worker_name = 'worker')
|
59
|
+
return nil if locked?
|
60
|
+
lock!(worker_name)
|
61
|
+
self.class.send(:include, JobitItems)
|
62
|
+
job_args = Marshal.restore(args)
|
63
|
+
num = repeat.to_i == 0 ? 1 : repeat.to_i
|
64
|
+
delay = repeat_delay.to_i
|
65
|
+
num.times do
|
66
|
+
process_job(job_args)
|
67
|
+
break if failed? # stop loop if job failed
|
68
|
+
increase_tries
|
69
|
+
sleep delay if delay > 0
|
70
|
+
end
|
71
|
+
#p self.class.send(:method_defined?, :perform)
|
72
|
+
#p self.class.send(:undef_method, :perform)
|
73
|
+
cleanup
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
def log_exception(error)
|
78
|
+
logger.error "* [JOB:#{name}] failed with #{error.class.name}: #{error.message} - #{tries} failed attempts"
|
79
|
+
logger.error(error)
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def logger
|
85
|
+
@logger ||= Jobit::Job.logger
|
86
|
+
end
|
87
|
+
|
88
|
+
def create_new
|
89
|
+
self.id = Time.now.to_f
|
90
|
+
self.name = name
|
91
|
+
self.message = ''
|
92
|
+
self.progress = 0
|
93
|
+
self.priority = 0
|
94
|
+
self.run_at = nil
|
95
|
+
self.repeat = 0
|
96
|
+
self.repeat_delay = 0
|
97
|
+
self.created_at = id
|
98
|
+
self.tries = 0
|
99
|
+
self.status = 'new'
|
100
|
+
self.keep = false
|
101
|
+
self
|
102
|
+
end
|
103
|
+
|
104
|
+
def reload
|
105
|
+
reloaded_job = Storage.find(id)
|
106
|
+
reloaded_job.each_with_index do |val, index|
|
107
|
+
self[index] = val
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def update
|
112
|
+
Jobit::Storage.update(id, self)
|
113
|
+
end
|
114
|
+
|
115
|
+
def cleanup
|
116
|
+
complete, failed, reissued = 0, 0, 0
|
117
|
+
if failed?
|
118
|
+
if tries < MAX_ATTEMPTS
|
119
|
+
self.tries += 1
|
120
|
+
self.run_at = Time.now + (tries ** 4) + 5
|
121
|
+
self.status = 'new'
|
122
|
+
unlock!
|
123
|
+
update
|
124
|
+
reissued = 1
|
125
|
+
else
|
126
|
+
logger.info "* [JOB:#{name}] Removing... Too many tries."
|
127
|
+
destroy unless keep?
|
128
|
+
failed = 1
|
129
|
+
end
|
130
|
+
elsif complete?
|
131
|
+
if keep?
|
132
|
+
set_progress(100)
|
133
|
+
set_stop_time
|
134
|
+
unlock!
|
135
|
+
else
|
136
|
+
destroy
|
137
|
+
end
|
138
|
+
complete = 1
|
139
|
+
end
|
140
|
+
[complete, failed, reissued]
|
141
|
+
end
|
142
|
+
|
143
|
+
def clear
|
144
|
+
options_size = self.size-1
|
145
|
+
|
146
|
+
for i in (0..options_size)
|
147
|
+
self[i]= nil
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Jobit
|
2
|
+
module Jobs
|
3
|
+
module Commands
|
4
|
+
def set_start_time
|
5
|
+
self.started_at = Time.now.to_f
|
6
|
+
update
|
7
|
+
end
|
8
|
+
|
9
|
+
def set_stop_time
|
10
|
+
self.stopped_at = Time.now.to_f
|
11
|
+
update
|
12
|
+
end
|
13
|
+
|
14
|
+
def set_status(new_status)
|
15
|
+
self.status = new_status
|
16
|
+
update
|
17
|
+
end
|
18
|
+
|
19
|
+
def set_error(text)
|
20
|
+
self.error = text
|
21
|
+
self.status = 'failed'
|
22
|
+
self.stopped_at = Time.now.to_f
|
23
|
+
self.failed_at = stopped_at
|
24
|
+
update
|
25
|
+
end
|
26
|
+
|
27
|
+
def set_tries(num)
|
28
|
+
self.tries = num
|
29
|
+
update
|
30
|
+
end
|
31
|
+
|
32
|
+
def increase_tries
|
33
|
+
self.tries += 1
|
34
|
+
update
|
35
|
+
end
|
36
|
+
|
37
|
+
def set_progress(num)
|
38
|
+
self.progress = num
|
39
|
+
update
|
40
|
+
end
|
41
|
+
|
42
|
+
def add_message(text, force = false)
|
43
|
+
if self.keep? || force
|
44
|
+
self.message += text
|
45
|
+
update
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def unlock!
|
50
|
+
self.locked_at= nil
|
51
|
+
self.locked_by= nil
|
52
|
+
update
|
53
|
+
end
|
54
|
+
|
55
|
+
def lock!(worker_name)
|
56
|
+
self.run_at= Time.now.to_f
|
57
|
+
self.locked_at= Time.now.to_f
|
58
|
+
self.locked_by= worker_name
|
59
|
+
update
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Jobit
|
2
|
+
module Jobs
|
3
|
+
module Statuses
|
4
|
+
def running?
|
5
|
+
status == 'running'
|
6
|
+
end
|
7
|
+
|
8
|
+
def failed?
|
9
|
+
status == 'failed'
|
10
|
+
end
|
11
|
+
|
12
|
+
def complete?
|
13
|
+
status == 'complete'
|
14
|
+
end
|
15
|
+
|
16
|
+
def stopped?
|
17
|
+
status == 'stopped'
|
18
|
+
end
|
19
|
+
|
20
|
+
def new?
|
21
|
+
status == 'new'
|
22
|
+
end
|
23
|
+
|
24
|
+
def keep?
|
25
|
+
keep
|
26
|
+
end
|
27
|
+
|
28
|
+
def locked?
|
29
|
+
locked_at != nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def current_job
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def session
|
37
|
+
{
|
38
|
+
:name => name,
|
39
|
+
:message => message,
|
40
|
+
:progress => progress,
|
41
|
+
:error => error,
|
42
|
+
:run_at => run_at,
|
43
|
+
:created_at => created_at,
|
44
|
+
:started_at => started_at,
|
45
|
+
:stopped_at =>stopped_at,
|
46
|
+
:tries => tries,
|
47
|
+
:status => status,
|
48
|
+
:failed_at => failed_at
|
49
|
+
}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Jobit
|
3
|
+
class Storage
|
4
|
+
|
5
|
+
def self.all_files
|
6
|
+
Dir.mkdir(Jobit::Job.jobs_path) unless Dir.exist?(Jobit::Job.jobs_path)
|
7
|
+
Dir.glob("#{Jobit::Job.jobs_path}/*").find_all { |x| File.file? x }
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.destroy_all
|
11
|
+
for file_name in self.all_files
|
12
|
+
File.delete(file_name)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.all
|
17
|
+
result = []
|
18
|
+
|
19
|
+
for file_name in self.all_files
|
20
|
+
obj = get_data_from_file(file_name)
|
21
|
+
result << Jobby.new(obj)
|
22
|
+
end
|
23
|
+
|
24
|
+
result
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.find(id)
|
28
|
+
file_name = self.make_file_name(id)
|
29
|
+
return nil unless File.exist?(file_name)
|
30
|
+
|
31
|
+
obj = get_data_from_file(file_name)
|
32
|
+
Jobby.new(obj)
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
def self.find_by_name(name)
|
37
|
+
|
38
|
+
result = nil
|
39
|
+
|
40
|
+
for file_name in self.all_files
|
41
|
+
obj = get_data_from_file(file_name)
|
42
|
+
next if obj.nil?
|
43
|
+
if obj.name == name
|
44
|
+
result = Jobby.new(obj)
|
45
|
+
break
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
result
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.where(search)
|
53
|
+
search_hash = {:name => search}
|
54
|
+
search_hash = search if search.is_a?(Hash)
|
55
|
+
|
56
|
+
result = []
|
57
|
+
|
58
|
+
for file_name in self.all_files
|
59
|
+
obj = get_data_from_file(file_name)
|
60
|
+
for key, val in search_hash
|
61
|
+
next unless obj.respond_to?(key)
|
62
|
+
result << Jobby.new(obj) if obj[key] == val
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
result
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.destroy(file)
|
70
|
+
file_name = self.make_file_name(file)
|
71
|
+
return false unless File.exist?(file_name)
|
72
|
+
File.delete(file_name)
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.create(file, content)
|
76
|
+
Dir.mkdir(Jobit::Job.jobs_path) unless Dir.exist?(Jobit::Job.jobs_path)
|
77
|
+
file_name = self.make_file_name(file)
|
78
|
+
data = Marshal.dump(content.to_hash)
|
79
|
+
File.open(file_name, 'w+b') { |f| f.write(data) }
|
80
|
+
true
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
def self.update(file, content)
|
85
|
+
file_name = self.make_file_name(file)
|
86
|
+
return false unless File.exist?(file_name)
|
87
|
+
File.open(file_name, 'w+b') { |f| f.write(Marshal.dump(content.to_hash)) }
|
88
|
+
true
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def self.get_data_from_file(file_name)
|
94
|
+
begin
|
95
|
+
file = File.open(file_name)
|
96
|
+
obj = Marshal.load(file).to_struct
|
97
|
+
rescue
|
98
|
+
return nil
|
99
|
+
ensure
|
100
|
+
file.close
|
101
|
+
end
|
102
|
+
obj
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.make_file_name(file)
|
106
|
+
File.join(Jobit::Job.jobs_path, file.to_s)
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class Hash
|
114
|
+
def to_struct
|
115
|
+
Struct.new(*keys).new(*values)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
class Struct
|
120
|
+
def to_hash
|
121
|
+
Hash[*members.zip(values).flatten]
|
122
|
+
end
|
123
|
+
end
|
data/lib/jobit/worker.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
module Jobit
|
2
|
+
class Worker
|
3
|
+
SLEEP = 5
|
4
|
+
|
5
|
+
def logger
|
6
|
+
@logger ||= Jobit::Job.logger
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(options={})
|
10
|
+
@quiet = options[:quiet]
|
11
|
+
#Delayed::Job.min_priority = options[:min_priority] if options.has_key?(:min_priority)
|
12
|
+
#Delayed::Job.max_priority = options[:max_priority] if options.has_key?(:max_priority)
|
13
|
+
end
|
14
|
+
|
15
|
+
def start
|
16
|
+
#say "*** Starting job worker #{Jobit::Job.worker_name}"
|
17
|
+
say "*** Starting job worker #{Jobit::Job.worker_name}"
|
18
|
+
|
19
|
+
trap('TERM') { say 'Exiting...'; $exit = true }
|
20
|
+
trap('INT') { say 'Exiting...'; $exit = true }
|
21
|
+
|
22
|
+
loop do
|
23
|
+
result = nil
|
24
|
+
|
25
|
+
realtime = Benchmark.realtime do
|
26
|
+
result = Jobit::Job.work_off
|
27
|
+
end
|
28
|
+
|
29
|
+
count = result.sum
|
30
|
+
|
31
|
+
break if $exit
|
32
|
+
|
33
|
+
if count.zero?
|
34
|
+
sleep(SLEEP)
|
35
|
+
else
|
36
|
+
say "#{count} jobs processed at %.4f j/s, %d failed ..." % [count / realtime, result[1]]
|
37
|
+
end
|
38
|
+
|
39
|
+
break if $exit
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def say(text)
|
44
|
+
puts text unless @quiet
|
45
|
+
logger.info text if logger
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'tasks')
|
data/lib/tasks/tasks.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# Re-definitions are appended to existing tasks
|
2
|
+
task :environment
|
3
|
+
|
4
|
+
namespace :jobit do
|
5
|
+
desc "Clear the Jobit job queue."
|
6
|
+
task :clear => :environment do
|
7
|
+
Jobit::Job.destroy_all
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Clear the Jobit filed jobs."
|
11
|
+
task :clear_failed => :environment do
|
12
|
+
Jobit::Job.destroy_failed
|
13
|
+
end
|
14
|
+
|
15
|
+
desc "Start a Jobit worker."
|
16
|
+
task :work => :environment do
|
17
|
+
p Rails.root
|
18
|
+
Jobit::Worker.new().start
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
3
|
+
|
4
|
+
describe Jobit::Storage do
|
5
|
+
|
6
|
+
def struct
|
7
|
+
struct = Struct.new(:id, :name)
|
8
|
+
struct.new(@file_name, @job_name)
|
9
|
+
end
|
10
|
+
|
11
|
+
before(:each) do
|
12
|
+
@file_name = Time.now.to_f
|
13
|
+
@job_name = "job#1"
|
14
|
+
@total_files = Jobit::Storage.all_files.size
|
15
|
+
Jobit::Storage.create(@file_name, struct).should eq(true)
|
16
|
+
end
|
17
|
+
|
18
|
+
after(:each) do
|
19
|
+
Jobit::Storage.destroy(@file_name)
|
20
|
+
end
|
21
|
+
|
22
|
+
after(:all) do
|
23
|
+
begin
|
24
|
+
Dir.delete(Jobit.jobs_path)
|
25
|
+
rescue
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
it "creates file with struct id equal to filename" do
|
31
|
+
job = Jobit::Storage.find(@file_name)
|
32
|
+
job.should be_a(Struct)
|
33
|
+
job.id.should eq(@file_name)
|
34
|
+
end
|
35
|
+
|
36
|
+
it ".find returns file value" do
|
37
|
+
job = Jobit::Storage.find(@file_name)
|
38
|
+
job.should be_a(Struct)
|
39
|
+
job.name.should eq(@job_name)
|
40
|
+
end
|
41
|
+
|
42
|
+
it ".all retrieves list of jobs" do
|
43
|
+
jobs = Jobit::Storage.all
|
44
|
+
jobs.should be_a(Array)
|
45
|
+
jobs.size.should eq(@total_files+1)
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
it ".find_by_name returns file value" do
|
50
|
+
job = Jobit::Storage.find_by_name(@job_name)
|
51
|
+
job.should be_a(Struct)
|
52
|
+
job.name.should eq(@job_name)
|
53
|
+
end
|
54
|
+
|
55
|
+
it ".find_by_name with wrong name returns nil" do
|
56
|
+
job = Jobit::Storage.find_by_name('wrong_name')
|
57
|
+
job.should eq(nil)
|
58
|
+
end
|
59
|
+
|
60
|
+
it ".where(name) returns file value" do
|
61
|
+
jobs = Jobit::Storage.where(@job_name)
|
62
|
+
jobs.should be_a(Array)
|
63
|
+
jobs.first.name.should eq(@job_name)
|
64
|
+
end
|
65
|
+
|
66
|
+
it ".where(hash) returns file value" do
|
67
|
+
jobs = Jobit::Storage.where({:name => @job_name})
|
68
|
+
jobs.should be_a(Array)
|
69
|
+
jobs.first.name.should eq(@job_name)
|
70
|
+
end
|
71
|
+
|
72
|
+
it ".where(wrong hash key) returns empty array" do
|
73
|
+
jobs = Jobit::Storage.where({:name1 => @job_name})
|
74
|
+
jobs.should be_a(Array)
|
75
|
+
jobs.size.should eq(0)
|
76
|
+
end
|
77
|
+
|
78
|
+
it ".where(wrong hash value) returns empty array" do
|
79
|
+
jobs = Jobit::Storage.where({:name => 'wrong_name'})
|
80
|
+
jobs.should be_a(Array)
|
81
|
+
jobs.size.should eq(0)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "find by name and update" do
|
85
|
+
job = Jobit::Storage.find_by_name(@job_name)
|
86
|
+
job.name = "new name"
|
87
|
+
Jobit::Storage.update(job.id, job)
|
88
|
+
job = Jobit::Storage.find(job.id)
|
89
|
+
job.should be_a(Struct)
|
90
|
+
job.name.should eq('new name')
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
data/spec/jobit_spec.rb
ADDED
@@ -0,0 +1,236 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
3
|
+
|
4
|
+
module JobitItems
|
5
|
+
|
6
|
+
def good_job(opt1, opt2)
|
7
|
+
set_progress(10)
|
8
|
+
unless opt1 == 'val1'
|
9
|
+
raise(NoMethodError, 'wrong arguments 0')
|
10
|
+
end
|
11
|
+
unless opt2 == 'val2'
|
12
|
+
raise(NoMethodError, 'wrong arguments 1')
|
13
|
+
end
|
14
|
+
set_progress(100)
|
15
|
+
end
|
16
|
+
|
17
|
+
def bad_job(text)
|
18
|
+
add_message "start"
|
19
|
+
set_progress(10)
|
20
|
+
add_message "end"
|
21
|
+
raise(NoMethodError, "Some dumb error #{text}")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe Jobit do
|
26
|
+
describe "Job" do
|
27
|
+
|
28
|
+
before(:each) do
|
29
|
+
@job_name = 'job-1'
|
30
|
+
@total_jobs = Jobit::Job.all.size
|
31
|
+
@new_job = Jobit::Job.add(@job_name, :good_job, 'val1', 'val2') {
|
32
|
+
{:priority => 1}
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
after(:each) do
|
37
|
+
@new_job.destroy
|
38
|
+
end
|
39
|
+
|
40
|
+
after(:all) do
|
41
|
+
begin
|
42
|
+
Dir.delete(Jobit::JOBS_PATH)
|
43
|
+
rescue
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
it "creates new job" do
|
49
|
+
@new_job.name.should eq(@job_name)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'returns nil if not found' do
|
53
|
+
job = Jobit::Job.find('123')
|
54
|
+
job.should eq(nil)
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'returns job if found by id' do
|
58
|
+
job = Jobit::Job.find(@new_job.id)
|
59
|
+
job.name.should eq(@job_name)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'returns job if found_by_name' do
|
63
|
+
job = Jobit::Job.find_by_name(@job_name)
|
64
|
+
job.name.should eq(@job_name)
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'returns array of all jobs' do
|
68
|
+
jobs = Jobit::Job.all
|
69
|
+
jobs.should be_a(Array)
|
70
|
+
jobs.size.should eq(@total_jobs+1)
|
71
|
+
jobs.first.name.should be_a(String)
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'returns array of found jobs' do
|
75
|
+
jobs = Jobit::Job.where(:name => @job_name)
|
76
|
+
jobs.should be_a(Array)
|
77
|
+
jobs.size.should eq(1)
|
78
|
+
jobs.first.name.should eq(@job_name)
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'returns empty array if wrong key' do
|
82
|
+
jobs = Jobit::Job.where(:name1 => @job_name)
|
83
|
+
jobs.should be_a(Array)
|
84
|
+
jobs.size.should eq(0)
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'returns empty array if wrong value' do
|
88
|
+
jobs = Jobit::Job.where(:name => 'wrong name')
|
89
|
+
jobs.should be_a(Array)
|
90
|
+
jobs.size.should eq(0)
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'destroys job' do
|
94
|
+
@new_job.destroy
|
95
|
+
job = Jobit::Job.find_by_name(@job_name)
|
96
|
+
job.should eq(nil)
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'update status' do
|
100
|
+
@new_job.status.should eq('new')
|
101
|
+
@new_job.set_status 'running'
|
102
|
+
@new_job.status.should eq('running')
|
103
|
+
job = Jobit::Job.find_by_name(@job_name)
|
104
|
+
job.status.should eq('running')
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'update error' do
|
108
|
+
@new_job.error.should eq(nil)
|
109
|
+
@new_job.set_error 'my error'
|
110
|
+
@new_job.error.should eq('my error')
|
111
|
+
job = Jobit::Job.find_by_name(@job_name)
|
112
|
+
job.error.should eq('my error')
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'update error' do
|
116
|
+
@new_job.tries.should eq(0)
|
117
|
+
@new_job.set_tries 1
|
118
|
+
@new_job.tries.should eq(1)
|
119
|
+
job = Jobit::Job.find_by_name(@job_name)
|
120
|
+
job.tries.should eq(1)
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'update progress' do
|
124
|
+
@new_job.progress.should eq(0)
|
125
|
+
@new_job.set_progress 10
|
126
|
+
@new_job.progress.should eq(10)
|
127
|
+
job = Jobit::Job.find_by_name(@job_name)
|
128
|
+
job.progress.should eq(10)
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'adds message' do
|
132
|
+
@new_job.message.should eq('')
|
133
|
+
@new_job.add_message('hello',true)
|
134
|
+
@new_job.message.should eq('hello')
|
135
|
+
job = Jobit::Job.find_by_name(@job_name)
|
136
|
+
job.message.should eq('hello')
|
137
|
+
@new_job.add_message(' there',true)
|
138
|
+
@new_job.message.should eq('hello there')
|
139
|
+
job = Jobit::Job.find_by_name(@job_name)
|
140
|
+
job.message.should eq('hello there')
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'running good job' do
|
144
|
+
@new_job.run_job
|
145
|
+
job = Jobit::Job.find_by_name(@job_name)
|
146
|
+
job.should eq(nil)
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'running good job 5 times' do
|
150
|
+
job = Jobit::Job.add('good_job', :good_job, 'val1', 'val2') { {
|
151
|
+
:priority => 0,
|
152
|
+
:repeat => 5
|
153
|
+
} }
|
154
|
+
job.keep?.should eq(false)
|
155
|
+
job.run_job
|
156
|
+
job = Jobit::Job.find_by_name('good_job')
|
157
|
+
job.should eq(nil)
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'running good job 5 times and keep file' do
|
161
|
+
job = Jobit::Job.add('good_job_keep', :good_job, 'val1', 'val2') { {
|
162
|
+
:priority => 0,
|
163
|
+
:repeat => 5,
|
164
|
+
:keep => true
|
165
|
+
} }
|
166
|
+
job.run_job
|
167
|
+
job = Jobit::Job.find_by_name('good_job_keep')
|
168
|
+
job.tries.should eq(5)
|
169
|
+
job.run_at.should_not eq(nil)
|
170
|
+
job.destroy
|
171
|
+
end
|
172
|
+
|
173
|
+
it 'running bad job' do
|
174
|
+
job = Jobit::Job.add('bad_job', :bad_job, 'val1') {
|
175
|
+
{
|
176
|
+
:priority => 0,
|
177
|
+
:keep => true
|
178
|
+
}
|
179
|
+
}
|
180
|
+
|
181
|
+
job.run_job
|
182
|
+
job = Jobit::Job.find_by_name('bad_job')
|
183
|
+
job.tries.should eq(1)
|
184
|
+
job.message.should eq('startend')
|
185
|
+
job.status.should eq('new')
|
186
|
+
job.progress.should eq(10)
|
187
|
+
job.destroy
|
188
|
+
end
|
189
|
+
|
190
|
+
it 'running bad job 5 times should run it just once' do
|
191
|
+
job = Jobit::Job.add('bad_job_5', :bad_job, 'val1') {
|
192
|
+
{
|
193
|
+
:priority => 0,
|
194
|
+
:repeat => 5
|
195
|
+
}
|
196
|
+
}
|
197
|
+
|
198
|
+
job.run_job
|
199
|
+
job = Jobit::Job.find_by_name('bad_job_5')
|
200
|
+
job.tries.should eq(1)
|
201
|
+
job.status.should eq('new')
|
202
|
+
job.progress.should eq(10)
|
203
|
+
job.destroy
|
204
|
+
end
|
205
|
+
|
206
|
+
it 'work_off test' do
|
207
|
+
@new_job.destroy
|
208
|
+
for i in (0..5)
|
209
|
+
Jobit::Job.add("work_off_#{i}", :good_job, 'val1', 'val2') { {
|
210
|
+
:priority => rand(6),
|
211
|
+
:keep => true
|
212
|
+
} }
|
213
|
+
end
|
214
|
+
Jobit::Job.add('work_off_bad', :bad_job, 'val1') { {
|
215
|
+
:priority => 2,
|
216
|
+
:keep => true
|
217
|
+
} }
|
218
|
+
|
219
|
+
Jobit::Job.work_off
|
220
|
+
jobs = []
|
221
|
+
for i in (0..5)
|
222
|
+
job = Jobit::Job.find_by_name("work_off_#{i}")
|
223
|
+
job.status.should eq('complete'), " #{job}"
|
224
|
+
job.destroy
|
225
|
+
job.id.should eq(nil)
|
226
|
+
end
|
227
|
+
|
228
|
+
job_failed = Jobit::Job.find_by_name('work_off_bad')
|
229
|
+
job_failed.tries.should eq(25)
|
230
|
+
job_failed.status.should eq('failed')
|
231
|
+
job_failed.destroy
|
232
|
+
|
233
|
+
end
|
234
|
+
|
235
|
+
end
|
236
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'jobit'
|
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jobit
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Victor Yunevich
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-12-03 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: daemons
|
16
|
+
requirement: &2162428060 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2162428060
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rspec
|
27
|
+
requirement: &2162427640 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2162427640
|
36
|
+
description: Process background jobs in queue.
|
37
|
+
email:
|
38
|
+
- v.t.g.m.b.x@gmail.com
|
39
|
+
executables: []
|
40
|
+
extensions: []
|
41
|
+
extra_rdoc_files: []
|
42
|
+
files:
|
43
|
+
- .gitignore
|
44
|
+
- .rspec
|
45
|
+
- CHANGELOG.md
|
46
|
+
- Gemfile
|
47
|
+
- LICENSE
|
48
|
+
- README.md
|
49
|
+
- Rakefile
|
50
|
+
- app/controllers/jobit/jobs_controller.rb
|
51
|
+
- config/routes.rb
|
52
|
+
- init.rb
|
53
|
+
- jobit.gemspec
|
54
|
+
- lib/jobit.rb
|
55
|
+
- lib/jobit/command.rb
|
56
|
+
- lib/jobit/engine.rb
|
57
|
+
- lib/jobit/job.rb
|
58
|
+
- lib/jobit/jobby.rb
|
59
|
+
- lib/jobit/jobs/commands.rb
|
60
|
+
- lib/jobit/jobs/statuses.rb
|
61
|
+
- lib/jobit/railtie.rb
|
62
|
+
- lib/jobit/storage.rb
|
63
|
+
- lib/jobit/version.rb
|
64
|
+
- lib/jobit/worker.rb
|
65
|
+
- lib/tasks/jobit.rake
|
66
|
+
- lib/tasks/tasks.rb
|
67
|
+
- spec/jobit/storage_spec.rb
|
68
|
+
- spec/jobit_spec.rb
|
69
|
+
- spec/spec_helper.rb
|
70
|
+
homepage: ''
|
71
|
+
licenses: []
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options: []
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
78
|
+
requirements:
|
79
|
+
- - ! '>='
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
requirements: []
|
89
|
+
rubyforge_project:
|
90
|
+
rubygems_version: 1.8.6
|
91
|
+
signing_key:
|
92
|
+
specification_version: 3
|
93
|
+
summary: Background jobs processing
|
94
|
+
test_files:
|
95
|
+
- spec/jobit/storage_spec.rb
|
96
|
+
- spec/jobit_spec.rb
|
97
|
+
- spec/spec_helper.rb
|