4949#include "cluster/cluster_cr_admit.h" /* spec-5.52 D2: insert-side admission gate */
5050#include "cluster/cluster_cr_apply.h"
5151#include "cluster/cluster_cr_cache.h"
52- #include "cluster/cluster_cr_pool.h" /* spec-5.51 D4: shared L2 CR pool */
52+ #include "cluster/cluster_cr_coordinator_stat.h" /* spec-5.57 D2/D3: coordinator boundary */
53+ #include "cluster/cluster_cr_pool.h" /* spec-5.51 D4: shared L2 CR pool */
5354#include "cluster/cluster_cr_tuple.h" /* spec-5.54: tuple-level / verdict-only fast path */
5455#include "cluster/cluster_conf.h" /* spec-3.24 D1: cluster_conf_has_peers */
5556#include "cluster/cluster_guc.h" /* cluster_cr_chain_walk_max_steps, cluster_node_id */
@@ -237,6 +238,55 @@ cr_scratch_ensure(void)
237238}
238239
239240
241+ /*
242+ * cr_coordinator_refuse_runtime_remote -- spec-5.57 D2: the single fail-closed
243+ * refusal routine for a class③ (runtime-warm remote) CR origin. Shared by the
244+ * CR-side pre-check (the real boundary, on a real remote UBA) AND the W2 test
245+ * injection (a synthetic class③), so the injection exercises the exact runtime
246+ * behavior the production pre-check uses.
247+ *
248+ * One class③ refusal is BOTH the boundary headline (cross_instance_cr_refused)
249+ * and specifically the remote-undo-read leg (remote_undo_read_refused); both
250+ * bump by construction. The rare W1 header-mismatch belt bumps only
251+ * cross_instance_cr_refused, giving the invariant
252+ * cross_instance_cr_refused >= remote_undo_read_refused.
253+ *
254+ * NON-DEGRADABLE (rule 8.A, §2.2): the ereport fires under ANY GUC value; the
255+ * GUC only gates the advisory counters / probe / LOG-once. This function never
256+ * returns (it always ereport(ERROR)s). The data plane is Stage 6 (#119).
257+ */
258+ static void
259+ pg_attribute_noreturn () cr_coordinator_refuse_runtime_remote (int origin_node )
260+ {
261+ if (cluster_cross_instance_cr_coordinator != CR_COORD_MODE_OFF ) {
262+ cluster_cr_coordinator_stat_bump (CR_COORD_CROSS_INSTANCE_CR_REFUSED );
263+ cluster_cr_coordinator_stat_bump (CR_COORD_REMOTE_UNDO_READ_REFUSED );
264+ }
265+ /* D0 measure-leg: count the class③ hit (behavior unchanged -- still fails). */
266+ if (cluster_cross_instance_cr_probe )
267+ cluster_cr_coordinator_stat_bump (CR_COORD_CROSS_INSTANCE_BOUNDARY_PROBE );
268+ /* forward mode: LOG-once that the data plane is Stage 6 (L213: once per
269+ * backend so the hot path is never flooded). */
270+ if (cluster_cross_instance_cr_coordinator == CR_COORD_MODE_FORWARD ) {
271+ static bool forward_logged = false;
272+
273+ if (!forward_logged ) {
274+ forward_logged = true;
275+ elog (LOG , "cluster.cross_instance_cr_coordinator=forward is a contract "
276+ "placeholder: cross-instance CR/undo data plane lands in Stage 6 "
277+ "(#119); reads stay fail-closed (Spec: spec-5.57)" );
278+ }
279+ }
280+ ereport (ERROR , (errcode (ERRCODE_CLUSTER_CR_CROSS_INSTANCE_UNSUPPORTED ),
281+ errmsg ("cluster CR cross-instance UBA encountered at the remote-undo-read "
282+ "leg (origin_node_id=%d, local=%d)" ,
283+ origin_node , cluster_node_id ),
284+ errhint ("Own-instance CR only unless the origin was materialized by merged "
285+ "recovery; the runtime cross-instance CR/undo data plane lands in "
286+ "Stage 6 (#119 undo-block Cache Fusion); see Spec: spec-5.57." )));
287+ }
288+
289+
240290/* ============================================================
241291 * Test injection hooks (spec-3.9 Step 7; SKIP-style precondition)
242292 * ============================================================ */
@@ -260,11 +310,11 @@ cr_check_error_injections(void)
260310 "cluster_inject_fault('cr_snapshot_too_old','none',0)." )));
261311
262312 if (cluster_cr_injection_armed ("cr_cross_instance" , & param ))
263- ereport ( ERROR ,
264- ( errcode ( ERRCODE_CLUSTER_CR_CROSS_INSTANCE_UNSUPPORTED ),
265- errmsg ( "cluster CR cross-instance UBA (injected; origin_node_id=%u, local=%d)" ,
266- ( uint32 ) param , cluster_node_id ),
267- errhint ( "test injection cr_cross_instance; spec-3.9 is own-instance only." )) );
313+ /* spec-5.57 D2/D3: synthetic class③ refusal -- drive the SAME fail-closed
314+ * routine the production pre-check uses (53R9G + both coordinator counters
315+ * + probe/forward), so the TAP injection legs exercise the real boundary
316+ * behavior deterministically ( param = synthetic origin_node_id). */
317+ cr_coordinator_refuse_runtime_remote (( int ) param );
268318
269319 if (cluster_cr_injection_armed ("cr_corruption" , & param )) {
270320 const char * kind = (param == 1 ) ? "uba_decode"
@@ -341,6 +391,39 @@ cr_walk_chain(char *scratch_page, UBA start_uba, SCN read_scn,
341391 ereport (ERROR , (errcode (ERRCODE_DATA_CORRUPTED ),
342392 errmsg ("cluster CR encountered a malformed UBA in the undo chain" )));
343393
394+ /*
395+ * spec-5.57 D2 (Q11-A): CR-side remote-UBA pre-check -- the read-path
396+ * coordinator boundary, applied BEFORE the undo read. Derive the
397+ * segment owner from the UBA and classify it (§0.1). A runtime-warm
398+ * cross-instance origin (class③: not own, not merged-materialized) is
399+ * the net-new boundary: fail closed HERE with the canonical 53R9G,
400+ * rather than letting cluster_undo_get_record() return 0 and conflate it
401+ * with own-instance retention-recycled undo (53R9F below). This is the
402+ * W3 hardening: the remote-undo-read leg now fails closed with the SAME
403+ * errcode as the W1 walker wall (errcode consolidation, CR-9). It is
404+ * NON-DEGRADABLE -- the ereport fires under any GUC value; the GUC only
405+ * gates the advisory counters (rule 8.A, §2.2). Own-instance is the
406+ * common OLTP path: classify returns OWN and we fall straight through.
407+ * The data plane (real remote undo fetch) is Stage 6 (#119).
408+ * See Spec: spec-5.57 §3.1 (W3) / §2.1 (three roles).
409+ */
410+ {
411+ NodeId cr_origin = uba_origin_node_id (uba );
412+ ClusterCrCoordOriginClass cr_origin_class
413+ = cluster_cr_coordinator_classify_origin (cr_origin );
414+
415+ if (cr_origin_class == CR_COORD_ORIGIN_RUNTIME_REMOTE ) {
416+ /* class③: fail closed via the shared refusal routine (53R9G +
417+ * both coordinator counters; non-degradable, §2.2). */
418+ cr_coordinator_refuse_runtime_remote ((int )cr_origin );
419+ } else if (cr_origin_class == CR_COORD_ORIGIN_MATERIALIZED_REMOTE ) {
420+ /* class②: merged-materialized remote, served from the local tree
421+ * (already shipped, spec-4.5a D8). Count the serve (advisory). */
422+ if (cluster_cross_instance_cr_coordinator != CR_COORD_MODE_OFF )
423+ cluster_cr_coordinator_stat_bump (CR_COORD_MATERIALIZED_REMOTE_SERVED );
424+ }
425+ }
426+
344427 len = cluster_undo_get_record (uba , record_buf .data , sizeof (record_buf .data ));
345428 if (len == 0 )
346429 ereport (ERROR ,
@@ -361,30 +444,40 @@ cr_walk_chain(char *scratch_page, UBA start_uba, SCN read_scn,
361444
362445 /* Own-instance, or a merged-materialized remote instance whose undo
363446 * lives in the local pg_undo/instance_<origin> tree (spec-4.5a D8).
364- * Anything else stays the spec-3.9 fail-closed.
447+ * Anything else stays fail-closed (Spec: spec-5.57 §3.1 W1).
448+ *
449+ * This is the W1 belt: the spec-5.57 D2 segment-derived pre-check above
450+ * (on uba_origin_node_id) catches the common class③ case BEFORE the undo
451+ * read, so this header-origin check now fires only on the rare segment-
452+ * vs-header mismatch (D8 §2.5 cross-check) -- defense-in-depth, never a
453+ * silent assumption.
365454 *
366455 * spec-5.56 C4 (reconfig contract, §3.3): this carve-out is ALSO the
367456 * fail-closed boundary that keeps the THIRD origin class — runtime warm
368457 * remote (not own, not merged-materialized) — OUT of the CR pool: it never
369- * constructs (ERROR below) so it never caches. The two pool-eligible
370- * classes are reconfig-INVARIANT and need NO membership/remaster
371- * invalidation: (①) own-instance pages/undo are unchanged by reconfig; (②)
372- * merged-materialized remote undo is durable in the local tree with a
373- * reconfig-invariant merge_recovered_lsn authority, and an origin rejoin's
374- * NEW writes are new versions => new key => MISS (already fenced by C1/key).
375- * read_scn is a GLOBAL SCN (AD-008), not a membership epoch (INV-C2). The
376- * runtime-warm-remote class's reconfig/remaster invalidation is forwarded to
377- * spec-5.57 (where construct stops fail-closing it); until then this ERROR
378- * is the C4 class-③ guard (INV-C3), not a silent assumption. */
458+ * constructs (ERROR) so it never caches. The two pool-eligible classes are
459+ * reconfig-INVARIANT and need NO membership/remaster invalidation: (①)
460+ * own-instance pages/undo are unchanged by reconfig; (②) merged-materialized
461+ * remote undo is durable in the local tree with a reconfig-invariant
462+ * merge_recovered_lsn authority, and an origin rejoin's NEW writes are new
463+ * versions => new key => MISS (already fenced by C1/key). read_scn is a
464+ * GLOBAL SCN (AD-008), not a membership epoch (INV-C2). spec-5.57 freezes
465+ * this class③ fail-closed as the read-path coordinator boundary (CR-9); the
466+ * runtime-warm-remote data plane lands in Stage 6 (#119). */
379467 if (hdr -> origin_node_id != (uint16 )cluster_node_id
380- && !cluster_merged_instance_is_materialized ((int )hdr -> origin_node_id ))
381- ereport (ERROR , (errcode (ERRCODE_CLUSTER_CR_CROSS_INSTANCE_UNSUPPORTED ),
382- errmsg ("cluster CR cross-instance UBA encountered "
383- "(origin_node_id=%u, local=%d)" ,
384- hdr -> origin_node_id , cluster_node_id ),
385- errhint ("Own-instance CR only unless the origin was materialized by "
386- "merged recovery; runtime cross-instance CR is Stage 4 "
387- "(Cache Fusion CR coordinator)." )));
468+ && !cluster_merged_instance_is_materialized ((int )hdr -> origin_node_id )) {
469+ if (cluster_cross_instance_cr_coordinator != CR_COORD_MODE_OFF )
470+ cluster_cr_coordinator_stat_bump (CR_COORD_CROSS_INSTANCE_CR_REFUSED );
471+ ereport (ERROR ,
472+ (errcode (ERRCODE_CLUSTER_CR_CROSS_INSTANCE_UNSUPPORTED ),
473+ errmsg ("cluster CR cross-instance UBA encountered "
474+ "(origin_node_id=%u, local=%d)" ,
475+ hdr -> origin_node_id , cluster_node_id ),
476+ errhint ("Own-instance CR only unless the origin was materialized by "
477+ "merged recovery; the runtime cross-instance CR/undo data plane "
478+ "lands in Stage 6 (#119 undo-block Cache Fusion); see "
479+ "Spec: spec-5.57." )));
480+ }
388481
389482 /* I-chain-1: normal SCN stop. */
390483 if (scn_time_cmp (hdr -> write_scn , read_scn ) <= 0 )
0 commit comments