Skip to content

Commit 76c42e7

Browse files
authored
Fix lifecycle_type_backfill job never running and piling up (#5286)
* Fix lifecycle_type_backfill job never running and piling up The job's queue was missing from the generic worker's allowlist, so it was enqueued but never executed. Also make the scheduler skip enqueuing when an untouched job is already queued, and keep only the newest untouched job, removing the rest. * Derive generic worker queues from the scheduler The generic worker's queue list was maintained by hand and had to be kept in sync with the clock scheduler. A missing entry meant a job was enqueued but never consumed. Build the list from the scheduler's job definitions so new periodic jobs are wired up automatically.
1 parent 8d07371 commit 76c42e7

5 files changed

Lines changed: 73 additions & 17 deletions

File tree

lib/cloud_controller/clock/distributed_executor.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ def execute_job(name:, interval:, fudge:, timeout:)
1313
ClockJob.db.transaction do
1414
job = ClockJob.find(name:).lock!
1515

16+
remove_duplicate_untouched_jobs(job.name)
17+
1618
need_to_run_job = need_to_run_job?(job, interval, timeout, fudge)
1719

1820
if need_to_run_job
@@ -67,9 +69,33 @@ def job_in_progress?(job)
6769
end
6870
end
6971

72+
# Keep only the newest untouched job on the queue and remove the rest.
73+
def remove_duplicate_untouched_jobs(name)
74+
return if name == 'diego_sync'
75+
76+
newest = untouched_jobs(name).order(Sequel.desc(:created_at), Sequel.desc(:id)).first
77+
return if newest.nil?
78+
79+
deleted = untouched_jobs(name).exclude(id: newest.id).delete
80+
@logger.info("Removed #{deleted} duplicate untouched job(s) from queue #{name}") if deleted > 0
81+
end
82+
83+
def untouched_job_queued?(job)
84+
return false if job.name == 'diego_sync'
85+
86+
queued = untouched_jobs(job.name).any?
87+
@logger.info("An untouched job is already queued for #{job.name}, skipping enqueue") if queued
88+
queued
89+
end
90+
91+
def untouched_jobs(name)
92+
Delayed::Job.where(queue: name, failed_at: nil, locked_at: nil)
93+
end
94+
7095
def need_to_run_job?(job, interval, timeout, fudge=0)
7196
return true if never_run?(job)
7297
return false unless interval_elapsed?(job, interval, fudge)
98+
return false if untouched_job_queued?(job)
7399
return true unless job_in_progress?(job)
74100

75101
if timeout.nil?

lib/cloud_controller/clock/scheduler.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ class Scheduler
3030
{ name: 'lifecycle_type_backfill', class: Jobs::Runtime::LifecycleTypeBackfill }
3131
].freeze
3232

33+
def self.queue_names
34+
(CLEANUPS + FREQUENTS).pluck(:name)
35+
end
36+
3337
def initialize(config)
3438
@clock = Clock.new
3539
@config = config

lib/tasks/jobs.rake

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,30 +39,15 @@ namespace :jobs do
3939

4040
desc 'Start a delayed_job worker.'
4141
task :generic, %i[name num_threads thread_grace_period_seconds] => :environment do |_t, args|
42+
require 'cloud_controller/clock/scheduler'
4243
puts RUBY_DESCRIPTION
4344
args.with_defaults(name: ENV.fetch('HOSTNAME', nil))
4445
args.with_defaults(num_threads: nil)
4546
args.with_defaults(thread_grace_period_seconds: nil)
4647

4748
queues = [
4849
VCAP::CloudController::Jobs::Queues.generic,
49-
'app_usage_events',
50-
'audit_events',
51-
'failed_jobs',
52-
'service_operations_initial_cleanup',
53-
'service_operations_create_in_progress_cleanup',
54-
'service_usage_events',
55-
'completed_tasks',
56-
'expired_blob_cleanup',
57-
'expired_resource_cleanup',
58-
'expired_orphaned_blob_cleanup',
59-
'orphaned_blobs_cleanup',
60-
'pollable_job_cleanup',
61-
'pending_droplets',
62-
'pending_builds',
63-
'prune_completed_deployments',
64-
'prune_completed_builds',
65-
'prune_excess_app_revisions'
50+
*VCAP::CloudController::Scheduler.queue_names
6651
]
6752

6853
require 'cloud_controller/metrics/custom_process_id'

spec/unit/lib/cloud_controller/clock/distributed_executor_spec.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,40 @@ module VCAP::CloudController
232232
end
233233
end
234234
end
235+
236+
context 'when an untouched job is already queued and the job timeout has expired' do
237+
before do
238+
DistributedExecutor.new.execute_job(name: job_name, interval: 1.minute, fudge: 1.second, timeout: 5.minutes) do
239+
Delayed::Job.create!(queue: job_name, failed_at: nil, locked_at: nil)
240+
end
241+
Timecop.travel(Time.now.utc + 6.minutes)
242+
end
243+
244+
it 'does not enqueue another job' do
245+
executed = false
246+
DistributedExecutor.new.execute_job name: job_name, interval: 1.minute, fudge: 1.second, timeout: 5.minutes do
247+
executed = true
248+
end
249+
expect(executed).to be(false)
250+
end
251+
end
252+
253+
context 'when multiple untouched jobs are already queued and the job timeout has expired' do
254+
let!(:newer_job) { Delayed::Job.create!(queue: job_name, failed_at: nil, locked_at: nil, created_at: Time.now.utc) }
255+
let!(:older_job) { Delayed::Job.create!(queue: job_name, failed_at: nil, locked_at: nil, created_at: Time.now.utc - 1.hour) }
256+
let!(:locked_job) { Delayed::Job.create!(queue: job_name, failed_at: nil, locked_at: Time.now) }
257+
let!(:failed_job) { Delayed::Job.create!(queue: job_name, failed_at: Time.now, locked_at: nil) }
258+
259+
before do
260+
DistributedExecutor.new.execute_job(name: job_name, interval: 1.minute, fudge: 1.second, timeout: 5.minutes) {}
261+
Timecop.travel(Time.now.utc + 6.minutes)
262+
end
263+
264+
it 'purges all but the newest untouched job, it keeps locked and failed jobs' do
265+
DistributedExecutor.new.execute_job(name: job_name, interval: 1.minute, fudge: 1.second, timeout: 5.minutes) {}
266+
expect(Delayed::Job.where(queue: job_name).all).to contain_exactly(newer_job, locked_job, failed_job)
267+
end
268+
end
235269
end
236270
end
237271
end

spec/unit/lib/cloud_controller/clock/scheduler_spec.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,5 +236,12 @@ module VCAP::CloudController
236236
end
237237
end
238238
end
239+
240+
describe '.queue_names' do
241+
it 'returns the queue name of every scheduled job' do
242+
expected = (Scheduler::CLEANUPS + Scheduler::FREQUENTS).pluck(:name)
243+
expect(Scheduler.queue_names).to match_array(expected)
244+
end
245+
end
239246
end
240247
end

0 commit comments

Comments
 (0)