Add failure backoff for sync auto triggers

This commit is contained in:
Rijad Zuzo
2026-02-15 20:23:02 +01:00
parent 5228799abb
commit 831c217e29
3 changed files with 64 additions and 3 deletions
+18 -1
View File
@@ -2,6 +2,23 @@
Updated: 2026-02-15
## Latest Milestone (2026-02-15): Sync Auto-Trigger Backoff
Improved sync trigger resilience for repeated failures:
- Updated `lib/features/sync/sync_auto_trigger_controller.dart`:
- tracks consecutive sync failures
- applies exponential cooldown backoff before auto-trigger retries
- resets backoff after a successful sync run
- Added coverage:
- `test/features/sync/sync_auto_trigger_controller_test.dart`
- validates cooldown behavior after failures
Validation for this milestone:
- `flutter analyze` -> pass
- `flutter test` -> pass
## Latest Milestone (2026-02-15): Reminder Delivery Interface Wiring
Implemented the notification scheduling boundary and connected it to local data
@@ -68,7 +85,7 @@ Validation for this milestone:
- current UX shows and dismisses rejections; still missing guided
resolve/retry/inspect flows per entity.
3. Sync trigger maturity:
- add connectivity-aware triggers and backoff policy for repeated failures.
- connectivity-aware triggers are still missing.
4. Test depth:
- add integration tests for auth + sync + CRUD end-to-end app flows.
@@ -1,3 +1,5 @@
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:relationship_saver/features/sync/sync_coordinator.dart';
@@ -18,6 +20,7 @@ class SyncAutoTriggerController {
bool _running = false;
DateTime? _lastTriggeredAt;
int _consecutiveFailures = 0;
@visibleForTesting
bool get isRunning => _running;
@@ -25,6 +28,9 @@ class SyncAutoTriggerController {
@visibleForTesting
DateTime? get lastTriggeredAt => _lastTriggeredAt;
@visibleForTesting
int get consecutiveFailures => _consecutiveFailures;
/// Triggers `syncNow` when not already running and outside cooldown window.
Future<bool> trigger({bool ignoreCooldown = false}) async {
if (_running) {
@@ -35,19 +41,34 @@ class SyncAutoTriggerController {
final DateTime? last = _lastTriggeredAt;
if (!ignoreCooldown &&
last != null &&
now.difference(last) < minimumInterval) {
now.difference(last) < _cooldownForCurrentState()) {
return false;
}
_running = true;
_lastTriggeredAt = now;
try {
await _syncNow();
final SyncRunResult result = await _syncNow();
if (result.success) {
_consecutiveFailures = 0;
} else {
_consecutiveFailures += 1;
}
return true;
} catch (_) {
_consecutiveFailures += 1;
return true;
} finally {
_running = false;
}
}
Duration _cooldownForCurrentState() {
if (_consecutiveFailures <= 0) {
return minimumInterval;
}
final int cappedFailures = math.min(_consecutiveFailures, 4);
final int multiplier = 1 << cappedFailures;
return minimumInterval * multiplier;
}
}
@@ -30,6 +30,29 @@ void main() {
expect(controller.lastTriggeredAt, now);
});
test('applies backoff after failed sync attempts', () async {
DateTime now = DateTime(2026, 2, 15, 14);
bool shouldFail = true;
final SyncAutoTriggerController controller = SyncAutoTriggerController(
syncNow: () async => _result(success: !shouldFail),
now: () => now,
minimumInterval: const Duration(seconds: 60),
);
final bool first = await controller.trigger();
now = now.add(const Duration(seconds: 61));
final bool second = await controller.trigger();
now = now.add(const Duration(seconds: 60));
shouldFail = false;
final bool third = await controller.trigger();
expect(first, isTrue);
expect(second, isFalse);
expect(third, isTrue);
expect(controller.consecutiveFailures, 0);
});
test('does not run a second sync while a run is still in progress', () async {
int callCount = 0;
final Completer<void> completer = Completer<void>();