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

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions