Skip to content

fix: back off pending migration metadata retries - #1462

Merged
piotr-iohk merged 6 commits into
masterfrom
fix/rn-migration-retry-backoff
Oct 12, 2026
Merged

piotr-iohk merged 6 commits into
masterfrom
fix/rn-migration-retry-backoff

Conversation

@piotr-iohk

@piotr-iohk piotr-iohk commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Related: synonymdev/bitkit-ios#852
Follow-up to #1396.

This PR slows automatic retries for unresolved React Native activity metadata while preserving recovery when the activity later arrives. Android already avoids rebuilding full payment history on this path; iOS #852 receives the matching schedule and also removes its background history rebuild.

Description

  • Persists a retry schedule starting at one minute and doubling to an hourly cap so restarts do not reset the cadence.
  • Retains unresolved tags, transfers and boosts for later recovery, and clears the schedule after successful recovery or cleanup.
  • Keeps initial migration completion immediate and verifies repeated sync events respect backoff without sweeping unseen activities or restarting the node.

Out of Scope

  • No expiry or deletion of unresolved migration metadata.
  • No change to initial channel migration or payment processing.

Design

N/A — no UI changes.

Preview

N/A

QA Notes

Journeys

N/A — not drivable; see Manual Tests.

Manual Tests

  • 1 With an unresolved RN marker, observe repeated wallet syncs and relaunch before the next retry is due → metadata retry cadence persists and doubles to the hourly cap; the marker stays recoverable when its activity later appears — RN-era fault injection and migration log inspection not in Capabilities.

Automated Checks

  • added RNMigrationRetryScheduleTest.kt — serialization/reload, repeated sync rejection, doubling/hourly cap and clock rollback
  • added RNMigrationRetryPersistenceTest.kt — a real temporary DataStore is closed and reopened with a recreated service; saved delays block early retries, late tags recover, and recovery/cleanup removes the schedule
  • updated AppViewModelSendFlowTest.kt — due retries preserve existing unseen/history/node protections; six syncs during backoff do no metadata reapplication or payment-history rebuild

Local validation at 635913d3c: development compilation, all 3,951 unit tests and detekt passed.

Independent full review covered the iOS and Android local fixes with no actionable findings. The service-level DataStore restart/recovery/cleanup follow-up was independently reviewed and tested. The proposed populated-wallet/device backup measurement remains unrun.

Comment thread app/src/main/java/to/bitkit/services/MigrationService.kt Fixed
Comment thread app/src/main/java/to/bitkit/services/MigrationService.kt Fixed
Comment thread app/src/main/java/to/bitkit/services/MigrationService.kt Fixed
Comment thread app/src/main/java/to/bitkit/services/MigrationService.kt Fixed
Comment thread app/src/main/java/to/bitkit/services/MigrationService.kt Fixed
@piotr-iohk
piotr-iohk marked this pull request as ready for review October 12, 2026 11:39
@greptile-apps

greptile-apps Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

[Medium impact] Adds exponential backoff to migration metadata recovery retries.

The PR appears safe to merge, with a non-blocking request to test the saved schedule through MigrationService.

Findings

  1. P2 Saved schedule lacks test coverage ▶

Summary

Adds a saved retry delay for unresolved React Native migration data.

  • Wallet syncs retry unresolved migration data on a growing schedule.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Wallet sync completes] --> B{Initial migration pending?}
  B -->|Yes| C[Complete migration immediately]
  B -->|No| D{Pending data and retry due?}
  D -->|No| E[Skip metadata retry]
  D -->|Yes| F[Reapply pending metadata]
  C --> G{Pending work remains?}
  F --> G
  G -->|Yes| H[Save doubled delay capped at one hour]
  G -->|No| I[Remove schedule and clean up]
Loading

Reviews (1) · Last reviewed commit: "chore: rename changelog fragment" · Reviewed by Greptile

@talosmachina talosmachina left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No findings. Gates the post-migration retry on each wallet sync behind a persisted backoff (1 min doubling to 1 h, reset on clock rollback, cleared with the rest of the RN migration data). Reviewed 654a0bf, full tier, reasoned from the code: this Android branch was not built here, and CI was still running at review time.

What I checked, and 5 candidates I ruled out

Read in full: RNMigrationRetrySchedule, shouldRetryPendingMigrationData, recordPendingMigrationRetry, reapplyMetadataAfterSync, clearPersistedMigrationData, canCleanupAfterMigration in MigrationService.kt. handleSyncCompleted, retryPendingMigrationData, completeRNRemoteBackupRestore, finishMigrationSuccessfully and finishMigrationWithFallbackSync in AppViewModel.kt.
Call sites traced: reapplyMetadataAfterSync (four callers; only the sync-retry path is gated, so initial migration and remote restore still apply at once), hasPendingMigrationRetries (now only reached through the new gate).
CI: pending at review time (build, build-local, lint).

Ruled out

  • Schedule left behind after recovery: recordPendingMigrationRetry removes the key when canCleanupAfterMigration is true, and clearPersistedMigrationData removes it too.
  • Backoff starts before the first retry: the schedule is written only at the end of reapplyMetadataAfterSync, so the initial migration records a 1 min wait and the first gated retry comes after it, as the PR describes.
  • Exception mid-reapply skips the record: the schedule is then missing or unchanged, so the next sync retries as master does today. No regression.
  • Blocktank order retries slowed too: the gate covers every pending item behind canCleanupAfterMigration, so a failed order fetch now retries on the same backoff. The data stays persisted and is still recovered, so the only effect is a later retry.
  • Mock stub with a default argument: shouldRetryPendingMigrationData(any()) matches the $default call, because the default now is computed before the mocked instance method runs.

Merge confidence: 4/5, no findings, but CI had not finished and the branch was not built here.

@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Regtest APK

Built from b169c41 (run).

Download bitkit-dev-debug universal APK (expires in 30 days).

@piotr-iohk
piotr-iohk requested a review from jvsena42 October 12, 2026 12:24

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code scan at b169c41: no findings. Device result follows.

Checked:

  • Schedule: afterAttempt gives 60, 120, 240, 480, 960, 1920, 3600 s and stays at 3600; a marker is never dropped, so the floor is one pass per hour (MigrationService.kt:82-96).
  • Gate: only retryPendingMigrationData is gated (AppViewModel.kt:1971); the completion branches still run immediately. recordPendingMigrationRetry is the last statement of reapplyMetadataAfterSync, so all four callers record.
  • Persistence: the schedule is read from the DataStore on every check, with no in-memory copy, so a relaunch keeps the deadline. A corrupt or missing value reads as due and is overwritten with a valid 60 s schedule. A clock moved backwards grants one pass and keeps doubling from the stored delay.
  • Cleared on a clean pass (:393-394), in clearPersistedMigrationData (:363) and therefore on wallet wipe (WipeWalletUseCase.kt:97).
  • A cancelled pass writes no schedule (the record is the last statement), so the next sync re-runs it. Two passes cannot interleave: isCompletingMigration is set with no suspension after the branch test.
  • On master the Android retry never rebuilt payment history or marked activities seen, so the per-pass ACTIVITY backup measured on synonymdev/bitkit-ios#852 has no Android counterpart. What was unbounded here was the pass itself: a DataStore write every 10 s, and a Blocktank fetch when order ids are pending.
  • AppViewModelSendFlowTest.kt only moves the stub to the new gate and adds the six-sync test; no send-path code changes.

perf: relevant (re-times repeated work).

Device gate (partial): b169c41 — not run yet; pending the pass rate with one unresolvable marker, 3 minutes idle and across a relaunch, on this build and on master.

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at b169c41. The code scan is in my earlier review today; this is the device result.

Device gate: b169c41 — journeys: none listed; perf: 4 metadata retry passes in 8 min 20 s with one unresolvable marker (master: 20 in 200 s, one per wallet sync), gaps 68 s / 120 s / 248 s; after a force-stop and relaunch 3 s into the 240 s wait, 24 wallet syncs passed with no retry and the pass ran 8 s after the stored deadline; stored schedule delayMillis: 480000 at the end.

Measured on a Pixel 9 emulator (regtest, 41 payments), app idle on Home, with rnPendingMetadata holding one tag for an activity id that is not in the wallet, written straight into the rn_migration DataStore.

Log lines (UTC)

master, same marker: one pass per sync for the whole window.

13:21:00.578 WARN [MigrationService.kt:989] Activity not found for tags: id='0000000014621462…ffffffff'
13:21:10.112 WARN [MigrationService.kt:989] Activity not found for tags: id='0000000014621462…ffffffff'
13:21:20.123 WARN [MigrationService.kt:989] Activity not found for tags: id='0000000014621462…ffffffff'
13:21:30.173 WARN [MigrationService.kt:989] Activity not found for tags: id='0000000014621462…ffffffff'

This PR:

13:23:40.038 INFO [MigrationService.kt:1810] Applying remote metadata (tags: 1)
13:24:48.489 INFO [MigrationService.kt:1810] Applying remote metadata (tags: 1)
13:26:48.494 INFO [MigrationService.kt:1810] Applying remote metadata (tags: 1)
13:26:51     force-stop, relaunch 13:26:54
13:30:56.054 INFO [MigrationService.kt:1810] Applying remote metadata (tags: 1)

⚠️ Not run: Manual Test 1 as written (an RN-era wallet whose marker later becomes resolvable) — needs an RN restore with a late activity. The marker here never resolves.
⚠️ Not measured: the hourly cap. The schedule was followed up to the 480 s step.

Skipped — covered by CI at b169c41: unit tests, detekt, lint, e2e (all green).

One item outside this PR's scope is in a separate comment.

@jvsena42

jvsena42 commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

Outside this PR's scope, no change needed here. Found while measuring the retry rate; filed as #1464.

The ACTIVITY backup is uploaded after every wallet sync, about every 10 s, while the app sits idle on Home.

  • master: every SyncCompleted calls walletRepo.debounceSyncByEvent() (AppViewModel.kt:1948/:1953), which runs activityRepo.syncActivities() (WalletRepo.kt:310-318). syncActivities ends in an unconditional notifyActivitiesChanged() (ActivityRepo.kt:141), and BackupRepo answers every emission with markBackupRequired(ACTIVITY) (BackupRepo.kt:385, :410-416), whether or not a payment changed.
  • It does not depend on the migration marker: 47 ACTIVITY uploads in 8 min 20 s on this PR's build while the retry was backing off, and the same cadence earlier today on another build with no marker at all.
Reproduction: device log, idle on Home (UTC)

Pixel 9 emulator, regtest, 41 payments, nothing sent or received:

13:28:46.947 DEBUG [ActivityRepo.kt:115] syncActivities called - ActivityRepo
13:28:47.016 INFO  [CoreService.kt:827]  Synced 41 payments successfully, 0 failed - ActivityService
13:28:52.035 DEBUG [BackupRepo.kt:514]   Backup starting for: 'ACTIVITY' - BackupRepo
13:28:52.293 INFO  [BackupRepo.kt:538]   Backup succeeded for: 'ACTIVITY' - BackupRepo
13:28:57.011 DEBUG [ActivityRepo.kt:115] syncActivities called - ActivityRepo
13:28:57.083 INFO  [CoreService.kt:827]  Synced 41 payments successfully, 0 failed - ActivityService
13:29:02.119 DEBUG [BackupRepo.kt:514]   Backup starting for: 'ACTIVITY' - BackupRepo
13:29:02.380 INFO  [BackupRepo.kt:538]   Backup succeeded for: 'ACTIVITY' - BackupRepo

To repeat it: open the app on a wallet with a backup configured, leave it on Home for two minutes and count Backup succeeded for: 'ACTIVITY' lines.

The iOS side had the same shape on its retry path (syncLdkNodePayments ending in activitiesChangedSubject.send()), which synonymdev/bitkit-ios#852 stopped calling per retry. A possible fix here is to notify only when the sync added or updated a row.

@piotr-iohk
piotr-iohk merged commit 97923d8 into master Oct 12, 2026
21 checks passed
@piotr-iohk
piotr-iohk deleted the fix/rn-migration-retry-backoff branch October 12, 2026 14:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants