Skip to content

refactor(appkit): direct Arrow IPC streaming + hardened result delivery#472

Merged
MarioCadenas merged 2 commits into
stack/arrow-3-inline-arrow-fixfrom
arrow329-hardening
Jul 7, 2026
Merged

refactor(appkit): direct Arrow IPC streaming + hardened result delivery#472
MarioCadenas merged 2 commits into
stack/arrow-3-inline-arrow-fixfrom
arrow329-hardening

Conversation

@MarioCadenas

Copy link
Copy Markdown
Collaborator

Reworks and hardens the Arrow/JSON result-delivery path from #329.

Base: arrow329-merge-main = #329's arrow work merged with current main. So this PR's diff is just the rework (not main's ~78 commits) — reviewable in isolation, then it can fold back toward #329 / main.

What changed

Delivery

  • ARROW_STREAM streams the raw Arrow IPC bytes on the /query response body — no SSE, no server-side stash, no second /arrow-result request. The stash was process-local (broke horizontal scaling) and buffered whole payloads; it and its route are removed.
  • Capability fallback centralized + errorCode-first in result-delivery.ts: INLINE+ARROW_STREAM (Reyden) → EXTERNAL_LINKS → structured ARROW_DELIVERY_UNSUPPORTED; the JSON path retries as ARROW_STREAM and decodes server-side for inline-only warehouses.
  • EXTERNAL_LINKS chunks pipe response.body one network read at a time (peak memory = one read buffer). Multi-chunk results now resolve every chunk via next_chunk_index in the request's identity context (OBO-correct) — previously only chunk 0 was streamed (silent truncation).
  • Real column names ride X-Appkit-Arrow-Columns (+ a statement-id ref header + /columns/:id endpoint for very wide schemas); the client relabels the positional col_N schema.

Hardening (pre-merge review, 3 reviewers)

Sev Fix
Critical writeChunk no longer hangs on a client disconnect mid-backpressure — races drain vs close/error, so the stream unwinds and the upstream reader is cancelled instead of leaking
High multi-chunk EXTERNAL_LINKS silent truncation (fix above)
Medium INLINE arrow projects statement_id onto the result → wide-schema -Ref header works on Reyden
Medium /columns resolves under the user identity, then the service principal (fixes OBO wide-schema fallback)
Low arrow 500 forwards the actionable clientMessage; retry backoff is abortable
Cleanup removed dead ExecutionError.stashExhausted + stale stash//arrow-result/"64 MiB" comments

Verification

  • pnpm -r typecheck, biome check, build:package + publint — clean
  • 3220 tests pass, 1 skipped, including new regression tests: writeChunk backpressure/disconnect, multi-chunk pagination, abortable backoff, OBO /columns fallback
  • ⚠️ Multi-chunk external, OBO+external, and wide-schema /columns are unit-tested only — no warm standard warehouse was available to live-verify. Gated integration test is ready: APPKIT_INTEGRATION_WAREHOUSE_ID=<wh> pnpm exec vitest run arrow-delivery.integration

Builds on #329.

Rework of the Arrow/JSON result-delivery path on top of #329's arrow
foundation (base branch is #329 merged with current main).

Delivery:
- ARROW_STREAM queries stream the raw Arrow IPC bytes on the /query
  response body — no SSE, no server-side stash, no second /arrow-result
  request. The inline stash was process-local (broke horizontal scaling)
  and buffered whole payloads; it and its route are removed.
- Capability fallback centralized in result-delivery.ts (errorCode-first
  classification): INLINE+ARROW_STREAM (Reyden) -> EXTERNAL_LINKS -> a
  structured ARROW_DELIVERY_UNSUPPORTED error; the JSON path retries as
  ARROW_STREAM and decodes server-side when the warehouse is inline-only.
- EXTERNAL_LINKS chunks pipe response.body one network read at a time
  (peak memory = one read buffer). Multi-chunk results resolve every
  chunk's links via next_chunk_index in the request's identity context
  (OBO-correct) — previously only chunk 0 was streamed (silent truncation).
- Real column names ride an X-Appkit-Arrow-Columns header (statement-id
  ref + /columns endpoint for wide schemas); the client relabels the
  positional col_N schema.

Hardening (pre-merge review):
- writeChunk no longer hangs on a client disconnect mid-backpressure —
  it races drain vs close/error so the stream unwinds and the upstream
  reader is cancelled instead of leaking.
- Fail-fast first-byte timeout returns 503 WAREHOUSE_UNAVAILABLE on a
  stuck warehouse.
- /columns resolves under the user identity then the service principal
  (fixes OBO wide-schema fallback).
- Retry backoff is abortable; arrow errors forward the actionable
  clientMessage.

Green: typecheck, 3220 tests + 1 skipped, biome, build + publint.
Multi-chunk external, OBO+external, and wide-schema /columns are
unit-tested only (no warm standard warehouse to live-verify).
@MarioCadenas MarioCadenas requested a review from a team as a code owner July 6, 2026 15:22
@MarioCadenas MarioCadenas requested review from calvarjorge and removed request for a team July 6, 2026 15:22
@MarioCadenas MarioCadenas reopened this Jul 6, 2026
@MarioCadenas MarioCadenas changed the base branch from arrow329-merge-main to stack/arrow-3-inline-arrow-fix July 6, 2026 15:37
@MarioCadenas MarioCadenas requested a review from atilafassina July 6, 2026 16:22
…Chunk close guard

Addresses three defects surfaced in external review of the Arrow rework:

- Idle-timeout vs backpressure (arrow-stream-processor.ts): the per-chunk
  idle timeout was armed before reader.read() but stayed armed across the
  downstream `yield`, so a slow client backpressuring writeChunk past the
  timeout (60s) was mistaken for an upstream stall and aborted a healthy
  download. The timeout now covers only the read (armed before, cleared in a
  finally the instant it resolves).

- OBO warehouse startup ran as the service principal (analytics.ts): the
  direct-binary Arrow path called getWorkspaceClient() + ensureWarehouseRunning
  bare in the handler body, so warehouse auto-start ran as the SP even for
  .obo.sql requests — unlike the SSE path, which runs readiness inside the
  executor (user) context. Readiness now runs via the request executor
  (asUser for OBO) through _ensureArrowWarehouseReady, matching the SSE path.

- writeChunk hang on an already-closed socket (analytics.ts): if res was
  already destroyed/ended when writeChunk was entered, res.write returned
  false while drain/close/error had already fired, so the promise never
  settled and wedged the for-await loop + upstream reader. It now rejects up
  front when res.destroyed || res.writableEnded.

Regression tests added: slow-consumer-between-reads (no false abort),
already-closed writeChunk, and OBO readiness routed through the user executor.
Green: typecheck, 3222 tests + 1 skipped, biome, build + publint.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
@MarioCadenas MarioCadenas merged commit f1ef36b into stack/arrow-3-inline-arrow-fix Jul 7, 2026
@MarioCadenas MarioCadenas deleted the arrow329-hardening branch July 7, 2026 12:05
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.

2 participants