Refactoring #3
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type VideoSlotRunner interface {
|
type VideoSlotRunner interface {
|
||||||
@@ -40,11 +41,21 @@ var (
|
|||||||
ErrSyncSlotRequired = errors.New("sync slot is required")
|
ErrSyncSlotRequired = errors.New("sync slot is required")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type SessionSnapshot struct {
|
||||||
|
Desired SessionConfig
|
||||||
|
Plan SessionPlan
|
||||||
|
Generation uint64
|
||||||
|
}
|
||||||
|
|
||||||
type SessionController struct {
|
type SessionController struct {
|
||||||
videoSlot VideoSlotRunner
|
videoSlot VideoSlotRunner
|
||||||
audioSlot AudioSlotRunner
|
audioSlot AudioSlotRunner
|
||||||
syncSlot SyncSlotRunner
|
syncSlot SyncSlotRunner
|
||||||
canSync SyncPredicate
|
canSync SyncPredicate
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
snapshot SessionSnapshot
|
||||||
|
hasSnapshot bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSessionController(
|
func NewSessionController(
|
||||||
@@ -100,6 +111,13 @@ func (c *SessionController) Run(
|
|||||||
desired := initial
|
desired := initial
|
||||||
runtime := c.startSessionRuntime(ctx, plan)
|
runtime := c.startSessionRuntime(ctx, plan)
|
||||||
|
|
||||||
|
generation := uint64(1)
|
||||||
|
c.publish(SessionSnapshot{
|
||||||
|
Desired: initial,
|
||||||
|
Plan: plan,
|
||||||
|
Generation: generation,
|
||||||
|
})
|
||||||
|
|
||||||
for {
|
for {
|
||||||
var runtimeDone <-chan struct{}
|
var runtimeDone <-chan struct{}
|
||||||
if runtime != nil {
|
if runtime != nil {
|
||||||
@@ -144,9 +162,17 @@ func (c *SessionController) Run(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if plan.Topology != nextPlan.Topology {
|
||||||
|
generation++
|
||||||
|
}
|
||||||
desired = nextDesired
|
desired = nextDesired
|
||||||
plan = nextPlan
|
plan = nextPlan
|
||||||
runtime = nextRuntime
|
runtime = nextRuntime
|
||||||
|
c.publish(SessionSnapshot{
|
||||||
|
Desired: desired,
|
||||||
|
Plan: plan,
|
||||||
|
Generation: generation,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -301,3 +327,18 @@ func sessionRuntimeSendError(ctx context.Context, runtime *sessionRuntime) error
|
|||||||
<-runtime.done
|
<-runtime.done
|
||||||
return unexpectedSessionRuntimeError(runtime.result)
|
return unexpectedSessionRuntimeError(runtime.result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *SessionController) Snapshot() (SessionSnapshot, bool) {
|
||||||
|
c.mu.RLock()
|
||||||
|
snapshot := c.snapshot
|
||||||
|
ok := c.hasSnapshot
|
||||||
|
c.mu.RUnlock()
|
||||||
|
return snapshot, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SessionController) publish(snapshot SessionSnapshot) {
|
||||||
|
c.mu.Lock()
|
||||||
|
c.snapshot = snapshot
|
||||||
|
c.hasSnapshot = true
|
||||||
|
c.mu.Unlock()
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package playback
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -187,6 +188,25 @@ func receiveIndependentStarts(t *testing.T, events <-chan controllerEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func waitControllerSnapshot(
|
||||||
|
t *testing.T,
|
||||||
|
controller *SessionController,
|
||||||
|
match func(SessionSnapshot) bool,
|
||||||
|
) SessionSnapshot {
|
||||||
|
t.Helper()
|
||||||
|
deadline := time.Now().Add(time.Second)
|
||||||
|
for {
|
||||||
|
if snapshot, ok := controller.Snapshot(); ok && match(snapshot) {
|
||||||
|
return snapshot
|
||||||
|
}
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
snapshot, ok := controller.Snapshot()
|
||||||
|
t.Fatalf("snapshot timed out: %#v, available=%t", snapshot, ok)
|
||||||
|
}
|
||||||
|
time.Sleep(time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSessionControllerUpdatesOnlyChangedIndependentSlot(t *testing.T) {
|
func TestSessionControllerUpdatesOnlyChangedIndependentSlot(t *testing.T) {
|
||||||
events := make(chan controllerEvent, 32)
|
events := make(chan controllerEvent, 32)
|
||||||
controller := newRecordingController(t, events)
|
controller := newRecordingController(t, events)
|
||||||
@@ -277,3 +297,126 @@ func TestSessionControllerCancellationStopsAndJoinsRuntime(t *testing.T) {
|
|||||||
t.Fatal("Run() did not stop after cancellation")
|
t.Fatal("Run() did not stop after cancellation")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSessionControllerSnapshotTracksDesiredPlanAndGeneration(t *testing.T) {
|
||||||
|
events := make(chan controllerEvent, 64)
|
||||||
|
controller := newRecordingController(t, events)
|
||||||
|
if snapshot, ok := controller.Snapshot(); ok {
|
||||||
|
t.Fatalf("Snapshot() before Run = %#v, true; want unavailable", snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
initial := validCommandSession()
|
||||||
|
initial.SyncRequested = false
|
||||||
|
commands := make(chan SessionCommand)
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() { done <- controller.Run(context.Background(), initial, commands) }()
|
||||||
|
receiveIndependentStarts(t, events)
|
||||||
|
|
||||||
|
snapshot := waitControllerSnapshot(t, controller, func(snapshot SessionSnapshot) bool {
|
||||||
|
return snapshot.Desired == initial
|
||||||
|
})
|
||||||
|
if snapshot.Generation != 1 || snapshot.Plan.Topology != TopologyIndependent {
|
||||||
|
t.Fatalf("initial snapshot = %#v", snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
newVideo := FeedConfig{Domain: "/new-video", UUID: "new-video", Active: true}
|
||||||
|
commands <- SessionCommand{Kind: CommandSetVideo, Config: newVideo}
|
||||||
|
receiveControllerEvent(t, events)
|
||||||
|
snapshot = waitControllerSnapshot(t, controller, func(snapshot SessionSnapshot) bool {
|
||||||
|
return snapshot.Desired.Video == newVideo
|
||||||
|
})
|
||||||
|
if snapshot.Generation != 1 || snapshot.Plan.Topology != TopologyIndependent {
|
||||||
|
t.Fatalf("same-topology snapshot = %#v", snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
commands <- SessionCommand{Kind: CommandEnableSync}
|
||||||
|
for {
|
||||||
|
if event := receiveControllerEvent(t, events); event.unit == UnitSync && event.action == "start" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snapshot = waitControllerSnapshot(t, controller, func(snapshot SessionSnapshot) bool {
|
||||||
|
return snapshot.Plan.Topology == TopologySynchronized
|
||||||
|
})
|
||||||
|
if snapshot.Generation != 2 || !snapshot.Desired.SyncRequested {
|
||||||
|
t.Fatalf("sync snapshot = %#v", snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
commands <- SessionCommand{Kind: CommandDisableSync}
|
||||||
|
for {
|
||||||
|
event := receiveControllerEvent(t, events)
|
||||||
|
if event.action == "start" && (event.unit == UnitVideo || event.unit == UnitAudio) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snapshot = waitControllerSnapshot(t, controller, func(snapshot SessionSnapshot) bool {
|
||||||
|
return snapshot.Plan.Topology == TopologyIndependent && !snapshot.Desired.SyncRequested
|
||||||
|
})
|
||||||
|
if snapshot.Generation != 3 {
|
||||||
|
t.Fatalf("independent snapshot generation = %d, want 3", snapshot.Generation)
|
||||||
|
}
|
||||||
|
|
||||||
|
close(commands)
|
||||||
|
if err := <-done; err != nil {
|
||||||
|
t.Fatalf("Run() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionControllerSnapshotRetainsUnavailableSyncRequest(t *testing.T) {
|
||||||
|
events := make(chan controllerEvent, 32)
|
||||||
|
controller, err := NewSessionController(
|
||||||
|
recordingVideoSlot{events},
|
||||||
|
recordingAudioSlot{events},
|
||||||
|
recordingSyncSlot{events},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
initial := validCommandSession()
|
||||||
|
initial.SyncRequested = false
|
||||||
|
commands := make(chan SessionCommand)
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() { done <- controller.Run(context.Background(), initial, commands) }()
|
||||||
|
receiveIndependentStarts(t, events)
|
||||||
|
commands <- SessionCommand{Kind: CommandEnableSync}
|
||||||
|
|
||||||
|
snapshot := waitControllerSnapshot(t, controller, func(snapshot SessionSnapshot) bool {
|
||||||
|
return snapshot.Desired.SyncRequested
|
||||||
|
})
|
||||||
|
if snapshot.Plan.Topology != TopologyIndependent || snapshot.Generation != 1 {
|
||||||
|
t.Fatalf("unsupported-sync snapshot = %#v", snapshot)
|
||||||
|
}
|
||||||
|
close(commands)
|
||||||
|
if err := <-done; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionControllerSnapshotConcurrentReads(t *testing.T) {
|
||||||
|
events := make(chan controllerEvent, 32)
|
||||||
|
controller := newRecordingController(t, events)
|
||||||
|
initial := validCommandSession()
|
||||||
|
initial.SyncRequested = false
|
||||||
|
commands := make(chan SessionCommand)
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() { done <- controller.Run(context.Background(), initial, commands) }()
|
||||||
|
receiveIndependentStarts(t, events)
|
||||||
|
waitControllerSnapshot(t, controller, func(SessionSnapshot) bool { return true })
|
||||||
|
|
||||||
|
var readers sync.WaitGroup
|
||||||
|
for range 8 {
|
||||||
|
readers.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer readers.Done()
|
||||||
|
for range 1_000 {
|
||||||
|
controller.Snapshot()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
readers.Wait()
|
||||||
|
close(commands)
|
||||||
|
if err := <-done; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user