Skip to content

feat(upload): async device-upload (202 + poll) + per-file socket-timeout isolation (ADR-0008) - #57

Merged
BigBill1418 merged 2 commits into
mainfrom
claude/device-upload-async
Jun 15, 2026
Merged

feat(upload): async device-upload (202 + poll) + per-file socket-timeout isolation (ADR-0008)#57
BigBill1418 merged 2 commits into
mainfrom
claude/device-upload-async

Conversation

@BigBill1418

Copy link
Copy Markdown
Owner

Client leg of the device-upload async decoupling — the last open item from the DroneOpsCommand ground-up audit (P2-2). Pairs with DroneOpsCommand ADR-0023 / backend v2.71.0 (already live on BOS-HQ: the /device-upload/async + /device-upload/status/{batch_id} routes and the async_upload_available health flag are deployed and verified).

Why

The legacy synchronous upload holds the HTTP connection open for the entire server-side parse of each flight log (120 s read-timeout on both ends). On field cellular/wifi a slow parse trips the timeout — and a SocketTimeoutException on file k set aborted = true, marking every remaining file ERROR without attempting it. One slow log nuked the rest of the sortie.

What

Stage C — per-file socket-timeout isolation

  • Inline per-file outcome logic extracted into a pure, JVM-testable classifyUploadOutcome(...) (upload/UploadOutcome.kt).
  • SocketTimeoutException now fails only that file; the batch continues. UnknownHostException (dead host) and HTTP 401/403 (bad key) remain correct batch-wide aborts.

Stage D — async adoption

  • When the server advertises async_upload_available, POST …/device-upload/async202 {batch_id} (connection released after the byte-stream) → poll …/device-upload/status/{batch_id} (2 s → 5 s after 60 s, 10-min ceiling), driving per-file status from per_file[0].state. A 202 body already marking the file skipped (SHA-256 dedup) short-circuits with no poll.
  • Graceful fallback: async_upload_available defaults false → a new APK on an old/legacy server (or before the backend deploy) transparently uses the unchanged synchronous path. Client death mid-poll is safe (server completes; dedup reconciles next launch).
  • ApiClient split timeouts: upload readTimeout 30 s, poll 15 s; connectTimeout 20 s unchanged; writeTimeout 120 s (bounds slow cellular byte-upload).

Tests

UploadOutcomeTest (10) + PollEnvelopeTest (13), JVM/JUnit (CI runs them — no JDK on the authoring host). Gson field names verified byte-for-byte against ADR-0023 and the deployed backend envelopes.

Contract / compat

  • Backwards-compatible: the legacy synchronous route is unchanged server-side; old APKs in the field keep working indefinitely.
  • No version.properties bump in this PR (CI auto-bumps on merge; a manual bump would fold into the squash and suppress the release).

Operator end-to-end check (after the release APK installs)

Run a multi-file sortie with one deliberately large record; confirm in Diagnostics that a slow file no longer blocks the others and the connection returns immediately with a 202.

ADR: docs/adr/0008-device-upload-async-poll-client.md (cross-references DroneOpsCommand ADR-0023).

🤖 Generated with Claude Code

Bill Barnard and others added 2 commits June 15, 2026 04:59
…out isolation

Client leg of the device-upload async decoupling (ADR-0008; pairs with
DroneOpsCommand ADR-0023 / backend v2.71.0). Closes audit P2-2.

Stage C — per-file socket-timeout isolation:
- Extract the inline per-file outcome logic into a pure, JVM-testable
  classifyUploadOutcome() (upload/UploadOutcome.kt). A SocketTimeoutException
  now fails only that file; the batch continues. UnknownHost + 401/403 stay
  correct batch-wide aborts. (Was: one slow file set aborted=true and nuked
  every remaining file in the sortie.)

Stage D — async adoption:
- When device-health advertises async_upload_available, POST
  /device-upload/async -> 202 {batch_id} (connection released after the
  byte-stream), poll /device-upload/status/{batch_id} (2s -> 5s after 60s,
  10-min ceiling), drive per-file UploadStatus from per_file[0].state. 202
  body already-skipped short-circuits with no poll.
- Graceful fallback: async_upload_available defaults false, so a new APK on
  an old/legacy server (or before the backend deploy) uses the unchanged
  synchronous path. Client death mid-poll is safe (server completes; dedup
  reconciles).
- ApiClient split timeouts: upload readTimeout 30s, poll 15s; connect 20s
  unchanged; writeTimeout 120s (bounds slow cellular byte-upload).

Tests: UploadOutcomeTest (10) + PollEnvelopeTest (13), JVM/JUnit. No version
bump (CI auto-bumps version.properties on merge).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI test gate (.github/workflows/ci.yml):
- Runs testDebugUnitTest on pull_request(main) + push(claude/**), on the
  same self-hosted BOS runner pool release.yml uses (provisions JDK 17 +
  Android SDK per run). Closes the gap where release.yml only ran
  assembleRelease and the unit tests never executed in CI. Least-privilege
  (contents: read), concurrency-cancel, uploads the test report.

Release-readiness doc reconciliation (Terry pass):
- ROADMAP/PROGRESS/ADR-0008 still said this work was "designed, not started"
  while the code shipped in this same PR — flipped to as-shipped, P2-2 noted.
- CHANGELOG test counts corrected to the real @test counts:
  UploadOutcomeTest 10->9, PollEnvelopeTest 13->11 (verified by running the
  suite in a gradle:8.10-jdk17 container: 20 passed).
- README: added "Upload reliability" section + a 4-endpoint backend table
  (health/legacy/async/status) documenting capability-detect + fallback.
- CLAUDE.md: upload-flow + [UPLOAD] diag-channel description updated.
- No version bump (CI owns version.properties).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@BigBill1418
BigBill1418 merged commit c66931a into main Jun 15, 2026
2 checks passed
@BigBill1418
BigBill1418 deleted the claude/device-upload-async branch June 15, 2026 05:56
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