|
53 | 53 | # L8 wake-up self-heal: node1's first cross-node read after the |
54 | 54 | # storm succeeds (bounded retries allowed: the inadmissible |
55 | 55 | # BELOW_HORIZON arm is fail-closed and observe heals it) |
| 56 | +# L10 GATED self-heal proof (runs before L9), RED->GREEN twin |
| 57 | +# on ONE driver with NO cleanout bypass: a recycled node0 |
| 58 | +# xid first fails closed 53R97 under the default |
| 59 | +# cluster.crossnode_runtime_visibility = off (verdict wire |
| 60 | +# provably untouched), then -- GUC flipped on, nothing else |
| 61 | +# changed -- resolves through the COMMITTED_BELOW_HORIZON |
| 62 | +# origin-verdict arm: exact data terminal + rtvis verdict |
| 63 | +# counter evidence |
56 | 64 | # L9 restart clean |
57 | 65 | # |
58 | 66 | # Spec: spec-5.22e (cluster undo retention brake) S2.1 sampling rule + |
@@ -500,6 +508,238 @@ sub floor_lag |
500 | 508 | } |
501 | 509 | is($sum, '64|t', "L8 node1 cross-node read healed (tries=$tries)"); |
502 | 510 |
|
| 511 | +# ============================================================ |
| 512 | +# L10: the GATED wake-up self-heal, for real -- the |
| 513 | +# COMMITTED_BELOW_HORIZON resolve arm under |
| 514 | +# cluster.crossnode_runtime_visibility = on, with NO owner-side |
| 515 | +# cleanout bypass (no FREEZE, no CHECKPOINT). |
| 516 | +# |
| 517 | +# L8 above only proves the read heals AFTER a VACUUM (FREEZE) |
| 518 | +# removed the remote ITL refs -- the rtvis resolve machinery |
| 519 | +# (cluster_runtime_visibility.c rtvis_try_origin_verdict) never ran |
| 520 | +# there because the GUC defaults off. This leg drives it as a |
| 521 | +# RED->GREEN twin on ONE driver: |
| 522 | +# |
| 523 | +# 1. cluster.tt_status_hint_emit_mode = disabled (PGC_SIGHUP, |
| 524 | +# default all_status) BEFORE the fixture write: otherwise the |
| 525 | +# V4 hint wire installs the fixture xid's terminal status into |
| 526 | +# node1's TT overlay at commit time and the read resolves from |
| 527 | +# the overlay without ever touching the verdict wire. This is |
| 528 | +# NOT an assertion-weakening bypass: it deterministically |
| 529 | +# manufactures the "hint never arrived" state the resolve path |
| 530 | +# exists for (the 256-slot outbound ring drops hints under |
| 531 | +# storm load anyway), FORCING the mechanism under test to carry |
| 532 | +# the read. cluster.crossnode_runtime_visibility stays at its |
| 533 | +# default (off) until the RED twin below has run. |
| 534 | +# 2. Fixture: ONE node0 transaction inserts 64 rows -> a single |
| 535 | +# fresh remote ITL ref (local_xid == raw_xid). With |
| 536 | +# cluster.xid_striping off in this rig, cluster_xid_is_mine() |
| 537 | +# is always false at the origin, so ONLY the spec-5.22f |
| 538 | +# fresh-ref AUTHORITATIVE ask is ever served -- the fixture must |
| 539 | +# stay fresh (single writer xact, page never revisited by |
| 540 | +# node0, so no delayed cleanout stamps it). |
| 541 | +# 3. Churn: a node0 writer laps the TT pool (same |
| 542 | +# tt_retention_rollover_count evidence as L2), so the fixture |
| 543 | +# xid's TT slot is provably rebound; the origin's complete |
| 544 | +# by-xid scan then 0-matches and -- retention legs (a)-(d) + |
| 545 | +# CLOG committed -- serves COMMITTED_BELOW_HORIZON{H}. |
| 546 | +# 4. RED twin: with the GUC still off, node1's read of the fixture |
| 547 | +# MUST fail closed 53R97 ("cluster TT status unknown", |
| 548 | +# heapam_visibility.c) and node1's verdict-wire counter MUST |
| 549 | +# stay flat (cluster_runtime_visibility.c gates the whole |
| 550 | +# resolve on the GUC before any wire touch). This proves the |
| 551 | +# recycled-slot window is genuinely open at the moment the |
| 552 | +# GREEN twin runs -- the GREEN result cannot be an overlay / |
| 553 | +# hint / cleanout side door. |
| 554 | +# 5. cluster.crossnode_runtime_visibility = on (PGC_SUSET, reload) |
| 555 | +# on BOTH nodes: the requester's classify / fresh-ref widening |
| 556 | +# gates on it AND the origin's lms_undo_verdict_serve refuses |
| 557 | +# while it is off (cluster_cr_server.c). |
| 558 | +# 6. GREEN twin: node1 re-reads the same fixture. A snapshot |
| 559 | +# read_scn behind the shipped horizon takes the inadmissible |
| 560 | +# fail-closed arm and the Lamport observe heals the NEXT |
| 561 | +# snapshot (bounded retries, the documented self-heal); the |
| 562 | +# healed snapshot admits the bound and every row resolves |
| 563 | +# visible. |
| 564 | +# |
| 565 | +# HARD ASSERTS: RED twin fails closed with the wire flat + exact |
| 566 | +# terminal data on the GREEN twin (count=64 AND sum=2080 -- not |
| 567 | +# L8's sum >= 0 shape) + node1's rtvis verdict counters moved |
| 568 | +# (below_horizon / exact), proving the verdict wire resolved the |
| 569 | +# read rather than an overlay / hint / cleanout side door. |
| 570 | +# |
| 571 | +# Known hazard (documented, not worked around): if L8 had to bounce |
| 572 | +# node1, the pre-existing rejoin silent-empty-read defect (see L8 |
| 573 | +# comment) can surface here as count=0 -- that is a REAL defect this |
| 574 | +# leg must fail on, not paper over. |
| 575 | +# ============================================================ |
| 576 | +for my $node ($pair->node0, $pair->node1) |
| 577 | +{ |
| 578 | + $node->safe_psql('postgres', |
| 579 | + "ALTER SYSTEM SET cluster.tt_status_hint_emit_mode = 'disabled'"); |
| 580 | + $node->safe_psql('postgres', 'SELECT pg_reload_conf()'); |
| 581 | +} |
| 582 | +ok( wait_for( |
| 583 | + sub { |
| 584 | + $pair->node0->safe_psql('postgres', |
| 585 | + 'SHOW cluster.tt_status_hint_emit_mode') eq 'disabled' |
| 586 | + && $pair->node1->safe_psql('postgres', |
| 587 | + 'SHOW cluster.tt_status_hint_emit_mode') eq 'disabled'; |
| 588 | + }, |
| 589 | + 15), |
| 590 | + 'L10a status-hint wire disabled via reload (both nodes)'); |
| 591 | + |
| 592 | +# Coinciding-filepath fixture on shared storage (same t/394 recipe as |
| 593 | +# L1b; no shared catalog in this rig). |
| 594 | +my $l10tbl; |
| 595 | +for my $i (1 .. 12) |
| 596 | +{ |
| 597 | + my $t = "t403l10_$i"; |
| 598 | + $_->safe_psql('postgres', "CREATE TABLE $t (k int, v int)") |
| 599 | + for ($pair->node0, $pair->node1); |
| 600 | + my $p0 = $pair->node0->safe_psql('postgres', "SELECT pg_relation_filepath('$t')"); |
| 601 | + my $p1 = $pair->node1->safe_psql('postgres', "SELECT pg_relation_filepath('$t')"); |
| 602 | + if (($p0 // '') eq ($p1 // '')) { $l10tbl = $t; last; } |
| 603 | +} |
| 604 | +ok(defined $l10tbl, 'L10b coinciding-filepath fixture table found'); |
| 605 | +die 'no coinciding filepath found for L10' unless defined $l10tbl; |
| 606 | +diag("L10 fixture table=$l10tbl"); |
| 607 | + |
| 608 | +# ONE transaction writes all 64 rows: exact expected terminal data is |
| 609 | +# count=64, sum(v)=sum(1..64)=2080, and the page ITL stays a single |
| 610 | +# fresh entry bound to this xid. |
| 611 | +$pair->node0->safe_psql('postgres', |
| 612 | + "INSERT INTO $l10tbl SELECT g, g FROM generate_series(1, 64) g"); |
| 613 | + |
| 614 | +# Churn phase: lap the TT pool on node0 so the fixture xid's slot is |
| 615 | +# rebound (>= 2 laps hard floor, target 3; single background writer on |
| 616 | +# a node0-only table so the fixture pages are never revisited). |
| 617 | +$pair->node0->safe_psql('postgres', 'CREATE TABLE t403l10_churn (k int)'); |
| 618 | +my $churnfile = "$tmpdir/churn.sql"; |
| 619 | +{ |
| 620 | + open(my $fh, '>', $churnfile) or die "cannot write $churnfile: $!"; |
| 621 | + print $fh "INSERT INTO t403l10_churn VALUES (1);\n" for (1 .. $WRITER_LINES); |
| 622 | + close($fh); |
| 623 | +} |
| 624 | +my $roll_pre_l10 = state_val($pair->node0, 'undo', 'tt_retention_rollover_count'); |
| 625 | +my ($c_in, $c_out, $c_err) = ('', '', ''); |
| 626 | +my $churn = IPC::Run::start( |
| 627 | + [ |
| 628 | + 'psql', '-X', '-A', '-t', '-q', |
| 629 | + '-d', $pair->node0->connstr('postgres'), |
| 630 | + '-f', $churnfile |
| 631 | + ], |
| 632 | + \$c_in, \$c_out, \$c_err); |
| 633 | +my $churn_t0 = time(); |
| 634 | +my $roll_delta_l10 = 0; |
| 635 | +while (time() - $churn_t0 < 120) |
| 636 | +{ |
| 637 | + $roll_delta_l10 = |
| 638 | + state_val($pair->node0, 'undo', 'tt_retention_rollover_count') - $roll_pre_l10; |
| 639 | + last if $roll_delta_l10 >= 3 * $POOL_SEGMENTS; |
| 640 | + usleep(400_000); |
| 641 | +} |
| 642 | +$churn->kill_kill; |
| 643 | +diag(sprintf('L10 churn: %d TT rollovers in %.0fs (hard floor %d)', |
| 644 | + $roll_delta_l10, time() - $churn_t0, 2 * $POOL_SEGMENTS)); |
| 645 | +cmp_ok($roll_delta_l10, '>=', 2 * $POOL_SEGMENTS, |
| 646 | + 'L10c churn lapped the TT pool (fixture slot provably rebound)'); |
| 647 | + |
| 648 | +# ------------------------------------------------------------ |
| 649 | +# RED twin: cluster.crossnode_runtime_visibility is still at its |
| 650 | +# default (off). The same read the GREEN twin will heal below MUST |
| 651 | +# fail closed here -- 53R97 "cluster TT status unknown" |
| 652 | +# (heapam_visibility.c) -- and node1's verdict-wire counter MUST stay |
| 653 | +# flat: cluster_runtime_visibility.c refuses before any wire touch |
| 654 | +# while the GUC is off, so a moving counter (or a succeeding read) |
| 655 | +# would mean a side door carried it. Transient non-53R97 errors |
| 656 | +# (post-churn GES tail, the L8 class) are retried; a SUCCEEDING read |
| 657 | +# is a hard fail, never retried. |
| 658 | +# ------------------------------------------------------------ |
| 659 | +my $wire_pre_neg = state_val($pair->node1, 'cr', 'rtvis_verdict_wire_count'); |
| 660 | +my ($neg_hit_53r97, $neg_unexpected) = (0, ''); |
| 661 | +for my $i (1 .. 10) |
| 662 | +{ |
| 663 | + my ($rc, $stdout, $stderr) = |
| 664 | + $pair->node1->psql('postgres', "SELECT count(*), sum(v) FROM $l10tbl"); |
| 665 | + if ($rc == 0) |
| 666 | + { |
| 667 | + $neg_unexpected = 'read SUCCEEDED with rtvis off: [' . ($stdout // '') . ']'; |
| 668 | + last; |
| 669 | + } |
| 670 | + if (($stderr // '') =~ /cluster TT status unknown for xid/) |
| 671 | + { |
| 672 | + $neg_hit_53r97 = 1; |
| 673 | + last; |
| 674 | + } |
| 675 | + $neg_unexpected = join(' / ', split(/\n/, $stderr // '')); |
| 676 | + diag(sprintf('L10d RED-twin attempt %d: rc=%d non-53R97 err=[%s]', |
| 677 | + $i, $rc, $neg_unexpected)); |
| 678 | + usleep(1_500_000); |
| 679 | +} |
| 680 | +ok($neg_hit_53r97, |
| 681 | + 'L10d RED twin HARD ASSERT: rtvis off fails the read closed 53R97 ' |
| 682 | + . '(cluster TT status unknown)') |
| 683 | + or diag("L10d terminal state: $neg_unexpected"); |
| 684 | +is(state_val($pair->node1, 'cr', 'rtvis_verdict_wire_count') - $wire_pre_neg, |
| 685 | + 0, 'L10e RED twin: verdict wire provably untouched while the GUC is off'); |
| 686 | + |
| 687 | +# GREEN twin arming: flip ONLY the GUC; driver and fixture unchanged. |
| 688 | +for my $node ($pair->node0, $pair->node1) |
| 689 | +{ |
| 690 | + $node->safe_psql('postgres', |
| 691 | + 'ALTER SYSTEM SET cluster.crossnode_runtime_visibility = on'); |
| 692 | + $node->safe_psql('postgres', 'SELECT pg_reload_conf()'); |
| 693 | +} |
| 694 | +ok( wait_for( |
| 695 | + sub { |
| 696 | + $pair->node0->safe_psql('postgres', |
| 697 | + 'SHOW cluster.crossnode_runtime_visibility') eq 'on' |
| 698 | + && $pair->node1->safe_psql('postgres', |
| 699 | + 'SHOW cluster.crossnode_runtime_visibility') eq 'on'; |
| 700 | + }, |
| 701 | + 15), |
| 702 | + 'L10f crossnode_runtime_visibility on via reload (both nodes)'); |
| 703 | + |
| 704 | +# Requester-side rtvis verdict counters live on node1 (category 'cr'). |
| 705 | +my $bh_pre = state_val($pair->node1, 'cr', 'rtvis_verdict_below_horizon_count'); |
| 706 | +my $ex_pre = state_val($pair->node1, 'cr', 'rtvis_verdict_exact_count'); |
| 707 | +my $inadm_pre = state_val($pair->node1, 'cr', 'rtvis_verdict_inadmissible_count'); |
| 708 | +my $wire_pre = state_val($pair->node1, 'cr', 'rtvis_verdict_wire_count'); |
| 709 | + |
| 710 | +# node1 cross-node read, bounded retries: the inadmissible |
| 711 | +# BELOW_HORIZON arm is fail-closed and the wire observe heals the next |
| 712 | +# snapshot (plus post-churn GES tail tolerance, as in L8). |
| 713 | +my ($l10_out, $l10_tries) = ('', 0); |
| 714 | +for my $i (1 .. 15) |
| 715 | +{ |
| 716 | + $l10_tries = $i; |
| 717 | + my ($rc, $stdout, $stderr) = |
| 718 | + $pair->node1->psql('postgres', "SELECT count(*), sum(v) FROM $l10tbl"); |
| 719 | + if ($rc == 0 && $stdout =~ /^64\|2080$/) |
| 720 | + { |
| 721 | + $l10_out = $stdout; |
| 722 | + last; |
| 723 | + } |
| 724 | + diag(sprintf('L10 attempt %d: rc=%d out=[%s] err=[%s]', |
| 725 | + $i, $rc, $stdout // '', join(' / ', split(/\n/, $stderr // '')))); |
| 726 | + usleep(1_500_000); |
| 727 | +} |
| 728 | +is($l10_out, '64|2080', |
| 729 | + "L10 HARD ASSERT: node1 healed read returned exact terminal data (tries=$l10_tries)"); |
| 730 | + |
| 731 | +my $bh_d = state_val($pair->node1, 'cr', 'rtvis_verdict_below_horizon_count') - $bh_pre; |
| 732 | +my $ex_d = state_val($pair->node1, 'cr', 'rtvis_verdict_exact_count') - $ex_pre; |
| 733 | +my $inadm_d = state_val($pair->node1, 'cr', 'rtvis_verdict_inadmissible_count') - $inadm_pre; |
| 734 | +my $wire_d = state_val($pair->node1, 'cr', 'rtvis_verdict_wire_count') - $wire_pre; |
| 735 | +diag("L10 node1 rtvis verdict deltas: wire=$wire_d below_horizon=$bh_d " |
| 736 | + . "exact=$ex_d inadmissible=$inadm_d"); |
| 737 | +cmp_ok($bh_d + $ex_d, '>', 0, |
| 738 | + 'L10g HARD ASSERT: verdict resolve path really carried the read ' |
| 739 | + . '(below_horizon + exact advanced)'); |
| 740 | +cmp_ok($bh_d, '>', 0, |
| 741 | + 'L10h below-horizon arm served the recycled fixture xid'); |
| 742 | + |
503 | 743 | # ============================================================ |
504 | 744 | # L9: restart clean. |
505 | 745 | # ============================================================ |
|
0 commit comments