jobbr 1.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 +10 -0
- data/.rspec +1 -0
- data/Gemfile +22 -0
- data/Gemfile.lock +164 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +3 -0
- data/Rakefile +28 -0
- data/app/assets/images/jobbr/.gitkeep +0 -0
- data/app/assets/javascripts/jobbr/application.js.coffee +34 -0
- data/app/assets/stylesheets/jobbr/application.css.scss +79 -0
- data/app/controllers/jobbr/application_controller.rb +19 -0
- data/app/controllers/jobbr/delayed_jobs_controller.rb +17 -0
- data/app/controllers/jobbr/jobs_controller.rb +17 -0
- data/app/controllers/jobbr/runs_controller.rb +12 -0
- data/app/helpers/jobbr/application_helper.rb +36 -0
- data/app/models/jobbr/delayed_job.rb +38 -0
- data/app/models/jobbr/job.rb +110 -0
- data/app/models/jobbr/log_message.rb +15 -0
- data/app/models/jobbr/run.rb +61 -0
- data/app/models/jobbr/scheduled_job.rb +29 -0
- data/app/models/jobbr/standalone_tasks.rb +56 -0
- data/app/views/jobbr/jobs/_job_list.html.haml +23 -0
- data/app/views/jobbr/jobs/index.html.haml +6 -0
- data/app/views/jobbr/jobs/show.html.haml +30 -0
- data/app/views/jobbr/runs/_logs.html.haml +7 -0
- data/app/views/jobbr/runs/show.html.haml +31 -0
- data/app/views/layouts/jobbr/application.html.haml +20 -0
- data/config/locales/jobbr.en.yml +39 -0
- data/config/routes.rb +11 -0
- data/jobbr.gemspec +25 -0
- data/lib/jobbr.rb +4 -0
- data/lib/jobbr/engine.rb +7 -0
- data/lib/jobbr/logger.rb +55 -0
- data/lib/jobbr/mongoid.rb +54 -0
- data/lib/jobbr/version.rb +3 -0
- data/lib/jobbr/whenever.rb +24 -0
- data/lib/tasks/jobbr_tasks.rake +14 -0
- data/script/rails +8 -0
- data/spec/dummy/README.rdoc +261 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/javascripts/application.js +15 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.gitkeep +0 -0
- data/spec/dummy/app/models/.gitkeep +0 -0
- data/spec/dummy/app/models/scheduled_jobs/dummy_scheduled_job.rb +15 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +62 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +37 -0
- data/spec/dummy/config/environments/production.rb +67 -0
- data/spec/dummy/config/environments/test.rb +37 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +15 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/mongoid.yml +80 -0
- data/spec/dummy/config/routes.rb +4 -0
- data/spec/dummy/config/schedule.rb +10 -0
- data/spec/dummy/lib/assets/.gitkeep +0 -0
- data/spec/dummy/log/.gitkeep +0 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +25 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/models/delayed_job_spec.rb +37 -0
- data/spec/models/scheduled_job_spec.rb +106 -0
- data/spec/spec_helper.rb +32 -0
- data/vendor/assets/fonts/FontAwesome.otf +0 -0
- data/vendor/assets/fonts/fontawesome-webfont.eot +0 -0
- data/vendor/assets/fonts/fontawesome-webfont.svg +284 -0
- data/vendor/assets/fonts/fontawesome-webfont.ttf +0 -0
- data/vendor/assets/fonts/fontawesome-webfont.woff +0 -0
- data/vendor/assets/javascripts/bootstrap.js +7 -0
- data/vendor/assets/javascripts/jquery-pjax.js +677 -0
- data/vendor/assets/stylesheets/bootstrap.css.scss +705 -0
- data/vendor/assets/stylesheets/font-awesome.css.scss +534 -0
- metadata +275 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
module Jobbr
|
2
|
+
module ApplicationHelper
|
3
|
+
|
4
|
+
def delayed_job_creation_path(delayed_job_class, params = {})
|
5
|
+
delayed_jobs_path(params.merge(job_name: delayed_job_class.name.underscore))
|
6
|
+
end
|
7
|
+
|
8
|
+
def delayed_job_polling_path(id = ':job_id')
|
9
|
+
delayed_job_path(id)
|
10
|
+
end
|
11
|
+
|
12
|
+
def status_icon_class(job_status)
|
13
|
+
if job_status == :waiting
|
14
|
+
"job-status #{job_status} icon-circle-blank"
|
15
|
+
elsif job_status == :running
|
16
|
+
"job-status #{job_status} icon-refresh icon-spin"
|
17
|
+
elsif job_status == :success
|
18
|
+
"job-status #{job_status} icon-certificate"
|
19
|
+
else
|
20
|
+
"job-status #{job_status} icon-exclamation-sign"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def display_scheduling(job)
|
25
|
+
every = job.class.every
|
26
|
+
if every
|
27
|
+
scheduling = ChronicDuration.output(every[0])
|
28
|
+
if every[1] && !every[1].empty?
|
29
|
+
scheduling = "#{scheduling} at #{every[1][:at]}"
|
30
|
+
end
|
31
|
+
scheduling
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Jobbr
|
2
|
+
|
3
|
+
class DelayedJob < Jobbr::Job
|
4
|
+
|
5
|
+
field :delayed, type: Boolean, default: true
|
6
|
+
|
7
|
+
# hack to work around multiple inheritance issue with Mongoid
|
8
|
+
default_scope ->{ Job.where(delayed: true, :_type.ne => nil) }
|
9
|
+
|
10
|
+
def perform(params, run)
|
11
|
+
raise NotImplementedError.new :message => 'Must be implemented'
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.run_delayed(params, delayed = true)
|
15
|
+
job = instance
|
16
|
+
job_run = Run.create(status: :waiting, started_at: Time.now, job: job)
|
17
|
+
if delayed
|
18
|
+
job.delay.run(job_run, params)
|
19
|
+
else
|
20
|
+
job.run(job_run, params)
|
21
|
+
end
|
22
|
+
job_run
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.run_delayed_by_name(job_class_name, params, delayed = true)
|
26
|
+
job = instance(job_class_name)
|
27
|
+
job_run = Run.create(status: :waiting, started_at: Time.now, job: job)
|
28
|
+
if delayed
|
29
|
+
job.delay.run(job_run, params)
|
30
|
+
else
|
31
|
+
job.run(job_run, params)
|
32
|
+
end
|
33
|
+
job_run
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'jobbr/logger'
|
2
|
+
|
3
|
+
module Jobbr
|
4
|
+
|
5
|
+
class Job
|
6
|
+
|
7
|
+
include Mongoid::Document
|
8
|
+
include Mongoid::Timestamps
|
9
|
+
|
10
|
+
MAX_RUN_PER_JOB = 50
|
11
|
+
|
12
|
+
has_many :runs, class_name: 'Jobbr::Run', dependent: :destroy
|
13
|
+
|
14
|
+
scope :by_name, ->(name) { where(_type: /.*#{name.underscore.camelize}/) }
|
15
|
+
|
16
|
+
def self.instance(instance_type = nil)
|
17
|
+
if instance_type
|
18
|
+
job_class = instance_type.camelize.constantize
|
19
|
+
else
|
20
|
+
job_class = self
|
21
|
+
end
|
22
|
+
job_class.find_or_create_by(_type: job_class.name)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.run
|
26
|
+
instance.run
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.description(desc = nil)
|
30
|
+
@description = desc if desc
|
31
|
+
@description
|
32
|
+
end
|
33
|
+
|
34
|
+
def run(job_run = nil, params = {})
|
35
|
+
if job_run
|
36
|
+
job_run.status = :running
|
37
|
+
job_run.save!
|
38
|
+
else
|
39
|
+
job_run = Run.create(status: :running, started_at: Time.now, job: self)
|
40
|
+
end
|
41
|
+
|
42
|
+
# prevent Run collection to grow beyond max_run_per_job
|
43
|
+
job_runs = Run.where(job: self).order_by(started_at: 1)
|
44
|
+
runs_count = job_runs.count
|
45
|
+
if runs_count > max_run_per_job
|
46
|
+
job_runs.limit(runs_count - max_run_per_job).each(&:destroy)
|
47
|
+
end
|
48
|
+
|
49
|
+
# overidding Rails.logger
|
50
|
+
old_logger = Rails.logger
|
51
|
+
Rails.logger = Jobbr::Logger.new(Rails.logger, job_run)
|
52
|
+
|
53
|
+
begin
|
54
|
+
if self.delayed?
|
55
|
+
perform(params, job_run)
|
56
|
+
else
|
57
|
+
perform
|
58
|
+
end
|
59
|
+
job_run.status = :success
|
60
|
+
rescue Exception => e
|
61
|
+
job_run.status = :failure
|
62
|
+
logger.error(e.message)
|
63
|
+
logger.error(e.backtrace)
|
64
|
+
raise e
|
65
|
+
ensure
|
66
|
+
Rails.logger = old_logger
|
67
|
+
job_run.finished_at = Time.now
|
68
|
+
job_run.save!
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def last_run
|
73
|
+
@last_run ||= Run.for_job(self).first
|
74
|
+
end
|
75
|
+
|
76
|
+
def average_run_time
|
77
|
+
return 0 if runs.empty?
|
78
|
+
(runs.map { |run| run.run_time }.compact.inject { |sum, el| sum + el }.to_f / runs.length).round(2)
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_param
|
82
|
+
name.parameterize
|
83
|
+
end
|
84
|
+
|
85
|
+
def name
|
86
|
+
self._type.demodulize.underscore.humanize
|
87
|
+
end
|
88
|
+
|
89
|
+
def scheduled?
|
90
|
+
self.is_a? Jobbr::ScheduledJob
|
91
|
+
end
|
92
|
+
|
93
|
+
def delayed?
|
94
|
+
self.is_a? Jobbr::DelayedJob
|
95
|
+
end
|
96
|
+
|
97
|
+
protected
|
98
|
+
|
99
|
+
# mocking purpose
|
100
|
+
def max_run_per_job
|
101
|
+
MAX_RUN_PER_JOB
|
102
|
+
end
|
103
|
+
|
104
|
+
def logger
|
105
|
+
Rails.logger
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Jobbr
|
2
|
+
|
3
|
+
class Run
|
4
|
+
|
5
|
+
include Mongoid::Document
|
6
|
+
include Mongoid::Timestamps
|
7
|
+
|
8
|
+
field :status, type: Symbol
|
9
|
+
field :started_at, type: Time
|
10
|
+
field :finished_at, type: Time
|
11
|
+
field :progress, type: Integer, default: 0
|
12
|
+
field :result
|
13
|
+
|
14
|
+
belongs_to :job
|
15
|
+
embeds_many :log_messages, class_name: 'Jobbr::LogMessage'
|
16
|
+
|
17
|
+
index(job_id: 1, started_at: -1)
|
18
|
+
|
19
|
+
scope :for_job, ->(job) { Run.where(job_id: job.id).order_by(started_at: -1) }
|
20
|
+
|
21
|
+
def run_time
|
22
|
+
@run_time ||= if finished_at && started_at
|
23
|
+
finished_at - started_at
|
24
|
+
else
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def next
|
30
|
+
return nil if index == 0
|
31
|
+
@next ||= Run.for_job(job).all[index - 1]
|
32
|
+
end
|
33
|
+
|
34
|
+
def previous
|
35
|
+
@previous ||= Run.for_job(job).all[index + 1]
|
36
|
+
end
|
37
|
+
|
38
|
+
def messages(limit = 1000)
|
39
|
+
limit = [log_messages.length, limit].min
|
40
|
+
log_messages[-limit..-1]
|
41
|
+
end
|
42
|
+
|
43
|
+
def result=(result)
|
44
|
+
write_attribute(:result, result)
|
45
|
+
save!
|
46
|
+
end
|
47
|
+
|
48
|
+
def progress=(progress)
|
49
|
+
write_attribute(:progress, progress)
|
50
|
+
save!
|
51
|
+
end
|
52
|
+
|
53
|
+
protected
|
54
|
+
|
55
|
+
def index
|
56
|
+
@index ||= job.runs.index(self)
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Jobbr
|
2
|
+
|
3
|
+
class ScheduledJob < Job
|
4
|
+
|
5
|
+
field :scheduled, type: Boolean, default: true
|
6
|
+
|
7
|
+
default_scope ->{ Job.where(scheduled: true, :_type.ne => nil) }
|
8
|
+
|
9
|
+
def perform
|
10
|
+
raise NotImplementedError.new :message => 'Must be implemented'
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.every(every = nil, options = {})
|
14
|
+
@every = [every, options] if every
|
15
|
+
@every
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.task_name(with_namespace = false)
|
19
|
+
task_name = name.demodulize.underscore
|
20
|
+
if with_namespace
|
21
|
+
"jobbr:#{task_name}"
|
22
|
+
else
|
23
|
+
task_name
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Jobbr
|
2
|
+
module StandaloneTasks
|
3
|
+
|
4
|
+
# Build the information about the crontab jobs which
|
5
|
+
# will used to generate the corresponding rake tasks.
|
6
|
+
# If a block is passed, then it will iterate over each information.
|
7
|
+
#
|
8
|
+
# @params [ String ] name Name of the kind of jobs (scheduled_job)
|
9
|
+
#
|
10
|
+
def self.all(name, &block)
|
11
|
+
self.mock_job
|
12
|
+
require File.join(File.dirname(__FILE__), "#{name}.rb")
|
13
|
+
|
14
|
+
dependencies = ['Jobbr::Job', "Jobbr::#{name.to_s.camelize}"]
|
15
|
+
|
16
|
+
# load all the classes for the specific kind
|
17
|
+
list = Dir[Rails.root.join('app', 'models', name.to_s.pluralize, '*.rb')].map do |file|
|
18
|
+
require file
|
19
|
+
klass = "#{name.to_s.pluralize.camelize}::#{File.basename(file, '.rb').camelize}".constantize
|
20
|
+
dependencies << klass.name
|
21
|
+
{
|
22
|
+
name: klass.task_name.to_sym,
|
23
|
+
desc: klass.description,
|
24
|
+
klass_name: klass.name,
|
25
|
+
dependencies: (dependencies[0..1] + [klass.name]).map { |n| "#{n.underscore}.rb" }
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
# clean our Job mock and make sure to unload its children as well
|
30
|
+
dependencies.reverse.each do |name|
|
31
|
+
module_name, klass_name = name.split('::')
|
32
|
+
module_name.constantize.send(:remove_const, klass_name.to_sym)
|
33
|
+
end
|
34
|
+
|
35
|
+
list.each(&block)
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
# Mock Jobbr::Job which avoids to load the Mongoid stack
|
41
|
+
#
|
42
|
+
def self.mock_job
|
43
|
+
c = Class.new do
|
44
|
+
def self.field(*args); end
|
45
|
+
def self.default_scope(*args); end
|
46
|
+
def self.description(desc = nil)
|
47
|
+
@description = desc if desc
|
48
|
+
@description
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
::Jobbr.const_set 'Job', c
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
%h5= title
|
2
|
+
|
3
|
+
%table.table.table-striped.table-hover
|
4
|
+
%thead
|
5
|
+
%tr
|
6
|
+
%th= t('.status')
|
7
|
+
%th= t('.job_name')
|
8
|
+
%th= t('.last_run')
|
9
|
+
%th= t('.average_run_time')
|
10
|
+
- jobs.each do |job|
|
11
|
+
%tr
|
12
|
+
%td
|
13
|
+
%i{'class' => status_icon_class(job.last_run.status)}
|
14
|
+
%td= job.name.humanize
|
15
|
+
%td= l job.last_run.started_at.localtime
|
16
|
+
%td= ChronicDuration.output(job.average_run_time)
|
17
|
+
%td
|
18
|
+
.btn-toolbar
|
19
|
+
.btn-group
|
20
|
+
= link_to job_path(job), class: 'btn', title: t('.see_all_runs') do
|
21
|
+
%i.icon-list
|
22
|
+
= link_to job_run_path(job, job.last_run), class: 'btn', title: t('.see_last_run') do
|
23
|
+
%i.icon-download
|
@@ -0,0 +1,30 @@
|
|
1
|
+
%ul.breadcrumb
|
2
|
+
%li
|
3
|
+
= link_to t('jobbr.jobs.index.title'), jobs_path
|
4
|
+
%span.divider /
|
5
|
+
%li.active= @job.name.humanize
|
6
|
+
|
7
|
+
.well
|
8
|
+
- if @job.scheduled? && @job.class.every
|
9
|
+
%p= raw t('.scheduling', scheduling: display_scheduling(@job))
|
10
|
+
= raw t('.average_run_time', run_time: ChronicDuration.output(@job.average_run_time, format: :long))
|
11
|
+
|
12
|
+
= render 'jobbr/runs/logs', run: @job.last_run, title: t('.last_run_logs'), size: 'small'
|
13
|
+
|
14
|
+
%table.table.table-striped.table-hover
|
15
|
+
%thead
|
16
|
+
%tr
|
17
|
+
%th= t('.status')
|
18
|
+
%th= t('.last_run')
|
19
|
+
%th= t('.duration')
|
20
|
+
- @runs.each do |run|
|
21
|
+
%tr
|
22
|
+
%td
|
23
|
+
%i{'class' => status_icon_class(run.status)}
|
24
|
+
%td= l run.started_at.localtime
|
25
|
+
%td= ChronicDuration.output((run.finished_at - run.started_at).round(2)) rescue 'N/A'
|
26
|
+
%td
|
27
|
+
.btn-toolbar
|
28
|
+
.btn-group
|
29
|
+
= link_to job_run_path(@job, run), class: 'btn', title: t('.see_run') do
|
30
|
+
%i.icon-download
|
@@ -0,0 +1,31 @@
|
|
1
|
+
%ul.breadcrumb
|
2
|
+
%li
|
3
|
+
= link_to t('jobbr.jobs.index.title'), jobs_path
|
4
|
+
%span.divider /
|
5
|
+
%li
|
6
|
+
= link_to @job.name.humanize, job_path(@job)
|
7
|
+
%span.divider /
|
8
|
+
%li.active
|
9
|
+
= l @run.started_at
|
10
|
+
%i{'class' => status_icon_class(@run.status)}
|
11
|
+
.btn-toolbar
|
12
|
+
.btn-group
|
13
|
+
- if @run.previous
|
14
|
+
= link_to job_run_path(@job, @run.previous), class: 'btn', title: t('.previous_run') do
|
15
|
+
%i.icon-step-backward
|
16
|
+
- else
|
17
|
+
= link_to '#', class: 'btn disabled' do
|
18
|
+
%i.icon-step-backward
|
19
|
+
- if @run.next
|
20
|
+
= link_to job_run_path(@job, @run.next), class: 'btn', title: t('.next_run') do
|
21
|
+
%i.icon-step-forward
|
22
|
+
- else
|
23
|
+
= link_to '#', class: 'btn disabled' do
|
24
|
+
%i.icon-step-forward
|
25
|
+
|
26
|
+
- if @run.run_time
|
27
|
+
.well
|
28
|
+
= t('.run_time', run_time: ChronicDuration.output(@run.run_time.round(2), format: :long))
|
29
|
+
|
30
|
+
= render 'logs', run: @run, title: t('.logs'), size: 'large'
|
31
|
+
|