Add TileProvision configuration and gRPC service for tile delivery
ci/woodpecker/push/01-test Pipeline failed
ci/woodpecker/push/02-build-push unknown status

- Introduced new TileProvision settings in appsettings.json, including MaxTilesPerBatch and ProgressEmitIntervalSeconds.
- Configured TileProvisionConfig in Program.cs to bind the new settings.
- Added gRPC service for RouteTileDelivery in Program.cs to handle tile delivery requests.
- Updated SatelliteProvider.Api.csproj to include Grpc.AspNetCore package and added protobuf file for tile provision.
- Enhanced AuthenticationServiceCollectionExtensions to handle JWT token extraction from the Authorization header.
- Registered additional services in RouteManagementServiceCollectionExtensions for tile processing.

These changes enhance the API's capability to manage tile provisioning and delivery efficiently.
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-06-23 13:18:59 +03:00
parent 62d6b8310a
commit 275ee1b554
22 changed files with 1469 additions and 3 deletions
@@ -0,0 +1,95 @@
syntax = "proto3";
package satellite.v1;
import "google/protobuf/timestamp.proto";
option csharp_namespace = "Satellite.V1";
service RouteTileDelivery {
rpc DeliverRouteTiles(DeliverRouteTilesRequest) returns (stream RouteTileEvent);
}
message DeliverRouteTilesRequest {
RouteSpec route = 1;
repeated ClientTileRecord client_tiles = 2;
}
message RouteSpec {
string route_id = 1;
repeated Waypoint waypoints = 2;
double region_size_meters = 3;
int32 zoom = 4;
repeated GeofencePolygon geofences = 5;
bool include_geofence_tiles = 6;
}
message Waypoint {
double lat = 1;
double lon = 2;
}
message GeofencePolygon {
repeated Waypoint vertices = 1;
}
message ClientTileRecord {
int32 z = 1;
int32 x = 2;
int32 y = 3;
double resolution_m_per_px = 4;
google.protobuf.Timestamp captured_at = 5;
optional string source = 6;
bytes content_sha256 = 7;
}
message RouteTileEvent {
oneof payload {
RouteManifest manifest = 1;
TileBatch batch = 2;
ProgressUpdate progress = 3;
DeliveryComplete complete = 4;
DeliveryError error = 5;
}
}
message RouteManifest {
uint32 total_candidates = 1;
uint32 skipped_by_client = 2;
uint32 to_deliver = 3;
}
message TileBatch {
uint32 batch_seq = 1;
repeated TilePayload tiles = 2;
}
message TilePayload {
int32 z = 1;
int32 x = 2;
int32 y = 3;
double resolution_m_per_px = 4;
google.protobuf.Timestamp captured_at = 5;
string source = 6;
bytes jpeg = 7;
bytes content_sha256 = 8;
uint32 route_priority = 9;
}
message ProgressUpdate {
uint32 delivered = 1;
uint32 total = 2;
uint32 downloading = 3;
}
message DeliveryComplete {
uint32 delivered = 1;
uint32 skipped_client = 2;
uint32 skipped_server_filter = 3;
}
message DeliveryError {
string code = 1;
string message = 2;
bool retryable = 3;
}
@@ -0,0 +1,143 @@
# Contract: RouteTileDelivery (gRPC)
**Component**: c11_tilemanager (consumer), satellite-provider (producer)
**Epic**: AZ-976
**ADR**: ADR-013 (architecture.md)
**Proto**: `tile_provision.proto``package satellite.v1`
**Version**: 0.3.0
**Status**: proposed
**Last Updated**: 2026-06-19
## Purpose
Operator-side **pre-flight cache provisioning**. Client sends route + onboard tile catalog once; server streams `RouteTileEvent` messages until `DeliveryComplete` or `DeliveryError`.
satellite-provider does **not** receive `flight_id` — that is a C6 bookkeeping concern on the gps-denied side only (`route_id` is the wire correlation id).
C11/C12 on the **operator workstation** only. ADR-004: airborne image must not import stubs or open this channel.
## RPC
```protobuf
service RouteTileDelivery {
rpc DeliverRouteTiles(DeliverRouteTilesRequest) returns (stream RouteTileEvent);
}
```
| Concern | Rule |
|---------|------|
| Auth | gRPC metadata `authorization: Bearer <JWT>` |
| TLS | Required in production; `SATELLITE_PROVIDER_TLS_INSECURE=1` dev knob |
| Idempotency | `RouteSpec.route_id` (UUID string) |
| Resume | Client persists last acked `batch_seq` per `route_id` locally (not on wire) |
## Request
### `DeliverRouteTilesRequest`
| Field | Description |
|-------|-------------|
| `route` | Corridor geometry + single zoom |
| `client_tiles` | Onboard inventory snapshot (route intersection only) |
### `RouteSpec`
| Field | Maps from gps-denied |
|-------|----------------------|
| `route_id` | Client-generated UUID per provision job |
| `waypoints` | `replay_input.tlog_route.RouteSpec.waypoints` |
| `region_size_meters` | `RouteSpec.suggested_region_size_meters` |
| `zoom` | Single slippy zoom level (confirmed sufficient) |
| `geofences` | Optional inclusion polygons |
| `include_geofence_tiles` | Union geofence tiles with corridor grid |
### `ClientTileRecord`
Canonical key: **`(z, x, y)`**. `source` is informational only — **not** used in skip logic.
| Field | C6 mapping |
|-------|------------|
| `resolution_m_per_px` | RESTRICT-SAT-4 (lower = better) |
| `captured_at` | `TileMetadata.capture_timestamp` |
| `content_sha256` | `TileMetadata.content_sha256_hex` (raw 32 bytes) |
## Server skip rule (client catalog)
For each server candidate tile, **omit from stream** when `client_tiles` has matching `(z,x,y)` and **any** of:
1. `client.content_sha256` is non-empty and **equals** server payload hash → skip (byte-identical)
2. `client.resolution_m_per_px <= server.resolution_m_per_px` **and** `client.captured_at >= server.captured_at` → skip (metadata-sufficient)
`source` is **not** compared.
`RouteManifest.skipped_by_client` counts tiles removed by this rule.
## Sector — not on this wire
**Sector** (`active_conflict` vs `stable_rear`) controls **how stale a tile may be before C6 rejects it on write** (AC-NEW-6 freshness). It is an operator decision about the geographic area, not something satellite-provider needs to deliver tiles.
| Layer | Who applies sector |
|-------|-------------------|
| satellite-provider | Does not need sector — streams tiles by route geometry |
| C11 client write | Reads sector from **C11/C12 config** (same as today) when calling C6 freshness gate |
No `SectorClass` field on the gRPC request.
## Response stream: `RouteTileEvent`
Typical sequence:
1. **`RouteManifest`** — `total_candidates`, `skipped_by_client`, `to_deliver`
2. **`TileBatch`** — monotonic `batch_seq`; on-disk hits first, then freshly fetched
3. **`ProgressUpdate`** — optional
4. **`DeliveryComplete`** or **`DeliveryError`**
### `DeliveryComplete` counters
| Field | Meaning |
|-------|---------|
| `delivered` | Tiles actually sent in `TileBatch` streams |
| `skipped_client` | Same as manifest `skipped_by_client` (echo for client verify) |
| `skipped_server_filter` | Tiles SP required but **did not send** after client dedup — see below |
#### `skipped_server_filter` — what counts
Tiles that entered the post-client-dedup work queue but never appeared in a batch:
| Reason | Example |
|--------|---------|
| **Fetch failed** | External imagery provider 404/timeout after retries |
| **Below SP min resolution** | SP refuses to store/serve below its configured floor |
| **Geometry clip** | Tile dropped after server-side corridor/geofence validation |
| **Operational cap** | Job hit max-tiles / rate limit (if SP enforces) |
Tiles skipped by the **client catalog rule** are **not** included here (they are `skipped_client`).
If SP has no server-side filters in v1, `skipped_server_filter` may be **0**; the field is reserved for observability.
### `TilePayload`
| Field | Notes |
|-------|-------|
| `content_sha256` | 32-byte SHA-256 of `jpeg`; matches C6 DB invariant |
| `route_priority` | Lower = earlier along route |
## Client write path (gps-denied)
`RouteTileDeliveryClient` (C11):
- Assigns C6 `flight_id` from operator context locally (not from SP)
- Applies RESTRICT-SAT-4, **sector-based freshness**, AZ-308 budget, download journal
- Resumes via persisted `route_id` + `batch_seq`
## Migration
REST `route_client` + `HttpTileDownloader` remain fallback until AZ-979 benchmark.
## Change log
| Version | Date | Change |
|---------|------|--------|
| 0.3.0 | 2026-06-19 | `ClientTileRecord.content_sha256`; sequential field nums on `TilePayload`; sector/flight_id off wire; skip rule + `skipped_server_filter` defined |
| 0.2.0 | 2026-06-19 | `satellite.v1.RouteTileDelivery` + `RouteTileEvent` oneof |
| 0.1.0 | 2026-06-19 | Initial draft (superseded) |