|
| 1 | +// SPDX-License-Identifier: BUSL-1.1 |
| 2 | + |
| 3 | +//! Route a single-vShard-homed plan to its ONE owning Data-Plane core. |
| 4 | +//! |
| 5 | +//! `gather_single_owning_core` is the single-node sibling of |
| 6 | +//! [`super::gather::gather_all_cores`] for plans whose whole collection lives on |
| 7 | +//! exactly one vShard (document / kv / columnar / timeseries / spatial / vector |
| 8 | +//! / text). Instead of broadcasting the plan to every core — which seeds an |
| 9 | +//! identity scalar-aggregate row on each empty non-owning core and returns one |
| 10 | +//! row per core for a no-`GROUP BY` aggregate — it resolves the collection's |
| 11 | +//! owning vShard and dispatches the bare plan to it alone, exactly mirroring |
| 12 | +//! what the cluster branch of [`super::gather::gather_all_vshards`] does via the |
| 13 | +//! gateway. The owning core already holds every row of a single-vShard-homed |
| 14 | +//! collection, so this returns the identical row set a broadcast would have, |
| 15 | +//! minus the empty cores' spurious contributions. |
| 16 | +
|
| 17 | +use crate::bridge::envelope::{ErrorCode, PhysicalPlan, Status}; |
| 18 | +use crate::control::server::dispatch_utils::dispatch_to_data_plane_with_txn; |
| 19 | +use crate::control::server::payload_merge::{encode_msgpack_array, extract_msgpack_elements}; |
| 20 | +use crate::control::state::SharedState; |
| 21 | +use crate::types::{DatabaseId, TenantId, TraceId, TxnId, VShardId}; |
| 22 | + |
| 23 | +use super::gather::{GatherOutcome, gather_all_cores}; |
| 24 | + |
| 25 | +/// Single-node routing decision for a resolved `Exchange{Gather}` child plan. |
| 26 | +/// |
| 27 | +/// Mirrors the cluster branch of [`super::gather::gather_all_vshards`]: |
| 28 | +/// |
| 29 | +/// - A cluster-partitioned leaf (graph traversal / array) spreads its rows |
| 30 | +/// across cores by node-id / tile-id, so it fans to every local core via |
| 31 | +/// [`gather_all_cores`]. |
| 32 | +/// - A single-vShard-homed collection (document / kv / columnar / timeseries / |
| 33 | +/// spatial / vector / text) lives wholly on ONE core; the bare plan routes to |
| 34 | +/// that owning core via [`gather_single_owning_core`]. Broadcasting would seed |
| 35 | +/// a scalar-aggregate identity row on every empty non-owning core — so a |
| 36 | +/// no-`GROUP BY` aggregate returns one row PER core instead of one merged row |
| 37 | +/// — and would duplicate a plain scan's rows across cores. |
| 38 | +/// - A plan with no resolvable collection (e.g. `ProviderScan` carrying embedded |
| 39 | +/// rows) keeps the broadcast fallback unchanged. |
| 40 | +pub async fn gather_single_node( |
| 41 | + state: &SharedState, |
| 42 | + tenant_id: TenantId, |
| 43 | + database_id: DatabaseId, |
| 44 | + plan: PhysicalPlan, |
| 45 | + trace_id: TraceId, |
| 46 | + txn_id: Option<TxnId>, |
| 47 | +) -> crate::Result<GatherOutcome> { |
| 48 | + if nodedb_physical::physical_plan::plan_contains_cluster_partitioned_leaf(&plan) { |
| 49 | + return gather_all_cores(state, tenant_id, database_id, plan, trace_id, txn_id).await; |
| 50 | + } |
| 51 | + if let Some(collection) = plan.collection() { |
| 52 | + let vshard_id = VShardId::from_collection_in_database(database_id, collection); |
| 53 | + return gather_single_owning_core( |
| 54 | + state, |
| 55 | + tenant_id, |
| 56 | + database_id, |
| 57 | + plan, |
| 58 | + vshard_id, |
| 59 | + trace_id, |
| 60 | + txn_id, |
| 61 | + ) |
| 62 | + .await; |
| 63 | + } |
| 64 | + gather_all_cores(state, tenant_id, database_id, plan, trace_id, txn_id).await |
| 65 | +} |
| 66 | + |
| 67 | +/// Dispatch `plan` to the single Data-Plane core that owns `vshard_id` and |
| 68 | +/// gather the one bounded response into a [`GatherOutcome`]. |
| 69 | +/// |
| 70 | +/// `vshard_id` is the collection's owning vShard |
| 71 | +/// (`VShardId::from_collection_in_database(database_id, collection)`); the |
| 72 | +/// dispatcher's `VShardRouter` resolves it to the one core holding the |
| 73 | +/// collection's rows. |
| 74 | +/// |
| 75 | +/// The returned outcome carries that core's own `watermark_lsn` / |
| 76 | +/// `read_version_lsn` and exactly one `shard_watermarks` entry keyed to the |
| 77 | +/// collection's vShard — matching the cluster `dispatch_local` path so an |
| 78 | +/// in-transaction read records the same OCC read-set entry the write-set uses |
| 79 | +/// (writes home to the same `from_collection_in_database` vShard). Aggregate |
| 80 | +/// finalization (`finalize_aggregate`) is a passthrough over the merged array, |
| 81 | +/// so one complete aggregate row in yields one row out. |
| 82 | +pub async fn gather_single_owning_core( |
| 83 | + state: &SharedState, |
| 84 | + tenant_id: TenantId, |
| 85 | + database_id: DatabaseId, |
| 86 | + plan: PhysicalPlan, |
| 87 | + vshard_id: VShardId, |
| 88 | + trace_id: TraceId, |
| 89 | + txn_id: Option<TxnId>, |
| 90 | +) -> crate::Result<GatherOutcome> { |
| 91 | + // `Box::pin` breaks an async-fn recursion cycle: `dispatch_to_data_plane_*` |
| 92 | + // re-enters `resolve_exchange_in_plan`. The plan handed here is the bare, |
| 93 | + // Exchange-free child of the resolved Gather, so the re-entrant resolve is a |
| 94 | + // no-op — but the future must be heap-indirected so its size stays finite. |
| 95 | + let resp = Box::pin(dispatch_to_data_plane_with_txn( |
| 96 | + state, |
| 97 | + tenant_id, |
| 98 | + database_id, |
| 99 | + vshard_id, |
| 100 | + plan, |
| 101 | + trace_id, |
| 102 | + txn_id, |
| 103 | + )) |
| 104 | + .await?; |
| 105 | + |
| 106 | + // Propagate a Data-Plane error status the same way `gather_all_cores` does: |
| 107 | + // a `NotFound` from the owning core means "no such slice here" and reads |
| 108 | + // back as an empty (still validatable) observation; any OTHER error status |
| 109 | + // must surface as a dispatch error rather than being silently swallowed as |
| 110 | + // an empty success (e.g. an FTS query-validation rejection, a constraint |
| 111 | + // error, or a deadline). Without this the owning-core path would drop every |
| 112 | + // Data-Plane error on a single-vShard read. |
| 113 | + if resp.status == Status::Error |
| 114 | + && let Some(ec) = resp.error_code.as_deref() |
| 115 | + && !matches!(ec, ErrorCode::NotFound) |
| 116 | + { |
| 117 | + return Err(crate::Error::Dispatch { |
| 118 | + detail: format!("{ec:?}"), |
| 119 | + }); |
| 120 | + } |
| 121 | + |
| 122 | + let payload_bytes: &[u8] = resp.payload.as_ref(); |
| 123 | + let all_elements = extract_msgpack_elements(payload_bytes); |
| 124 | + let merged_array = encode_msgpack_array(&all_elements); |
| 125 | + |
| 126 | + Ok(GatherOutcome { |
| 127 | + raw: payload_bytes.to_vec(), |
| 128 | + merged_array, |
| 129 | + watermark_lsn: resp.watermark_lsn, |
| 130 | + read_version_lsn: resp.read_version_lsn, |
| 131 | + shard_watermarks: vec![(vshard_id, resp.watermark_lsn)], |
| 132 | + }) |
| 133 | +} |
0 commit comments