Merge origin/main into codex/gemini-embedding-batch

# Conflicts:
#	apps/aether-gateway/src/ai_serving/api.rs
#	apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/request.rs
#	apps/aether-gateway/src/ai_serving/planner/standard/family/request.rs
#	apps/aether-gateway/src/ai_serving/transport.rs
#	apps/aether-gateway/src/handlers/admin/provider/query/models/model_test/summary.rs
#	apps/aether-gateway/src/handlers/admin/provider/query/models/model_test/tests.rs
#	crates/aether-data/src/repository/candidate_selection/postgres.rs
#	crates/aether-model-fetch/src/strategy.rs
This commit is contained in:
MMEXA
2026-05-18 19:02:19 +00:00
446 changed files with 53043 additions and 4780 deletions

View File

@@ -80,7 +80,7 @@ headers; changes there should come only from `compose_schema.sh generate`.
| Auth config | `auth_modules`, `oauth_providers`, `ldap_configs` | `repository/auth_modules`, `repository/oauth_providers`, `repository/users` | Good candidate for generated CRUD. |
| Proxy nodes | `proxy_nodes`, `proxy_node_events` | `repository/proxy_nodes` | Good candidate for generated CRUD plus handwritten heartbeat update. |
| Wallet/billing | `wallets`, `wallet_transactions`, `wallet_daily_usage_ledgers`, `payment_orders`, `payment_callbacks`, `refund_requests`, `redeem_code_batches`, `redeem_codes`, `billing_rules`, `dimension_collectors` | `repository/wallet`, `repository/billing`, `repository/settlement` | Keep settlement/ledger math explicit; generate table definitions and simple reads. |
| Usage/audit | `usage`, `usage_body_blobs`, `usage_http_audits`, `usage_routing_snapshots`, `usage_settlement_snapshots`, `request_candidates`, `audit_logs` | `repository/usage`, `repository/candidates`, `repository/audit` | Keep core write/audit queries handwritten. |
| Usage/audit | `usage`, `usage_counter_deltas`, `usage_body_blobs`, `usage_http_audits`, `usage_routing_snapshots`, `usage_settlement_snapshots`, `request_candidates`, `audit_logs` | `repository/usage`, `repository/candidates`, `repository/audit` | Keep core write/audit queries handwritten. |
| Runtime tasks | `video_tasks`, `gemini_file_mappings`, `announcements`, `announcement_reads` | `repository/video_tasks`, `repository/gemini_file_mappings`, `repository/announcements` | Good candidate for generated CRUD except polling claim logic. |
| Stats | `stats_*`, `schema_backfills` | backend aggregation modules | Keep aggregation SQL per-driver; generate table/index definitions only. |
| System | `system_configs` | `repository/system` through backend dispatch | Good candidate for generated CRUD. |
@@ -105,3 +105,12 @@ as explicit generated/override fragments.
plan if it must move across databases.
6. If a baseline fragment changes, run `compose_schema.sh compose` before tests
so the executable SQL artifact is regenerated from the source manifest.
## Performance Notes
- Shared hot counters must use `bigint` and be updated by durable outbox flush
workers, not request transactions.
- `usage_counter_deltas` is append-only until processed; processed rows are
retained briefly for audit/replay and then batch-deleted by maintenance.
- Candidate selection joins stay handwritten but are protected in the gateway by
a short TTL, single-flight cache invalidated by provider/routing writes.

View File

@@ -0,0 +1,279 @@
# Usage Counter Root Fix
This worktree implements the first production cut of the root fix for
usage-related lock contention and hot-row pressure.
## Problem
The current Postgres write path mixes:
- request facts (`usage`, audit snapshots, settlement state)
- shared counters (`api_keys`, `provider_api_keys`, `global_models`)
- provider quota window JSON updates
- wallet settlement writes
That means a single request can hold a transaction while touching multiple shared rows, and high-frequency traffic can serialize on the same `api_key_id` / `provider_api_key_id` / wallet rows.
Relevant code paths:
- `crates/aether-data/src/repository/usage/postgres/mod.rs`
- `crates/aether-data/src/repository/settlement/postgres.rs`
- `crates/aether-usage-runtime/src/runtime.rs`
## Goal
Remove shared-hot-row updates from the request path without losing correctness.
The request path must become:
1. write immutable request facts
2. write durable delta records
3. commit
All shared counters must be derived later by a worker.
## Target Architecture
### 1. Facts first
Keep `usage` as the source of truth for per-request facts:
- request identity
- status transitions
- billing state
- token / cost / latency payloads
- audit snapshots
### 2. Durable counter outbox
Add a new append-only delta table, for example:
`usage_counter_deltas`
Each row should represent one logical contribution:
- `kind` (`api_key`, `provider_api_key`, `model`, `provider_monthly`,
`proxy_node`, `management_token`, `api_key_last_used`, `window`)
- `target_id`
- delta fields
- request id / revision
- `processed_at`
This table is the bridge between request facts and derived counters.
### 3. Flush worker
Add a background worker that:
1. reads unprocessed deltas with `FOR UPDATE SKIP LOCKED`
2. aggregates them in memory by `(kind, target_id)`
3. applies one UPDATE per target
4. marks the source delta rows processed in the same transaction
This keeps memory useful as a buffer, but never as the only source of truth.
### 4. Read models
Move high-read counters to separate tables or compact materialized read models:
- `api_key_usage_counters`
- `provider_api_key_usage_counters`
- `model_usage_counters`
- `provider_monthly_usage_counters`
- `provider_api_key_window_usage_counters`
Keep the original business tables (`api_keys`, `provider_api_keys`, `global_models`) for configuration and compatibility only.
## Implementation Boundary
This branch should land the fix in phases, but the direction must not change:
1. Postgres gets the durable outbox and counter flush path first, because the
reported production lock wait is on Postgres row locks.
2. MySQL and SQLite keep their current usage write behavior until their smaller
deployment paths are moved to the same contract.
3. Request-path Postgres writes may still write `usage`, audit blobs, routing
snapshots, and settlement pricing snapshots. They must not directly update
shared aggregate rows.
4. Compatibility mirror columns may be updated by the flush worker only, never
by the request transaction.
5. Dashboard/statistics read paths should be migrated after the write pressure
is removed, otherwise we risk mixing a large read refactor with the lock fix.
The first executable migration creates the outbox. The first code patch makes
`upsert_usage_record` enqueue deltas in the same transaction as the usage fact
write, then a background worker batches those deltas with
`FOR UPDATE SKIP LOCKED`. Dedicated counter read-model tables remain a follow-up
after the request-path lock pressure is removed.
## Locking Model
### Keep
- advisory lock per `request_id` for idempotent request transitions
- wallet row lock only inside settlement, where correctness depends on it
### Remove from request path
- direct `UPDATE api_keys`
- direct `UPDATE provider_api_keys`
- direct `UPDATE global_models`
- direct `UPDATE providers.monthly_used_usd`
- direct `FOR UPDATE` on provider quota JSON for request-level usage windows
## Memory Cache Rules
Allowed:
- short TTL snapshot cache for read-only admin UI data
- short TTL + single-flight cache for provider/model candidate selection rows
- worker-side delta aggregation buffer
- per-key last-used-at max tracking before flush
Not allowed:
- using memory as the only accounting source
- using memory as the only settlement source
- depending on a clipped Redis stream as the only record of usage
## Rollout Plan
1. Stop counting `pending` / `streaming` as shared counter contributions.
2. Introduce the delta outbox and worker.
3. Redirect request path to facts + outbox only.
4. Migrate reads to the new counter tables.
5. Decommission synchronous hot-row updates.
6. Move provider quota windows out of request transactions.
## Implemented In This Branch
- `usage_counter_deltas` durable outbox for api key, provider api key, model,
provider monthly, proxy node, management token, and api key last-used counters.
- Postgres usage upsert writes request facts plus outbox rows, not shared counter
rows.
- Postgres settlement enqueues provider monthly usage deltas instead of updating
`providers.monthly_used_usd` in the request transaction.
- Gateway request-adjacent proxy node, management token, and api key last-used
writes enqueue durable deltas and fall back to direct writes only when the
usage writer is unavailable.
- Gateway provider/model candidate selection reads use a 5 second in-memory TTL
cache with per-key single-flight. Provider/routing catalog writes invalidate
this cache alongside provider transport and scheduler affinity caches.
- Gateway maintenance worker flushes deltas in batches and aggregates in memory
inside the worker before applying compatibility counter updates.
- Daily quota lookup index on `(user_entitlement_id, usage_date)` removes the
avoidable aggregate scan in quota settlement checks.
- Hot counter columns are widened to `bigint` where old bootstrap schemas still
used `integer`, preventing long-running counter overflow.
## Integration Pressure Tests
The hotspot benchmarks start a managed local Postgres instance, run the
migrations, seed a single hot target, then monitor `pg_stat_activity` while the
load is running. Use a separate target directory when the main worktree target
lock is not writable.
Usage write path, one hot `api_key` / `provider_api_key` / `global_model`:
```sh
CARGO_TARGET_DIR=/tmp/aether-rootfix-target \
cargo run -p aether-testkit --bin usage_counter_hotspot_baseline -- \
--requests 5000 \
--concurrency 200 \
--flush-interval-ms 50 \
--monitor-interval-ms 20 \
--output /tmp/usage_counter_hotspot_after_5000.json
```
Settlement path, one hot provider monthly counter:
```sh
CARGO_TARGET_DIR=/tmp/aether-rootfix-target \
cargo run -p aether-testkit --bin usage_settlement_hotspot_baseline -- \
--requests 5000 \
--concurrency 200 \
--flush-interval-ms 50 \
--monitor-interval-ms 20 \
--output /tmp/usage_settlement_hotspot_after_5000.json
```
Auxiliary hot counters, one hot proxy node / management token / api key
last-used target:
```sh
CARGO_TARGET_DIR=/tmp/aether-rootfix-target \
cargo run -p aether-testkit --bin usage_aux_counter_hotspot_baseline -- \
--requests 5000 \
--concurrency 200 \
--flush-interval-ms 50 \
--monitor-interval-ms 20 \
--output /tmp/usage_aux_counter_hotspot_after_5000.json
```
Latest local run on this worktree:
- usage hotspot: 5000 requests, 200 concurrency, p95 173 ms, 0 failures,
15000 outbox rows processed, 0 pending rows, 0 `api_keys` /
`provider_api_keys` / `global_models` update waiters.
- settlement hotspot: 5000 requests, 200 concurrency, p95 39 ms, 0 failures,
5000 provider monthly deltas processed, `providers.monthly_used_usd = 5.0`,
0 provider update waiters.
Run the auxiliary counter hotspot after changing outbox schemas or gateway
fallback routing; it should drain all pending outbox rows and report zero
request-path waiters for `proxy_nodes`, `management_tokens`, and `api_keys`.
## Runtime Observability
The same outbox health signals used by the pressure tools are exposed through
admin runtime endpoints:
- `GET /api/admin/system/stats`
- `GET /api/admin/monitoring/system-status`
- `GET /api/admin/stats/performance/providers`
These responses include `usage_counter`:
- `status`: `idle`, `catching_up`, or `backlogged`
- `outbox_pending_rows`
- `outbox_processed_rows`
- `oldest_pending_created_at_unix_secs`
- `oldest_pending_age_secs`
- `latest_processed_at_unix_secs`
- `pending_by_kind`
Operational alerting should page when `status = backlogged`, when pending rows
continue growing across several flush intervals, or when the oldest pending age
stays above one minute. A transient non-zero backlog is acceptable during catch
up bursts.
## Remaining Correctness Locks
Wallet debit settlement and daily quota consumption still use database locks
because they protect money/quota correctness, not derived counters. Removing
those locks safely requires a separate wallet debit ledger/reservation worker:
1. request settlement writes an immutable debit intent keyed by `request_id`
2. a per-wallet worker claims intents with `FOR UPDATE SKIP LOCKED`
3. the worker applies balance changes and writes final settlement snapshots
4. request-facing APIs read `pending/settled/insufficient_quota` from the
settlement snapshot
Do not replace this with memory-only balance caches. A cache may accelerate
read-side availability estimates, but the durable ledger must remain the source
of truth.
## Acceptance Criteria
- request transactions no longer update shared counter rows
- counter updates become batchable and replayable
- lock wait time on `api_keys` / `provider_api_keys` drops sharply under concurrency
- wallet settlement remains correct and isolated
- dashboard/statistics reads stay fast from dedicated read models
## Open Decisions
- table names for counter read models
- whether provider window counters live in Postgres only or are dual-written into Redis for display latency
- flush cadence and batch size defaults
- whether to keep compatibility snapshot columns as low-frequency mirrors