Files
seaweedfs/weed/server/blockcmd/dispatch.go
pingqiu 43dbebfa04 refactor: close bounded recovery drain and invalidation seams
Move removed-replica drain and replica-scoped invalidation onto explicit core-command paths so the widened multi-replica runtime no longer depends on coarse host-side recovery handling.

Made-with: Cursor
2026-04-04 11:01:12 -07:00

158 lines
4.8 KiB
Go

package blockcmd
import (
"fmt"
engine "github.com/seaweedfs/seaweedfs/sw-block/engine/replication"
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol"
)
// Ops executes concrete backend-integrated command operations.
// It does not own command dispatch or host-side event semantics.
type Ops interface {
ApplyRole(assignment blockvol.BlockVolumeAssignment) (bool, error)
StartReceiver(assignment blockvol.BlockVolumeAssignment) (bool, error)
ConfigureShipper(volumeID string, replicas []engine.ReplicaAssignment) (executed bool, shipperConnected bool, err error)
StartRecoveryTask(replicaID string, assignment blockvol.BlockVolumeAssignment) (bool, error)
DrainRecoveryTask(replicaID, reason string) (bool, error)
InvalidateSession(volumeID, replicaID, reason string) (bool, error)
StartCatchUp(replicaID string, targetLSN uint64) (bool, error)
StartRebuild(replicaID string, targetLSN uint64) (bool, error)
}
// HostEffects applies server-adapter side effects after concrete command
// execution succeeds. These effects stay out of the backend-binding layer.
type HostEffects interface {
RecordCommand(volumeID, name string)
EmitCoreEvent(ev engine.Event)
PublishProjection(volumeID string, projection engine.PublicationProjection) error
}
// Dispatcher runs engine commands against concrete ops and host effects.
type Dispatcher struct {
ops Ops
effects HostEffects
}
func NewDispatcher(ops Ops, effects HostEffects) *Dispatcher {
return &Dispatcher{ops: ops, effects: effects}
}
func (d *Dispatcher) Run(cmds []engine.Command, assignment *blockvol.BlockVolumeAssignment) error {
for _, cmd := range cmds {
switch v := cmd.(type) {
case engine.ApplyRoleCommand:
a, ok, err := requireAssignment("apply_role", v.VolumeID, assignment)
if err != nil || !ok {
return err
}
executed, err := d.ops.ApplyRole(a)
if err != nil {
return err
}
if executed {
d.effects.RecordCommand(v.VolumeID, "apply_role")
}
case engine.StartReceiverCommand:
a, ok, err := requireAssignment("start_receiver", v.VolumeID, assignment)
if err != nil || !ok {
return err
}
if a.ReplicaDataAddr == "" || a.ReplicaCtrlAddr == "" {
continue
}
executed, err := d.ops.StartReceiver(a)
if err != nil {
return err
}
if executed {
d.effects.RecordCommand(v.VolumeID, "start_receiver")
d.effects.EmitCoreEvent(engine.ReceiverReadyObserved{ID: v.VolumeID})
}
case engine.ConfigureShipperCommand:
executed, connected, err := d.ops.ConfigureShipper(v.VolumeID, v.Replicas)
if err != nil {
return err
}
if executed {
d.effects.RecordCommand(v.VolumeID, "configure_shipper")
d.effects.EmitCoreEvent(engine.ShipperConfiguredObserved{ID: v.VolumeID})
if connected {
d.effects.EmitCoreEvent(engine.ShipperConnectedObserved{ID: v.VolumeID})
}
}
case engine.StartRecoveryTaskCommand:
a, ok, err := requireAssignment("start_recovery_task", v.VolumeID, assignment)
if err != nil || !ok {
return err
}
if v.ReplicaID == "" {
continue
}
executed, err := d.ops.StartRecoveryTask(v.ReplicaID, a)
if err != nil {
return err
}
if executed {
d.effects.RecordCommand(v.VolumeID, "start_recovery_task")
}
case engine.DrainRecoveryTaskCommand:
if v.ReplicaID == "" {
continue
}
executed, err := d.ops.DrainRecoveryTask(v.ReplicaID, v.Reason)
if err != nil {
return err
}
if executed {
d.effects.RecordCommand(v.VolumeID, "drain_recovery_task")
}
case engine.InvalidateSessionCommand:
executed, err := d.ops.InvalidateSession(v.VolumeID, v.ReplicaID, v.Reason)
if err != nil {
return err
}
if executed {
d.effects.RecordCommand(v.VolumeID, "invalidate_session")
}
case engine.StartCatchUpCommand:
if v.ReplicaID == "" {
continue
}
executed, err := d.ops.StartCatchUp(v.ReplicaID, v.TargetLSN)
if err != nil {
return err
}
if executed {
d.effects.RecordCommand(v.VolumeID, "start_catchup")
}
case engine.StartRebuildCommand:
if v.ReplicaID == "" {
continue
}
executed, err := d.ops.StartRebuild(v.ReplicaID, v.TargetLSN)
if err != nil {
return err
}
if executed {
d.effects.RecordCommand(v.VolumeID, "start_rebuild")
}
case engine.PublishProjectionCommand:
if err := d.effects.PublishProjection(v.VolumeID, v.Projection); err != nil {
return fmt.Errorf("publish projection %s: %w", v.VolumeID, err)
}
}
}
return nil
}
func requireAssignment(action, volumeID string, assignment *blockvol.BlockVolumeAssignment) (blockvol.BlockVolumeAssignment, bool, error) {
if assignment == nil {
return blockvol.BlockVolumeAssignment{}, false, nil
}
if assignment.Path != volumeID {
return blockvol.BlockVolumeAssignment{}, false, fmt.Errorf("block service: core %s path mismatch %q != %q", action, assignment.Path, volumeID)
}
return *assignment, true, nil
}