From 1eaab147939e5be36b8cf6c33b31953447867043 Mon Sep 17 00:00:00 2001 From: Dmitry Sergeev Date: Thu, 27 Aug 2026 00:43:08 +0300 Subject: [PATCH] define playback retry rules and states --- internal/playback/retry.go | 23 +++++++++ internal/playback/retry_test.go | 91 +++++++++++++++++++++++++++++++++ internal/playback/state.go | 12 +++++ internal/source/errors_test.go | 4 +- internal/test.go | 7 --- 5 files changed, 128 insertions(+), 9 deletions(-) create mode 100644 internal/playback/retry.go create mode 100644 internal/playback/retry_test.go create mode 100644 internal/playback/state.go delete mode 100644 internal/test.go diff --git a/internal/playback/retry.go b/internal/playback/retry.go new file mode 100644 index 0000000..b9754e3 --- /dev/null +++ b/internal/playback/retry.go @@ -0,0 +1,23 @@ +package playback + +import "time" + +func (p RetryPolicy) canRetry(failedAttempts int) bool { + return p.MaxAttempts == 0 || failedAttempts < p.MaxAttempts +} + +func (p RetryPolicy) retryDelay(failedAttempts int) time.Duration { + delay := p.InitialDelay + + for attempt := 1; attempt < failedAttempts; attempt++ { + if delay >= p.MaxDelay/2 { + return p.MaxDelay + } + delay *= 2 + } + + if delay > p.MaxDelay { + return p.MaxDelay + } + return delay +} diff --git a/internal/playback/retry_test.go b/internal/playback/retry_test.go new file mode 100644 index 0000000..019cce7 --- /dev/null +++ b/internal/playback/retry_test.go @@ -0,0 +1,91 @@ +package playback + +import ( + "testing" + "time" +) + +func TestFiniteAttempts(t *testing.T) { + rp := RetryPolicy{ + MaxAttempts: 3, + InitialDelay: 500 * time.Millisecond, + MaxDelay: 10 * time.Second, + } + if !rp.canRetry(1) { + t.Fatal("MaxAttempts=3, failed=1, but can't retry") + } + if !rp.canRetry(2) { + t.Fatal("MaxAttempts=3, failed=2, but can't retry") + } + if rp.canRetry(3) { + t.Fatal("MaxAttempts=3, failed=3, but can retry") + } +} + +func TestOneAllowedAttempt(t *testing.T) { + rp := RetryPolicy{ + MaxAttempts: 1, + InitialDelay: 500 * time.Millisecond, + MaxDelay: 10 * time.Second, + } + if rp.canRetry(1) { + t.Fatal("MaxAttempts=1, failed=1, but can retry") + } +} + +func TestUnlimitedAttempts(t *testing.T) { + rp := RetryPolicy{ + MaxAttempts: 0, + InitialDelay: 500 * time.Millisecond, + MaxDelay: 10 * time.Second, + } + for failedAttempts := 1; failedAttempts <= 10; failedAttempts++ { + if !rp.canRetry(failedAttempts) { + t.Fatalf( + "canRetry(%d) = false for unlimited policy", + failedAttempts, + ) + } + } +} + +func TestBackoff(t *testing.T) { + rp := RetryPolicy{ + MaxAttempts: 0, + InitialDelay: 500 * time.Millisecond, + MaxDelay: 10 * time.Second, + } + want := []time.Duration{ + 500 * time.Millisecond, + 1 * time.Second, + 2 * time.Second, + 4 * time.Second, + 8 * time.Second, + 10 * time.Second, + 10 * time.Second, + } + for i, wantDelay := range want { + failedAttempts := i + 1 + got := rp.retryDelay(failedAttempts) + if got != wantDelay { + t.Errorf( + "retryDelay(%d) = %s, want %s", + failedAttempts, + got, + wantDelay, + ) + } + } +} + +func TestRetryDelayLargeFailureCount(t *testing.T) { + policy := RetryPolicy{ + MaxAttempts: 0, + InitialDelay: 500 * time.Millisecond, + MaxDelay: 10 * time.Second, + } + + if got := policy.retryDelay(1_000_000); got != policy.MaxDelay { + t.Fatalf("retryDelay() = %s, want cap %s", got, policy.MaxDelay) + } +} diff --git a/internal/playback/state.go b/internal/playback/state.go new file mode 100644 index 0000000..af6f185 --- /dev/null +++ b/internal/playback/state.go @@ -0,0 +1,12 @@ +package playback + +type State uint8 + +const ( + StateIdle State = iota + StateConnecting + StatePlaying + StateReconnecting + StateFailed + StateStopping +) diff --git a/internal/source/errors_test.go b/internal/source/errors_test.go index a025178..56786f9 100644 --- a/internal/source/errors_test.go +++ b/internal/source/errors_test.go @@ -12,7 +12,7 @@ func TestKindOfThroughWrapping(t *testing.T) { outer := fmt.Errorf("worker failed: %w", wrapped) if got := KindOf(outer); got != ErrorKindUnavailable { - t.Fatalf("errorKind() = %v, want %v", got, ErrorKindUnavailable) + t.Fatalf("KindOf() = %v, want %v", got, ErrorKindUnavailable) } if !errors.Is(outer, base) { t.Fatal("wrapped error does not preserve its cause") @@ -24,7 +24,7 @@ func TestKindOfThroughWrapping(t *testing.T) { func TestKindOfUnknown(t *testing.T) { if got := KindOf(errors.New("ordinary error")); got != ErrorKindUnknown { - t.Fatalf("errorKind() = %v, want %v", got, ErrorKindUnknown) + t.Fatalf("KindOf() = %v, want %v", got, ErrorKindUnknown) } } diff --git a/internal/test.go b/internal/test.go deleted file mode 100644 index 6b4b9fa..0000000 --- a/internal/test.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -import "mxl-player/internal/imgui" - -func main() { - imgui.New() -}