Skip to content

fix(migration): stream export manifest into zip to bound memory (COLUMBA-DS) - #1189

Merged
torlando-tech merged 3 commits into
mainfrom
fix/migration-export-manifest-oom
Oct 10, 2026
Merged

torlando-tech merged 3 commits into
mainfrom
fix/migration-export-manifest-oom

Conversation

@torlando-tech

Copy link
Copy Markdown
Owner

Problem

COLUMBA-DS: fatal OutOfMemoryError during export on a Galaxy S25 Ultra (build 2.3.0-beta+20300002, 2026-10-01). The crash happens after the crypto OOM fix (PR #1128) — this is the next unbounded allocation in the same flow:

MigrationExporter.createExportZip (line 663)
  -> Json.encodeToString(bundle)          // whole manifest as one String
  -> JsonToStringWriter.ensureAdditionalCapacity
  -> Arrays.copyOf -> OOM (291 MB char buffer)

createExportZip serialized the entire MigrationBundle with json.encodeToString(bundle).toByteArray() — buffering the full manifest text plus a second full byte array on top of the in-memory bundle. The crashing user's manifest was ~150 MB of JSON.

Fix

Write the manifest with json.encodeToStream(MigrationBundle.serializer(), bundle, zipOut), which streams the JSON into the zip entry through a bounded buffer — the same shape the import side already uses (json.decodeFromStream). Manifest bytes are unchanged.

New unit test pins the contract: encodeToStream output is byte-identical to encodeToString output for a fully-populated bundle.

Verification

  • Full migration unit-test suite green, including the three MigrationCallHistoryRoundTripTest production exportData -> import round-trip tests (these caught an intermediate wrong-overload resolution during development)
  • ktlint + detekt clean

Note: the in-memory MigrationBundle data model itself (all DAO data collected into lists) remains the residual memory ceiling for very large datasets; this PR removes the serialization copy that actually crashed the device.

Fixes COLUMBA-DS

…MBA-DS)

Creating the export ZIP serialized the whole MigrationBundle with json.encodeToString(bundle), buffering the full manifest as a String plus a second byte array on top of the in-memory bundle. A ~150 MB manifest OOM'd the device in JsonToStringWriter (291 MB buffer allocation, COLUMBA-DS).

Write the manifest with json.encodeToStream(serializer, bundle, zipOut), which streams the JSON into the zip entry through a bounded buffer - the same shape the import side already uses (json.decodeFromStream). Manifest bytes are unchanged: a new unit test pins encodeToStream output equal to encodeToString output for a fully-populated bundle.

Verified: full migration unit-test suite green, including the production exportData -> import round-trip tests; ktlint + detekt clean.

Fixes COLUMBA-DS
@greptile-apps

greptile-apps Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[High impact] The PR appears safe to merge; no new actionable issue was found.

Summary

The PR writes the export manifest directly into the ZIP with encodeToStream, avoiding a full JSON string and byte-array copy.

  • Export ZIPs write manifest JSON as it is encoded.

All three previous threads are unnumbered. Their requested changes are present, so no duplicate comments are needed.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    A[MigrationBundle in memory] --> B[encodeToStream]
    B --> C[manifest.json ZIP entry]
    C --> D[Export archive]
Loading

Reviews (4) · Last reviewed commit: "address greptile review feedback (greplo..." · Reviewed by Greptile

Comment thread app/src/test/java/network/columba/app/migration/MigrationDataTest.kt Outdated
@codecov

codecov Bot commented Oct 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…BA-DS)

Forks a 256MB-heap child JVM that runs the real createExportZip on a
~100 MB manifest bundle and verifies the result through the import-side
decodeFromStream.

Pre-fix the child dies with OutOfMemoryError in
JsonToStringWriter.ensureAdditionalCapacity - the same stack as the
production COLUMBA-DS event; post-fix it completes with bounded memory.
Verified both directions before/after the fix on this branch.
Comment thread app/src/test/java/network/columba/app/migration/MigrationExporterOomTest.kt Outdated
@torlando-tech

Copy link
Copy Markdown
Owner Author

@greptile review

- Pass the test task's full runtime classpath to the forked child
  (columba.test.runtimeClasspath, set in doFirst) instead of relying on
  the Gradle worker's java.class.path, which does not include test
  classes. Verified: with the java.class.path fallback removed, the test
  still passes on the exposed classpath alone.
- Run the manifest equivalence test with the exporter's production Json
  settings (prettyPrint + ignoreUnknownKeys) so it pins the bytes the
  real export writes.
- Delete the child's temp work dir (and export archive) in a finally
  block so repeated runs leave nothing on disk.
@torlando-tech

Copy link
Copy Markdown
Owner Author

@greptile review

@torlando-tech
torlando-tech merged commit aca1611 into main Oct 10, 2026
15 checks passed
@torlando-tech
torlando-tech deleted the fix/migration-export-manifest-oom branch October 10, 2026 16:49
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