Skip to content

Commit dd9ed9b

Browse files
committed
feat(startup): wire gate-based sequencer and gateway into server bootstrap
main.rs is updated to construct StartupSequencer gates before each subsystem, install the real startup gate on SharedState after open(), and construct the Gateway + PlanCacheInvalidator before listeners bind. Additional bootstrap changes: - WAL validation runs before wal_gate fires; corrupt segments are now fatal rather than silently ignored, preventing startup on a corrupted WAL - WAL replay failure is now fatal (was a warn + empty state) - ShutdownBus is wired with system metrics for per-phase drain telemetry - A NODEDB_TEST_SLOW_DRAIN_TASK env hook enables drain-abort integration tests Supporting changes: - recovery_check module added for catalog sanity check on startup - catalog_entry post_apply and invalidation tests updated - system metrics expanded for startup and shutdown phase telemetry - WAL manager gains validate_for_startup - Error types extended for new startup and gateway error variants
1 parent a28c491 commit dd9ed9b

38 files changed

Lines changed: 3274 additions & 56 deletions

nodedb/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ tempfile = "3"
144144
tokio-postgres = { workspace = true }
145145
proptest = "1"
146146
nodedb-types = { workspace = true }
147+
reqwest = { workspace = true }
147148

148149
[features]
149150
default = []

nodedb/src/control/catalog_entry/post_apply/mod.rs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ use crate::control::state::SharedState;
5252
/// is infallible today (all typed functions log on failure and
5353
/// return).
5454
pub fn apply_post_apply_side_effects_sync(entry: &CatalogEntry, shared: &Arc<SharedState>) {
55+
// Gateway plan-cache invalidation: on any descriptor mutation, evict
56+
// stale cached plans that reference the changed descriptor.
57+
// This is a single, unconditional call per DDL commit — negligible overhead.
58+
invalidate_gateway_cache_for_entry(entry, shared);
59+
5560
match entry {
5661
CatalogEntry::PutCollection(stored) => {
5762
// Owner record install is sync; Data Plane register is
@@ -189,3 +194,175 @@ pub fn spawn_post_apply_async_side_effects(entry: CatalogEntry, shared: Arc<Shar
189194
});
190195
}
191196
}
197+
198+
/// Notify the gateway plan-cache invalidator after a DDL descriptor mutation.
199+
///
200+
/// Extracts the descriptor name and new version from the entry and calls
201+
/// `PlanCacheInvalidator::invalidate`. This is best-effort: if the gateway
202+
/// has not been constructed yet (`gateway_invalidator == None`) the call is
203+
/// a no-op.
204+
///
205+
/// ## Invalidation decision table (all 31 variants — exhaustive, no `_ => {}`)
206+
///
207+
/// The gateway plan cache keys on `(sql_hash, ph_hash, GatewayVersionSet)`.
208+
/// A `GatewayVersionSet` lists `(collection_name, descriptor_version)` pairs
209+
/// extracted from the `PhysicalPlan` by `touched_collections`. A DDL entry
210+
/// requires invalidation only if it changes the observable plan shape for
211+
/// an already-cached plan. Verified against `planner/`, `rls_injection.rs`,
212+
/// and the `PhysicalPlan` definition.
213+
///
214+
/// | Entry kind | Invalidate? | Reason |
215+
/// |-----------------------------------------|-------------|--------|
216+
/// | PutCollection / DeactivateCollection | ✅ yes | collection schema baked into plan |
217+
/// | PutSequence / DeleteSequence | ❌ no | sequences resolved at handler level (pgwire `transaction_cmds.rs`), not in PhysicalPlan |
218+
/// | PutSequenceState | ❌ no | runtime counter state, not plan shape |
219+
/// | PutTrigger / DeleteTrigger | ❌ no | triggers dispatched by Event Plane post-execution; no trigger fields in any PhysicalPlan variant |
220+
/// | PutFunction / DeleteFunction | ❌ no | functions looked up at eval time, not inlined |
221+
/// | PutProcedure / DeleteProcedure | ❌ no | same as functions |
222+
/// | PutSchedule / DeleteSchedule | ❌ no | scheduler runs independently |
223+
/// | PutChangeStream / DeleteChangeStream | ❌ no | CDC Event Plane concern |
224+
/// | PutUser / DeactivateUser | ❌ no | authz checked at exec time |
225+
/// | PutRole / DeleteRole | ❌ no | same |
226+
/// | PutApiKey / RevokeApiKey | ❌ no | same |
227+
/// | PutMaterializedView / DeleteMaterializedView | ❌ no | MV definition is its own catalog object; write-path `materialized_sum_sources` is set at collection-register time via PutCollection, not updated by PutMaterializedView independently |
228+
/// | PutTenant / DeleteTenant | ❌ no | tenant identity does not affect plan shape |
229+
/// | PutRlsPolicy / DeleteRlsPolicy | ❌ no | `execute_sql` is only called from CDC path (no RLS injection via `inject_rls`); per-session pgwire cache has its own DDL invalidation |
230+
/// | PutPermission / DeletePermission | ❌ no | permission checked at exec time |
231+
/// | PutOwner / DeleteOwner | ❌ no | ownership does not affect plan shape |
232+
pub(crate) fn invalidate_gateway_cache_for_entry(entry: &CatalogEntry, shared: &Arc<SharedState>) {
233+
let Some(ref inv) = shared.gateway_invalidator else {
234+
return;
235+
};
236+
match entry {
237+
// ── Collection mutations that change the plan shape ──────────────────
238+
CatalogEntry::PutCollection(stored) => {
239+
inv.invalidate(&stored.name, stored.descriptor_version.max(1));
240+
}
241+
CatalogEntry::DeactivateCollection { name, .. } => {
242+
// Treat deactivation as version 0 (collection gone — any cached
243+
// plan for it is stale).
244+
inv.invalidate(name, 0);
245+
}
246+
247+
// ── Sequence: resolved at handler level, not baked into PhysicalPlan ─
248+
CatalogEntry::PutSequence(_) => {
249+
// no-op: sequences resolved in pgwire transaction_cmds.rs before
250+
// planning; StoredSequence never appears in a PhysicalPlan variant.
251+
}
252+
CatalogEntry::DeleteSequence { .. } => {
253+
// no-op: same reason as PutSequence.
254+
}
255+
CatalogEntry::PutSequenceState(_) => {
256+
// no-op: runtime counter state — the planner never reads seq state.
257+
}
258+
259+
// ── Trigger: dispatched by Event Plane post-execution ────────────────
260+
CatalogEntry::PutTrigger(_) => {
261+
// no-op: triggers are AFTER-fire; no trigger field exists in any
262+
// PhysicalPlan variant; Event Plane reads the trigger registry
263+
// directly at fire time.
264+
}
265+
CatalogEntry::DeleteTrigger { .. } => {
266+
// no-op: same as PutTrigger.
267+
}
268+
269+
// ── Function / Procedure: looked up at eval time, not inlined ────────
270+
CatalogEntry::PutFunction(_) => {
271+
// no-op: UDFs looked up in function_registry at eval time via
272+
// `wasm/` executor; never inlined into a PhysicalPlan.
273+
}
274+
CatalogEntry::DeleteFunction { .. } => {
275+
// no-op: same as PutFunction.
276+
}
277+
CatalogEntry::PutProcedure(_) => {
278+
// no-op: stored procedures parsed and executed at CALL time via
279+
// `procedural/executor`; body not baked into any PhysicalPlan.
280+
}
281+
CatalogEntry::DeleteProcedure { .. } => {
282+
// no-op: same as PutProcedure.
283+
}
284+
285+
// ── Schedule: cron runs independently of the plan cache ──────────────
286+
CatalogEntry::PutSchedule(_) => {
287+
// no-op: ScheduleRegistry drives the scheduler loop; no plan shape
288+
// changes result from a new/updated schedule definition.
289+
}
290+
CatalogEntry::DeleteSchedule { .. } => {
291+
// no-op: same as PutSchedule.
292+
}
293+
294+
// ── Change stream: CDC Event Plane concern ────────────────────────────
295+
CatalogEntry::PutChangeStream(_) => {
296+
// no-op: CDC stream definitions route WriteEvents in the Event
297+
// Plane; they do not alter how a collection's plan is constructed.
298+
}
299+
CatalogEntry::DeleteChangeStream { .. } => {
300+
// no-op: same as PutChangeStream.
301+
}
302+
303+
// ── User / Role / ApiKey: authz checked at exec, not baked into plan ─
304+
CatalogEntry::PutUser(_) => {
305+
// no-op: user identity checked in credential store at exec time.
306+
}
307+
CatalogEntry::DeactivateUser { .. } => {
308+
// no-op: same as PutUser.
309+
}
310+
CatalogEntry::PutRole(_) => {
311+
// no-op: role membership checked at exec time via RoleStore.
312+
}
313+
CatalogEntry::DeleteRole { .. } => {
314+
// no-op: same as PutRole.
315+
}
316+
CatalogEntry::PutApiKey(_) => {
317+
// no-op: API key checked at connection/exec time via ApiKeyStore.
318+
}
319+
CatalogEntry::RevokeApiKey { .. } => {
320+
// no-op: same as PutApiKey.
321+
}
322+
323+
// ── Materialized view: MV definition is a separate catalog object ────
324+
CatalogEntry::PutMaterializedView(_) => {
325+
// no-op: MaterializedView metadata is its own catalog object and
326+
// does not directly modify any PhysicalPlan. The `materialized_sum_sources`
327+
// field in DocumentOp::Register is set at collection-register time
328+
// (driven by PutCollection), not updated independently by
329+
// PutMaterializedView. Any schema change that would affect plans
330+
// cascades through PutCollection instead.
331+
}
332+
CatalogEntry::DeleteMaterializedView { .. } => {
333+
// no-op: same as PutMaterializedView.
334+
}
335+
336+
// ── Tenant: identity does not affect plan shape ───────────────────────
337+
CatalogEntry::PutTenant(_) => {
338+
// no-op: tenant identity used for quota enforcement at exec time.
339+
}
340+
CatalogEntry::DeleteTenant { .. } => {
341+
// no-op: same as PutTenant.
342+
}
343+
344+
// ── RLS policy: execute_sql callers (CDC) do not inject RLS ──────────
345+
CatalogEntry::PutRlsPolicy(_) => {
346+
// no-op: the gateway execute_sql path (CDC consume_remote) calls
347+
// plan_sql without RLS injection; per-session pgwire plan cache
348+
// has its own DDL-aware invalidation that handles RLS changes.
349+
}
350+
CatalogEntry::DeleteRlsPolicy { .. } => {
351+
// no-op: same as PutRlsPolicy.
352+
}
353+
354+
// ── Permission / Owner: not baked into plan ───────────────────────────
355+
CatalogEntry::PutPermission(_) => {
356+
// no-op: permission grants checked at exec time via PermissionStore.
357+
}
358+
CatalogEntry::DeletePermission { .. } => {
359+
// no-op: same as PutPermission.
360+
}
361+
CatalogEntry::PutOwner(_) => {
362+
// no-op: ownership does not influence plan structure.
363+
}
364+
CatalogEntry::DeleteOwner { .. } => {
365+
// no-op: same as PutOwner.
366+
}
367+
}
368+
}

0 commit comments

Comments
 (0)