Cleanout #4

Merged
itten merged 7 commits from cleanout into main 2026-09-02 00:46:14 +03:00
5 changed files with 125 additions and 19 deletions
Showing only changes of commit ea8c9eeddd - Show all commits
+27
View File
@@ -811,6 +811,33 @@ func main() {
} else { } else {
cimgui.Text("End behavior: stop") cimgui.Text("End behavior: stop")
} }
cimgui.Text(fmt.Sprintf(
"Failure behavior: %s",
configuredPlaylist.OnFailure,
))
if hasPlaylistSnapshot && playlistSnapshot.HasFailure {
failure := playlistSnapshot.Failure
name := failure.EntryName
if name == "" {
name = fmt.Sprintf("Entry %d", failure.EntryIndex+1)
}
cimgui.SeparatorText("Last failure")
cimgui.TextWrapped(fmt.Sprintf(
"%s: %s failed",
name,
failure.Status.Unit,
))
cimgui.Text(fmt.Sprintf(
"Attempts: %d | failed attempts: %d",
failure.Status.Attempt,
failure.Status.FailedAttempts,
))
cimgui.Text(fmt.Sprintf("Policy: %s", failure.Policy))
if failure.Status.Err != nil {
cimgui.TextWrapped(failure.Status.Err.Error())
}
}
preview := "No entry selected" preview := "No entry selected"
if hasPlaylistSnapshot && playlistSnapshot.State.HasSelection { if hasPlaylistSnapshot && playlistSnapshot.State.HasSelection {
+2 -2
View File
@@ -4,8 +4,8 @@ Size=400,400
Collapsed=0 Collapsed=0
[Window][Settings & Info] [Window][Settings & Info]
Pos=580,0 Pos=1220,0
Size=700,720 Size=700,1080
Collapsed=0 Collapsed=0
[Window][Stats] [Window][Stats]
+34
View File
@@ -17,6 +17,7 @@ const (
type PlaylistEvent struct { type PlaylistEvent struct {
Revision uint64 Revision uint64
Kind PlaylistEventKind Kind PlaylistEventKind
Failure Status
} }
// PlaylistReadiness is retained as an alias for callers that only publish // PlaylistReadiness is retained as an alias for callers that only publish
@@ -47,6 +48,15 @@ type PlaylistController struct {
mu sync.RWMutex mu sync.RWMutex
snapshot PlaylistSnapshot snapshot PlaylistSnapshot
hasSnapshot bool hasSnapshot bool
failure PlaylistFailure
}
type PlaylistFailure struct {
EntryIndex int
EntryName string
Revision uint64
Policy PlaylistFailurePolicy
Status Status
} }
type PlaylistSnapshot struct { type PlaylistSnapshot struct {
@@ -54,6 +64,8 @@ type PlaylistSnapshot struct {
Entry PlaylistEntry Entry PlaylistEntry
Revision uint64 Revision uint64
Timing PlaylistTimingState Timing PlaylistTimingState
Failure PlaylistFailure
HasFailure bool
} }
var ( var (
@@ -155,6 +167,7 @@ func (c *PlaylistController) Run(
continue continue
} }
if apply { if apply {
c.clearFailure()
stopTimer() stopTimer()
select { select {
case <-ctx.Done(): case <-ctx.Done():
@@ -179,6 +192,13 @@ func (c *PlaylistController) Run(
continue continue
} }
stopTimer() stopTimer()
c.setFailure(PlaylistFailure{
EntryIndex: state.CurrentIndex,
EntryName: c.playlist.Entries[state.CurrentIndex].Name,
Revision: revision,
Policy: c.playlist.OnFailure,
Status: ready.Failure,
})
// A failed entry must not retain a live or apparently active // A failed entry must not retain a live or apparently active
// duration clock, even when the policy is to wait. // duration clock, even when the policy is to wait.
timing = NewPlaylistTiming(revision, timing.Duration) timing = NewPlaylistTiming(revision, timing.Duration)
@@ -289,11 +309,25 @@ func (c *PlaylistController) publish(
Entry: entry, Entry: entry,
Revision: revision, Revision: revision,
Timing: timing, Timing: timing,
Failure: c.failure,
HasFailure: c.failure.Revision != 0,
} }
c.hasSnapshot = true c.hasSnapshot = true
c.mu.Unlock() c.mu.Unlock()
} }
func (c *PlaylistController) setFailure(failure PlaylistFailure) {
c.mu.Lock()
c.failure = failure
c.mu.Unlock()
}
func (c *PlaylistController) clearFailure() {
c.mu.Lock()
c.failure = PlaylistFailure{}
c.mu.Unlock()
}
func stopPlaylistTimer(timer playlistTimer) { func stopPlaylistTimer(timer playlistTimer) {
if timer == nil || timer.Stop() { if timer == nil || timer.Stop() {
return return
+31 -2
View File
@@ -2,6 +2,7 @@ package playback
import ( import (
"context" "context"
"errors"
"testing" "testing"
"time" "time"
) )
@@ -152,7 +153,15 @@ func TestPlaylistControllerFailurePolicy(t *testing.T) {
commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 0} commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 0}
<-sessions <-sessions
events <- PlaylistEvent{Revision: 1, Kind: PlaylistEventFailed} failureErr := errors.New("flow unavailable")
events <- PlaylistEvent{
Revision: 1,
Kind: PlaylistEventFailed,
Failure: Status{
Unit: UnitVideo, State: StateFailed,
Attempt: 2, FailedAttempts: 2, Err: failureErr,
},
}
if test.wantAdvance { if test.wantAdvance {
select { select {
@@ -160,16 +169,23 @@ func TestPlaylistControllerFailurePolicy(t *testing.T) {
case <-time.After(time.Second): case <-time.After(time.Second):
t.Fatal("failure did not advance playlist") t.Fatal("failure did not advance playlist")
} }
snapshot, _ := controller.Snapshot() snapshot := waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
return snapshot.State.CurrentIndex == 1 && snapshot.Revision == 2
})
if snapshot.State.CurrentIndex != 1 || snapshot.Revision != 2 { if snapshot.State.CurrentIndex != 1 || snapshot.Revision != 2 {
t.Fatalf("snapshot = %#v, want index 1 revision 2", snapshot) t.Fatalf("snapshot = %#v, want index 1 revision 2", snapshot)
} }
assertPlaylistFailure(t, snapshot, failureErr)
} else { } else {
select { select {
case command := <-sessions: case command := <-sessions:
t.Fatalf("unexpected session command: %#v", command) t.Fatalf("unexpected session command: %#v", command)
case <-time.After(20 * time.Millisecond): case <-time.After(20 * time.Millisecond):
} }
snapshot := waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
return snapshot.HasFailure
})
assertPlaylistFailure(t, snapshot, failureErr)
} }
cancel() cancel()
@@ -179,3 +195,16 @@ func TestPlaylistControllerFailurePolicy(t *testing.T) {
}) })
} }
} }
func assertPlaylistFailure(t *testing.T, snapshot PlaylistSnapshot, wantErr error) {
t.Helper()
if !snapshot.HasFailure {
t.Fatal("snapshot has no playlist failure")
}
failure := snapshot.Failure
if failure.EntryIndex != 0 || failure.EntryName != "first" ||
failure.Status.Unit != UnitVideo || failure.Status.Attempt != 2 ||
failure.Status.FailedAttempts != 2 || !errors.Is(failure.Status.Err, wantErr) {
t.Fatalf("failure = %#v", failure)
}
}
+23 -7
View File
@@ -112,13 +112,14 @@ func (c *PlaylistReadinessCoordinator) Run(ctx context.Context) error {
continue continue
} }
statuses := c.statuses.SnapshotAll() statuses := c.statuses.SnapshotAll()
if IsSessionFailed(sessionSnapshot, statuses) { if failure, failed := SessionFailureStatus(sessionSnapshot, statuses); failed {
if playlistSnapshot.Revision == emittedFailedRevision { if playlistSnapshot.Revision == emittedFailedRevision {
continue continue
} }
failed := PlaylistEvent{ failed := PlaylistEvent{
Revision: playlistSnapshot.Revision, Revision: playlistSnapshot.Revision,
Kind: PlaylistEventFailed, Kind: PlaylistEventFailed,
Failure: failure,
} }
select { select {
case <-ctx.Done(): case <-ctx.Done():
@@ -217,24 +218,39 @@ func IsSessionFailed(
session SessionSnapshot, session SessionSnapshot,
statuses PlaybackStatusSnapshot, statuses PlaybackStatusSnapshot,
) bool { ) bool {
_, failed := SessionFailureStatus(session, statuses)
return failed
}
func SessionFailureStatus(
session SessionSnapshot,
statuses PlaybackStatusSnapshot,
) (Status, bool) {
if statuses.Generation != session.Generation { if statuses.Generation != session.Generation {
return false return Status{}, false
} }
switch session.Plan.Topology { switch session.Plan.Topology {
case TopologyIndependent: case TopologyIndependent:
return (session.Plan.Video.Active && statusIsFailed( if session.Plan.Video.Active && statusIsFailed(
statuses.Video, statuses.HasVideo, session.Generation, session.Plan.Video, statuses.Video, statuses.HasVideo, session.Generation, session.Plan.Video,
)) || (session.Plan.Audio.Active && statusIsFailed( ) {
return statuses.Video, true
}
if session.Plan.Audio.Active && statusIsFailed(
statuses.Audio, statuses.HasAudio, session.Generation, session.Plan.Audio, statuses.Audio, statuses.HasAudio, session.Generation, session.Plan.Audio,
)) ) {
return statuses.Audio, true
}
return Status{}, false
case TopologySynchronized: case TopologySynchronized:
return statuses.HasSync && failed := statuses.HasSync &&
statuses.Sync.Generation == session.Generation && statuses.Sync.Generation == session.Generation &&
statuses.Sync.State == StateFailed && statuses.Sync.State == StateFailed &&
sameSyncSource(statuses.Sync.Pair, session.Plan.Sync) sameSyncSource(statuses.Sync.Pair, session.Plan.Sync)
return statuses.Sync, failed
default: default:
return false return Status{}, false
} }
} }