Add failure backoff for sync auto triggers
This commit is contained in:
+18
-1
@@ -2,6 +2,23 @@
|
|||||||
|
|
||||||
Updated: 2026-02-15
|
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
|
## Latest Milestone (2026-02-15): Reminder Delivery Interface Wiring
|
||||||
|
|
||||||
Implemented the notification scheduling boundary and connected it to local data
|
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
|
- current UX shows and dismisses rejections; still missing guided
|
||||||
resolve/retry/inspect flows per entity.
|
resolve/retry/inspect flows per entity.
|
||||||
3. Sync trigger maturity:
|
3. Sync trigger maturity:
|
||||||
- add connectivity-aware triggers and backoff policy for repeated failures.
|
- connectivity-aware triggers are still missing.
|
||||||
4. Test depth:
|
4. Test depth:
|
||||||
- add integration tests for auth + sync + CRUD end-to-end app flows.
|
- 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:flutter/foundation.dart';
|
||||||
import 'package:relationship_saver/features/sync/sync_coordinator.dart';
|
import 'package:relationship_saver/features/sync/sync_coordinator.dart';
|
||||||
|
|
||||||
@@ -18,6 +20,7 @@ class SyncAutoTriggerController {
|
|||||||
|
|
||||||
bool _running = false;
|
bool _running = false;
|
||||||
DateTime? _lastTriggeredAt;
|
DateTime? _lastTriggeredAt;
|
||||||
|
int _consecutiveFailures = 0;
|
||||||
|
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
bool get isRunning => _running;
|
bool get isRunning => _running;
|
||||||
@@ -25,6 +28,9 @@ class SyncAutoTriggerController {
|
|||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
DateTime? get lastTriggeredAt => _lastTriggeredAt;
|
DateTime? get lastTriggeredAt => _lastTriggeredAt;
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
int get consecutiveFailures => _consecutiveFailures;
|
||||||
|
|
||||||
/// Triggers `syncNow` when not already running and outside cooldown window.
|
/// Triggers `syncNow` when not already running and outside cooldown window.
|
||||||
Future<bool> trigger({bool ignoreCooldown = false}) async {
|
Future<bool> trigger({bool ignoreCooldown = false}) async {
|
||||||
if (_running) {
|
if (_running) {
|
||||||
@@ -35,19 +41,34 @@ class SyncAutoTriggerController {
|
|||||||
final DateTime? last = _lastTriggeredAt;
|
final DateTime? last = _lastTriggeredAt;
|
||||||
if (!ignoreCooldown &&
|
if (!ignoreCooldown &&
|
||||||
last != null &&
|
last != null &&
|
||||||
now.difference(last) < minimumInterval) {
|
now.difference(last) < _cooldownForCurrentState()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_running = true;
|
_running = true;
|
||||||
_lastTriggeredAt = now;
|
_lastTriggeredAt = now;
|
||||||
try {
|
try {
|
||||||
await _syncNow();
|
final SyncRunResult result = await _syncNow();
|
||||||
|
if (result.success) {
|
||||||
|
_consecutiveFailures = 0;
|
||||||
|
} else {
|
||||||
|
_consecutiveFailures += 1;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
_consecutiveFailures += 1;
|
||||||
return true;
|
return true;
|
||||||
} finally {
|
} finally {
|
||||||
_running = false;
|
_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);
|
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 {
|
test('does not run a second sync while a run is still in progress', () async {
|
||||||
int callCount = 0;
|
int callCount = 0;
|
||||||
final Completer<void> completer = Completer<void>();
|
final Completer<void> completer = Completer<void>();
|
||||||
|
|||||||
Reference in New Issue
Block a user