Class: DailyFocusOrphanReaperWorker
- Inherits:
-
Object
- Object
- DailyFocusOrphanReaperWorker
- 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
-
#perform ⇒ Object
Runs the job.
Instance Method Details
#perform ⇒ Object
Runs the job.
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 |