From 8e1dac2163796845e864140d6f559b8ddbaeda8c Mon Sep 17 00:00:00 2001 From: Nicholas Jakobsen Date: Sun, 2 Aug 2026 23:43:05 -0700 Subject: [PATCH] feat: Add a way to retry imports that previously failed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nothing re-runs a spatial import once it has failed, so a record broken by a defect stays broken after the defect is fixed. Every host wanting to sweep those up has to write the same three things by hand: the jsonb predicate for the failure status, the iteration, and the choice to queue rather than run inline — and getting the last one wrong leaves a record flagged as failed even when the retry succeeded, because `#update_features!` called directly bypasses the `SpatialProcessingJob` callbacks that maintain the status cache. `::with_failed_feature_updates` and `::retry_failed_feature_updates!` put all three in the gem that owns the status cache. Options pass through to the queued job, so a caller can lower the priority to keep a large sweep behind live uploads. This belongs here rather than in a host concern because `referrals` and `sites` carry `spatial_processing_status_cache` without including Connect Engine's `SpatialFeaturesDefaults`; anything defined there would miss them. Refs https://github.com/culturecode/stolo_connect/issues/1962 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HLgkg1oN6dkKfLzhKyDUFt --- .../queued_spatial_processing.rb | 22 +++++++++++++ lib/spatial_features/version.rb | 2 +- .../queued_spatial_processing_spec.rb | 31 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/lib/spatial_features/has_spatial_features/queued_spatial_processing.rb b/lib/spatial_features/has_spatial_features/queued_spatial_processing.rb index c3027be..4fc2a30 100644 --- a/lib/spatial_features/has_spatial_features/queued_spatial_processing.rb +++ b/lib/spatial_features/has_spatial_features/queued_spatial_processing.rb @@ -3,6 +3,28 @@ module QueuedSpatialProcessing extend ActiveSupport::Concern mattr_accessor :priority_offset, default: 0 # Offsets the queued priority of spatial tasks. Lower numbers run with higher priority + class_methods do + # Records whose most recent feature import failed. `->>` yields NULL for a missing + # key, so a record that has never been imported is not included. + def with_failed_feature_updates + where("spatial_processing_status_cache->>'update_features!' = 'failure'") + end + + # Re-import everything whose last attempt failed. Nothing else retries these, so a + # record broken by a defect stays broken after the defect is fixed unless something + # sweeps it up. + # + # Queued rather than run inline, so a caller (a deploy migration, a console) doesn't + # block on reimporting shapefiles, and so `SpatialProcessingJob`'s callbacks maintain + # `spatial_processing_status_cache` — calling `#update_features!` directly bypasses + # them and leaves a record flagged as failed even when the retry succeeded. + def retry_failed_feature_updates!(**options) + with_failed_feature_updates.find_each do |record| + record.delay_update_features!(**options) + end + end + end + def self.update_cached_status(record, method_name, state) return unless record.has_attribute?(:spatial_processing_status_cache) diff --git a/lib/spatial_features/version.rb b/lib/spatial_features/version.rb index 51669ab..0d9b80a 100644 --- a/lib/spatial_features/version.rb +++ b/lib/spatial_features/version.rb @@ -1,3 +1,3 @@ module SpatialFeatures - VERSION = "3.11.0" + VERSION = "3.11.1" end diff --git a/spec/lib/spatial_features/has_spatial_features/queued_spatial_processing_spec.rb b/spec/lib/spatial_features/has_spatial_features/queued_spatial_processing_spec.rb index a76da39..1e1398c 100644 --- a/spec/lib/spatial_features/has_spatial_features/queued_spatial_processing_spec.rb +++ b/spec/lib/spatial_features/has_spatial_features/queued_spatial_processing_spec.rb @@ -43,6 +43,37 @@ end end + describe '::retry_failed_feature_updates!', delayed_job: false do + let(:klass) { new_dummy_class(:spatial_processing_status_cache => :jsonb) } + + def status!(record, state) + SpatialFeatures::QueuedSpatialProcessing.update_cached_status(record, 'update_features!', state) + end + + it 'queues a feature update for each record whose last import failed' do + failed = klass.create.tap {|record| status!(record, 'failure') } + + expect { klass.retry_failed_feature_updates! } + .to change { failed.spatial_processing_jobs('update_features!').count } + .by(1) + end + + it 'ignores records that succeeded, and records never imported at all' do + klass.create.tap {|record| status!(record, 'success') } + klass.create # no import attempted, so no key in the cache at all + + expect { klass.retry_failed_feature_updates! }.not_to change { Delayed::Job.count } + end + + it 'passes options through to the queued job' do + klass.create.tap {|record| status!(record, 'failure') } + + klass.retry_failed_feature_updates!(:priority => 10) + + expect(Delayed::Job.last.priority).to eq(10) + end + end + describe '#clear_feature_update_error_status' do let(:klass) { new_dummy_class(:spatial_processing_status_cache => :jsonb) }