marty 9.5.1 → 10.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/README.md +40 -31
- data/app/components/marty/background_job/schedule_jobs_dashboard.rb +2 -2
- data/app/components/marty/background_job/schedule_jobs_grid.rb +22 -5
- data/app/components/marty/background_job/schedule_jobs_logs.rb +12 -0
- data/app/components/marty/base_rule_view.rb +0 -4
- data/app/components/marty/delorean_rule_view.rb +11 -5
- data/app/components/marty/promise_view.rb +15 -0
- data/app/jobs/marty/cron_job.rb +30 -28
- data/app/jobs/marty/delorean_background_job.rb +8 -0
- data/app/models/marty/background_job/schedule.rb +24 -1
- data/app/models/marty/delorean_rule.rb +1 -1
- data/app/models/marty/vw_promise.rb +6 -4
- data/app/services/marty/background_job/fetch_missing_in_schedule_cron_jobs.rb +8 -6
- data/app/services/marty/background_job/update_schedule.rb +15 -8
- data/app/services/marty/jobs/schedule.rb +8 -4
- data/app/services/marty/promises/delorean/create.rb +2 -1
- data/app/services/marty/promises/ruby/create.rb +2 -1
- data/config/locales/en.yml +1 -0
- data/db/migrate/510_schedule_job_to_remove_old_promises.rb +4 -2
- data/db/migrate/523_add_timeout_to_promises.rb +5 -0
- data/db/migrate/524_add_timeout_to_promise_view.rb +45 -0
- data/db/migrate/525_add_arguments_to_jobs_schedules.rb +10 -0
- data/db/migrate/526_add_schedule_id_to_delayed_jobs.rb +14 -0
- data/lib/marty/delayed_job/queue_adapter.rb +38 -0
- data/lib/marty/delayed_job/scheduled_job_plugin.rb +9 -6
- data/lib/marty/diagnostic/environment_variables.rb +1 -1
- data/lib/marty/monkey.rb +11 -0
- data/lib/marty/promise_job.rb +2 -1
- data/lib/marty/promise_ruby_job.rb +2 -1
- data/lib/marty/version.rb +1 -1
- data/spec/dummy/.foreman +2 -0
- data/spec/dummy/Procfile +2 -0
- data/spec/dummy/app/jobs/test_failing_job.rb +5 -1
- data/spec/dummy/app/models/gemini/my_rule.rb +2 -0
- data/spec/dummy/app/models/gemini/xyz_rule.rb +2 -0
- data/spec/dummy/delorean/jobs.dl +6 -0
- data/spec/features/delayed_jobs_grid_spec.rb +3 -2
- data/spec/features/schedule_jobs_dashboard_spec.rb +12 -12
- data/spec/jobs/cron_job_spec.rb +16 -12
- data/spec/jobs/delorean_background_job_spec.rb +50 -0
- data/spec/models/promise_spec.rb +1 -0
- data/spec/services/background_job/fetch_missing_in_schedule_cron_jobs_spec.rb +5 -3
- data/spec/services/jobs/schedule_spec.rb +5 -4
- metadata +12 -2
@@ -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
|
@@ -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
|
29
|
-
|
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.
|
5
|
+
in_dashboard = Marty::BackgroundJob::Schedule.all
|
6
6
|
|
7
|
-
names_conditions = in_dashboard.map do |
|
8
|
-
"%job_class: #{
|
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
|
-
|
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
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
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(
|
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
|
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::
|
16
|
-
|
17
|
-
|
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
|
data/config/locales/en.yml
CHANGED
@@ -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.
|
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
|
-
|
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,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
|
-
|
18
|
-
|
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
|
30
|
+
block&.call(worker, job)
|
28
31
|
end
|
29
32
|
end
|
30
33
|
end
|
data/lib/marty/monkey.rb
CHANGED
@@ -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
|
data/lib/marty/promise_job.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
33
|
+
"#{msg} (Triggered by #{e.class})"
|
33
34
|
)
|
34
35
|
timeout_error.set_backtrace(e.backtrace)
|
35
36
|
|
data/lib/marty/version.rb
CHANGED
data/spec/dummy/.foreman
ADDED
data/spec/dummy/Procfile
ADDED