AGENTS.md@src/frontend · git:20261104.fd74b92 · 2026-11-04 · sha256 8a3f16a3aa18e639

AGENTS.md@src/frontend git:20261104.fd74b92A

Immutable. This exact content never changes and is served at /api/v1/blob/8a3f16a3aa18e639.

# frontend — Agent & Contributor Guide

Navigation aid for `src/frontend`. Keep it short and point to code. Paths are
relative to the repo root.

Repo-wide rules that apply here: [`.agents/architecture-invariants.md`](../../.agents/architecture-invariants.md).

## What this crate does

Frontend is the request entry point and orchestration layer. It accepts
multi-protocol requests (gRPC, HTTP, MySQL, PostgreSQL, InfluxDB, OTLP, Jaeger,
Prometheus, OpenTSDB), checks permissions, parses/plans SQL, and dispatches:
reads go to the query engine (`query` crate), writes go to the inserter/deleter
(`operator` crate).

Boundary with `servers`: the `servers` crate implements the wire protocols and
network I/O; `frontend` provides the business logic by implementing handler
traits (`SqlQueryHandler`, `GrpcQueryHandler`, `InfluxdbLineProtocolHandler`,
...). In standalone mode the `standalone` crate bridges the frontend to an
embedded datanode `RegionServer`; in distributed mode the frontend talks to
remote datanodes via `operator`/`client`.

## Module map

| Module | Path | Purpose |
| --- | --- | --- |
| `instance` | `src/frontend/src/instance.rs` | `Instance`: the core handler; implements `SqlQueryHandler`, `PrometheusHandler`, etc. |
| `instance/builder` | `src/frontend/src/instance/builder.rs` | `FrontendBuilder` assembles `Instance` from its dependencies |
| `instance/grpc` | `src/frontend/src/instance/grpc.rs` | `GrpcQueryHandler`: insert/delete/query/promql over gRPC |
| `instance/region_query` | `src/frontend/src/instance/region_query.rs` | Routes distributed region reads to datanodes |
| `instance/*` | `src/frontend/src/instance/` | Per-protocol handlers (`influxdb.rs`, `promql.rs`, `otlp/`, `jaeger.rs`, `logs.rs`, `prom_store.rs`, ...) |
| `frontend` | `src/frontend/src/frontend.rs` | `Frontend` lifecycle wrapper (`FrontendOptions`, start/shutdown) |
| `server` | `src/frontend/src/server.rs` | `Services`: builds and wires the protocol servers |
| `heartbeat` | `src/frontend/src/heartbeat.rs` | Heartbeat to metasrv; handles suspend / cache invalidation |
| `service_config` | `src/frontend/src/service_config/` | Per-protocol option structs |

Shared frontend cache counters live in `src/common/frontend/src/metrics.rs`,
including caches owned by pipeline and protocol handlers.

## Request lifecycles

- **SQL query** (`instance.rs`): `do_query_inner` handles parsing, interceptors,
  permission checks, timeout/cancellation, and delegates planning/execution to
  `StatementExecutor`. Distributed scans enter through `region_query.rs`.
- **Logical-table batch DDL** (`instance.rs`): dedicated SQL handler → bounded
  parse/interception → all-member permissions → operator CREATE preparation →
  one logical-table batch procedure. Keep pre-submission validation side-effect free.
- **Insert** (`instance/grpc.rs`): `handle_inserts` / `handle_row_inserts` →
  `check_permission` → `operator`'s `Inserter` (schema validation, optional
  auto-create, partition routing, meter admission) → local `RegionServer`
  (standalone) or RPC to datanodes (distributed). `FrontendBuilder` configures
  the operator write dispatcher with frontend retry options for distributed
  inserts, deletes, and bulk writes. Arrow bulk inserts pass the
  request channel to `Inserter` and check meter admission for each nonempty batch.
- Finite ingestion requests split internally admit their total rows per database
  before dispatch (`operator::insert::admit_write` / `admit_row_insert_batches`).
  The returned context covers chunks and derived writes while preserving WCU
  accounting and the original protocol channel.
- Internal gRPC listeners mark requests with `Channel::Internal` in middleware
  (`server.rs`), including requests handled by Enterprise Flight wrappers.

- **Flight bulk insert** (`instance/grpc.rs`): initializes on the first batch after
  the lazy schema handshake; checks permissions and reconciles missing columns
  through `Inserter` once per stream, then reuses the refreshed table.

- **OTLP trace lookup tables** (`instance/otlp/trace_ingest.rs`): `Instance` owns
  a shared cache of confirmed service/operation writes, which bypass batching.
  `otlp.trace_aux_cache_size` sets its estimated byte budget at startup; zero disables it.
  Async main writes retain completion results; a frontend task writes auxiliary
  rows only for confirmed chunks before populating the cache.
  Cold async trace requests reserve a shared auxiliary slot before main writes,
  bounded separately by the table batcher's `max_inflight_requests`; confirmed
  cache hits bypass this admission. The slot lasts through auxiliary completion.
  These tables are ingestion-managed; manual mutation requires restarting
  serving frontends.

- **OTLP metrics** (`instance/otlp.rs`): authorize source tables before schema
  preflight can reject points or expose their schemas. Skip and count exemplars
  when metric-engine ingestion is disabled or the source is an ordinary table,
  retaining metric rows. Authorize retained exemplar writes before accessing
  the exemplar destination or reserving expansion memory.
  Reserve exemplar expansion with the transport's memory limiter before building
  rows; retain the permit through `WriteProgress` until writes complete.
  Expansion must not wait while the request holds transport memory permits.

- **Logical-table batching** (`instance/logical_batcher.rs`): `Services` initializes
  one shared batcher for opted-in HTTP Prom and OTLP metric-engine
  writes. Downstream routers can initialize it when enabling replacement endpoints.
  `Instance::handle_otlp_metric_row_inserts` shares OTLP eligibility and dispatch
  between converters and falls back for incompatible tables.
  The schema adapter holds a weak instance reference to avoid an ownership cycle.

- **Table batching** (`instance/builder.rs`): protocol entry points opt in through
  `QueryContext`. The primary inserter prepares eligible ordinary-table writes
  for `servers::batcher::table::TablePendingRowsBatcher`. A separate execution-only
  inserter, with no batcher attached, sends the prepared bulk writes to datanodes,
  avoiding recursive batching. The batcher handles successful-write Flow notifications.

## Public surface

- `Instance` (`instance.rs`) — the business-logic container.
- `Frontend` (`frontend.rs`) — lifecycle wrapper around `Instance` + servers + heartbeat.
- Created from `cmd`: `src/cmd/src/frontend.rs` (distributed) and
  `src/cmd/src/standalone.rs` (standalone, with embedded datanode).

## When you change X, also touch Y

- **`servers` handler traits**: a new/changed protocol handler requires the
  matching `impl` here.
- **`operator` Inserter/Deleter or `query` QueryEngine API**: update the call
  sites in `instance.rs` / `instance/grpc.rs`.
- **`session::QueryContext`**: new context fields thread through most handlers.
- **`sql` statements**: new statement kinds need handling in `query_statement`.

## Testing

```bash
cargo nextest run -p frontend
```

## Gotchas

- Keep the frontend/servers split straight: wire format and network live in
  `servers`; permissions, planning, and routing live here.
- Standalone vs distributed diverge in datanode access (the `standalone` crate's
  local `RegionServer` adapter vs `NodeClients` RPC), MetaClient usage, and
  whether heartbeat matters. The builder defaults local invalidation to its
  layered cache registry; callers may supply a custom invalidator.

## Maintenance contract

Update this file when you add a protocol handler, change the query/insert
lifecycle, or change how `Instance` is constructed or wired to `servers`.