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.
Files changed (86) hide show
  1. data/.gitignore +10 -0
  2. data/.rspec +1 -0
  3. data/Gemfile +22 -0
  4. data/Gemfile.lock +164 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.rdoc +3 -0
  7. data/Rakefile +28 -0
  8. data/app/assets/images/jobbr/.gitkeep +0 -0
  9. data/app/assets/javascripts/jobbr/application.js.coffee +34 -0
  10. data/app/assets/stylesheets/jobbr/application.css.scss +79 -0
  11. data/app/controllers/jobbr/application_controller.rb +19 -0
  12. data/app/controllers/jobbr/delayed_jobs_controller.rb +17 -0
  13. data/app/controllers/jobbr/jobs_controller.rb +17 -0
  14. data/app/controllers/jobbr/runs_controller.rb +12 -0
  15. data/app/helpers/jobbr/application_helper.rb +36 -0
  16. data/app/models/jobbr/delayed_job.rb +38 -0
  17. data/app/models/jobbr/job.rb +110 -0
  18. data/app/models/jobbr/log_message.rb +15 -0
  19. data/app/models/jobbr/run.rb +61 -0
  20. data/app/models/jobbr/scheduled_job.rb +29 -0
  21. data/app/models/jobbr/standalone_tasks.rb +56 -0
  22. data/app/views/jobbr/jobs/_job_list.html.haml +23 -0
  23. data/app/views/jobbr/jobs/index.html.haml +6 -0
  24. data/app/views/jobbr/jobs/show.html.haml +30 -0
  25. data/app/views/jobbr/runs/_logs.html.haml +7 -0
  26. data/app/views/jobbr/runs/show.html.haml +31 -0
  27. data/app/views/layouts/jobbr/application.html.haml +20 -0
  28. data/config/locales/jobbr.en.yml +39 -0
  29. data/config/routes.rb +11 -0
  30. data/jobbr.gemspec +25 -0
  31. data/lib/jobbr.rb +4 -0
  32. data/lib/jobbr/engine.rb +7 -0
  33. data/lib/jobbr/logger.rb +55 -0
  34. data/lib/jobbr/mongoid.rb +54 -0
  35. data/lib/jobbr/version.rb +3 -0
  36. data/lib/jobbr/whenever.rb +24 -0
  37. data/lib/tasks/jobbr_tasks.rake +14 -0
  38. data/script/rails +8 -0
  39. data/spec/dummy/README.rdoc +261 -0
  40. data/spec/dummy/Rakefile +7 -0
  41. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  42. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  43. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  44. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  45. data/spec/dummy/app/mailers/.gitkeep +0 -0
  46. data/spec/dummy/app/models/.gitkeep +0 -0
  47. data/spec/dummy/app/models/scheduled_jobs/dummy_scheduled_job.rb +15 -0
  48. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  49. data/spec/dummy/config.ru +4 -0
  50. data/spec/dummy/config/application.rb +62 -0
  51. data/spec/dummy/config/boot.rb +10 -0
  52. data/spec/dummy/config/database.yml +25 -0
  53. data/spec/dummy/config/environment.rb +5 -0
  54. data/spec/dummy/config/environments/development.rb +37 -0
  55. data/spec/dummy/config/environments/production.rb +67 -0
  56. data/spec/dummy/config/environments/test.rb +37 -0
  57. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  58. data/spec/dummy/config/initializers/inflections.rb +15 -0
  59. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  60. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  61. data/spec/dummy/config/initializers/session_store.rb +8 -0
  62. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  63. data/spec/dummy/config/locales/en.yml +5 -0
  64. data/spec/dummy/config/mongoid.yml +80 -0
  65. data/spec/dummy/config/routes.rb +4 -0
  66. data/spec/dummy/config/schedule.rb +10 -0
  67. data/spec/dummy/lib/assets/.gitkeep +0 -0
  68. data/spec/dummy/log/.gitkeep +0 -0
  69. data/spec/dummy/public/404.html +26 -0
  70. data/spec/dummy/public/422.html +26 -0
  71. data/spec/dummy/public/500.html +25 -0
  72. data/spec/dummy/public/favicon.ico +0 -0
  73. data/spec/dummy/script/rails +6 -0
  74. data/spec/models/delayed_job_spec.rb +37 -0
  75. data/spec/models/scheduled_job_spec.rb +106 -0
  76. data/spec/spec_helper.rb +32 -0
  77. data/vendor/assets/fonts/FontAwesome.otf +0 -0
  78. data/vendor/assets/fonts/fontawesome-webfont.eot +0 -0
  79. data/vendor/assets/fonts/fontawesome-webfont.svg +284 -0
  80. data/vendor/assets/fonts/fontawesome-webfont.ttf +0 -0
  81. data/vendor/assets/fonts/fontawesome-webfont.woff +0 -0
  82. data/vendor/assets/javascripts/bootstrap.js +7 -0
  83. data/vendor/assets/javascripts/jquery-pjax.js +677 -0
  84. data/vendor/assets/stylesheets/bootstrap.css.scss +705 -0
  85. data/vendor/assets/stylesheets/font-awesome.css.scss +534 -0
  86. 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,15 @@
1
+ module Jobbr
2
+
3
+ class LogMessage
4
+
5
+ include Mongoid::Document
6
+
7
+ field :message, type: String
8
+ field :kind, type: Symbol
9
+ field :date, type: Time
10
+
11
+ embedded_in :run
12
+
13
+ end
14
+
15
+ 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,6 @@
1
+ %ul.breadcrumb
2
+ %li.active= t('.title')
3
+
4
+ = render 'job_list', title: t('.scheduled_jobs'), jobs: @scheduled_jobs
5
+ %br
6
+ = render 'job_list', title: t('.delayed_jobs'), jobs: @delayed_jobs
@@ -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,7 @@
1
+ %h5= title
2
+ .well.logs{class: size}
3
+ - run.messages.each do |msg|
4
+ %span.date{class: msg.kind}= "[#{msg.date.localtime.strftime('%H:%M:%S')}]"
5
+ %span.kind{class: msg.kind}= "[#{msg.kind}]"
6
+ %span.message= raw(msg.message)
7
+ %br
@@ -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
+