Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/release-engineering/fbc-update-planner
go 1.25.8

require (
github.com/avast/retry-go/v4 v4.7.0
github.com/spf13/pflag v1.0.10
sigs.k8s.io/yaml v1.6.0
)
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
github.com/avast/retry-go/v4 v4.7.0 h1:yjDs35SlGvKwRNSykujfjdMxMhMQQM0TnIjJaHB+Zio=
github.com/avast/retry-go/v4 v4.7.0/go.mod h1:ZMPDa3sY2bKgpLtap9JRUgk2yTAba7cgiFhqxY2Sg6Q=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
33 changes: 15 additions & 18 deletions pkg/plcc/plcc.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import (
"sort"
"strings"
"time"

"github.com/avast/retry-go/v4"
)

// PackagesNotFoundError is returned when requested package names are not found in the catalog.
Expand Down Expand Up @@ -105,28 +107,23 @@ func Fetch() (*Catalog, error) {
return FetchFrom(APIURL, &http.Client{Timeout: 30 * time.Second})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] architecture-coherence

New dependency github.com/avast/retry-go/v4 is not reflected in CLAUDE.md's Tech Stack section, which currently lists only sigs.k8s.io/yaml and spf13/pflag.

Suggested fix: Update CLAUDE.md Tech Stack section to include avast/retry-go/v4 for HTTP retry logic.


var sleepFunc = time.Sleep
var retryOptions = []retry.Option{
retry.Attempts(3),
retry.Delay(60 * time.Second),
retry.DelayType(retry.BackOffDelay),
retry.LastErrorOnly(true),
}

// FetchFrom retrieves the product catalog from the given URL using the provided HTTP client.
// It retries up to 3 times with exponential backoff on errors.
// It makes up to 3 attempts with exponential backoff on errors.
func FetchFrom(url string, client *http.Client) (*Catalog, error) {
const maxRetries = 3
var lastErr error

for attempt := range maxRetries {
if attempt > 0 {
// attempt 1: 60s, attempt 2: 120s
sleepFunc(time.Duration(60<<(attempt-1)) * time.Second)
}

catalog, err := fetch(url, client)
if err == nil {
return catalog, nil
}
lastErr = err
catalog, err := retry.DoWithData(func() (*Catalog, error) {
return fetch(url, client)
}, retryOptions...)
if err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] error-message-informativeness

The error message changed from 'after 3 attempts: ...' to 'after retries: ...', losing the attempt count and introducing a terminology inconsistency with the comment on line 117 ('makes up to 3 attempts'). Since the count is a compile-time constant (retry.Attempts(3)), the information loss is minor but including it is a cheap improvement.

Suggested fix: Change to fmt.Errorf("after 3 attempts: %%w", err) to restore the count and align with the comment's terminology.

return nil, fmt.Errorf("after retries: %w", err)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] error-message-change

The error message changed from 'after 3 attempts: ' to 'after retries: ', losing the attempt count from the diagnostic message. No downstream code or test depends on this exact text, and the underlying error is still properly wrapped via %w, so errors.Is/errors.As continue to work. This is a minor loss of information in error diagnostics that could be useful when debugging production failures.

Suggested fix: Consider preserving the attempt count, e.g. fmt.Errorf("after 3 attempts: %w", err).

}

return nil, fmt.Errorf("after %d attempts: %w", maxRetries, lastErr)
return catalog, nil
}

func fetch(url string, client *http.Client) (*Catalog, error) {
Expand Down
18 changes: 10 additions & 8 deletions pkg/plcc/plcc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (
"reflect"
"testing"
"time"

"github.com/avast/retry-go/v4"
)

func TestPackages(t *testing.T) {
Expand Down Expand Up @@ -274,12 +276,12 @@ func TestSortByPackage(t *testing.T) {
}
}

// mockSleep disables retry backoff delays for the duration of the test.
func mockSleep(t *testing.T) {
// mockRetry disables retry backoff delays for the duration of the test.
func mockRetry(t *testing.T) {
t.Helper()
original := sleepFunc
sleepFunc = func(time.Duration) {}
t.Cleanup(func() { sleepFunc = original })
original := retryOptions
retryOptions = append([]retry.Option(nil), append(original, retry.Delay(0))...)
t.Cleanup(func() { retryOptions = original })
}

func TestFetchFrom(t *testing.T) {
Expand Down Expand Up @@ -310,7 +312,7 @@ func TestFetchFrom(t *testing.T) {
}

func TestFetchFromHTTPError(t *testing.T) {
mockSleep(t)
mockRetry(t)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
Expand All @@ -323,7 +325,7 @@ func TestFetchFromHTTPError(t *testing.T) {
}

func TestFetchFromHTTPErrorRetries(t *testing.T) {
mockSleep(t)
mockRetry(t)
attempts := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
attempts++
Expand All @@ -341,7 +343,7 @@ func TestFetchFromHTTPErrorRetries(t *testing.T) {
}

func TestFetchFromRetry(t *testing.T) {
mockSleep(t)
mockRetry(t)
attempts := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
attempts++
Expand Down
Loading