feat: Phase 16M/17 + promote fixes + testrunner updates

Phase 16M: explicit replica readiness on heartbeat seam
- master.proto: optional bool replica_ready = 19 (proto regenerated on M01)
- block_heartbeat_proto.go: write/read ReplicaReady with presence semantics
- master_block_registry.go: replicaReadyObservedFromHeartbeat prefers
  explicit proto field, falls back to address heuristic when absent
- volume_server_block.go: heartbeat emits ReplicaReady from core projection

Phase 17: host effects extraction + stop line
- phase-17-log.md: Batch 10/11 delivery notes

Promote fixes:
- master_block_failover.go: deterministic replica addrs from path hash
- qa_promote_replication_test.go: address-upgrade trigger test
- qa_promote_rejoin_live_test.go: new live rejoin test

Testrunner:
- devops.go: action improvements
- recovery-baseline-failover.yaml, suite-ha-failover.yaml: scenario updates
- cp11b3-manual-promote.yaml: promote scenario alignment
- fresh_volume_write_test.go: new component test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
pingqiu
2026-04-05 11:38:05 -07:00
co-authored by Claude Opus 4.6
parent 7855d5240c
commit cf16e53b04
22 changed files with 1204 additions and 82 deletions
+30 -12
View File
@@ -84,7 +84,9 @@ func (q *BlockAssignmentQueue) Confirm(server string, path string, epoch uint64)
}
// ConfirmFromHeartbeat batch-confirms assignments that match reported heartbeat info.
// An assignment is confirmed if the VS reports (path, epoch) that matches.
// Same-epoch refresh assignments that carry replica transport are only confirmed
// once the heartbeat reflects that transport, so they are not dropped before
// the promoted VS actually applies them.
func (q *BlockAssignmentQueue) ConfirmFromHeartbeat(server string, infos []blockvol.BlockVolumeInfoMessage) {
if len(infos) == 0 {
return
@@ -97,26 +99,42 @@ func (q *BlockAssignmentQueue) ConfirmFromHeartbeat(server string, infos []block
return
}
// Build a set of reported (path, epoch) pairs.
type key struct {
path string
epoch uint64
}
reported := make(map[key]bool, len(infos))
for _, info := range infos {
reported[key{info.Path, info.Epoch}] = true
}
// Keep only assignments not confirmed.
kept := pending[:0]
for _, a := range pending {
if !reported[key{a.Path, a.Epoch}] {
if !assignmentConfirmedByHeartbeat(a, infos) {
kept = append(kept, a)
}
}
q.queues[server] = kept
}
func assignmentConfirmedByHeartbeat(a blockvol.BlockVolumeAssignment, infos []blockvol.BlockVolumeInfoMessage) bool {
for _, info := range infos {
if info.Path != a.Path || info.Epoch != a.Epoch {
continue
}
expectedData, expectedCtrl, requiresReplicaTransport := assignmentReplicaTransport(a)
if !requiresReplicaTransport {
return true
}
if info.ReplicaDataAddr == expectedData && info.ReplicaCtrlAddr == expectedCtrl {
return true
}
}
return false
}
func assignmentReplicaTransport(a blockvol.BlockVolumeAssignment) (dataAddr, ctrlAddr string, ok bool) {
if a.ReplicaDataAddr != "" || a.ReplicaCtrlAddr != "" {
return a.ReplicaDataAddr, a.ReplicaCtrlAddr, true
}
if len(a.ReplicaAddrs) == 1 {
return a.ReplicaAddrs[0].DataAddr, a.ReplicaAddrs[0].CtrlAddr, true
}
return "", "", false
}
// Pending returns the number of pending assignments for the server.
func (q *BlockAssignmentQueue) Pending(server string) int {
q.mu.Lock()