Skip to content

Fix #220: bootstrap sync pipeline on headless service restart#221

Merged
stef-k merged 5 commits into
mainfrom
fix/220-headless-sync-pipeline
Feb 22, 2026
Merged

Fix #220: bootstrap sync pipeline on headless service restart#221
stef-k merged 5 commits into
mainfrom
fix/220-headless-sync-pipeline

Conversation

@stef-k
Copy link
Copy Markdown
Owner

@stef-k stef-k commented Feb 22, 2026

Summary

  • Extracts sync pipeline bootstrap into LocationPipelineWiring (static, idempotent, atomic guard with reset-on-failure)
  • Adds idempotency guards (Interlocked.CompareExchange) to QueueDrainService.StartAsync(), TimelineSyncService.StartAsync(), and LocalTimelineStorageService.SubscribeToEvents() — prevents duplicate timers, connectivity subscriptions, and event handlers under concurrent or retry calls
  • Replaces inline bootstrap in App.xaml.cs with single call to shared bootstrapper
  • Triggers bootstrap from LocationTrackingService.LogLocationToQueue() fallback path when delegates are null (headless restart after process kill)
  • Fixes isRunningChecker to reflect both QueueDrain and TimelineSync drain loop states

Root 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 in App.xaml.cs, so all of these remain null. Locations queue in SQLite via the bare DatabaseService fallback but the drain loop starter is also null — locations sit in the queue until the user opens the app.

Files Changed

File Change
Services/LocationPipelineWiring.cs NEW — shared idempotent bootstrapper
App.xaml.cs Replaced ~130 lines of inline sync bootstrap with one call
Platforms/Android/Services/LocationTrackingService.cs Added bootstrap trigger in fallback path
Services/QueueDrainService.cs Interlocked.CompareExchange guard on StartAsync()
Services/TimelineSyncService.cs Interlocked.CompareExchange guard on StartAsync()
Services/LocalTimelineStorageService.cs Interlocked.CompareExchange guard on SubscribeToEvents()

Test plan

  • Scenario A (Primary): Start tracking, background/close app, move across thresholds — server receives locations without opening app
  • Scenario C (Root cause): Simulate OS process kill + service restart — service re-bootstraps, drain resumes without UI launch
  • Scenario E: Parallel bootstrap race — exactly one effective startup per service, no duplicate timers/subscriptions
  • Scenario G: Partial bootstrap failure + recovery — guard resets, next location fix retries, no duplicate state
  • Regression: Normal app open/resume flow continues to work, timeline entries not duplicated

Fixes #220

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
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

…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)
@github-actions
Copy link
Copy Markdown

Code Coverage

Package Line Rate Branch Rate Complexity Health
WayfarerMobile.Core 79% 68% 633
Summary 79% (790 / 994) 68% (354 / 519) 633

@stef-k stef-k merged commit ada2a26 into main Feb 22, 2026
1 check passed
@stef-k stef-k deleted the fix/220-headless-sync-pipeline branch February 22, 2026 17:58
stef-k added a commit that referenced this pull request Feb 22, 2026
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.

[Bug]: App stales if closed and not sync location data to server

1 participant