mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-05-18 15:51:29 +00:00
Add host-side protocol state seam that derives per-replica execution state from V2 sender/session snapshots and blocks live-tail WAL shipping while an active recovery session is in progress. New file: weed/server/block_protocol_state.go - replicaProtocolExecutionState derived from engine snapshots - LiveEligible=false during active catch-up/rebuild sessions - bindProtocolExecutionPolicy wires policy into BlockVol - syncProtocolExecutionState called after assignments + core events Data plane changes: - WALShipper.Ship() checks liveShippingPolicy before dial/send - BlockVol.SetLiveShippingPolicy persists across shipper group rebuilds - ShipperGroup propagates policy to all shippers Design contract: sw-block/design/v2-protocol-aware-execution.md Scope: WAL-first rollout only. Prevents illegal live-tail delivery during active recovery. Does not change snapshot/build behavior or move backlog. Next wave: bounded WAL catch-up under same contract. Tests: 4 unit/component tests for phase gate behavior, plus bootstrap seam tests that confirmed the two pre-existing bugs locally. 13 files changed, 900 insertions, 69 deletions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
165 lines
4.7 KiB
Go
165 lines
4.7 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"sync"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol"
|
|
)
|
|
|
|
// BlockAssignmentQueue holds pending assignments per volume server.
|
|
// Assignments are retained until confirmed by a matching heartbeat (F1).
|
|
type BlockAssignmentQueue struct {
|
|
mu sync.Mutex
|
|
queues map[string][]blockvol.BlockVolumeAssignment // server -> pending
|
|
}
|
|
|
|
// NewBlockAssignmentQueue creates an empty queue.
|
|
func NewBlockAssignmentQueue() *BlockAssignmentQueue {
|
|
return &BlockAssignmentQueue{
|
|
queues: make(map[string][]blockvol.BlockVolumeAssignment),
|
|
}
|
|
}
|
|
|
|
// Enqueue adds a single assignment to the server's queue.
|
|
func (q *BlockAssignmentQueue) Enqueue(server string, a blockvol.BlockVolumeAssignment) {
|
|
q.mu.Lock()
|
|
defer q.mu.Unlock()
|
|
q.queues[server] = append(q.queues[server], a)
|
|
}
|
|
|
|
// EnqueueBatch adds multiple assignments to the server's queue.
|
|
func (q *BlockAssignmentQueue) EnqueueBatch(server string, as []blockvol.BlockVolumeAssignment) {
|
|
if len(as) == 0 {
|
|
return
|
|
}
|
|
q.mu.Lock()
|
|
defer q.mu.Unlock()
|
|
q.queues[server] = append(q.queues[server], as...)
|
|
}
|
|
|
|
// Peek returns a copy of pending assignments for the server without removing them.
|
|
// Stale assignments (superseded by a newer epoch for the same path) are pruned.
|
|
func (q *BlockAssignmentQueue) Peek(server string) []blockvol.BlockVolumeAssignment {
|
|
q.mu.Lock()
|
|
defer q.mu.Unlock()
|
|
|
|
pending := q.queues[server]
|
|
if len(pending) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Prune stale: keep only the latest epoch per path.
|
|
latest := make(map[string]uint64, len(pending))
|
|
for _, a := range pending {
|
|
if a.Epoch > latest[a.Path] {
|
|
latest[a.Path] = a.Epoch
|
|
}
|
|
}
|
|
pruned := pending[:0]
|
|
for _, a := range pending {
|
|
if a.Epoch >= latest[a.Path] {
|
|
pruned = append(pruned, a)
|
|
}
|
|
}
|
|
q.queues[server] = pruned
|
|
|
|
// Return a copy.
|
|
out := make([]blockvol.BlockVolumeAssignment, len(pruned))
|
|
copy(out, pruned)
|
|
return out
|
|
}
|
|
|
|
// Confirm removes a matching assignment (same path and epoch) from the server's queue.
|
|
func (q *BlockAssignmentQueue) Confirm(server string, path string, epoch uint64) {
|
|
q.mu.Lock()
|
|
defer q.mu.Unlock()
|
|
|
|
pending := q.queues[server]
|
|
for i, a := range pending {
|
|
if a.Path == path && a.Epoch == epoch {
|
|
q.queues[server] = append(pending[:i], pending[i+1:]...)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// ConfirmFromHeartbeat batch-confirms assignments that match reported heartbeat info.
|
|
// 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
|
|
}
|
|
q.mu.Lock()
|
|
defer q.mu.Unlock()
|
|
|
|
pending := q.queues[server]
|
|
if len(pending) == 0 {
|
|
return
|
|
}
|
|
|
|
// Keep only assignments not confirmed.
|
|
kept := pending[:0]
|
|
for _, a := range pending {
|
|
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 {
|
|
// A primary refresh assignment that carries replica transport should not
|
|
// be confirmed while the local V2 core still projects allocated_only.
|
|
// Otherwise the master can drop the refresh based only on legacy
|
|
// transport fields before the VS actually re-applies the assignment to
|
|
// the core and grows replica membership.
|
|
if blockvol.RoleFromWire(a.Role) == blockvol.RolePrimary &&
|
|
info.EngineProjectionMode != "" &&
|
|
info.EngineProjectionMode == "allocated_only" {
|
|
return false
|
|
}
|
|
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()
|
|
defer q.mu.Unlock()
|
|
return len(q.queues[server])
|
|
}
|
|
|
|
// TotalPending returns the total number of pending assignments across all servers.
|
|
func (q *BlockAssignmentQueue) TotalPending() int {
|
|
q.mu.Lock()
|
|
defer q.mu.Unlock()
|
|
total := 0
|
|
for _, queue := range q.queues {
|
|
total += len(queue)
|
|
}
|
|
return total
|
|
}
|