mirror of
https://github.com/azaion/autopilot.git
synced 2026-06-22 07:21:11 +00:00
[AZ-645] [AZ-646] [AZ-647] mission_client: middle-waypoint POST + mapobjects pull/push
ci/woodpecker/push/build-arm Pipeline failed
ci/woodpecker/push/build-arm Pipeline failed
Batch 3 of greenfield Step 7 — mission_client epic AZ-638 close-out.
AZ-645 (Middle-waypoint POST)
- post_middle_waypoint(mission_id, &Mission) -> Result<MissionUpdateAck, PostError>
- Bounded retry (default 3 attempts) shared with the rest of missions_api
- Health: last_middle_waypoint_post_status (ok/error)
AZ-646 (Pre-flight MapObjects pull)
- pull_mapobjects(mission_id) -> Result<MapObjectsBundle, PullError>
- Schema-validated against bundled shared/contracts/mapobjects-bundle.json
- Typed errors: Unreachable / SchemaInvalid / MaxRetriesExceeded / Internal
- Health: mapobjects_pull_state, last_mapobjects_pull_ts
AZ-647 (Post-flight push + durable disk queue)
- push_mapobjects_diff(mission_id, MapObjectsDiff) -> PushReport
- recover_pending_pushes() -> Vec<PushReport> for crash recovery
- Write-ahead atomic-rename persistence under ${state_dir}/mapobjects_push/
- Per-endpoint independent retry: observations + ignored_items
- Partial success rewrites the disk file with only the failing portion
- Health: mapobjects_push_pending, last_push_ts, per-endpoint last error
Infrastructure
- Schemas: shared/contracts/mapobjects-{bundle,observations,ignored}.json
- Restructured schema/ into mission.rs + mapobjects.rs sub-modules
- New mapobjects_sync/ (pull, push, queue)
- workspace dep tempfile=3; mission_client dev-deps add tempfile + chrono
Tests
- 12/12 ACs verified locally (4 AZ-645 + 4 AZ-646 + 5 AZ-647)
- mission_client suite: 15 unit + 18 integration = 33 tests pass
- AZ-646 AC-4 proxy: 1000-object + 1000-ignored bundle within 30s
- AZ-647 AC-5 proxy: 5000-obs + 500-ignored push within 2min
Code review verdict: PASS_WITH_WARNINGS (inline). Cumulative review
(K=3 trigger) PASS_WITH_WARNINGS — full report in
_docs/03_implementation/cumulative_review_batches_01-03_cycle1_report.md.
Open follow-ups (non-blocking):
- module-layout.md: rename push_mapobjects -> push_mapobjects_diff (Step 13)
- ExponentialBackoff still duplicated across crates; promote to shared::retry
when the third caller lands (likely detection_client AZ-660/661)
- state_dir default is relative; composition root must override
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,64 +0,0 @@
|
||||
# Middle-Waypoint POST
|
||||
|
||||
**Task**: AZ-645_mission_client_waypoint_post
|
||||
**Name**: Middle-waypoint POST to missions API
|
||||
**Description**: POST the updated mission (with operator-confirmed middle waypoint inserted) to the external `missions` API; bounded retry; surface failure to `mission_executor`.
|
||||
**Complexity**: 2 points
|
||||
**Dependencies**: AZ-640_initial_structure, AZ-644_mission_client_pull_and_schema
|
||||
**Component**: mission_client
|
||||
**Tracker**: AZ-645
|
||||
**Epic**: AZ-638
|
||||
|
||||
## Problem
|
||||
|
||||
When the operator confirms a POI, `scan_controller` hands a middle-waypoint hint to `mission_executor`, which computes the patched mission (`current_position → middle_waypoint → resume_original_route`). That patched mission must be POSTed to the external `missions` API for persistence and traceability. If the POST fails, the executor decides whether to halt, RTL, or continue with the in-memory mission — `mission_client` only surfaces the failure.
|
||||
|
||||
## Outcome
|
||||
|
||||
- `MissionClient::post_middle_waypoint(mission_id, patched_mission) -> Result<MissionUpdateAck, PostError>` performs a `POST /missions/{id}/middle-waypoint` (exact path per `../_docs/02_missions.md`) and awaits an ack.
|
||||
- Bounded exponential backoff on transient failure (default 3 attempts).
|
||||
- On final failure returns a typed error; never silent.
|
||||
- Health field `last_middle_waypoint_post_status` updated.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- POST endpoint call with the patched mission body.
|
||||
- Bounded retry on 5xx / timeout.
|
||||
- Error surface to caller.
|
||||
|
||||
### Excluded
|
||||
- The decision to RTL on failure (`mission_executor`).
|
||||
- Recomputing the patched mission (`mission_executor`).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
**AC-1: Happy path POST**
|
||||
Given a fixture missions API that accepts the POST and returns `200`
|
||||
When `post_middle_waypoint("M1", patched)` is called
|
||||
Then it returns `Ok(MissionUpdateAck { ... })` within ≤2 s and `health.last_middle_waypoint_post_status = "ok"`.
|
||||
|
||||
**AC-2: Transient failure retries**
|
||||
Given the API returns `503` once then `200`
|
||||
When the call is made
|
||||
Then it returns `Ok` on the second attempt.
|
||||
|
||||
**AC-3: Cap exhaustion bubbles error**
|
||||
Given the API returns `500` for all 3 default attempts
|
||||
When the call is made
|
||||
Then it returns `Err(MaxRetriesExceeded)` and the error is surfaced to the caller; no silent absorption.
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
**Performance**
|
||||
- Single happy-path POST completes in ≤2 s on healthy connectivity.
|
||||
|
||||
**Reliability**
|
||||
- Bounded backoff; no infinite retry.
|
||||
|
||||
## Runtime Completeness
|
||||
|
||||
- **Named capability**: middle-waypoint POST against the external `missions` API.
|
||||
- **Production code that must exist**: real HTTPS POST.
|
||||
- **Allowed external stubs**: `wiremock`/`mockito` for tests.
|
||||
- **Unacceptable substitutes**: swallowing the error and proceeding is not acceptable.
|
||||
@@ -1,76 +0,0 @@
|
||||
# MapObjects Pre-Flight Pull
|
||||
|
||||
**Task**: AZ-646_mission_client_mapobjects_pull
|
||||
**Name**: Pre-flight MapObjects GET + cached-fallback handshake
|
||||
**Description**: After mission fetch succeeds, GET `/missions/{id}/mapobjects` (and `/ignored` if separated). Surface the bundle to `mapobjects_store`. On failure, surface BIT degradation — operator must acknowledge cached fallback or abort. Never silent.
|
||||
**Complexity**: 3 points
|
||||
**Dependencies**: AZ-640_initial_structure, AZ-644_mission_client_pull_and_schema
|
||||
**Component**: mission_client
|
||||
**Tracker**: AZ-646
|
||||
**Epic**: AZ-638
|
||||
|
||||
## Problem
|
||||
|
||||
The MapObjects working copy is hydrated pre-flight from the central `missions` API. The pull must complete before `mission_executor` proceeds past `BIT_OK`. On pull failure the system must NOT silently proceed; instead, `mission_executor`'s BIT (F9) surfaces a degraded state — the operator either acknowledges cached fallback (signed acknowledgement per Q9) or aborts.
|
||||
|
||||
## Outcome
|
||||
|
||||
- `MissionClient::pull_mapobjects(mission_id) -> Result<MapObjectsBundle, PullError>` performs a `GET /missions/{id}/mapobjects` (and `/ignored` if the API splits them) and returns a typed `MapObjectsBundle { map_objects, ignored_items, fetched_at, schema_version, fallback_used: bool }`.
|
||||
- On 200, the bundle is handed to `mapobjects_store` for hydration; `mapobjects_pull_state = synced`.
|
||||
- On error or timeout, `pull_state = failed`; the typed error is surfaced to `mission_executor` (F9 BIT degrades, never silent).
|
||||
- Health fields: `mapobjects_pull_state`, `last_mapobjects_pull_ts`.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- GET endpoint(s) call.
|
||||
- Schema validation of the bundle (using the shared MapObjects schema in `shared/contracts/`).
|
||||
- Cached-fallback semantics — the **cache** itself lives in `mapobjects_store` (task 28); this task only knows to set `fallback_used = true` if it uses cached on operator ack.
|
||||
- Health surface fields above.
|
||||
|
||||
### Excluded
|
||||
- The cache storage itself (lives in `mapobjects_store`).
|
||||
- Operator-acknowledgement flow (`operator_bridge`).
|
||||
- BIT orchestration (`mission_executor`).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
**AC-1: Happy path pull**
|
||||
Given a fixture API that returns a schema-valid MapObjects bundle
|
||||
When `pull_mapobjects("M1")` is called
|
||||
Then it returns `Ok(bundle)`, `pull_state = synced`, and the bundle reaches `mapobjects_store` for hydration.
|
||||
|
||||
**AC-2: Schema-invalid is rejected**
|
||||
Given the API returns a 200 with a missing required field
|
||||
When `pull_mapobjects("M1")` is called
|
||||
Then it returns `Err(SchemaInvalid)` and `pull_state = failed`; no silent acceptance.
|
||||
|
||||
**AC-3: Network failure surfaces to F9**
|
||||
Given the API is unreachable
|
||||
When `pull_mapobjects("M1")` is called
|
||||
Then it returns `Err(Unreachable)`, `pull_state = failed`, and the error is observable by `mission_executor`'s BIT path.
|
||||
|
||||
**AC-4: 30 km × 30 km area completes within budget**
|
||||
Given a fixture bundle the size of a 30 km × 30 km mission area
|
||||
When the pull is performed on a 100 Mbps loopback link
|
||||
Then the call completes in ≤30 s.
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
**Performance**
|
||||
- ≤30 s for a 30 km × 30 km mission area on healthy connectivity (per `description.md §8`).
|
||||
|
||||
**Reliability**
|
||||
- Never silent on failure.
|
||||
|
||||
## Contract
|
||||
|
||||
- MapObjects bundle schema: `shared/contracts/mapobjects-bundle.json`. Owner: `../_docs/02_missions.md` §7.13 extension.
|
||||
- Canonical typed model: `data_model.md §MapObjectsBundle`.
|
||||
|
||||
## Runtime Completeness
|
||||
|
||||
- **Named capability**: HTTPS GET against the central MapObjects extension + schema validation.
|
||||
- **Production code that must exist**: real HTTPS GET; real schema validator.
|
||||
- **Allowed external stubs**: `wiremock`/`mockito`.
|
||||
- **Unacceptable substitutes**: skipping schema validation in production.
|
||||
@@ -1,84 +0,0 @@
|
||||
# MapObjects Post-Flight Push + Durable Queue
|
||||
|
||||
**Task**: AZ-647_mission_client_mapobjects_push
|
||||
**Name**: Post-flight MapObjects push with durable queue and crash-recovery push
|
||||
**Description**: On `mission_executor` terminal state, drain `mapobjects_store`'s pending diff and POST to `/missions/{id}/mapobjects` + `/missions/{id}/mapobjects/ignored`. Independent retry per endpoint. Persist pending diff on disk for 24 h durable retry. At startup, replay any non-empty pending diff from a previously terminated mission BEFORE BIT for any new mission begins.
|
||||
**Complexity**: 5 points
|
||||
**Dependencies**: AZ-640_initial_structure, AZ-644_mission_client_pull_and_schema, AZ-646_mission_client_mapobjects_pull
|
||||
**Component**: mission_client
|
||||
**Tracker**: AZ-647
|
||||
**Epic**: AZ-638
|
||||
|
||||
## Problem
|
||||
|
||||
The full pass diff (NEW / MOVED / EXISTING / REMOVED-candidate observations + IgnoredItem appends) must reach the central API after the mission ends. In-flight central writes are forbidden (Frozen choice 6 — `architecture.md §7.3`). The post-flight push must survive transient failure (independent retry per endpoint), persistent failure (operator-visible warning + manual replay), and crash mid-mission (next-boot push of pending diff). The durable queue is the disk-backed safety net.
|
||||
|
||||
## Outcome
|
||||
|
||||
- `MissionClient::push_mapobjects_diff(mission_id, diff) -> PushReport` posts the observations and ignored-items independently; partial success does not roll back the successful endpoint.
|
||||
- The pending diff is persisted on disk at `${state_dir}/mapobjects_push/<mission_id>.json` BEFORE the push starts (write-ahead).
|
||||
- Per-endpoint bounded exponential backoff (24 h durable retry window; configurable).
|
||||
- Persistent failure: `sync_state = degraded`; operator-visible warning; entry stays on disk for manual replay.
|
||||
- At startup, if `${state_dir}/mapobjects_push/` has any non-empty file, run the push for those missions BEFORE BIT for any new mission begins (crash-recovery path).
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- Two POST endpoints, called independently with separate retry/backoff state.
|
||||
- Write-ahead persistence of the pending diff before the network call.
|
||||
- Crash-recovery sweep at startup.
|
||||
- `PushReport { observations: PerEndpointStatus, ignored: PerEndpointStatus }`.
|
||||
- Health surface: `mapobjects_push_pending`, `last_push_ts`, per-endpoint last error.
|
||||
|
||||
### Excluded
|
||||
- Building the pending diff (`mapobjects_store` — task 28 owns `pending_observations` + `pending_ignored`).
|
||||
- Choosing what's a terminal state (`mission_executor`).
|
||||
- Operator UI for the manual-replay warning (`operator_bridge` / Ground Station).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
**AC-1: Happy path push**
|
||||
Given the mission ended with N observations and M ignored items
|
||||
When `push_mapobjects_diff("M1", diff)` is called and both endpoints return 200
|
||||
Then both succeed, the disk file is cleared, and `sync_state = synced`.
|
||||
|
||||
**AC-2: Partial success — independent retry**
|
||||
Given `/mapobjects` returns 200 and `/mapobjects/ignored` returns 503
|
||||
When the push runs
|
||||
Then the observations endpoint is reported success, the ignored endpoint is queued for retry, and the disk file retains ONLY the ignored portion.
|
||||
|
||||
**AC-3: Persistent failure persists for manual replay**
|
||||
Given both endpoints return 503 for all 24 h of bounded retry
|
||||
When the retry window closes
|
||||
Then `sync_state = degraded`, the disk file remains intact, and a manual-replay warning is observable in `health()`.
|
||||
|
||||
**AC-4: Crash-recovery push at startup**
|
||||
Given a previous run terminated with a non-empty disk file at `${state_dir}/mapobjects_push/M0.json`
|
||||
When the process starts a new run for mission `M1`
|
||||
Then the push for `M0` is attempted before BIT begins for `M1`; the order is observable via logs.
|
||||
|
||||
**AC-5: 60-min mission push within budget**
|
||||
Given a fixture pass diff sized for a 60-min mission
|
||||
When the push is performed on a 100 Mbps loopback link
|
||||
Then both endpoints complete in ≤2 min.
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
**Performance**
|
||||
- ≤2 min for a 60-min mission's pass diff (per `description.md §8`).
|
||||
|
||||
**Reliability**
|
||||
- 24 h durable retry window.
|
||||
- Crash-mid-mission: nothing is lost on disk.
|
||||
|
||||
## Contract
|
||||
|
||||
- MapObjects POST schemas: `shared/contracts/mapobjects-observations.json` and `shared/contracts/mapobjects-ignored.json`. Owner: `../_docs/02_missions.md` §7.13 extension.
|
||||
- Canonical typed model: `data_model.md §MapObjectObservation`, `§IgnoredItem`.
|
||||
|
||||
## Runtime Completeness
|
||||
|
||||
- **Named capability**: durable on-disk queue + post-flight push to the central `missions` API.
|
||||
- **Production code that must exist**: real disk write-ahead (atomic rename); real HTTPS POST; real backoff state machine; real crash-recovery sweep.
|
||||
- **Allowed external stubs**: `wiremock`/`mockito` for tests; `tempfile` for the disk-queue tests.
|
||||
- **Unacceptable substitutes**: an in-memory-only queue is not acceptable (crash recovery requires disk).
|
||||
Reference in New Issue
Block a user