Class: DailyFocusOrphanReaperWorker

Inherits:
Object
  • Object
show all
Includes:
Sidekiq::Job
Defined in:
app/workers/daily_focus_orphan_reaper_worker.rb

Overview

Recovers Daily Focus briefings orphaned in processing.

When a DailyFocusAnalysisRepWorker is killed mid-run (deploy restart, OOM,
hard kill), the briefing is stranded: Sidekiq::Shutdown is an Interrupt,
not a StandardError, so the worker's rescue never advances the status to
failed, and the ensure releases the column lock — leaving
daily_focus_status='processing' with no worker on it. AssistantLockCleanupWorker
clears the stale lock but never touches the status, and the next scheduled run
skips the rep because a briefing already exists. The rep sits in "Processing"
forever (2026-06-16 10:05 UTC deploy stranded 3 reps).

This sweep re-enqueues DailyFocusAnalysisRepWorker for each orphan, preserving
its covered reps. The rep worker reclaims the orphan and regenerates in place
(under a per-rep advisory lock, so this can't create duplicates even if it
races a Sidekiq retry).

Runs every 10 minutes on weekday business hours via sidekiq-scheduler.

Constant Summary collapse

MAX_SILENT_RECOVERIES =

Re-enqueues past this many sweeps and the recovery isn't working — something
is failing the same way every time (provider outage, a poison prompt), and
silently retrying every 10 minutes hides it. Three attempts ≈ 30 minutes,
still inside the morning window where a re-run is useful.

3

Instance Method Summary collapse

Instance Method Details

#performObject

Runs the job.

Returns:

  • (Object)

    the result



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'app/workers/daily_focus_orphan_reaper_worker.rb', line 34

def perform
  cutoff = DailyFocus::ORPHAN_AFTER.ago
  recovered = 0

  AssistantConversation
    .daily_focus_processing(Date.current)
    .where(updated_at: ...cutoff)
    .find_each do |conversation|
      # Belt-and-suspenders: skip anything a live worker is still on (the
      # updated_at filter already excludes recent activity, but a fresh column
      # lock means hands off regardless).
      next if DailyFocus.briefing_live?(conversation)

      target_id = conversation.daily_focus_target_employee_id
      next unless target_id

      DailyFocusAnalysisRepWorker.perform_async(
        target_id, nil, Array(conversation.daily_focus_covered_employee_ids)
      )
      recovered += 1
      report_repeat_failure(conversation, count_recovery(conversation))
    end

  Rails.logger.info("[DailyFocusOrphanReaperWorker] Re-enqueued #{recovered} orphaned daily-focus briefing(s)") if recovered.positive?
end