|
| 1 | +module VCAP::CloudController |
| 2 | + module Jobs |
| 3 | + module Runtime |
| 4 | + class DelayedJobsRecover < VCAP::CloudController::Jobs::CCJob |
| 5 | + RECOVERABLE_OPERATIONS = %w[ |
| 6 | + service_instance.create |
| 7 | + ].freeze |
| 8 | + |
| 9 | + def perform |
| 10 | + logger.info('Recover halted delayed jobs') |
| 11 | + recover |
| 12 | + end |
| 13 | + |
| 14 | + def max_attempts |
| 15 | + 1 |
| 16 | + end |
| 17 | + |
| 18 | + private |
| 19 | + |
| 20 | + def recover |
| 21 | + # find delayed jobs where failed_at is set (permanently failed) |
| 22 | + # and still within the max polling duration (not expired) |
| 23 | + cutoff_time = Time.now - default_maximum_duration_seconds |
| 24 | + dead_delayed_jobs = Delayed::Job. |
| 25 | + exclude(failed_at: nil). |
| 26 | + where { created_at > cutoff_time }. |
| 27 | + order(:created_at). |
| 28 | + limit(batch_size) |
| 29 | + |
| 30 | + dead_delayed_jobs.each do |delayed| |
| 31 | + # pollable job state can be POLLING or FAILED depending on whether the failure |
| 32 | + # hook managed to persist before the db connection was lost |
| 33 | + pollable = PollableJobModel.where(delayed_job_guid: delayed.guid). |
| 34 | + where(state: [PollableJobModel::POLLING_STATE, PollableJobModel::FAILED_STATE]). |
| 35 | + first |
| 36 | + next unless pollable |
| 37 | + next unless RECOVERABLE_OPERATIONS.include?(pollable.operation) |
| 38 | + |
| 39 | + # last_operation.state must be 'in progress'. This confirms the broker is still |
| 40 | + # working on the operation and CC is the one that gave up, not the broker |
| 41 | + entity = find_entity(pollable) |
| 42 | + next unless entity |
| 43 | + next unless entity.last_operation&.state == 'in progress' |
| 44 | + |
| 45 | + reenqueue(pollable, delayed) |
| 46 | + end |
| 47 | + end |
| 48 | + |
| 49 | + def find_entity(pollable) |
| 50 | + # TODO: resource_type field can be used |
| 51 | + case pollable.operation |
| 52 | + when 'service_instance.create' |
| 53 | + ManagedServiceInstance.first(guid: pollable.resource_guid) |
| 54 | + end |
| 55 | + end |
| 56 | + |
| 57 | + def reenqueue(pollable, delayed) |
| 58 | + # re-verify atomically that the pollable job still points to this dead delayed_job. |
| 59 | + # if another process already re-enqueued a new job, pollable.delayed_job_guid was |
| 60 | + # updated to the new delayed_job's guid, so where clause returns nil and we skip safely. |
| 61 | + PollableJobModel.db.transaction do |
| 62 | + pjob = PollableJobModel.where(guid: pollable.guid, |
| 63 | + delayed_job_guid: delayed.guid, |
| 64 | + state: [PollableJobModel::POLLING_STATE, PollableJobModel::FAILED_STATE]). |
| 65 | + for_update.first |
| 66 | + return unless pjob |
| 67 | + |
| 68 | + # bring the record into a clean polling state |
| 69 | + pjob.update(cf_api_error: nil, state: PollableJobModel::POLLING_STATE) |
| 70 | + |
| 71 | + # unwrap the serialized handler and re-enqueue via the reoccurring job |
| 72 | + inner_job = Jobs::Enqueuer.unwrap_job(delayed.payload_object) |
| 73 | + inner_job.send(:enqueue_next_job, pjob) |
| 74 | + end |
| 75 | + end |
| 76 | + |
| 77 | + def default_maximum_duration_seconds |
| 78 | + Config.config.get(:broker_client_max_async_poll_duration_minutes).minutes |
| 79 | + end |
| 80 | + |
| 81 | + def logger |
| 82 | + @logger ||= Steno.logger('cc.background') |
| 83 | + end |
| 84 | + |
| 85 | + def batch_size |
| 86 | + 10 |
| 87 | + end |
| 88 | + end |
| 89 | + end |
| 90 | + end |
| 91 | +end |
0 commit comments