mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
Wire V2 promotion into production binary: - Add --block.v2Promotion CLI flag on weed master (default false) - MasterOption.BlockV2Promotion → NewMasterServer wires flag + querier - defaultBlockVSQueryEvidence placeholder (returns explicit error until proto regen on M01 enables gRPC evidence RPC) Fix three fail-closed violations found by tester: 1. blockV2Promotion=true + nil querier now fails closed with explicit log instead of silently falling back to V1 2. Partial evidence (any candidate query failed) now fails closed — unreachable candidate may be the most durable, promoting from incomplete evidence violates durability-first ordering 3. Clear EngineProjectionMode in applyPromotionLocked (already in previous commit, verified in tests here) 2 new tests: NilQuerier_FailsClosed, PartialEvidenceFailure_FailsClosed. Total T3 tests: 7, all pass. Existing V1 failover tests unaffected. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
99 lines
3.9 KiB
Go
99 lines
3.9 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// BlockPromotionEvidenceQuerier queries one volume server for fresh promotion
|
|
// evidence at failover time. The default implementation will use gRPC once
|
|
// proto is regenerated. For testing, a direct in-process implementation is
|
|
// used.
|
|
type BlockPromotionEvidenceQuerier func(ctx context.Context, server, path string, expectedEpoch uint64) (BlockPromotionEvidence, error)
|
|
|
|
// queryBlockPromotionEvidence queries one candidate for fresh evidence with a
|
|
// bounded timeout. Returns the evidence or an error. The master must not
|
|
// promote based on stale/cached data — only fresh evidence from this call.
|
|
func queryBlockPromotionEvidence(querier BlockPromotionEvidenceQuerier, server, path string, expectedEpoch uint64) (BlockPromotionEvidence, error) {
|
|
if querier == nil {
|
|
return BlockPromotionEvidence{}, fmt.Errorf("promotion evidence querier is nil")
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
return querier(ctx, server, path, expectedEpoch)
|
|
}
|
|
|
|
// queryAllCandidateEvidence queries multiple candidates in sequence and returns
|
|
// all successfully collected evidence. Failed queries are recorded as errors
|
|
// but do not stop collection from other candidates.
|
|
func queryAllCandidateEvidence(querier BlockPromotionEvidenceQuerier, candidates []promotionCandidate) ([]BlockPromotionEvidence, []error) {
|
|
var evidence []BlockPromotionEvidence
|
|
var errs []error
|
|
for _, c := range candidates {
|
|
ev, err := queryBlockPromotionEvidence(querier, c.server, c.path, c.expectedEpoch)
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf("evidence query %s %s: %w", c.server, c.path, err))
|
|
continue
|
|
}
|
|
ev.Server = c.server
|
|
evidence = append(evidence, ev)
|
|
}
|
|
return evidence, errs
|
|
}
|
|
|
|
// selectDurabilityFirstCandidate selects the best promotion candidate from
|
|
// fresh evidence using durability-first ordering:
|
|
//
|
|
// 1. Filter: only eligible candidates
|
|
// 2. Rank: highest CommittedLSN
|
|
// 3. Tie-break: highest WALHeadLSN
|
|
// 4. Tie-break: highest HealthScore
|
|
//
|
|
// Returns an error if no eligible candidate exists (fail-closed).
|
|
func selectDurabilityFirstCandidate(evidence []BlockPromotionEvidence) (BlockPromotionEvidence, error) {
|
|
var eligible []BlockPromotionEvidence
|
|
for _, ev := range evidence {
|
|
if ev.Eligible {
|
|
eligible = append(eligible, ev)
|
|
}
|
|
}
|
|
if len(eligible) == 0 {
|
|
return BlockPromotionEvidence{}, fmt.Errorf("no eligible promotion candidate: %d queried, 0 eligible", len(evidence))
|
|
}
|
|
best := eligible[0]
|
|
for _, ev := range eligible[1:] {
|
|
if ev.CommittedLSN > best.CommittedLSN {
|
|
best = ev
|
|
} else if ev.CommittedLSN == best.CommittedLSN {
|
|
if ev.WALHeadLSN > best.WALHeadLSN {
|
|
best = ev
|
|
} else if ev.WALHeadLSN == best.WALHeadLSN {
|
|
if ev.HealthScore > best.HealthScore {
|
|
best = ev
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return best, nil
|
|
}
|
|
|
|
// promotionCandidate is the minimal info needed to query one candidate.
|
|
type promotionCandidate struct {
|
|
server string
|
|
path string
|
|
expectedEpoch uint64
|
|
}
|
|
|
|
// defaultBlockVSQueryEvidence is the production evidence querier.
|
|
// Once proto is regenerated on M01, this will call the VS gRPC
|
|
// QueryBlockPromotionEvidence RPC. Until then, it returns an explicit
|
|
// error so the fail-closed path is exercised.
|
|
func (ms *MasterServer) defaultBlockVSQueryEvidence(ctx context.Context, server, path string, expectedEpoch uint64) (BlockPromotionEvidence, error) {
|
|
// TODO(T2-transport): Replace with gRPC call after proto regen:
|
|
// operation.WithVolumeServerClient(false, pb.ServerAddress(server), ms.grpcDialOption, func(client volume_server_pb.VolumeServerClient) error {
|
|
// resp, err := client.QueryBlockPromotionEvidence(ctx, &volume_server_pb.QueryBlockPromotionEvidenceRequest{...})
|
|
// })
|
|
return BlockPromotionEvidence{}, fmt.Errorf("V2 promotion evidence RPC not yet available (pending proto regen on M01): server=%s path=%s", server, path)
|
|
}
|