mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-10-14 16:03:27 +00:00
* feat(datanode): execute nested MergeScan plans on the datanode Give a datanode the ability to decode and execute a plan whose MergeScan carries a nested plan (e.g. an inner MergeScan over a build table), so a distributed sub-plan pushed to a datanode can itself fan out to other datanodes. This is the execution/codec capability only; it is not wired to any user-facing switch and produces no plan by itself. Codec (src/query/src/query_engine/default_serializer.rs): - MergeScanAwareSerializer keeps the request's SessionState and the engine catalog manager, and decodes a MergeScan payload's tables by (catalog, schema, table) name through the engine catalog rather than the region-aware request catalog list (which binds every table to the request's region and breaks on mismatched columns). Datanode (src/datanode): - New DatanodeRegionQueryHandler (region_query.rs) serves region queries for the MergeScan nodes of a received plan, resolving region leaders via PartitionRuleManager and dispatching to peer datanodes. - Wire KvBackendCatalogManager + PartitionRuleManager + NodeClients into the datanode's catalog/cache registry (with_dist_planner stays false: a datanode never rewrites its own plan). Bug fixes uncovered while wiring this up: - cache registry: register the table/table_info/table_name/table_route/ partition_info caches the datanode's catalog manager and partition rule manager require (previously missing -> panic on lookup). - default_serializer: decode_sub_plan bridge no longer drives a future on a current-thread runtime from within another executor (spawn a dedicated thread + runtime when not on a multi-thread runtime). Tested by query_engine::default_serializer unit tests: nested MergeScan round-trip and engine-catalog resolution with mismatched column names. No user-facing behavior change. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * fix(query): keep GreptimeDB function bindings after rebuilding decode state SessionStateBuilder::new_from_existing collects the function registry (HashMap) into a Vec and build() re-registers each entry; register_udf registers aliases before the function's own name and the Vec order is unstable. Rebuilding the session state to install MergeScanAwareSerializer after the GreptimeDB functions were registered could therefore rebind a name/alias (e.g. date_format, an alias of DataFusion's to_char) back to the built-in function. This path runs for every decoded plan, not only nested MergeScan ones. Extract the GreptimeDB function registration into register_greptime_functions and re-apply it after the final build of both the top-level decode state and the MergeScan payload state. Add regression tests asserting date_format stays bound to the GreptimeDB implementation on both paths. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * fix(datanode): address review on nested MergeScan capability Three follow-ups from review of the nested MergeScan execution capability: 1. Layer the datanode caches (P2b). table_cache derives from table_info_cache + table_name_cache, and partition_info_cache derives from table_route_cache, but they were all registered in a single flat CacheRegistry whose invalidate() runs concurrently with no ordering. A derived cache could be invalidated and refilled from a base cache that had not been invalidated yet, leaving stale metadata. Build the base caches and the derived caches as separate layers of a LayeredCacheRegistry (reusing the existing layering mechanism) so base caches finish invalidating before derived ones. Both the production path (cmd) and the test scaffolding now build the same layered registry; the flat build_datanode_cache_registry is removed. 2. Don't let a nested MergeScan's internal region query compete for the concurrency permit (P2a). An outer query holds its permit until its stream is fully consumed, and a nested MergeScan makes the datanode issue region queries to peer datanodes as execution stages of that same query. If such an inner stage also took a permit, the outer query waiting on it would deadlock against its own held permit (or time out when max_concurrent_queries > 0). Mark inner DN->DN region queries as internal stages: QueryRequest gains an flag carried over the wire in the request header's query_context extensions (reserved key, not injectable via hints), and the region server skips acquiring a concurrency permit for internal-stage requests on both acquire sites. Outer queries still acquire normally. 3. Add an end-to-end capability test (tests-integration/.../nested_merge_scan_capability_test.rs): hand-build Join(<local probe region>, MergeScan(build regions on both datanodes)) with mismatched column names and duplicate build keys, ship it to a datanode over a real TCP Flight endpoint, consume the stream fully and compare the row multiset against the frontend's reference join result, assert a real cross-datanode DoGet via an RPC counter, and assert an inner region failure fails the whole query. Also migrate the with_real_datanode_grpc_addr + DatanodeRpcStats test scaffolding. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * fix(query): adapt date_format regression test to the updated DataFusion fork ScalarUDFImpl's trait bounds changed so its impl can no longer be downcast via as_any(); assert the function equals the GreptimeDB implementation instead, which to_char can never satisfy. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * test(datanode): cover the internal stage under the concurrency limit Add an end-to-end regression test for the nested MergeScan capability under max_concurrent_queries = 1: both datanodes' only permit is held by the outer queries' streams while their inner MergeScans fetch the build regions from the same datanodes over real Flight connections. The result consumption is bounded by a timeout, so a broken link anywhere in the chain (marking the inner request internal, carrying the marker in the request header, honoring it at the receiving acquire sites) fails the test instead of deadlocking. Also add a minimal with_datanode_options_override hook to the cluster builder, and fix the test module doc: the outer request is handed to RegionServer::handle_remote_read in process; only the inner region queries go over the wire. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * test(datanode): cover cold route loads under unrelated invalidations The table route cache uses InitStrategy::VersionChecked, whose version counter belongs to the whole CacheContainer rather than to a single table: any TableId invalidation bumps the shared version, so a cold load of one table's route retries when an unrelated table is invalidated concurrently. Document this as a known limitation on the region query handler, and pin the behavior with an integration test: run the nested plans while a background task invalidates an unrelated table id on every datanode, assert the results still match the expected multiset (retries only add work, they never return a stale route), and report the observed load amplification via the cache miss counter (2 loads without invalidations, 8 over three runs with 24 unrelated invalidations in this run). Also expose the datanode cache registries on the test cluster so a test can drive the local invalidation path that the heartbeat handler drives after a metasrv broadcast. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * docs(query): trim nested plan decoder commentary Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * refactor: remove redundant nested scan support plumbing Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * fix(query): decode nested MergeScan payloads asynchronously Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * test(query): avoid unnecessary extension name allocation Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * fix(query): retain extension decoding for manual session states Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * refactor(datanode): keep nested scan query admission unchanged Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * fix(datanode): honor query handler factories and trim nested scan fixtures Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * fix(datanode): retain required cache error import Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * refactor(query): reuse registered state for nested scan decoding Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * test(query): verify payload information schema table functions Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * test(query): retain nested scan runtime and cold-route guards Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * refactor(query): address nested scan review feedback Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * fix(session): preserve read preference across region query hops Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * test(session): verify strict context rejection at Flow ingress Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * test(query): remove redundant serializer error case Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> --------- Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
229 lines
10 KiB
Markdown
229 lines
10 KiB
Markdown
# GreptimeDB Compatibility Test Framework
|
|
|
|
Compatibility tests verify that one GreptimeDB version can restart on state written by another version.
|
|
|
|
SQL restart tests are run via `cargo sqlness compat` and reuse the sqlness-runner infrastructure.
|
|
|
|
## Wire fixtures
|
|
|
|
Frozen protobuf fixtures are exercised by the Rust tests that own their context
|
|
conversion, not by SQLness case discovery. The
|
|
[legacy query-context fixture](wire/read_preference/README.md) checks omitted
|
|
read-preference defaulting and unrelated metadata preservation:
|
|
|
|
```shell
|
|
cargo nextest run -p session -E 'test(test_legacy_query_context_wire_defaults_to_leader)'
|
|
```
|
|
|
|
## Quick Start
|
|
|
|
```shell
|
|
# Self-compat smoke test (current binary only):
|
|
cargo run -p sqlness-runner -- compat
|
|
|
|
# Test from a specific released version to current:
|
|
cargo run -p sqlness-runner -- compat --from-version v0.9.5
|
|
|
|
# Test between two local binary directories:
|
|
cargo run -p sqlness-runner -- compat --from-bins-dir ./bins/old --to-bins-dir ./bins/new
|
|
|
|
# Test a downgrade from the current build to a released binary:
|
|
cargo run -p sqlness-runner -- compat --from-bins-dir ./bins/current --to-version v1.1.4
|
|
|
|
# Run a compatibility case in standalone mode:
|
|
cargo run -p sqlness-runner -- compat --topology standalone --test-filter "downgrade_compatibility"
|
|
|
|
# Run a specific case:
|
|
cargo run -p sqlness-runner -- compat --test-filter "basic_table"
|
|
|
|
# Preview which cases would run (no services started):
|
|
cargo run -p sqlness-runner -- compat --dry-run --from-version v0.9.5
|
|
|
|
# See all options:
|
|
cargo run -p sqlness-runner -- compat --help
|
|
```
|
|
|
|
## Prerequisites
|
|
|
|
- **Docker** (for etcd): PR1 always uses Docker etcd for distributed metadata. External metadata stores are future work.
|
|
- **From binary**: Either `--from-version <version>` to auto-pull a release, or `--from-bins-dir <path>` to use a local build. The binary `greptime` must exist directly inside the given directory.
|
|
- **To binary**: Defaults to the current debug build (`target/debug/greptime`). Override with `--to-bins-dir <path>` or fetch a release with `--to-version <version>`.
|
|
- **Custom target-dir**: If you use a non-default `CARGO_TARGET_DIR`, the debug binary won't be at `target/debug/greptime`. Pass `--from-bins-dir` / `--to-bins-dir` explicitly pointing to your custom target directory. Alternatively, run `cargo build -p greptime` without a custom target-dir.
|
|
|
|
## Case Format
|
|
|
|
Each compat case is a directory under `tests/compatibility/cases/` containing three required files plus an expected output file:
|
|
|
|
```
|
|
my_case/
|
|
case.toml # Metadata (required)
|
|
setup.sql # SQL to run on the from version (required)
|
|
verify.sql # SQL to run on the to version (required)
|
|
verify.result # Expected output from verify.sql
|
|
```
|
|
|
|
### `case.toml` — Required Metadata
|
|
|
|
```toml
|
|
name = "my_case"
|
|
reason = "Why this compatibility case exists"
|
|
introduced_by = "PR #1234 or feature name"
|
|
topologies = ["distributed", "standalone"]
|
|
from_range = ["*"]
|
|
to_range = ["*"]
|
|
features = ["table"]
|
|
owner = "team-name"
|
|
# optional:
|
|
namespace = "my_explicit_namespace" # defaults to sanitized directory name
|
|
```
|
|
|
|
**Required fields**: `name`, `reason`, `introduced_by`, `topologies`, `from_range`, `to_range`, `features`, `owner`.
|
|
|
|
### Old-Stage Datanode Configuration Overlay
|
|
|
|
To apply a datanode configuration overlay while running the old stage, add this
|
|
strict optional table to `case.toml`:
|
|
|
|
```toml
|
|
[old_config]
|
|
datanode = "old-datanode.overlay.toml"
|
|
```
|
|
|
|
`datanode` is required whenever `[old_config]` is present; empty tables and
|
|
unknown keys are rejected. The reference is relative to the case directory and
|
|
must remain confined to that directory. The sidecar is native datanode TOML,
|
|
which the runner loads and preflights before starting services or creating
|
|
state.
|
|
|
|
The runner first applies the datanode baseline, then merges the sidecar. Tables
|
|
merge recursively only when both values are tables. Scalars, type mismatches,
|
|
arrays, and arrays of tables replace the baseline value atomically. In
|
|
particular, `region_engine` has no special merge behavior.
|
|
|
|
Runner-owned fields cannot be changed by an overlay: `mode`, `node_id`,
|
|
`storage.data_home`, `meta_client_options.metasrv_addrs`, and `wal.provider`,
|
|
plus `wal.dir` for Raft WAL or `wal.broker_endpoints` for Kafka WAL. The runner
|
|
restores these fields to its baseline values, or deletes them when the baseline
|
|
has no value. It warns about protected-field overrides without printing their
|
|
values.
|
|
|
|
### Version-Range Filtering
|
|
|
|
`from_range` and `to_range` control which binary versions a case applies to:
|
|
|
|
| Entry | Meaning |
|
|
|-------|---------|
|
|
| `"*"` | Matches any version (including unknown). |
|
|
| `"vX.Y.Z"` or `"=vX.Y.Z"` | Matches exactly version X.Y.Z. |
|
|
| `">=vX.Y.Z"` | Matches X.Y.Z or later. |
|
|
| `">vX.Y.Z"` | Matches versions strictly later than X.Y.Z. |
|
|
| `"<=vX.Y.Z"` | Matches X.Y.Z or earlier. |
|
|
| `"<vX.Y.Z"` | Matches versions strictly earlier than X.Y.Z. |
|
|
|
|
The range list is **OR**: a case matches if **any** entry matches.
|
|
|
|
**Best-effort enforcement**: The runner tries to determine the effective version:
|
|
- `--from-version` is used directly.
|
|
- `--from-bins-dir` / `--to-bins-dir` (or the default debug build) runs `<binary> --version` to infer the version.
|
|
- When the version **cannot** be determined (e.g. binary missing or `--version` fails), non-wildcard ranges are **skipped** with a message; wildcard (`*`) ranges still match.
|
|
|
|
**Example** (`legacy_jsonb`):
|
|
```toml
|
|
from_range = ["<=v1.1.0"]
|
|
to_range = [">=v1.1.1"]
|
|
```
|
|
This case only runs when the old binary is <= v1.1.0 and the new binary is >= v1.1.1.
|
|
|
|
### CI Version Window
|
|
|
|
The CI job uses `tests/compatibility/ci.toml` to choose the small sliding
|
|
window of recent released `from` versions to test against the PR-built `to`
|
|
binary:
|
|
|
|
```toml
|
|
from_versions = ["v1.0.0", "v1.1.0"]
|
|
```
|
|
|
|
Keep this window small for PR and merge-queue latency: the goal is to catch
|
|
upgrade compatibility issues from recent releases to the latest build, not to
|
|
retest every historical version on every PR. Case-level `from_range`/`to_range`
|
|
still decides which cases run for each version pair; the CI window only decides
|
|
which old binaries are sampled. Broader historical windows belong in nightly or
|
|
release-validation workflows.
|
|
|
|
The GitHub Actions workflow delegates the window loading and compat invocation
|
|
to `.github/scripts/run-compat.py`; the workflow YAML should stay as a thin
|
|
wrapper around artifact download/extraction and this script.
|
|
|
|
`downgrade_to_versions` optionally lists releases that CI restarts after the
|
|
PR-built cluster. Those runs select only the `downgrade_compatibility` case in
|
|
both distributed and standalone topologies.
|
|
|
|
### `setup.sql` — Setup Phase (From Version)
|
|
|
|
SQL statements executed on the **from** version cluster. These must succeed (any error fails the case). Setup output is NOT compared against any result file.
|
|
|
|
Rules:
|
|
- Statements are semicolon-terminated
|
|
- `--` prefix for ordinary comments
|
|
- `-- SQLNESS ...` interceptor comments follow ordinary sqlness semantics
|
|
### `verify.sql` — Verify Phase (To Version)
|
|
|
|
SQL statements executed on the **to** version cluster. Output is compared against `verify.result` in sqlness snapshot style.
|
|
|
|
### `verify.result` — Expected Output
|
|
|
|
Expected output in sqlness format. If this file is missing, the runner generates it from actual output and **fails** — the author must review, commit the generated file, and rerun.
|
|
|
|
```
|
|
<statement>;
|
|
|
|
<output>
|
|
|
|
<next statement>;
|
|
|
|
<output>
|
|
|
|
```
|
|
|
|
If output differs from expected, the run fails and `verify.result` is updated with actual output.
|
|
|
|
## PR1 Limitations
|
|
|
|
- **Sqlness interceptors**: `-- SQLNESS ...` comments are applied per statement using the same interceptor registry as the ordinary sqlness runner, including the GreptimeDB `PROTOCOL` interceptor. For `PROTOCOL POSTGRES`, the namespace prelude uses `SET search_path` instead of `USE`. Avoid unqualified PostgreSQL-protocol table names starting with `pg_`: GreptimeDB's current PostgreSQL compatibility parser rewrites them to `pg_catalog.<table>`.
|
|
- **Distributed topology**: The compat runner starts 1 metasrv + 3 datanodes + 1 frontend + 1 flownode. Standalone compatibility runs need no external metadata store.
|
|
- **No comment-based compat config**: The compat runner does not define extra compatibility configuration in SQL comments; sqlness comments keep their normal sqlness meaning.
|
|
|
|
## Namespace Isolation
|
|
|
|
Each case runs in its own database namespace to prevent cross-case interference:
|
|
|
|
- Default namespace is derived from the case directory name (sanitized to `[a-z][a-z0-9_]*`)
|
|
- Override with `namespace` in `case.toml`
|
|
- Duplicate namespaces are **rejected** at discovery time (before version filtering)
|
|
- Before each statement, the runner executes a namespace prelude (not written to verify.result): `CREATE DATABASE IF NOT EXISTS <ns>` via gRPC; then `USE <ns>` for gRPC/MySQL statements or `SET search_path TO '<ns>'` for PostgreSQL statements.
|
|
|
|
## Batch Behavior
|
|
|
|
- The baseline (no-overlay) profile runs first. Cases whose old datanode TOML
|
|
is semantically equivalent share one profile; profiles run serially and in
|
|
isolation.
|
|
- Each profile has its own state and etcd lifecycle. Its overlay is applied
|
|
only to old-stage datanodes and remains in effect through old-stage setup
|
|
restarts. The current stage always uses a clean configuration.
|
|
- Cases run **serially** (no parallelism in PR1). Namespace state is
|
|
session/protocol state and cannot be shared concurrently.
|
|
- Same namespace across cases is rejected.
|
|
- Without fail-fast, the runner verifies only cases whose setup succeeded.
|
|
With fail-fast, it cleans up the active profile before stopping.
|
|
- `--dry-run` displays the selected profiles, cases, and sidecar paths without
|
|
printing configuration values; it starts no services.
|
|
|
|
## xfail Policy (Future)
|
|
|
|
For PR1, all cases are expected to pass. Future PRs will add `xfail` support with required `issue` and `expiry` fields.
|
|
|
|
## Cross-Job Distributed State
|
|
|
|
PR1 runs setup and verify in the **same job** (same process). Cross-job artifact restore for distributed state is not supported in PR1 due to port randomization and etcd lease expiration.
|