Files
planet/docs/technical/en/data-job-earth-sync-architecture.md
rayd1o 5bf5c73ca0
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / delivery (push) Has been cancelled
release / images (push) Has been cancelled
release: bump version to 0.66.0
2026-05-26 03:41:47 +08:00

142 lines
6.8 KiB
Markdown

# Data Jobs and Outbox Architecture
This document records the technical boundary for Planet v1 data jobs, database outbox, and Earth refresh. For each data product's business purpose and end-to-end flow, see [Business Architecture and Data Flows](/home/ray/dev/linkong/planet/docs/technical/en/platform-data-flows.md).
## Architecture Boundary
- PostgreSQL is the durable v1 job ledger and outbox. Kafka, Celery, and RQ are intentionally not part of v1.
- `collection_tasks` records collection, data clearing, cache clearing, and non-database Earth refresh jobs.
- `earth_data_change_events` records fact-table or derived-table changes and is the reliable source for Earth sync.
- `LISTEN/NOTIFY` is only the low-latency wakeup path; the listener still polls unconsumed outbox rows.
- Redis is mainly cache, auth helper, OTP / rate limit, temporary logs, and WebSocket support. It is not the durable queue.
## Database Change Sync
```mermaid
flowchart LR
Write["Fact or derived table write"] --> Trigger["PostgreSQL trigger"]
Trigger --> Outbox["earth_data_change_events"]
Trigger --> Notify["planet_earth_data_changes"]
Outbox --> Listener["earth_db_change_listener"]
Notify --> Listener
Listener --> Adapter["earth_layer_adapters"]
Adapter --> Cache["cache invalidation"]
Cache --> WS["earth_updates"]
```
1. Collection, clearing, location resolution, or projection jobs write fact or derived tables.
2. Statement triggers write outbox rows for `INSERT` / `UPDATE` / `DELETE`.
3. The listener wakes through notify or finds pending rows through polling.
4. The listener maps `table + source` to Earth layers, refresh strategy, and cache patterns through `earth_layer_adapters.py`.
5. The listener merges short-window same-layer events, invalidates cache, and broadcasts `earth_updates`.
6. The outbox row is marked consumed only after successful broadcast; failed rows stay retryable.
DB changes no longer create default `earth_refresh` jobs, so they are not blocked by long same-source collection or clearing jobs. `earth_refresh` remains for manual cache clearing and non-DB refresh hints.
## Data Job Queue
`collection_tasks` is the unified job ledger. Workers claim `queued` jobs with PostgreSQL `FOR UPDATE SKIP LOCKED`; write jobs for the same `source` run serially, while different sources may run in parallel.
| task_type | Purpose |
| --- | --- |
| `collect` | Run a built-in datasource collector |
| `clear_data` | Delete collected rows and declared derived rows for the source |
| `clear_cache` | Delete Earth / dashboard cache for the source |
| `earth_refresh` | Invalidate Earth layer cache and broadcast a refresh hint for non-DB changes |
API handlers only create jobs and return `task_id`. Execution, progress, cancellation, and terminal state are written back by workers and pushed to the frontend through the `datasource_tasks` channel.
Cancellation means “keep committed batches”: clicking stop marks the job as `cancelling` and cancels the in-memory coroutine. Already committed batches remain; unfinished batches follow the collector or cleanup rollback path.
## Earth Sync Event Model
The unified event model is `earth.layer.changed`:
```json
{
"event": "earth.layer.changed",
"action": "database_changed",
"source": "celestrak_tle",
"table": "collected_data",
"operation": "DELETE",
"layers": ["satellites"],
"refresh_strategy": "clear_then_reload",
"records_processed": 11125,
"occurred_at": "2026-05-25T10:20:30Z"
}
```
| strategy | Purpose |
| --- | --- |
| `clear_then_reload` | Clear local frontend layer objects first, then force a refetch. Prefer this for deletes. |
| `reload` | Keep old objects until fresh data returns. Use it for location, metadata, or non-destructive updates. |
| `delta` | Used only for `earth_interactables`; upsert or remove objects by id. |
APIs must return HTTP 200 with an empty collection for real zero-data states; 5xx is reserved for real endpoint failures. After a delete event, if refetch fails, the frontend should keep the cleared state and show a lightweight error instead of restoring stale objects.
## Layer Adapter Contract
`earth_layer_adapters.py` is the single registry for sources, derived tables, Earth layers, cache patterns, and refresh strategy. New layers should be added through an adapter entry, not through one-off button handlers, collector branches, or frontend special cases.
Each adapter must declare:
- Which source or table feeds which Earth layer.
- Which Earth cache key patterns must be invalidated.
- Which owned derived tables must be removed during `clear_data`.
- The default refresh strategy for that layer.
When a source is cleared, the `clear_data` job first deletes `collected_data.source = <source>`, then deletes adapter-owned derived rows. Direct derived-table edits also trigger the outbox, so background jobs, admin APIs, and SQL repair scripts reach Earth as long as they mutate fact or derived tables.
## Operations and Troubleshooting
Check whether outbox rows are piling up:
```sql
SELECT id, table_name, operation, source, occurred_at
FROM earth_data_change_events
WHERE consumed_at IS NULL
ORDER BY id
LIMIT 20;
```
Check that triggers exist:
```sql
SELECT tgname, tgrelid::regclass
FROM pg_trigger
WHERE tgname LIKE 'tr_planet_%_changed_%'
ORDER BY 2, 1;
```
Useful log events:
- `earth.db_changes.connected`: the listener connected to PostgreSQL and started listening.
- `earth.db_changes.outbox_polled`: polling found unconsumed outbox rows.
- `earth.db_changes.broadcasted`: an Earth refresh broadcast was produced.
- `data_job.started` / `data_job.completed`: job execution state.
If Earth does not update, check in order: fact table changed, outbox was consumed, adapter covers the `source/table`, the listener is online, frontend WebSocket is connected, and the visualization API returns HTTP 200 with either an empty collection or fresh data.
## Kafka-ready Boundaries
Business code avoids depending on a concrete queue implementation by preserving these boundaries:
- `JobQueue`: submit, claim, cancel, and complete data jobs.
- `DataChangeBus`: publish database fact changes.
- `EarthLayerAdapterRegistry`: declare source, layer, cache, and derived-data relationships.
Kafka becomes appropriate when:
- Several independent services must consume the same data-change stream.
- AIS, BGP, or sensor streams become sustained high-throughput inputs.
- Consumer groups, replay, and service decoupling are required.
Spark becomes appropriate when:
- Historical data reaches tens or hundreds of millions of rows and PostgreSQL aggregation becomes expensive.
- Cross-source, long-window, spatiotemporal analysis is needed.
- Raw data lands in Parquet / Iceberg / Delta and the system starts producing offline derived data products.
For second-level continuous stream processing, evaluate Flink first. Spark is a better fit for batch or micro-batch analytics.