.vbw-planning/milestones/ui-fixes-and-smart-scraping/phases/02-feed-reliability/.context-lead.md
Not available
Not available
No matching requirements found
None
Codebase mapping exists in .vbw-planning/codebase/. Key files:
ARCHITECTURE.mdCONCERNS.mdPATTERNS.mdDEPENDENCIES.mdSTRUCTURE.mdCONVENTIONS.mdTESTING.mdSTACK.mdRead ARCHITECTURE.md, CONCERNS.md, and STRUCTURE.md first to bootstrap codebase understanding.
FetchRunner (lib/source_monitor/fetching/fetch_runner.rb) coordinates fetches with PG advisory locksFeedFetcher (lib/source_monitor/fetching/feed_fetcher.rb) performs HTTP request, parses with Feedjira, processes entriesAdvisoryLock (lib/source_monitor/fetching/advisory_lock.rb) wraps pg_try_advisory_lock — non-blocking, raises NotAcquiredError immediatelyFetchRunner catches NotAcquiredError and re-raises as ConcurrencyErrorFetchFeedJob (app/jobs/source_monitor/fetch_feed_job.rb) retries ConcurrencyError 5 times with 30s wait — this is the problematic path for force-fetchSourceRetriesController#create → FetchRunner.enqueue(source_id, force: true)enqueue sets fetch_status: "queued" and enqueues FetchFeedJob.perform_later(source.id, force: true)FetchFeedJob#perform passes force: true to FetchRunner.new — but force flag only affects circuit breaker check (skip_due_to_circuit), not lock behaviorConcurrencyError triggers retry_on (5 attempts, 30s each) — user sees "failed" after 2.5 min of retriesFetchError base class with original_error, response, code, http_statusTimeoutError, ConnectionError, HTTPError, ParsingError, UnexpectedResponseErrorCODE constant (e.g., "timeout", "connection", "parsing")parse_feed → Feedjira.parse fails → ParsingError (misleading)FeedFetcher#parse_feed (line 212-216): calls Feedjira.parse(body) with no content-type or body inspectionParsingError<title>Just a moment</title>, cf-challenge, etc.)SourceHealthMonitor (lib/source_monitor/health/source_health_monitor.rb) uses rolling success rate from recent fetch_logshealthy, warning, critical, declining, improving, auto_pausedhealth_status, health_status_changed_at, rolling_success_rate, auto_paused_until, auto_paused_atFetchLog model with success, items_created, items_updated, items_failed, http_response_headersSourceUpdater#create_fetch_log — stores response details, duration, error infoSourceMonitor::HTTP.client uses Faraday with retry middleware (4 retries by default)"Mozilla/5.0 (compatible; SourceMonitor/#{VERSION})" — already browser-like from Phase 1 decisionsource.custom_headersIf-None-Match (etag), If-Modified-Since (last_modified)RetryPolicy maps error types to retry configs (attempts, wait, circuit_wait)FetchError for new error types (add BlockedError)update_columns for status updates (matches existing pattern)consecutive_failures helper already (line 183) — can leverage for auto-pause by countafter_save :sync_log_entry creates unified LogEntry — new fields propagatefetch_circuit_opened_at, fetch_circuit_untiltext/html content type. Must check body content, not just content-type header.consecutive_fetch_failures counter to Source, increment on failure, reset on success — simpler than changing rate-based system.BlockedError < FetchError with CODE="blocked"FeedFetcher#parse_feed — before Feedjira, check for CF markerserror_category enum to FetchLog (network, parse, blocked, auth, unknown)SourceUpdater#create_fetch_log based on error classFetchFeedJob: when force: true, don't retry_on ConcurrencyError — rescue and return with user-facing messageFetchRunner.enqueue or add check: if source.fetch_status == "fetching", skip with messageCache-Control: no-cache headerconsecutive_fetch_failures integer to Source (migration)auto_paused_until, auto_paused_at)