architecture cleanup

This commit is contained in:
Dmitry Sergeev
2026-09-02 00:08:53 +03:00
parent f3f53fc7ad
commit e7b032ce77
10 changed files with 141 additions and 122 deletions
+11 -12
View File
@@ -20,10 +20,6 @@ type PlaylistEvent struct {
Failure Status
}
// PlaylistReadiness is retained as an alias for callers that only publish
// ready events. Its zero Kind is PlaylistEventReady.
type PlaylistReadiness = PlaylistEvent
type playlistTimer interface {
C() <-chan time.Time
Stop() bool
@@ -100,7 +96,7 @@ func NewPlaylistController(
func (c *PlaylistController) Run(
ctx context.Context,
commands <-chan PlaylistCommand,
readiness <-chan PlaylistReadiness,
events <-chan PlaylistEvent,
) error {
state := PlaylistState{}
revision := uint64(0)
@@ -182,13 +178,13 @@ func (c *PlaylistController) Run(
state = next
c.publish(state, revision, timing)
case ready, ok := <-readiness:
case event, ok := <-events:
if !ok {
readiness = nil
events = nil
continue
}
if ready.Kind == PlaylistEventFailed {
if ready.Revision != revision {
if event.Kind == PlaylistEventFailed {
if event.Revision != revision {
continue
}
stopTimer()
@@ -197,7 +193,7 @@ func (c *PlaylistController) Run(
EntryName: c.playlist.Entries[state.CurrentIndex].Name,
Revision: revision,
Policy: c.playlist.OnFailure,
Status: ready.Failure,
Status: event.Failure,
})
// A failed entry must not retain a live or apparently active
// duration clock, even when the policy is to wait.
@@ -228,8 +224,11 @@ func (c *PlaylistController) Run(
c.publish(state, revision, timing)
continue
}
if event.Kind != PlaylistEventReady {
continue
}
if timing.Paused &&
ready.Revision == timing.Revision &&
event.Revision == timing.Revision &&
timing.Duration > 0 &&
!timing.Expired {
timing.Ready = true
@@ -238,7 +237,7 @@ func (c *PlaylistController) Run(
}
nextTiming, started := StartPlaylistTiming(
timing,
ready.Revision,
event.Revision,
c.now(),
)
if !started {
@@ -68,9 +68,11 @@ func TestPlaylistControllerStartsOneTimerForMatchingReadiness(t *testing.T) {
return snapshot.Revision == 1
})
readiness <- PlaylistReadiness{Revision: 0}
readiness <- PlaylistEvent{Revision: 0}
assertNoPlaylistTimer(t, timers)
readiness <- PlaylistReadiness{Revision: 1}
readiness <- PlaylistEvent{Revision: 1, Kind: PlaylistEventKind(99)}
assertNoPlaylistTimer(t, timers)
readiness <- PlaylistEvent{Revision: 1}
timer := receiveFakePlaylistTimer(t, timers)
snapshot := waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
@@ -79,7 +81,7 @@ func TestPlaylistControllerStartsOneTimerForMatchingReadiness(t *testing.T) {
if snapshot.Timing.Deadline != now.Add(10*time.Second) {
t.Fatalf("deadline = %v, want %v", snapshot.Timing.Deadline, now.Add(10*time.Second))
}
readiness <- PlaylistReadiness{Revision: 1}
readiness <- PlaylistEvent{Revision: 1}
assertNoPlaylistTimer(t, timers)
if timer.isStopped() {
t.Fatal("timer stopped after duplicate readiness")
@@ -101,7 +103,7 @@ func TestPlaylistControllerTimerExpiryAdvances(t *testing.T) {
commands <- PlaylistCommand{Kind: PlaylistNext}
_ = receivePlaylistSession(t, sessions)
readiness <- PlaylistReadiness{Revision: 1}
readiness <- PlaylistEvent{Revision: 1}
timer := receiveFakePlaylistTimer(t, timers)
timer.fire(now.Add(10 * time.Second))
@@ -132,7 +134,7 @@ func TestPlaylistControllerLoopingTimerExpiryWraps(t *testing.T) {
commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 1}
_ = receivePlaylistSession(t, sessions)
readiness <- PlaylistReadiness{Revision: 1}
readiness <- PlaylistEvent{Revision: 1}
timer := receiveFakePlaylistTimer(t, timers)
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
return snapshot.Timing.Started
@@ -160,7 +162,7 @@ func TestPlaylistControllerFinalExpiryStopsWithoutCommand(t *testing.T) {
commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 1}
_ = receivePlaylistSession(t, sessions)
readiness <- PlaylistReadiness{Revision: 1}
readiness <- PlaylistEvent{Revision: 1}
timer := receiveFakePlaylistTimer(t, timers)
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
return snapshot.Timing.Started
@@ -192,7 +194,7 @@ func TestPlaylistControllerManualSelectionStopsOldTimer(t *testing.T) {
commands <- PlaylistCommand{Kind: PlaylistNext}
_ = receivePlaylistSession(t, sessions)
readiness <- PlaylistReadiness{Revision: 1}
readiness <- PlaylistEvent{Revision: 1}
oldTimer := receiveFakePlaylistTimer(t, timers)
commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 1}
@@ -229,7 +231,7 @@ func TestPlaylistControllerZeroDurationDoesNotCreateTimer(t *testing.T) {
commands <- PlaylistCommand{Kind: PlaylistNext}
_ = receivePlaylistSession(t, sessions)
readiness <- PlaylistReadiness{Revision: 1}
readiness <- PlaylistEvent{Revision: 1}
assertNoPlaylistTimer(t, timers)
close(commands)
@@ -244,7 +246,7 @@ func TestPlaylistControllerCancellationStopsTimer(t *testing.T) {
commands <- PlaylistCommand{Kind: PlaylistNext}
_ = receivePlaylistSession(t, sessions)
readiness <- PlaylistReadiness{Revision: 1}
readiness <- PlaylistEvent{Revision: 1}
timer := receiveFakePlaylistTimer(t, timers)
cancel()
if err := waitForPlaylistResult(t, result); !errors.Is(err, context.Canceled) {
@@ -262,7 +264,7 @@ func TestPlaylistControllerPauseAndResumeTimer(t *testing.T) {
commands <- PlaylistCommand{Kind: PlaylistNext}
_ = receivePlaylistSession(t, sessions)
readiness <- PlaylistReadiness{Revision: 1}
readiness <- PlaylistEvent{Revision: 1}
oldTimer := receiveFakePlaylistTimer(t, timers)
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
return snapshot.Timing.Started
@@ -344,7 +346,7 @@ func TestPlaylistControllerRecordsQueuedReadinessWhilePaused(t *testing.T) {
return snapshot.Timing.Paused
})
readiness <- PlaylistReadiness{Revision: 1}
readiness <- PlaylistEvent{Revision: 1}
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
return snapshot.Timing.Paused && snapshot.Timing.Ready
})
@@ -368,7 +370,7 @@ func startTimedPlaylistController(
) (
*PlaylistController,
chan PlaylistCommand,
chan PlaylistReadiness,
chan PlaylistEvent,
chan SessionCommand,
chan *fakePlaylistTimer,
time.Time,
@@ -390,7 +392,7 @@ func startTimedPlaylistController(
return timer
}
commands := make(chan PlaylistCommand, 16)
readiness := make(chan PlaylistReadiness, 16)
readiness := make(chan PlaylistEvent, 16)
ctx, cancel := context.WithCancel(context.Background())
result := make(chan error, 1)
go func() { result <- controller.Run(ctx, commands, readiness) }()
@@ -18,45 +18,45 @@ type PlaybackStatusSnapshotSource interface {
SnapshotAll() PlaybackStatusSnapshot
}
type playlistReadinessTicker interface {
type playlistEventTicker interface {
C() <-chan time.Time
Stop()
}
type playlistReadinessTickerFactory func(time.Duration) playlistReadinessTicker
type playlistEventTickerFactory func(time.Duration) playlistEventTicker
type realPlaylistReadinessTicker struct {
type realPlaylistEventTicker struct {
ticker *time.Ticker
}
func (t realPlaylistReadinessTicker) C() <-chan time.Time { return t.ticker.C }
func (t realPlaylistReadinessTicker) Stop() { t.ticker.Stop() }
func (t realPlaylistEventTicker) C() <-chan time.Time { return t.ticker.C }
func (t realPlaylistEventTicker) Stop() { t.ticker.Stop() }
var (
ErrPlaylistSnapshotSourceRequired = errors.New("playlist snapshot source is required")
ErrSessionSnapshotSourceRequired = errors.New("session snapshot source is required")
ErrStatusSnapshotSourceRequired = errors.New("playback status snapshot source is required")
ErrPlaylistReadinessOutputRequired = errors.New("playlist readiness output channel is required")
ErrPlaylistReadinessInterval = errors.New("playlist readiness interval must be positive")
ErrPlaylistSnapshotSourceRequired = errors.New("playlist snapshot source is required")
ErrSessionSnapshotSourceRequired = errors.New("session snapshot source is required")
ErrStatusSnapshotSourceRequired = errors.New("playback status snapshot source is required")
ErrPlaylistEventOutputRequired = errors.New("playlist event output channel is required")
ErrPlaylistEventInterval = errors.New("playlist event interval must be positive")
)
type PlaylistReadinessCoordinator struct {
type PlaylistEventCoordinator struct {
playlist PlaylistSnapshotSource
session SessionSnapshotSource
statuses PlaybackStatusSnapshotSource
output chan<- PlaylistReadiness
output chan<- PlaylistEvent
interval time.Duration
newTicker playlistReadinessTickerFactory
newTicker playlistEventTickerFactory
}
func NewPlaylistReadinessCoordinator(
func NewPlaylistEventCoordinator(
playlist PlaylistSnapshotSource,
session SessionSnapshotSource,
statuses PlaybackStatusSnapshotSource,
output chan<- PlaylistReadiness,
output chan<- PlaylistEvent,
interval time.Duration,
) (*PlaylistReadinessCoordinator, error) {
) (*PlaylistEventCoordinator, error) {
if playlist == nil {
return nil, ErrPlaylistSnapshotSourceRequired
}
@@ -67,25 +67,25 @@ func NewPlaylistReadinessCoordinator(
return nil, ErrStatusSnapshotSourceRequired
}
if output == nil {
return nil, ErrPlaylistReadinessOutputRequired
return nil, ErrPlaylistEventOutputRequired
}
if interval <= 0 {
return nil, ErrPlaylistReadinessInterval
return nil, ErrPlaylistEventInterval
}
return &PlaylistReadinessCoordinator{
return &PlaylistEventCoordinator{
playlist: playlist,
session: session,
statuses: statuses,
output: output,
interval: interval,
newTicker: func(interval time.Duration) playlistReadinessTicker {
return realPlaylistReadinessTicker{ticker: time.NewTicker(interval)}
newTicker: func(interval time.Duration) playlistEventTicker {
return realPlaylistEventTicker{ticker: time.NewTicker(interval)}
},
}, nil
}
func (c *PlaylistReadinessCoordinator) Run(ctx context.Context) error {
func (c *PlaylistEventCoordinator) Run(ctx context.Context) error {
ticker := c.newTicker(c.interval)
defer ticker.Stop()
@@ -139,7 +139,7 @@ func (c *PlaylistReadinessCoordinator) Run(ctx context.Context) error {
continue
}
ready := PlaylistReadiness{Revision: playlistSnapshot.Revision}
ready := PlaylistEvent{Revision: playlistSnapshot.Revision}
select {
case <-ctx.Done():
return ctx.Err()
@@ -63,55 +63,55 @@ func (s *fakePlaybackStatusSnapshotSource) set(snapshot PlaybackStatusSnapshot)
s.mu.Unlock()
}
type fakePlaylistReadinessTicker struct {
type fakePlaylistEventTicker struct {
ch chan time.Time
mu sync.Mutex
stopped bool
}
func newFakePlaylistReadinessTicker() *fakePlaylistReadinessTicker {
return &fakePlaylistReadinessTicker{ch: make(chan time.Time, 16)}
func newFakePlaylistEventTicker() *fakePlaylistEventTicker {
return &fakePlaylistEventTicker{ch: make(chan time.Time, 16)}
}
func (t *fakePlaylistReadinessTicker) C() <-chan time.Time { return t.ch }
func (t *fakePlaylistReadinessTicker) Stop() {
func (t *fakePlaylistEventTicker) C() <-chan time.Time { return t.ch }
func (t *fakePlaylistEventTicker) Stop() {
t.mu.Lock()
t.stopped = true
t.mu.Unlock()
}
func (t *fakePlaylistReadinessTicker) tick() { t.ch <- time.Now() }
func (t *fakePlaylistReadinessTicker) isStopped() bool {
func (t *fakePlaylistEventTicker) tick() { t.ch <- time.Now() }
func (t *fakePlaylistEventTicker) isStopped() bool {
t.mu.Lock()
defer t.mu.Unlock()
return t.stopped
}
func TestNewPlaylistReadinessCoordinatorValidatesDependencies(t *testing.T) {
func TestNewPlaylistEventCoordinatorValidatesDependencies(t *testing.T) {
playlist := &fakePlaylistSnapshotSource{}
session := &fakeSessionSnapshotSource{}
statuses := &fakePlaybackStatusSnapshotSource{}
output := make(chan PlaylistReadiness)
output := make(chan PlaylistEvent)
tests := []struct {
name string
playlist PlaylistSnapshotSource
session SessionSnapshotSource
statuses PlaybackStatusSnapshotSource
output chan<- PlaylistReadiness
output chan<- PlaylistEvent
interval time.Duration
wantErr error
}{
{name: "playlist", session: session, statuses: statuses, output: output, interval: time.Millisecond, wantErr: ErrPlaylistSnapshotSourceRequired},
{name: "session", playlist: playlist, statuses: statuses, output: output, interval: time.Millisecond, wantErr: ErrSessionSnapshotSourceRequired},
{name: "statuses", playlist: playlist, session: session, output: output, interval: time.Millisecond, wantErr: ErrStatusSnapshotSourceRequired},
{name: "output", playlist: playlist, session: session, statuses: statuses, interval: time.Millisecond, wantErr: ErrPlaylistReadinessOutputRequired},
{name: "interval", playlist: playlist, session: session, statuses: statuses, output: output, wantErr: ErrPlaylistReadinessInterval},
{name: "output", playlist: playlist, session: session, statuses: statuses, interval: time.Millisecond, wantErr: ErrPlaylistEventOutputRequired},
{name: "interval", playlist: playlist, session: session, statuses: statuses, output: output, wantErr: ErrPlaylistEventInterval},
{name: "valid", playlist: playlist, session: session, statuses: statuses, output: output, interval: time.Millisecond},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
coordinator, err := NewPlaylistReadinessCoordinator(
coordinator, err := NewPlaylistEventCoordinator(
test.playlist,
test.session,
test.statuses,
@@ -119,7 +119,7 @@ func TestNewPlaylistReadinessCoordinatorValidatesDependencies(t *testing.T) {
test.interval,
)
if !errors.Is(err, test.wantErr) {
t.Fatalf("NewPlaylistReadinessCoordinator() error = %v, want %v", err, test.wantErr)
t.Fatalf("NewPlaylistEventCoordinator() error = %v, want %v", err, test.wantErr)
}
if test.wantErr != nil && coordinator != nil {
t.Fatalf("coordinator = %#v, want nil", coordinator)
@@ -174,10 +174,10 @@ func TestPlaylistEntryMatchesSession(t *testing.T) {
}
}
func TestPlaylistReadinessCoordinatorEmitsOncePerRevision(t *testing.T) {
func TestPlaylistEventCoordinatorEmitsOncePerRevision(t *testing.T) {
playlist, session, statuses := readyVideoSnapshots(1)
output := make(chan PlaylistReadiness, 4)
coordinator, ticker, cancel, result := startReadinessCoordinator(
output := make(chan PlaylistEvent, 4)
coordinator, ticker, cancel, result := startEventCoordinator(
t,
playlist,
session,
@@ -188,16 +188,16 @@ func TestPlaylistReadinessCoordinatorEmitsOncePerRevision(t *testing.T) {
defer cancel()
ticker.tick()
if got := receivePlaylistReadiness(t, output); got.Revision != 1 {
if got := receivePlaylistEvent(t, output); got.Revision != 1 {
t.Fatalf("readiness revision = %d, want 1", got.Revision)
}
ticker.tick()
assertNoPlaylistReadiness(t, output)
assertNoPlaylistEvent(t, output)
next := playlistSnapshotForVideo(2)
playlist.set(next, true)
ticker.tick()
if got := receivePlaylistReadiness(t, output); got.Revision != 2 {
if got := receivePlaylistEvent(t, output); got.Revision != 2 {
t.Fatalf("readiness revision = %d, want 2", got.Revision)
}
@@ -210,10 +210,10 @@ func TestPlaylistReadinessCoordinatorEmitsOncePerRevision(t *testing.T) {
}
}
func TestPlaylistReadinessCoordinatorWaitsForAllConditions(t *testing.T) {
func TestPlaylistEventCoordinatorWaitsForAllConditions(t *testing.T) {
playlist, session, statuses := readyVideoSnapshots(1)
output := make(chan PlaylistReadiness, 1)
_, ticker, cancel, result := startReadinessCoordinator(t, playlist, session, statuses, output)
output := make(chan PlaylistEvent, 1)
_, ticker, cancel, result := startEventCoordinator(t, playlist, session, statuses, output)
defer cancel()
tests := []struct {
@@ -256,7 +256,7 @@ func TestPlaylistReadinessCoordinatorWaitsForAllConditions(t *testing.T) {
statuses.set(validStatuses.snapshot)
test.mutate()
ticker.tick()
assertNoPlaylistReadiness(t, output)
assertNoPlaylistEvent(t, output)
})
}
@@ -264,10 +264,10 @@ func TestPlaylistReadinessCoordinatorWaitsForAllConditions(t *testing.T) {
_ = waitForPlaylistResult(t, result)
}
func TestPlaylistReadinessCoordinatorCancellationWhileBlockedSending(t *testing.T) {
func TestPlaylistEventCoordinatorCancellationWhileBlockedSending(t *testing.T) {
playlist, session, statuses := readyVideoSnapshots(1)
output := make(chan PlaylistReadiness)
_, ticker, cancel, result := startReadinessCoordinator(t, playlist, session, statuses, output)
output := make(chan PlaylistEvent)
_, ticker, cancel, result := startEventCoordinator(t, playlist, session, statuses, output)
ticker.tick()
time.Sleep(time.Millisecond)
@@ -322,15 +322,15 @@ func playlistSnapshotForVideo(revision uint64) PlaylistSnapshot {
}
}
func startReadinessCoordinator(
func startEventCoordinator(
t *testing.T,
playlist PlaylistSnapshotSource,
session SessionSnapshotSource,
statuses PlaybackStatusSnapshotSource,
output chan<- PlaylistReadiness,
) (*PlaylistReadinessCoordinator, *fakePlaylistReadinessTicker, context.CancelFunc, <-chan error) {
output chan<- PlaylistEvent,
) (*PlaylistEventCoordinator, *fakePlaylistEventTicker, context.CancelFunc, <-chan error) {
t.Helper()
coordinator, err := NewPlaylistReadinessCoordinator(
coordinator, err := NewPlaylistEventCoordinator(
playlist,
session,
statuses,
@@ -338,28 +338,28 @@ func startReadinessCoordinator(
time.Millisecond,
)
if err != nil {
t.Fatalf("NewPlaylistReadinessCoordinator() error = %v", err)
t.Fatalf("NewPlaylistEventCoordinator() error = %v", err)
}
ticker := newFakePlaylistReadinessTicker()
coordinator.newTicker = func(time.Duration) playlistReadinessTicker { return ticker }
ticker := newFakePlaylistEventTicker()
coordinator.newTicker = func(time.Duration) playlistEventTicker { return ticker }
ctx, cancel := context.WithCancel(context.Background())
result := make(chan error, 1)
go func() { result <- coordinator.Run(ctx) }()
return coordinator, ticker, cancel, result
}
func receivePlaylistReadiness(t *testing.T, output <-chan PlaylistReadiness) PlaylistReadiness {
func receivePlaylistEvent(t *testing.T, output <-chan PlaylistEvent) PlaylistEvent {
t.Helper()
select {
case readiness := <-output:
return readiness
case <-time.After(time.Second):
t.Fatal("timed out waiting for playlist readiness")
return PlaylistReadiness{}
return PlaylistEvent{}
}
}
func assertNoPlaylistReadiness(t *testing.T, output <-chan PlaylistReadiness) {
func assertNoPlaylistEvent(t *testing.T, output <-chan PlaylistEvent) {
t.Helper()
select {
case readiness := <-output:
+1 -1
View File
@@ -146,7 +146,7 @@ func TestPlaylistControllerFailurePolicy(t *testing.T) {
t.Fatalf("NewPlaylistController() error = %v", err)
}
commands := make(chan PlaylistCommand, 2)
events := make(chan PlaylistReadiness, 2)
events := make(chan PlaylistEvent, 2)
ctx, cancel := context.WithCancel(context.Background())
result := make(chan error, 1)
go func() { result <- controller.Run(ctx, commands, events) }()
+5 -5
View File
@@ -116,7 +116,7 @@ type playlistPipelineHarness struct {
sessions chan SessionCommand
session *fakeSessionSnapshotSource
statuses *fakePlaybackStatusSnapshotSource
ticker *fakePlaylistReadinessTicker
ticker *fakePlaylistEventTicker
cancel context.CancelFunc
results chan error
}
@@ -128,17 +128,17 @@ func startPlaylistPipeline(t *testing.T, playlist Playlist) *playlistPipelineHar
if err != nil {
t.Fatal(err)
}
events := make(chan PlaylistReadiness, 8)
events := make(chan PlaylistEvent, 8)
session := &fakeSessionSnapshotSource{}
statuses := &fakePlaybackStatusSnapshotSource{}
coordinator, err := NewPlaylistReadinessCoordinator(
coordinator, err := NewPlaylistEventCoordinator(
controller, session, statuses, events, time.Millisecond,
)
if err != nil {
t.Fatal(err)
}
ticker := newFakePlaylistReadinessTicker()
coordinator.newTicker = func(time.Duration) playlistReadinessTicker { return ticker }
ticker := newFakePlaylistEventTicker()
coordinator.newTicker = func(time.Duration) playlistEventTicker { return ticker }
commands := make(chan PlaylistCommand, 8)
ctx, cancel := context.WithCancel(context.Background())
results := make(chan error, 2)