.vbw-planning/milestones/07-rails-audit-and-refactoring/04-job-pipeline-reliability/01-PLAN.md
Extract ~60 lines of retry/circuit-breaker execution logic from FetchFeedJob into a dedicated Fetching::RetryOrchestrator service (S2). This makes retry logic independently testable and keeps the job shallow per engine conventions.
app/jobs/source_monitor/fetch_feed_job.rb (147 lines) contains handle_transient_error, enqueue_retry!, open_circuit!, reset_retry_state! methods that mix job concerns with domain logiclib/source_monitor/fetching/retry_policy.rb (90 lines) already makes retry decisions via Decision struct -- RetryOrchestrator will execute those decisionsCreate test/lib/source_monitor/fetching/retry_orchestrator_test.rb:
call with retry decision: updates source state (fetch_retry_attempt, next_fetch_at, fetch_status), enqueues FetchFeedJob with waitcall with circuit-open decision: updates source (fetch_circuit_opened_at, fetch_circuit_until, fetch_status to failed), does NOT enqueue retrycall with exhausted decision (neither retry nor circuit): resets retry state on source, returns exhausted statuswith_lockcreate_source! factory, mock RetryPolicy decision structsCreate lib/source_monitor/fetching/retry_orchestrator.rb:
SourceMonitor::Fetching::RetryOrchestratorResult = Struct.new(:status, :source, :error, :decision, keyword_init: true) with retry_enqueued?, circuit_opened?, exhausted? helpers.call(source:, error:, decision:, job_class: SourceMonitor::FetchFeedJob, now: Time.current) class methodenqueue_retry!, open_circuit!, reset_retry_state! logic from FetchFeedJobsource.with_lock { source.reload; source.update!(...) }lib/source_monitor.rb under the Fetching namespaceModify app/jobs/source_monitor/fetch_feed_job.rb:
handle_transient_error, enqueue_retry!, open_circuit!, reset_retry_state! methodshandle_transient_error call site, replace with:
decision = RetryPolicy.new(source:, error:, now: Time.current).decision
return raise error unless decision
result = RetryOrchestrator.call(source:, error:, decision:)
raise error if result.exhausted?
handle_concurrency_error in the job (it's concurrency-specific, not retry-policy)Update test/jobs/source_monitor/fetch_feed_job_test.rb:
bin/rails test test/lib/source_monitor/fetching/retry_orchestrator_test.rb -- all passbin/rails test test/jobs/source_monitor/fetch_feed_job_test.rb -- all passbin/rails test -- full suite passesbin/rubocop -- zero offenses