Files
seaweedfs/sw-block/engine/replication/phase14_command_test.go
T
pingqiuandClaude Opus 4.6 a6fc8545b9 feat: Phase 14A+14B — V2 core publication ownership + command semantics
14A: Publication as explicit core-owned state
- state.go: PublicationView on VolumeState, explicit gate reasons
- engine.go: mode→readiness→publication chain with named gates
  (awaiting_role_apply, awaiting_shipper_configured, awaiting_barrier_durability)
- projection.go: PublicationProjection carries publication truth
- RF=1/no-replicas → allocated_only (CP13-9 constraint in core)
- phase14_core_test.go: strengthened publication closure + RF=1 proof

14B: Command emission bounded by semantic gap
- engine.go: repeated same-assignment skips redundant commands,
  repeated same-reason BarrierRejected skips duplicate invalidation,
  command-state tracking on VolumeState
- command.go: new command types for bounded emission
- event.go: new boundary events
- phase14_command_test.go: exact command sequences frozen as proofs
  (primary/replica repeated assignment, assignment changed, repeated failure)
- phase14_boundary_test.go: boundary/recovery structural tests

All tests pass in sw-block/engine/replication.
Phase 14 docs updated (14A accepted, 14B active→14C planned).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 16:52:55 -07:00

228 lines
6.7 KiB
Go

package replication
import (
"reflect"
"testing"
)
func TestPhase14_CommandSequence_PrimaryAssignmentIsBounded(t *testing.T) {
core := NewCoreEngine()
ev := AssignmentDelivered{
ID: "vol-cmd-primary",
Epoch: 1,
Role: RolePrimary,
Replicas: []ReplicaAssignment{
{ReplicaID: "replica-1", Endpoint: Endpoint{DataAddr: "10.0.0.10:9333", CtrlAddr: "10.0.0.10:9334", Version: 1}},
},
}
result := core.ApplyEvent(ev)
assertCommandNames(t, result.Commands, []string{
"apply_role",
"configure_shipper",
"publish_projection",
})
result = core.ApplyEvent(ev)
assertCommandNames(t, result.Commands, nil)
}
func TestPhase14_CommandSequence_ReplicaAssignmentIsBounded(t *testing.T) {
core := NewCoreEngine()
ev := AssignmentDelivered{
ID: "vol-cmd-replica",
Epoch: 3,
Role: RoleReplica,
Replicas: []ReplicaAssignment{
{ReplicaID: "replica-1", Endpoint: Endpoint{DataAddr: "10.0.0.11:9333", CtrlAddr: "10.0.0.11:9334", Version: 1}},
},
}
result := core.ApplyEvent(ev)
assertCommandNames(t, result.Commands, []string{
"apply_role",
"start_receiver",
"publish_projection",
})
result = core.ApplyEvent(ev)
assertCommandNames(t, result.Commands, nil)
}
func TestPhase14_CommandSequence_AssignmentChangeReissuesNeededCommand(t *testing.T) {
core := NewCoreEngine()
initial := AssignmentDelivered{
ID: "vol-cmd-change",
Epoch: 5,
Role: RolePrimary,
Replicas: []ReplicaAssignment{
{ReplicaID: "replica-1", Endpoint: Endpoint{DataAddr: "10.0.0.12:9333", CtrlAddr: "10.0.0.12:9334", Version: 1}},
},
}
core.ApplyEvent(initial)
core.ApplyEvent(RoleApplied{ID: "vol-cmd-change"})
core.ApplyEvent(ShipperConfiguredObserved{ID: "vol-cmd-change"})
core.ApplyEvent(ShipperConnectedObserved{ID: "vol-cmd-change"})
changed := AssignmentDelivered{
ID: "vol-cmd-change",
Epoch: 5,
Role: RolePrimary,
Replicas: []ReplicaAssignment{
{ReplicaID: "replica-1", Endpoint: Endpoint{DataAddr: "10.0.0.13:9333", CtrlAddr: "10.0.0.13:9334", Version: 2}},
},
}
result := core.ApplyEvent(changed)
assertCommandNames(t, result.Commands, []string{
"configure_shipper",
"publish_projection",
})
}
func TestPhase14_CommandSequence_InvalidateOnlyOnNewFailureTransition(t *testing.T) {
core := NewCoreEngine()
core.ApplyEvent(AssignmentDelivered{
ID: "vol-cmd-failure",
Epoch: 1,
Role: RolePrimary,
Replicas: []ReplicaAssignment{
{ReplicaID: "replica-1", Endpoint: Endpoint{DataAddr: "10.0.0.14:9333", CtrlAddr: "10.0.0.14:9334", Version: 1}},
},
})
core.ApplyEvent(RoleApplied{ID: "vol-cmd-failure"})
core.ApplyEvent(ShipperConfiguredObserved{ID: "vol-cmd-failure"})
core.ApplyEvent(ShipperConnectedObserved{ID: "vol-cmd-failure"})
core.ApplyEvent(BarrierAccepted{ID: "vol-cmd-failure", FlushedLSN: 9})
result := core.ApplyEvent(BarrierRejected{ID: "vol-cmd-failure", Reason: "timeout"})
assertCommandNames(t, result.Commands, []string{
"invalidate_session",
"publish_projection",
})
result = core.ApplyEvent(BarrierRejected{ID: "vol-cmd-failure", Reason: "timeout"})
assertCommandNames(t, result.Commands, nil)
}
func TestPhase14_CommandSequence_PublishOnlyWhenProjectionChanges(t *testing.T) {
core := NewCoreEngine()
core.ApplyEvent(AssignmentDelivered{
ID: "vol-cmd-publish",
Epoch: 2,
Role: RolePrimary,
Replicas: []ReplicaAssignment{
{ReplicaID: "replica-1", Endpoint: Endpoint{DataAddr: "10.0.0.15:9333", CtrlAddr: "10.0.0.15:9334", Version: 1}},
},
})
result := core.ApplyEvent(RoleApplied{ID: "vol-cmd-publish"})
assertCommandNames(t, result.Commands, []string{
"publish_projection",
})
result = core.ApplyEvent(RoleApplied{ID: "vol-cmd-publish"})
assertCommandNames(t, result.Commands, nil)
}
func TestPhase14_CommandSequence_CatchUpStartIsBounded(t *testing.T) {
core := NewCoreEngine()
core.ApplyEvent(AssignmentDelivered{
ID: "vol-cmd-catchup",
Epoch: 6,
Role: RoleReplica,
Replicas: []ReplicaAssignment{
{ReplicaID: "replica-1", Endpoint: Endpoint{DataAddr: "10.0.0.16:9333", CtrlAddr: "10.0.0.16:9334", Version: 1}},
},
})
core.ApplyEvent(RoleApplied{ID: "vol-cmd-catchup"})
core.ApplyEvent(ReceiverReadyObserved{ID: "vol-cmd-catchup"})
result := core.ApplyEvent(CatchUpPlanned{ID: "vol-cmd-catchup", TargetLSN: 55})
assertCommandNames(t, result.Commands, []string{
"start_catchup",
"publish_projection",
})
result = core.ApplyEvent(CatchUpPlanned{ID: "vol-cmd-catchup", TargetLSN: 55})
assertCommandNames(t, result.Commands, nil)
}
func TestPhase14_CommandSequence_RebuildStartIsBounded(t *testing.T) {
core := NewCoreEngine()
core.ApplyEvent(AssignmentDelivered{
ID: "vol-cmd-rebuild",
Epoch: 7,
Role: RoleReplica,
Replicas: []ReplicaAssignment{
{ReplicaID: "replica-1", Endpoint: Endpoint{DataAddr: "10.0.0.17:9333", CtrlAddr: "10.0.0.17:9334", Version: 1}},
},
})
core.ApplyEvent(RoleApplied{ID: "vol-cmd-rebuild"})
core.ApplyEvent(ReceiverReadyObserved{ID: "vol-cmd-rebuild"})
core.ApplyEvent(NeedsRebuildObserved{ID: "vol-cmd-rebuild", Reason: "gap_too_large"})
result := core.ApplyEvent(RebuildStarted{ID: "vol-cmd-rebuild", TargetLSN: 80})
assertCommandNames(t, result.Commands, []string{
"start_rebuild",
"publish_projection",
})
result = core.ApplyEvent(RebuildStarted{ID: "vol-cmd-rebuild", TargetLSN: 80})
assertCommandNames(t, result.Commands, nil)
}
func TestPhase14_CommandSequence_AssignmentChangeAllowsFreshRecoveryStart(t *testing.T) {
core := NewCoreEngine()
core.ApplyEvent(AssignmentDelivered{
ID: "vol-cmd-reassign",
Epoch: 1,
Role: RoleReplica,
Replicas: []ReplicaAssignment{
{ReplicaID: "replica-1", Endpoint: Endpoint{DataAddr: "10.0.0.18:9333", CtrlAddr: "10.0.0.18:9334", Version: 1}},
},
})
core.ApplyEvent(RoleApplied{ID: "vol-cmd-reassign"})
core.ApplyEvent(ReceiverReadyObserved{ID: "vol-cmd-reassign"})
result := core.ApplyEvent(CatchUpPlanned{ID: "vol-cmd-reassign", TargetLSN: 90})
assertCommandNames(t, result.Commands, []string{
"start_catchup",
"publish_projection",
})
core.ApplyEvent(AssignmentDelivered{
ID: "vol-cmd-reassign",
Epoch: 2,
Role: RoleReplica,
Replicas: []ReplicaAssignment{
{ReplicaID: "replica-1", Endpoint: Endpoint{DataAddr: "10.0.0.19:9333", CtrlAddr: "10.0.0.19:9334", Version: 2}},
},
})
result = core.ApplyEvent(CatchUpPlanned{ID: "vol-cmd-reassign", TargetLSN: 90})
assertCommandNames(t, result.Commands, []string{
"start_catchup",
"publish_projection",
})
}
func assertCommandNames(t *testing.T, cmds []Command, want []string) {
t.Helper()
got := make([]string, 0, len(cmds))
for _, cmd := range cmds {
got = append(got, cmd.commandName())
}
if want == nil {
want = []string{}
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("commands=%v, want %v", got, want)
}
}