Skip to content

K2GO-507 feat(cx-core): export as a prepare/poll/fetch long operation - #691

Merged
luisguzman-adfa merged 5 commits into
mainfrom
feat/K2GO-507-cx-core-export-longop
Oct 24, 2026
Merged

luisguzman-adfa merged 5 commits into
mainfrom
feat/K2GO-507-cx-core-export-longop

Conversation

@luisguzman-adfa

Copy link
Copy Markdown
Collaborator

Large content export timed out: the box stages a Kolibri channel to a temp before
the first byte streams, so a single blocking GET exceeded the client read timeout.

Export is now a long operation on the core (cx_core 0.2.0):

  • POST /v1/export/prepare -> {opId, state, totalBytes}. Direct items (Kiwix,
    Calibre) are ready at once; a Kolibri channel stages in a worker thread.
  • GET /v1/export/status/{opId} -> {state, stagedBytes, totalBytes, error?}. The
    poll is the liveness signal, so there is no wall-clock stage timeout.
  • GET /v1/export/fetch/{opId}: streams the staged archive; the op clears on close.
  • DELETE /v1/export/{opId}: client abort.
  • GET /v1/version, and version added to /v1/apps. The role writes it to iiab.ini.

Op lifecycle (in-memory registry, one cleanup primitive):

  • A fetch pins its op for the stream's life (on_open) and releases it (on_close),
    so the TTL sweep never reclaims a temp that is being streamed.
  • A cancel or sweep stops the Kolibri staging subprocess, not just its temp.
  • Startup sweep reclaims orphaned temps, including the pre-change prefix for one
    release.

Python stdlib only. 150 unit tests.

…fetch

Replace open_export (which staged, preflighted and streamed in one call) with three
primitives: prepare_export reports an ExportPlan (stages?, total bytes, name), stage_export
stages into a caller-given dir, open_fetch opens the stream. The temp and staging-thread
lifecycle moves to the op layer; the adapter keeps only the per-app staging / streaming
logic. Read-only-on-source and traversal guards preserved.
…version

A large Kolibri channel stages for minutes before the first byte, so a single blocking
export request times out the client. Export becomes a long operation, uniform for all apps:
prepare starts staging in a background thread (or marks a direct app ready) and returns an
opId; status reports a staged-bytes heartbeat and flips to ready; fetch streams the bytes;
delete aborts. The old GET /v1/export/item is removed (no alias: unused).

export_ops.ExportOps owns the in-flight set and the one cleanup owner per temp: fetch
on-close (normal), delete (abort), a lazy TTL sweep (abandoned), and a startup sweep of the
op temps on disk (process death). State is in-memory only; the front recovers by re-prepare.

Also add cx_core.VERSION, GET /v1/version (and version in GET /v1/apps), and have the
cx_core role write the version into the [cx_core] section of iiab.ini, mirroring dash-node.
…ir is cleaned

The 409-before-ready test released the staging thread then let the TemporaryDirectory be
removed under it, a teardown race on Windows. Wait for the terminal state first.
…eep legacy temps

Review fixes on the export long operation:
- The TTL sweep could rmtree a live fetch's temp: a multi-gigabyte stream outlasts the TTL
  and bumps nothing, so a concurrent op request swept it mid-stream. Mark the op in_use when
  fetch opens the stream, skip in_use ops in the sweep, drop it in on_close. A process death
  loses the flag and the op, so the startup sweep still reclaims the orphan temp.
- An abort (DELETE / sweep) removed the temp but left the Kolibri export CLI running into a
  deleted dir. stage_export now takes a should_cancel callback; the op layer passes "op is
  gone", and the CLI runs under Popen polled once a second and is terminated on cancel.
- The startup sweep now also reclaims the pre-change "cxkolibri-" temp prefix for one release.
The in_use pin was set in fetch() before streaming started, so a failure in that window
(open_fetch raises, or the client disconnects before the stream body) left the op in_use
forever, since the sweep now skips in_use ops. Move the pin into the stream lifecycle: fetch()
returns the stream unpinned (op merely fresh), an on_open hook pins it at the top of the
server's streaming try, and the existing finally (on_close) clears it. A pre-stream failure
now leaves the op reclaimable by the normal sweep; open_fetch raising drops the op and its
temp and returns 500.
@luisguzman-adfa
luisguzman-adfa merged commit 0e6ba0b into main Oct 24, 2026
3 checks passed
@luisguzman-adfa
luisguzman-adfa deleted the feat/K2GO-507-cx-core-export-longop branch October 24, 2026 02:53
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