Skip to content

Commit 1524b3e

Browse files
eriedclaude
andcommitted
feat(upload): schedule a 60s follow-up sweep after trip finalize
The immediate post-finalize enqueue can hit a transient failure (server burp, BLE radio kicked the WiFi for a moment) and then sit on WorkManager's exponential backoff. A second swing 60s later, on a separate unique-work name so it doesn't REPLACE the immediate one, catches that case before the trip falls off into the next-ride / next- launch path. Both workers no-op on empty pending, so when the first attempt already drained the queue this costs nothing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent a0be2f0 commit 1524b3e

2 files changed

Lines changed: 50 additions & 1 deletion

File tree

app/src/main/java/com/eried/eucplanet/data/repository/TripRepository.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ class TripRepository @Inject constructor(
5454
// Without a sync folder there's nothing to defer, the trip just stays in
5555
// the local DB regardless, so the grace window is skipped entirely.
5656
private const val FINALIZE_GRACE_MS = 15_000L
57+
// Delay before the post-finalize follow-up upload sweep fires. Catches the
58+
// case where the immediate worker run hit a transient failure and was
59+
// parked on backoff; gives one extra swing without waiting for the next
60+
// ride or app launch.
61+
private const val FOLLOWUP_DELAY_SECONDS = 60L
5762
}
5863

5964
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@@ -464,14 +469,20 @@ class TripRepository @Inject constructor(
464469
_pendingTripId.value = null
465470
pendingFinalizeJob = null
466471
Log.i(TAG, "Trip finalized: ${trip.fileName} (sync=$willSync, eucstats=$willEucstats)")
467-
if (willSync) syncManager.enqueueTripUpload(appSettings)
472+
if (willSync) {
473+
syncManager.enqueueTripUpload(appSettings)
474+
// One free second swing 60s later in case the first attempt failed
475+
// transiently. Worker no-ops on empty pending.
476+
syncManager.enqueueTripUploadDelayed(appSettings, FOLLOWUP_DELAY_SECONDS)
477+
}
468478
// Eucstats: enqueue ANY time the rider has it on, not only when this
469479
// specific trip needs uploading. The worker walks every trip eligible
470480
// for upload (pending=1 / failed=3 / orphaned=0), so this is also the
471481
// automatic retry path: a trip that failed last ride gets one more
472482
// shot the next time the rider finishes a ride.
473483
if (appSettings.onlineUploadEnabled && syncManager.riderStoreId.value != null) {
474484
syncManager.enqueueEucStatsUpload(appSettings)
485+
syncManager.enqueueEucStatsUploadDelayed(appSettings, FOLLOWUP_DELAY_SECONDS)
475486
Log.i(TAG, "Eucstats upload enqueued (incl. retry sweep for prior failures)")
476487
}
477488
}

app/src/main/java/com/eried/eucplanet/data/sync/SyncManager.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ class SyncManager @Inject constructor(
6868
const val TRIPS_SUBFOLDER = "trips"
6969
const val UPLOAD_WORK_NAME = "trip_upload"
7070
const val EUCSTATS_UPLOAD_WORK_NAME = "eucstats_upload"
71+
const val UPLOAD_FOLLOWUP_WORK_NAME = "trip_upload_followup"
72+
const val EUCSTATS_UPLOAD_FOLLOWUP_WORK_NAME = "eucstats_upload_followup"
7173
}
7274

7375
// App-scoped so trip sync survives settings screen navigation.
@@ -659,6 +661,42 @@ class SyncManager @Inject constructor(
659661
)
660662
}
661663

664+
/**
665+
* Schedule a delayed follow-up sweep on a SEPARATE unique work name so it does
666+
* not REPLACE the immediate enqueue. Both workers exit instantly on empty
667+
* pending, so this is a no-op when the first attempt already drained the queue
668+
* (the "unless all was sync" branch). When the first attempt failed transiently
669+
* (no network, server burp), this is a free second swing outside WorkManager's
670+
* exponential backoff curve.
671+
*/
672+
fun enqueueTripUploadDelayed(settings: AppSettings, delaySeconds: Long) {
673+
if (settings.syncFolderUri == null) return
674+
val request = OneTimeWorkRequestBuilder<TripUploadWorker>()
675+
.setInitialDelay(delaySeconds, TimeUnit.SECONDS)
676+
.build()
677+
WorkManager.getInstance(context).enqueueUniqueWork(
678+
UPLOAD_FOLLOWUP_WORK_NAME,
679+
ExistingWorkPolicy.REPLACE,
680+
request
681+
)
682+
}
683+
684+
fun enqueueEucStatsUploadDelayed(settings: AppSettings, delaySeconds: Long) {
685+
val constraints = Constraints.Builder()
686+
.setRequiredNetworkType(NetworkType.CONNECTED)
687+
.build()
688+
val request = OneTimeWorkRequestBuilder<EucStatsUploadWorker>()
689+
.setConstraints(constraints)
690+
.setInitialDelay(delaySeconds, TimeUnit.SECONDS)
691+
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
692+
.build()
693+
WorkManager.getInstance(context).enqueueUniqueWork(
694+
EUCSTATS_UPLOAD_FOLLOWUP_WORK_NAME,
695+
ExistingWorkPolicy.REPLACE,
696+
request
697+
)
698+
}
699+
662700
/**
663701
* List CSV filenames in the trips subfolder. Returns null if the folder
664702
* is unavailable, empty list if the folder exists but has no trips yet.

0 commit comments

Comments
 (0)