marty 9.5.1 → 10.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/README.md +40 -31
  4. data/app/components/marty/background_job/schedule_jobs_dashboard.rb +2 -2
  5. data/app/components/marty/background_job/schedule_jobs_grid.rb +22 -5
  6. data/app/components/marty/background_job/schedule_jobs_logs.rb +12 -0
  7. data/app/components/marty/base_rule_view.rb +0 -4
  8. data/app/components/marty/delorean_rule_view.rb +11 -5
  9. data/app/components/marty/promise_view.rb +15 -0
  10. data/app/jobs/marty/cron_job.rb +30 -28
  11. data/app/jobs/marty/delorean_background_job.rb +8 -0
  12. data/app/models/marty/background_job/schedule.rb +24 -1
  13. data/app/models/marty/delorean_rule.rb +1 -1
  14. data/app/models/marty/vw_promise.rb +6 -4
  15. data/app/services/marty/background_job/fetch_missing_in_schedule_cron_jobs.rb +8 -6
  16. data/app/services/marty/background_job/update_schedule.rb +15 -8
  17. data/app/services/marty/jobs/schedule.rb +8 -4
  18. data/app/services/marty/promises/delorean/create.rb +2 -1
  19. data/app/services/marty/promises/ruby/create.rb +2 -1
  20. data/config/locales/en.yml +1 -0
  21. data/db/migrate/510_schedule_job_to_remove_old_promises.rb +4 -2
  22. data/db/migrate/523_add_timeout_to_promises.rb +5 -0
  23. data/db/migrate/524_add_timeout_to_promise_view.rb +45 -0
  24. data/db/migrate/525_add_arguments_to_jobs_schedules.rb +10 -0
  25. data/db/migrate/526_add_schedule_id_to_delayed_jobs.rb +14 -0
  26. data/lib/marty/delayed_job/queue_adapter.rb +38 -0
  27. data/lib/marty/delayed_job/scheduled_job_plugin.rb +9 -6
  28. data/lib/marty/diagnostic/environment_variables.rb +1 -1
  29. data/lib/marty/monkey.rb +11 -0
  30. data/lib/marty/promise_job.rb +2 -1
  31. data/lib/marty/promise_ruby_job.rb +2 -1
  32. data/lib/marty/version.rb +1 -1
  33. data/spec/dummy/.foreman +2 -0
  34. data/spec/dummy/Procfile +2 -0
  35. data/spec/dummy/app/jobs/test_failing_job.rb +5 -1
  36. data/spec/dummy/app/models/gemini/my_rule.rb +2 -0
  37. data/spec/dummy/app/models/gemini/xyz_rule.rb +2 -0
  38. data/spec/dummy/delorean/jobs.dl +6 -0
  39. data/spec/features/delayed_jobs_grid_spec.rb +3 -2
  40. data/spec/features/schedule_jobs_dashboard_spec.rb +12 -12
  41. data/spec/jobs/cron_job_spec.rb +16 -12
  42. data/spec/jobs/delorean_background_job_spec.rb +50 -0
  43. data/spec/models/promise_spec.rb +1 -0
  44. data/spec/services/background_job/fetch_missing_in_schedule_cron_jobs_spec.rb +5 -3
  45. data/spec/services/jobs/schedule_spec.rb +5 -4
  46. metadata +12 -2
@@ -0,0 +1,8 @@
1
+ module Marty
2
+ class DeloreanBackgroundJob < ::Marty::CronJob
3
+ def perform(script, node, attribute)
4
+ engine = Marty::ScriptSet.new.get_engine(script)
5
+ engine.evaluate(node, attribute, {})
6
+ end
7
+ end
8
+ end
@@ -8,20 +8,43 @@ module Marty
8
8
  REGEX = %r{\A(((\*?[\d/,\-]*)\s){3,4}(\*?([\d/,\-])*\s)(\*?([\d/,\-])*))\z}i
9
9
 
10
10
  validates :job_class, :cron, :state, presence: true
11
- validates :job_class, uniqueness: true
12
11
  validates :cron, format: { with: REGEX }
13
12
 
14
13
  validate :job_class_validation
14
+ validate :arguments_array_validation
15
+ validate :job_class_uniqueness_validation
16
+
17
+ has_one :delayed_job, class_name: '::Delayed::Job', dependent: :destroy
15
18
 
16
19
  ALL_STATES = %w[on off].freeze
17
20
  enum state: ALL_STATES.zip(ALL_STATES).to_h
18
21
 
22
+ scope :by_arguments, lambda { |arguments|
23
+ where('arguments = ?', arguments.to_json)
24
+ }
25
+
19
26
  def job_class_validation
20
27
  job_class.constantize.respond_to?(:schedule)
21
28
  rescue NameError
22
29
  errors.add(:job_class, "doesn't exist")
23
30
  false
24
31
  end
32
+
33
+ def arguments_array_validation
34
+ return if arguments.is_a? Array
35
+
36
+ errors.add(:arguments, 'must be an Array')
37
+ false
38
+ end
39
+
40
+ def job_class_uniqueness_validation
41
+ return unless self.class.by_arguments(arguments).
42
+ where.not(id: id).
43
+ where(job_class: job_class).any?
44
+
45
+ errors.add(:arguments, 'are not unique')
46
+ false
47
+ end
25
48
  end
26
49
  end
27
50
  end
@@ -1,7 +1,7 @@
1
1
  class Marty::DeloreanRule < Marty::BaseRule
2
2
  self.abstract_class = true
3
3
 
4
- validates :rule_type, :start_dt, presence: true
4
+ validates :start_dt, presence: true
5
5
 
6
6
  def validate
7
7
  super
@@ -23,16 +23,18 @@ class Marty::VwPromise < Marty::Base
23
23
  def user_id
24
24
  0
25
25
  end
26
- alias_method :job_id, :user_id
27
26
 
28
- def result
29
- nil
27
+ def job_id
28
+ 0
30
29
  end
31
- [:start_dt, :end_dt].each { |m| alias_method m, :result }
32
30
 
33
31
  def status
34
32
  true
35
33
  end
34
+
35
+ [:result, :start_dt, :end_dt, :timeout].each do |m|
36
+ define_method(m) { nil }
37
+ end
36
38
  end
37
39
 
38
40
  def self.root
@@ -2,17 +2,19 @@ module Marty
2
2
  module BackgroundJob
3
3
  module FetchMissingInScheduleCronJobs
4
4
  def self.call
5
- in_dashboard = Marty::BackgroundJob::Schedule.pluck(:job_class)
5
+ in_dashboard = Marty::BackgroundJob::Schedule.all
6
6
 
7
- names_conditions = in_dashboard.map do |job_class_name|
8
- "%job_class: #{job_class_name}\n%"
7
+ names_conditions = in_dashboard.map do |schedule|
8
+ "%job_class: #{schedule.job_class}\n%"
9
9
  end
10
10
 
11
- Delayed::Job.
11
+ djs = Delayed::Job.
12
12
  where.not(cron: nil).
13
13
  where.not(cron: '').
14
- where("handler ILIKE '%job_class:%'").
15
- where.not('handler ILIKE ANY ( array[?] )', names_conditions)
14
+ where("handler ILIKE '%job_class:%'")
15
+
16
+ djs.where.not('handler ILIKE ANY ( array[?] )', names_conditions).
17
+ or(djs.where.not(schedule_id: in_dashboard.map(&:id)))
16
18
  end
17
19
  end
18
20
  end
@@ -4,26 +4,33 @@ module Marty
4
4
  def self.call(id:, job_class:)
5
5
  model = Marty::BackgroundJob::Schedule.find_by(id: id)
6
6
 
7
- return remove_schedule(job_class: job_class) if model.blank?
8
- return remove_schedule(job_class: job_class) if model.off?
9
- return schedule(job_class: job_class) if model.on?
7
+ if model.blank? || model.off?
8
+ return remove_schedule(
9
+ schedule_id: id,
10
+ job_class: job_class
11
+ )
12
+ end
13
+
14
+ return schedule(schedule_obj: model) if model.on?
10
15
  end
11
16
 
12
- def self.remove_schedule(job_class:)
17
+ def self.remove_schedule(schedule_id:, job_class:)
13
18
  klass = job_class.constantize
14
- klass.remove_schedule if klass.respond_to?(:remove_schedule)
19
+ return true unless klass.respond_to?(:remove_schedule)
20
+
21
+ klass.remove_schedule(Delayed::Job.find_by(schedule_id: schedule_id))
15
22
 
16
23
  true
17
24
  rescue NameError
18
25
  false
19
26
  end
20
27
 
21
- def self.schedule(job_class:)
22
- klass = job_class.constantize
28
+ def self.schedule(schedule_obj:)
29
+ klass = schedule_obj.job_class.constantize
23
30
 
24
31
  return false unless klass.respond_to?(:schedule)
25
32
 
26
- klass.schedule
33
+ klass.schedule(schedule_obj: schedule_obj)
27
34
 
28
35
  true
29
36
  rescue NameError
@@ -3,7 +3,7 @@ module Marty
3
3
  module Schedule
4
4
  extend Delorean::Functions
5
5
 
6
- delorean_fn :call, sig: 0 do
6
+ delorean_fn :call do
7
7
  glob = Rails.root.join('app/jobs/**/*_job.rb')
8
8
  Dir.glob(glob).sort.each { |f| require f }
9
9
 
@@ -12,9 +12,13 @@ module Marty
12
12
 
13
13
  Delayed::Job.where.not(cron: nil).each(&:destroy!)
14
14
 
15
- Marty::CronJob.subclasses.map do |klass|
16
- klass.schedule
17
- [klass.name, klass.cron_expression]
15
+ Marty::BackgroundJob::Schedule.all.map do |schedule|
16
+ Marty::BackgroundJob::UpdateSchedule.call(
17
+ id: schedule.id,
18
+ job_class: schedule.job_class,
19
+ )
20
+
21
+ [schedule.job_class, schedule.arguments, schedule.cron]
18
22
  end
19
23
  end
20
24
  end
@@ -27,7 +27,8 @@ module Marty
27
27
  user_id: promise_params[:_user_id],
28
28
  parent_id: promise_params[:_parent_id],
29
29
  priority: priority,
30
- promise_type: 'delorean'
30
+ promise_type: 'delorean',
31
+ timeout: timeout
31
32
  )
32
33
 
33
34
  params[:_promise_id] = promise.id
@@ -27,7 +27,8 @@ module Marty
27
27
  user_id: promise_params[:_user_id],
28
28
  parent_id: promise_params[:_parent_id],
29
29
  priority: priority,
30
- promise_type: 'ruby'
30
+ promise_type: 'ruby',
31
+ timeout: timeout
31
32
  )
32
33
 
33
34
  begin
@@ -134,6 +134,7 @@ en:
134
134
  title: Job Name
135
135
  start_dt: Start Date
136
136
  end_dt: End Date
137
+ total_time: Total Time
137
138
  user_login: User Login
138
139
  cformat: Format
139
140
  schedule_dashboard:
@@ -2,13 +2,15 @@ class ScheduleJobToRemoveOldPromises < ActiveRecord::Migration[4.2]
2
2
  def up
3
3
  cron_every_hour = '0 * * * *'
4
4
 
5
- Marty::BackgroundJob::Schedule.create!(
5
+ schedule = Marty::BackgroundJob::Schedule.new(
6
6
  job_class: 'Marty::RemoveOldPromisesJob',
7
7
  cron: cron_every_hour,
8
8
  state: 'on'
9
9
  )
10
10
 
11
- ::Marty::RemoveOldPromisesJob.schedule
11
+ # Since we add `arguments` column to the model in later migrations,
12
+ # we should skip it's validation here
13
+ schedule.save!(validate: false)
12
14
  end
13
15
 
14
16
  def down
@@ -0,0 +1,5 @@
1
+ class AddTimeoutToPromises < ActiveRecord::Migration[4.2]
2
+ def change
3
+ add_column :marty_promises, :timeout, :integer
4
+ end
5
+ end
@@ -0,0 +1,45 @@
1
+ class AddTimeoutToPromiseView < ActiveRecord::Migration[4.2]
2
+ def up
3
+ execute <<~SQL
4
+ DROP VIEW IF EXISTS marty_vw_promises;
5
+ CREATE OR REPLACE VIEW marty_vw_promises
6
+ AS
7
+ SELECT
8
+ id,
9
+ title,
10
+ user_id,
11
+ cformat,
12
+ parent_id,
13
+ job_id,
14
+ status,
15
+ start_dt,
16
+ end_dt,
17
+ priority,
18
+ timeout
19
+ FROM marty_promises;
20
+
21
+ GRANT SELECT ON marty_vw_promises TO public;
22
+ SQL
23
+ end
24
+
25
+ def down
26
+ execute <<~SQL
27
+ DROP VIEW IF EXISTS marty_vw_promises;
28
+ CREATE OR REPLACE VIEW marty_vw_promises
29
+ AS
30
+ SELECT
31
+ id,
32
+ title,
33
+ user_id,
34
+ cformat,
35
+ parent_id,
36
+ job_id,
37
+ status,
38
+ start_dt,
39
+ end_dt
40
+ FROM marty_promises;
41
+
42
+ GRANT SELECT ON marty_vw_promises TO public;
43
+ SQL
44
+ end
45
+ end
@@ -0,0 +1,10 @@
1
+ class AddArgumentsToJobsSchedules < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :marty_background_job_schedules, :arguments, :jsonb, default: [], null: false
4
+
5
+ remove_index :marty_background_job_schedules, :job_class
6
+ add_index :marty_background_job_schedules, [:job_class, :arguments], unique: true
7
+
8
+ add_column :marty_background_job_logs, :arguments, :jsonb, default: [], null: false
9
+ end
10
+ end
@@ -0,0 +1,14 @@
1
+ class AddScheduleIdToDelayedJobs < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :delayed_jobs, :schedule_id, :integer
4
+ add_index :delayed_jobs, :schedule_id
5
+
6
+ reversible do |dir|
7
+ dir.up { set_ids }
8
+ end
9
+ end
10
+
11
+ def set_ids
12
+ ::Marty::Jobs::Schedule.call
13
+ end
14
+ end
@@ -0,0 +1,38 @@
1
+ # Copied from https://github.com/codez/delayed_cron_job/blob/master/lib/delayed_cron_job/active_job/queue_adapter.rb
2
+ # Only schedule_id is added
3
+ module Marty
4
+ module DelayedJob
5
+ module QueueAdapter
6
+ def self.included(klass)
7
+ klass.send(:alias_method, :enqueue, :enqueue_with_cron)
8
+ klass.send(:alias_method, :enqueue_at, :enqueue_at_with_cron)
9
+ end
10
+
11
+ def self.extended(klass)
12
+ meta = class << klass; self; end
13
+ meta.send(:alias_method, :enqueue, :enqueue_with_cron)
14
+ meta.send(:alias_method, :enqueue_at, :enqueue_at_with_cron)
15
+ end
16
+
17
+ def enqueue_with_cron(job)
18
+ enqueue_at(job, nil)
19
+ end
20
+
21
+ def enqueue_at_with_cron(job, timestamp)
22
+ options = {
23
+ queue: job.queue_name,
24
+ cron: job.cron,
25
+ schedule_id: job.schedule_id
26
+ }
27
+
28
+ options[:run_at] = Time.at(timestamp) if timestamp
29
+ options[:priority] = job.priority if job.respond_to?(:priority)
30
+ wrapper = ::ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper.new(job.serialize)
31
+ delayed_job = Delayed::Job.enqueue(wrapper, options)
32
+ job.provider_job_id = delayed_job.id if job.respond_to?(:provider_job_id=)
33
+
34
+ delayed_job
35
+ end
36
+ end
37
+ end
38
+ end
@@ -14,17 +14,20 @@ module Marty
14
14
  lifecycle.before(:error) do |worker, job, &block|
15
15
  if cron?(job)
16
16
  begin
17
- job_class_str = job.handler.split("\n").find do |line|
18
- line.include? 'job_class'
17
+ schedule = ::Marty::BackgroundJob::Schedule.find_by(id: job.schedule_id)
18
+
19
+ if schedule&.on?
20
+ job.cron = schedule.cron
21
+ job.schedule_id = schedule.id
22
+ else
23
+ job.cron = nil
24
+ job.schedule_id = nil
19
25
  end
20
- job_class_name = job_class_str.gsub('job_class:', '').strip
21
- job_class = job_class_name.constantize
22
- job.cron = job_class.cron_expression
23
26
  rescue StandardError
24
27
  end
25
28
  else
26
29
  # No cron job - proceed as normal
27
- block.call(worker, job)
30
+ block&.call(worker, job)
28
31
  end
29
32
  end
30
33
  end
@@ -4,7 +4,7 @@ module Marty::Diagnostic; class EnvironmentVariables < Base
4
4
  end
5
5
 
6
6
  def self.env filter = ''
7
- env = ENV.clone
7
+ env = ENV.to_h.clone
8
8
 
9
9
  to_delete = (Marty::Config['DIAG_ENV_BLOCK'] || []).map(&:upcase) + [
10
10
  'SCRIPT_URI', 'SCRIPT_URL'
@@ -396,5 +396,16 @@ end
396
396
 
397
397
  require 'delayed_cron_job'
398
398
  require_relative './delayed_job/scheduled_job_plugin.rb'
399
+ require_relative './delayed_job/queue_adapter.rb'
399
400
 
400
401
  Delayed::Worker.plugins << Marty::DelayedJob::ScheduledJobPlugin
402
+
403
+ if ActiveJob::QueueAdapters::DelayedJobAdapter.respond_to?(:enqueue)
404
+ ActiveJob::QueueAdapters::DelayedJobAdapter.extend(
405
+ Marty::DelayedJob::QueueAdapter
406
+ )
407
+ else
408
+ ActiveJob::QueueAdapters::DelayedJobAdapter.include(
409
+ Marty::DelayedJob::QueueAdapter
410
+ )
411
+ end
@@ -43,8 +43,9 @@ class Marty::PromiseJob < Struct.new(:promise,
43
43
 
44
44
  # log "DONE #{Process.pid} #{promise.id} #{Time.now.to_f} #{res}"
45
45
  rescue ::Delayed::WorkerTimeout => e
46
+ msg = ::Marty::Promise.timeout_message(promise)
46
47
  timeout_error = StandardError.new(
47
- ::Marty::Promise.timeout_message(promise)
48
+ "#{msg} (Triggered by #{e.class})"
48
49
  )
49
50
  timeout_error.set_backtrace(e.backtrace)
50
51
 
@@ -28,8 +28,9 @@ class Marty::PromiseRubyJob < Struct.new(:promise,
28
28
  mod = module_name.constantize
29
29
  res = { 'result' => mod.send(method_name, *method_args) }
30
30
  rescue ::Delayed::WorkerTimeout => e
31
+ msg = ::Marty::Promise.timeout_message(promise)
31
32
  timeout_error = StandardError.new(
32
- ::Marty::Promise.timeout_message(promise)
33
+ "#{msg} (Triggered by #{e.class})"
33
34
  )
34
35
  timeout_error.set_backtrace(e.backtrace)
35
36
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Marty
4
- VERSION = '9.5.1'
4
+ VERSION = '10.0.0'
5
5
  end
@@ -0,0 +1,2 @@
1
+ formation: worker=4,web=1
2
+ port: 3000
@@ -0,0 +1,2 @@
1
+ web: bundle exec rails s -e ${RAILS_ENV:-development}
2
+ worker: bundle exec rake jobs:work