jobbr 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|