Back to Source Monitor

VERIFICATION

.vbw-planning/milestones/polish-and-reliability/phases/06-fetch-throughput-defaults/VERIFICATION.md

0.13.013.2 KB
Original Source

Must-Have Checks

#IDTruth/ConditionStatusEvidence
1MH-01update_source_state! rescues broadcast errors separately from DB errorsPASSsource.update!(attrs) bare at line 91; broadcast wrapped in begin/rescue at lines 92-98 of fetch_runner.rb
2MH-02DB failures propagate as exceptions from update_source_state!PASSsource.update!(attrs) is NOT inside any rescue block
3MH-03FetchRunner#run has ensure block that resets fetch_status from 'fetching' to 'failed'PASSLines 72-78: ensure block with source.reload; source.update!(fetch_status: "failed") if source.fetch_status == "fetching"
4MH-04FollowUpHandler#call rescues StandardError per-item so failures don't block mark_complete!PASSLines 19-25: begin/rescue StandardError wraps each enqueuer_class.enqueue(...) call
5MH-05Two separate rescue blocks in fetch_runner.rb (broadcast + run)PASSgrep confirms 4 rescue lines: NotAcquiredError, StandardError (run), StandardError (ensure fallback), StandardError (broadcast)
6MH-06Test: DB update failure in update_source_state! raisesPASSfetch_runner_test.rb line 333: stubs update! to raise ActiveRecord::ConnectionNotEstablished, asserts raise
7MH-07Test: broadcast failure is swallowed, source still updatesPASSfetch_runner_test.rb line 357: stubs broadcast_source to raise, asserts source reaches "idle"
8MH-08Test: ensure resets fetch_status from fetching on unexpected errorPASSfetch_runner_test.rb line 376: failing fetcher + assert "failed" status after exception
9MH-09Test: follow_up_handler partial failure doesn't prevent other itemsPASSfollow_up_handler_test.rb line 14: call_count == 2 after first enqueue fails
10MH-10Test: follow_up_handler complete failure doesn't raisePASSfollow_up_handler_test.rb line 57: assert_nothing_raised with always-failing enqueuer
11MH-11Fixed-interval path uses adjusted_interval_with_jitter, not exact minutesPASSLines 31-33 of adaptive_interval.rb: fixed_seconds = fixed_minutes * 60.0; attributes[:next_fetch_at] = Time.current + adjusted_interval_with_jitter(fixed_seconds)
12MH-12Fixed-interval sources get ±jitter_percent variation on next_fetch_at (default 10%)PASSadjusted_interval_with_jitter calls jitter_offset which reads jitter_percent_value (default 0.1)
13MH-13jitter_proc override still works for fixed-interval pathPASSfeed_fetcher_adaptive_interval_test.rb line 225: proc override test passes (9/9 tests)
14MH-14Existing adaptive interval tests still pass unchangedPASS9 adaptive interval tests pass; existing tests use jitter: ->(_) { 0 } override
15MH-15FetchingSettings has scheduler_batch_size attr_accessor with default 25PASSfetching_settings.rb lines 12, 26: attr_accessor :scheduler_batch_size; @scheduler_batch_size = 25
16MH-16FetchingSettings has stale_timeout_minutes attr_accessor with default 5PASSfetching_settings.rb lines 13, 27: attr_accessor :stale_timeout_minutes; @stale_timeout_minutes = 5
17MH-17Scheduler.run reads batch size from config instead of DEFAULT_BATCH_SIZEPASSscheduler.rb line 12: def self.run(limit: SourceMonitor.config.fetching.scheduler_batch_size, now: Time.current)
18MH-18Scheduler uses stale_timeout from config instead of STALE_QUEUE_TIMEOUT constantPASSscheduler.rb lines 46-47: def stale_timeout; SourceMonitor.config.fetching.stale_timeout_minutes.minutes; end used at lines 23 and 79
19MH-19StalledFetchReconciler.default_stale_after reads from config instead of Scheduler constantPASSstalled_fetch_reconciler.rb line 46: SourceMonitor.config.fetching.stale_timeout_minutes.minutes with rescue NoMethodError fallback
20MH-20Host apps can override via SourceMonitor.configure { |c| c.fetching.scheduler_batch_size = 50 }PASSConfirmed by scheduler_test.rb line 268: sets batch_size=2, verifies only 2 enqueued from 5 sources
21MH-21All existing scheduler tests pass, new tests verify configurable defaultsPASS16 scheduler tests pass (4 new: default_batch_size, default_stale, configurable_batch, configurable_stale)
22MH-22Configuration has maintenance_queue_name attr_accessor defaulting to 'source_monitor_maintenance'PASSconfiguration.rb lines 25, 42: attr_accessor :maintenance_queue_name; @maintenance_queue_name = "#{DEFAULT_QUEUE_NAMESPACE}_maintenance"
23MH-23Configuration has maintenance_queue_concurrency attr_accessor defaulting to 1PASSconfiguration.rb lines 28, 45: @maintenance_queue_concurrency = 1
24MH-24queue_name_for(:maintenance) returns configured maintenance queue name with prefixPASSconfiguration.rb lines 71-72: when :maintenance; maintenance_queue_name
25MH-25concurrency_for(:maintenance) returns configured maintenance queue concurrencyPASSconfiguration.rb lines 93-94: when :maintenance; maintenance_queue_concurrency
26MH-26FetchFeedJob and ScheduleFetchesJob remain on :fetch queuePASSgrep source_monitor_queue :fetch returns only these 2 files
27MH-27ScrapeItemJob remains on :scrape queuePASSscrape_item_job.rb line 5: source_monitor_queue :scrape
28MH-287 jobs use :maintenance queue (SourceHealthCheckJob, ImportSessionHealthCheckJob, ImportOpmlJob, LogCleanupJob, ItemCleanupJob, FaviconFetchJob, DownloadContentImagesJob)PASSAll 7 job files show source_monitor_queue :maintenance
29MH-29Example solid_queue.yml includes all 3 SourceMonitor queuesPASSexamples/advanced_host/files/config/solid_queue.yml contains source_monitor_fetch, source_monitor_scrape, source_monitor_maintenance
30MH-30All existing job tests pass, new tests verify queue assignmentsPASSconfiguration_test.rb: 10 new queue separation tests all pass (95/95 total)

Artifact Checks

#IDArtifactExistsContainsStatus
1ART-01lib/source_monitor/fetching/fetch_runner.rbYESensure block + split rescue in update_source_state!PASS
2ART-02lib/source_monitor/fetching/completion/follow_up_handler.rbYESper-item begin/rescue StandardErrorPASS
3ART-03test/lib/source_monitor/fetching/completion/follow_up_handler_test.rbYES2 new tests for error resiliencePASS
4ART-04lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rbYESfixed-interval else-branch calls adjusted_interval_with_jitterPASS
5ART-05test/lib/source_monitor/fetching/feed_fetcher_adaptive_interval_test.rbYES3 new jitter tests (9 total)PASS
6ART-06lib/source_monitor/configuration/fetching_settings.rbYESscheduler_batch_size=25, stale_timeout_minutes=5 in reset!PASS
7ART-07lib/source_monitor/scheduler.rbYESself.run reads config, stale_timeout method, legacy constants remain as fallbacksPASS
8ART-08lib/source_monitor/fetching/stalled_fetch_reconciler.rbYESdefault_stale_after reads config with NoMethodError rescue fallbackPASS
9ART-09lib/source_monitor/configuration.rbYESmaintenance_queue_name, maintenance_queue_concurrency, queue_name_for(:maintenance), concurrency_for(:maintenance)PASS
10ART-10examples/advanced_host/files/config/solid_queue.ymlYES3-queue layout with comments explaining rolesPASS

Anti-Pattern Scan

#IDPatternStatusEvidence
1AP-01Swallowing DB errors in broad rescue in update_source_state!PASS (fixed)source.update!(attrs) at line 91 is bare — no rescue wrapping it
2AP-02Missing ensure block leaving source stuck in 'fetching'PASS (fixed)ensure block at lines 72-78 of fetch_runner.rb
3AP-03FollowUpHandler exceptions propagating past mark_complete!PASS (fixed)per-item rescue at lines 19-25 of follow_up_handler.rb
4AP-04Fixed-interval path uses exact minutes without jitter (thundering herd)PASS (fixed)adjusted_interval_with_jitter(fixed_seconds) at line 33 of adaptive_interval.rb
5AP-05Hardcoded DEFAULT_BATCH_SIZE=100 in Scheduler.runPASS (fixed)Scheduler.run default param now reads config; constant retained as legacy fallback only
6AP-06STALE_QUEUE_TIMEOUT constant used directly in StalledFetchReconcilerPASS (fixed)default_stale_after now reads config; constant only remains as legacy in scheduler.rb
7AP-07Stale constant reference: Scheduler::STALE_QUEUE_TIMEOUT in production codeWARNschedule_fetches_job.rb line 26 still uses Scheduler::DEFAULT_BATCH_SIZE as fallback for explicit calls with no options. When ScheduleFetchesJob runs with no args (normal recurring schedule), it passes 100 to Scheduler.run instead of the configured 25. The plan's must_have targets only Scheduler.run's default param — not ScheduleFetchesJob. Legacy constant still present but marked as "legacy fallback".
8AP-08Test isolation: follow_up_handler_test.rb fails when run standaloneFAILMissing require "source_monitor/fetching/completion/follow_up_handler" — both tests error with NameError: uninitialized constant FollowUpHandler when run in isolation. Full suite passes (1211/0) because the class loads from another test.
9AP-09No :nocov: guard on unreachable rescue in ensure blockPASSensure rescue at line 76 correctly annotated with # :nocov:
10AP-10Maintenance queue concurrency inconsistency between config default and example YAMLPASSBoth config default and YAML use concurrency: 1; ENV var override supported in YAML

Requirement Mapping

#IDRequirementPlan RefEvidenceStatus
1REQ-01REQ-FT-01/02/03: Error handling safety netPLAN-01 must_haves 1-6All 3 changes in fetch_runner.rb + follow_up_handler.rb; 5 new testsPASS
2REQ-02REQ-FT-04: Fixed-interval jitterPLAN-02 must_haves 1-4adaptive_interval.rb else-branch wired; 3 new tests passPASS
3REQ-03REQ-FT-06/07: Configurable batch size + stale timeoutPLAN-03 must_haves 1-7FetchingSettings new attrs; Scheduler + StalledFetchReconciler wired; 4 new testsPASS
4REQ-04REQ-FT-09/10: Maintenance queue separationPLAN-04 must_haves 1-87 jobs moved to :maintenance; config updated; example YAML updated; 10 new testsPASS
5REQ-05ScheduleFetchesJob uses configured batch size (implied)Not in PLAN-03 must_havesScheduleFetchesJob still uses DEFAULT_BATCH_SIZE (100) as fallback — out of plan scope but creates semantic gapWARN

Convention Compliance

#IDConventionFileStatusDetail
1CON-01RuboCop zero offensesAll changed filesPASSbin/rubocop exits 0 across 369 files
2CON-02Brakeman zero warningsAll filesPASS0 security warnings, 1 pre-existing ignored warning
3CON-03Minitest (no RSpec/FactoryBot)All test filesPASSAll new tests use ActiveSupport::TestCase, create_source! factory
4CON-04Configuration reset! pattern in FetchingSettingsfetching_settings.rbPASSNew attrs initialized in reset! following existing pattern
5CON-05frozen_string_literal headerAll new/changed filesPASSAll files include # frozen_string_literal: true
6CON-06Test file requires implementation filefollow_up_handler_test.rbFAILMissing require "source_monitor/fetching/completion/follow_up_handler" — standalone isolation broken
7CON-07:nocov: annotation on unreachable rescue pathsfetch_runner.rb line 76PASSDefensive ensure rescue annotated # :nocov:
8CON-08Shallow jobs (jobs delegate to services, no business logic)All job filesPASSJobs only call source_monitor_queue and delegate to Scheduler/FetchRunner

Summary

Tier: deep | Result: PASS | Passed: 37/38 | Failed: [CON-06 / AP-08 — follow_up_handler_test.rb missing require for standalone isolation]

Full test suite: 1211 runs, 3755 assertions, 0 failures, 0 errors, 0 skips RuboCop: 0 offenses | Brakeman: 0 warnings

One non-blocking issue found: test/lib/source_monitor/fetching/completion/follow_up_handler_test.rb is missing a require for its subject class. Both tests in the file fail with NameError when run in isolation (bin/rails test test/lib/source_monitor/fetching/completion/follow_up_handler_test.rb). They pass in the full suite because fetch_runner.rb already requires the class transitively. All must-haves from all 4 plans are satisfied. Core behavior is verified and correct.

One architectural note (WARN, not a failure): ScheduleFetchesJob#extract_limit falls back to Scheduler::DEFAULT_BATCH_SIZE (100) when called with no explicit options argument. In normal recurring schedule operation, this means the job sends limit=100 to Scheduler.run, bypassing the config.fetching.scheduler_batch_size = 25 default. This was not in scope for PLAN-03 but creates a semantic gap between the configured default and the job-driven default.