feat(upload): async device-upload (202 + poll) + per-file socket-timeout isolation (ADR-0008) - #57
Merged
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 theasync_upload_availablehealth 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
SocketTimeoutExceptionon file k setaborted = 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
classifyUploadOutcome(...)(upload/UploadOutcome.kt).SocketTimeoutExceptionnow 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
async_upload_available, POST…/device-upload/async→202 {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 fromper_file[0].state. A202body already marking the fileskipped(SHA-256 dedup) short-circuits with no poll.async_upload_availabledefaults 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).ApiClientsplit timeouts: uploadreadTimeout 30 s, poll15 s;connectTimeout 20 sunchanged;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
version.propertiesbump 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