Skip to content

fix: start background service from FCM handler when service is dead#489

Open
AndreaDiazCorreia wants to merge 3 commits intomainfrom
fix/fcm-service-start
Open

fix: start background service from FCM handler when service is dead#489
AndreaDiazCorreia wants to merge 3 commits intomainfrom
fix/fcm-service-start

Conversation

@AndreaDiazCorreia
Copy link
Member

@AndreaDiazCorreia AndreaDiazCorreia commented Feb 19, 2026

Summary

  • When a FCM push arrives and the background service is not running, the handler now starts the service and sends it the stored settings so it
    can connect to relays and process events
  • When the service is already running, sends a lightweight fcm-wake signal
  • Previously, the handler only saved a flag to SharedPreferences and the notification was lost

Test plan

  1. Install the app on a physical Android device
  2. Create or take an order (so there are active trade subscriptions)
  3. Kill the app from recents
  4. Wait ~1-2 minutes for Android to kill the background service
  5. Have the counterpart perform an action (send message, fiat sent, etc.)
  6. Expected: a local notification should appear on the device
  7. Verify logs with adb logcat | grep -E "FCM:|Service started|service-ready|fcm-wake"

Summary by CodeRabbit

  • Bug Fixes
    • More reliable background notification handling so subscriptions receive new events while the app runs in the background.
    • Improved background service startup and settings loading when processing push notifications, ensuring the service starts promptly and stays ready to handle incoming subscription events.

- Add fcm-wake handler to background service to acknowledge wake signals
- When FCM push arrives and service is running, send fcm-wake signal
- When service is dead, start it and send app settings from SharedPreferences
- Add 3-second timeout with 100ms polling to wait for service initialization
- Set fcm.pending_fetch flag before starting service
…andler

- Wrap jsonDecode in try-catch to prevent crashes on malformed settings
- Only invoke service.start if both service is running AND settings decoded successfully
- Add debug print for decode failures
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 19, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fe97084 and 80ae0cc.

📒 Files selected for processing (1)
  • lib/background/background.dart

Walkthrough

Background startup and FCM wake flow were changed: background event handlers (including a new fcm-wake) are registered earlier, database open is delayed until after service start, and the FCM background handler now either sends a wake signal or loads persisted settings and starts the service before signaling.

Changes

Cohort / File(s) Summary
FCM Background Handler
lib/services/fcm_service.dart
Reworked firebaseMessagingBackgroundHandler to: if the background service is running, send an fcm-wake event; if not, set fcm.pending_fetch, load appSettings from SharedPreferences, decode JSON with error handling, wait up to 3s for service start, and call start with decoded settings. Added imports for JSON and storage keys.
Background Service Wake Event & Init Ordering
lib/background/background.dart
Registered event handlers (including new fcm-wake) before awaiting DB initialization; moved DB open / EventStorage creation to after the start handler block. Added comment guarding against losing events during DB init.

Sequence Diagram(s)

sequenceDiagram
    participant FCM as Firebase Cloud Messaging
    participant Handler as FCM Background Handler
    participant Prefs as SharedPreferences
    participant BgService as Background Service
    participant DB as Database/EventStorage

    FCM->>Handler: Background message arrives
    Handler->>BgService: Is service running?
    alt Service running
        Handler->>BgService: Emit 'fcm-wake'
        BgService->>BgService: Handle wake (subscriptions active)
    else Service not running
        Handler->>Handler: set fcm.pending_fetch
        Handler->>Prefs: Load `appSettings`
        Prefs-->>Handler: Return settings JSON
        Handler->>Handler: Decode JSON (safe)
        Handler->>BgService: Start service with settings
        Handler->>Handler: Wait up to 3s for readiness
        BgService->>DB: After start, DB open / EventStorage init
        BgService->>BgService: Initialize subscriptions with settings
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • Catrya
  • grunch

Poem

🐰 A soft fcm-wake in the night so still,
I nibble settings from a hidden hill.
Decode, start, and hum a gentle tune,
Subscriptions wake beneath the moon.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly describes the main change: starting the background service from the FCM handler when the service is not running.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/fcm-service-start

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
lib/services/fcm_service.dart (1)

56-56: startService() return value is silently discarded.

startService() returns Future<bool>. If it returns false (e.g., Android denied service start due to background restrictions or battery optimization), the code falls through to the polling loop, burns the full 3-second budget, and exits with the service not running and fcm.pending_fetch already set — which is acceptable but could be made explicit.

✨ Optional early-exit improvement
-        await service.startService();
+        final started = await service.startService();
+        if (!started) return; // fcm.pending_fetch is already set as fallback
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/services/fcm_service.dart` at line 56, The call to service.startService()
(in fcm_service.dart) ignores its Future<bool> result so failures (false) fall
through to the polling loop; change the code to await the boolean, and if it is
false perform an explicit early exit: log or record the failure (update
fcm.pending_fetch or processLogger as appropriate) and return immediately
instead of entering the 3-second polling loop; ensure the check references
service.startService() and the existing fcm.pending_fetch handling so the
behavior stays consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/services/fcm_service.dart`:
- Line 66: Replace the debugPrint call with the project logger singleton: import
the logger from 'package:mostro_mobile/services/logger_service.dart' and call
logger.error(...) (or logger.w/ logger.d as appropriate) instead of debugPrint;
because this runs in an isolate where the logger transport may not be
registered, add a defensive fallback that uses debugPrint only if the logger
singleton is not available/initialized; also update the other two occurrences in
this file (the debugPrints at the existing lines ~85 and ~90) to follow the same
pattern so all FCM handler logging uses the logger singleton with a debugPrint
fallback.
- Around line 72-80: Move the service event handler registrations so they run
before the database open awaits to avoid lost events: inside serviceMain,
register service.on('start') and service.on('fcm-wake') listeners before calling
await openMostroDatabase(...); the start handler can still call
nostrService.init(settings) (it doesn't depend on db/eventStore), so reorder the
code so the on('start') and on('fcm-wake') listen() calls occur prior to await
openMostroDatabase(...) to eliminate the race where invoke('start', ...) is
delivered before the listener is attached.

---

Nitpick comments:
In `@lib/services/fcm_service.dart`:
- Line 56: The call to service.startService() (in fcm_service.dart) ignores its
Future<bool> result so failures (false) fall through to the polling loop; change
the code to await the boolean, and if it is false perform an explicit early
exit: log or record the failure (update fcm.pending_fetch or processLogger as
appropriate) and return immediately instead of entering the 3-second polling
loop; ensure the check references service.startService() and the existing
fcm.pending_fetch handling so the behavior stays consistent.

…revent race condition

- Move service.on() registrations above openMostroDatabase() call
- Prevents losing 'start' events invoked by FCM handler during db initialization
- Add comment explaining the ordering requirement
- Keep db and eventStore initialization after handlers are registered
Copy link
Contributor

@mostronatorcoder mostronatorcoder bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good fix for a real problem — FCM pushes arriving with a dead background service were silently lost. The approach (start service + send settings) is correct. The race condition fix in commit 80ae0cc (registering listeners before await openMostroDatabase) was the right call. Two issues remain:

Must Fix

1. debugPrint instead of logger singleton

Per AGENTS.md: "Always use the pre-configured singleton logger instance. Direct instantiation of Logger() is no longer permitted."

Line 66 uses debugPrint('FCM: Failed to decode settings: $e'). The two pre-existing debugPrint calls (lines 85, 90) were already there, but adding a new one violates the rule. I know the FCM background handler runs in a separate isolate where the logger singleton might not be available — if that's the case, document it with a comment explaining why debugPrint is used here as an exception. Don't just silently break the convention.

2. fcm.pending_fetch flag is set but never useful in the new flow

Line 56: await sharedPrefs.setBool('fcm.pending_fetch', true) is set before startService(). But the new code immediately starts the service and sends settings — so pending_fetch is never consumed in this path. Looking at the codebase, _checkPendingFetch in FCMService only clears the flag without triggering any real action. This is dead logic:

  • If startService() + invoke('start') succeeds → the service processes events directly, pending_fetch is never read
  • If startService() fails → the catch block sets pending_fetch again (line 87), which is also never consumed meaningfully

Either remove the flag from this flow (it's misleading), or make _checkPendingFetch actually do something useful as a fallback.

Minor

3. 3-second busy-wait poll in a background handler

The while (!(await service.isRunning())) loop with 100ms delays is a busy-wait in an FCM background handler. Android gives FCM handlers ~20 seconds, so 3 seconds isn't a timeout risk. But it's wasteful — consider using service.on('service-ready') as a signal instead of polling, or at least use exponential backoff (100ms → 200ms → 400ms) to reduce unnecessary isRunning() IPC calls.

4. No settings = silent failure

If settingsJson is null (fresh install, cleared data), the service starts but invoke('start') is never called. The service runs as a zombie — no relay connections, no notifications. At minimum log a warning so this is diagnosable.

What looks good

  • ✅ CI passes, no conflicts, mergeable
  • ✅ The listener reordering fix (80ae0cc) correctly eliminates the main race condition
  • fcm-wake for already-running services is lightweight and correct
  • ✅ Settings are read from SharedPreferences (correct approach for background isolate)
  • ✅ try/catch around JSON decode prevents crash on corrupted settings

Please address #1 and #2.

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.

1 participant