Skip to content

aw-sync: unbounded event fetch in sync_one() causes native OOM crash on Android #630

Description

@TimeToBuildBob

Summary

Android sync (syncBoth) OOM-crashes the native aw-server-rust process because the event-fetch side of sync_one() is unbounded, while the write side is already batched.

Confirmed as the dominant crash cluster in ActivityWatch/aw-android#176SIGABRT / std::sys::unix::abort in libaw_server.so, ~22 reports in current vitals, and very likely the root cause behind other unattributed native crashes given how sync-heavy the Android app is.

How sync works today (for context — there's no user-facing setting or trigger)

  • SyncScheduler.kt starts a Handler loop ~1 min after BackgroundService boots, then re-fires syncBothAsync() every 15 min (mobile/.../SyncScheduler.kt:15,38,49,95-106), with an AlarmManager fallback (SyncAlarmReceiver.kt:26) in case the process/Handler dies.
  • It is fully automatic — no on-demand button, no foreground/background trigger, no settings screen entry (confirmed no sync references in AWPreferences.kt / AuthSettingsActivity.kt).
  • syncBothAsync() → JNI syncBoth(port, hostname) (SyncInterface.kt:59,86-99, single-flight guarded) → Java_..._syncBoth in aw-server-rust/aw-sync/src/android.rs:162-212pull_all() / push_with_hostname() (sync_wrapper.rs) → sync_run()sync_datastores()sync_one() in aw-server-rust/aw-sync/src/sync.rs.

Root cause

sync_one() fetches events with get_events(bucket_id, resume_sync_at, None, None)limit: None (sync.rs:321-333). Both Datastore::get_events (local) and AwClient::get_events (HTTP) treat None as "return everything," so the entire event range since the last sync point loads into one Vec<Event> in memory, unbounded. There's already a TODO acknowledging this at sync.rs:323:

// TODO: Fetch at most ~5,000 events at a time (or so, to avoid timeout from huge buckets)

Only the write side is batched — BATCH_SIZE: usize = 5000 (sync.rs:351) is used when inserting into ds_to. The read/fetch side is not. A bucket with a large backlog (device offline a while, or a high-frequency AFK/window bucket) bulk-loads unbounded events → native OOM abort, which surfaces on Android as a libaw_server.so crash since aw-sync shares the process/address space with aw-server.

Fix

  • Primary: implement the TODO — page sync_one()'s fetch loop in chunks (e.g. 5,000 events, matching the existing write BATCH_SIZE) instead of one unbounded get_events() call.
  • Short-term Android mitigation (if the primary fix takes longer): cap sync lookback window (e.g. last N days) via a JNI param, to bound worst-case allocation on mobile specifically.

Related

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions