marty 9.5.1 → 10.0.0

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 (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