jobbr 1.0.1

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