Fix #220: bootstrap sync pipeline on headless service restart#221
Merged
Conversation
R3: Add Interlocked.CompareExchange guards to QueueDrainService.StartAsync(), TimelineSyncService.StartAsync(), and LocalTimelineStorageService.SubscribeToEvents() to prevent duplicate timers/subscriptions under concurrent or retry calls. R1: Extract LocationPipelineWiring as sole orchestrator of the sync pipeline bootstrap. Single method EnsureBootstrappedAsync() with atomic guard and reset-on-failure for transient errors. R2: Replace App.xaml.cs StartBackgroundServices() sync code with call to LocationPipelineWiring. Add bootstrap trigger in LocationTrackingService LogLocationToQueue() fallback path for headless restart after process kill. R4: Structured diagnostic log in LocationPipelineWiring.
When Android kills the app process and restarts the sticky LocationTrackingService, MAUI never initializes, leaving the sync pipeline (delegates, drain services, timeline storage) null. Locations queue in SQLite but never drain until the user opens the app. Root cause: The sync pipeline was wired exclusively in App.xaml.cs during MAUI startup, with no recovery path for headless restarts. Changes: R3 - Idempotency guards: - QueueDrainService.StartAsync(): Interlocked.CompareExchange guard with reset-on-failure (replaces volatile bool check) - TimelineSyncService.StartAsync(): same pattern - LocalTimelineStorageService.SubscribeToEvents(): Interlocked guard prevents duplicate event handlers that would create duplicate timeline entries R1 - LocationPipelineWiring (new): - Static class as sole orchestrator of sync pipeline bootstrap - EnsureBootstrappedAsync() with atomic guard + reset-on-failure - Resolves DI services, preloads secure settings, starts drain/sync services, initializes timeline storage, wires location delegates - All downstream calls are idempotent (R3 prerequisite) R2 - Dual bootstrap call sites: - App.xaml.cs: replaced inline StartBackgroundServices/ WireLocationTrackingDelegates with single call to bootstrapper - LocationTrackingService.LogLocationToQueue(): fires bootstrap in fallback path (null delegates = headless restart). Current location goes through bare DB fallback; future locations use wired delegates once bootstrap completes. R4 - Diagnostic logging: - Single structured log per bootstrap: success/failed, delegates, drain status Fixes: #220
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…otion Reset _bootstrapGuard when delegates aren't wired so the next location fix can retry. Promote PreloadSecureSettingsAsync to ISettingsService to eliminate the silent SettingsService downcast.
Implement the new ISettingsService interface member in the test mock to fix CI build failure.
- Restore detailed inline comments in WireLocationDelegates (return semantics, connectivity rationale, transient vs permanent failures) that were stripped during the move from App.xaml.cs - Add internal ResetForTesting() to LocationPipelineWiring with InternalsVisibleTo for the test project - Simplify fully-qualified LocationPipelineWiring reference in LocationTrackingService (using directive already present)
stef-k
added a commit
that referenced
this pull request
Feb 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
LocationPipelineWiring(static, idempotent, atomic guard with reset-on-failure)Interlocked.CompareExchange) toQueueDrainService.StartAsync(),TimelineSyncService.StartAsync(), andLocalTimelineStorageService.SubscribeToEvents()— prevents duplicate timers, connectivity subscriptions, and event handlers under concurrent or retry callsApp.xaml.cswith single call to shared bootstrapperLocationTrackingService.LogLocationToQueue()fallback path when delegates are null (headless restart after process kill)isRunningCheckerto reflect both QueueDrain and TimelineSync drain loop statesRoot Cause
When Android kills the app process and restarts the sticky
LocationTrackingService, MAUI never initializes. The sync pipeline (delegates, drain services, timeline storage) was wired exclusively inApp.xaml.cs, so all of these remain null. Locations queue in SQLite via the bareDatabaseServicefallback but the drain loop starter is also null — locations sit in the queue until the user opens the app.Files Changed
Services/LocationPipelineWiring.csApp.xaml.csPlatforms/Android/Services/LocationTrackingService.csServices/QueueDrainService.csInterlocked.CompareExchangeguard onStartAsync()Services/TimelineSyncService.csInterlocked.CompareExchangeguard onStartAsync()Services/LocalTimelineStorageService.csInterlocked.CompareExchangeguard onSubscribeToEvents()Test plan
Fixes #220