From 5347a3521eedff8075515efb7332c03b69c39532 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:54:05 +0000 Subject: [PATCH] Fix TwelveData retry flow through Sync lifecycle Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com> Agent-Logs-Url: https://github.com/we-promise/sure/sessions/ea7a06f4-8344-4a00-877a-eda8cdfdc16a --- app/jobs/sync_job.rb | 5 ++++- app/models/sync.rb | 27 +++++++++++++++++++++++++++ test/models/sync_test.rb | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/app/jobs/sync_job.rb b/app/jobs/sync_job.rb index 929f44857..059827cde 100644 --- a/app/jobs/sync_job.rb +++ b/app/jobs/sync_job.rb @@ -6,7 +6,10 @@ class SyncJob < ApplicationJob # to ensure the minute window has passed, then increase exponentially retry_on Provider::TwelveData::RateLimitError, wait: ->(executions) { [ 70 * (2 ** (executions - 1)), 600 ].min }, - attempts: 5 + attempts: 5 do |job, error| + sync = job.arguments.first + sync.fail_for_retry_exhaustion!(error.message) + end # Accept a runtime-only flag to influence sync behavior without persisting config def perform(sync, balances_only: false) diff --git a/app/models/sync.rb b/app/models/sync.rb index d1ba07a26..0fbf00e3d 100644 --- a/app/models/sync.rb +++ b/app/models/sync.rb @@ -89,6 +89,9 @@ class Sync < ApplicationRecord begin syncable.perform_sync(self) + rescue Provider::TwelveData::RateLimitError + reset_for_retry! + raise rescue => e fail! update(error: e.message) @@ -99,6 +102,17 @@ class Sync < ApplicationRecord end end + def fail_for_retry_exhaustion!(error_message) + Sync.transaction do + lock! + start! if may_start? + fail! if may_fail? + update!(error: error_message) + end + + parent&.finalize_if_all_children_finalized + end + # Finalizes the current sync AND parent (if it exists) def finalize_if_all_children_finalized Sync.transaction do @@ -106,6 +120,7 @@ class Sync < ApplicationRecord # If this is the "parent" and there are still children running, don't finalize. return unless all_children_finalized? + return if pending? if syncing? if has_failed_children? @@ -147,6 +162,18 @@ class Sync < ApplicationRecord end private + def reset_for_retry! + update_columns( + status: "pending", + error: nil, + pending_at: Time.current, + syncing_at: nil, + failed_at: nil, + completed_at: nil, + updated_at: Time.current + ) + end + def log_status_change Rails.logger.info("changing from #{aasm.from_state} to #{aasm.to_state} (event: #{aasm.current_event})") end diff --git a/test/models/sync_test.rb b/test/models/sync_test.rb index d1182fd51..3393940da 100644 --- a/test/models/sync_test.rb +++ b/test/models/sync_test.rb @@ -43,6 +43,38 @@ class SyncTest < ActiveSupport::TestCase assert_equal "test sync error", sync.error end + test "re-raises TwelveData rate limit errors and resets sync to pending" do + syncable = accounts(:depository) + sync = Sync.create!(syncable: syncable) + error = Provider::TwelveData::RateLimitError.new("rate limited") + + syncable.expects(:perform_sync).with(sync).raises(error) + syncable.expects(:perform_post_sync).never + syncable.expects(:broadcast_sync_complete).never + + assert_raises(Provider::TwelveData::RateLimitError) do + sync.perform + end + + sync.reload + assert_equal "pending", sync.status + assert_nil sync.error + assert_nil sync.syncing_at + assert_nil sync.failed_at + assert_nil sync.completed_at + end + + test "fail_for_retry_exhaustion! marks pending sync as failed" do + sync = Sync.create!(syncable: accounts(:depository)) + + sync.fail_for_retry_exhaustion!("rate limited too many times") + + sync.reload + assert_equal "failed", sync.status + assert_equal "rate limited too many times", sync.error + assert_not_nil sync.failed_at + end + test "can run nested syncs that alert the parent when complete" do family = families(:dylan_family) plaid_item = plaid_items(:one)