Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for dockerhub rate-limiting #19984

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
63 changes: 57 additions & 6 deletions src/pkg/reg/adapter/dockerhub/adapter.go
Expand Up @@ -21,7 +21,9 @@
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"

"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/lib/log"
Expand Down Expand Up @@ -124,6 +126,55 @@
return info
}

// Rate-limit aware wrapper function for client.Do()
// - Avoids being hit by limit by pausing requests when less than 'lowMark' requests remaining.
// - Pauses for given time when limit is hit.
// - Allows 2 more attempts before giving up.
// Reason: Observed (02/2024) penalty for hitting the limit is 120s, normal reset is 60s,
// so it is better to not hit the wall.
func (a *adapter) limitAwareDo(method string, path string, body io.Reader) (*http.Response, error) {
const lowMark = 8
var attemptsLeft = 3
for attemptsLeft > 0 {
clientResp, clientErr := a.client.Do(method, path, body)
if clientErr != nil {
return clientResp, clientErr
}

Check warning on line 142 in src/pkg/reg/adapter/dockerhub/adapter.go

View check run for this annotation

Codecov / codecov/patch

src/pkg/reg/adapter/dockerhub/adapter.go#L141-L142

Added lines #L141 - L142 were not covered by tests
if clientResp.StatusCode != http.StatusTooManyRequests {
reqsLeft, err := strconv.ParseInt(clientResp.Header.Get("x-ratelimit-remaining"), 10, 64)
if err != nil {
return clientResp, clientErr
}
if reqsLeft < lowMark {
resetTSC, err := strconv.ParseInt(clientResp.Header.Get("x-ratelimit-reset"), 10, 64)
if err == nil {
dur := time.Until(time.Unix(resetTSC, 0))
log.Infof("Rate-limit exhaustion eminent, sleeping for %.1f seconds", dur.Seconds())
time.Sleep(dur)
log.Info("Sleep finished, resuming operation")
}

Check warning on line 155 in src/pkg/reg/adapter/dockerhub/adapter.go

View check run for this annotation

Codecov / codecov/patch

src/pkg/reg/adapter/dockerhub/adapter.go#L148-L155

Added lines #L148 - L155 were not covered by tests
}
return clientResp, clientErr

Check warning on line 157 in src/pkg/reg/adapter/dockerhub/adapter.go

View check run for this annotation

Codecov / codecov/patch

src/pkg/reg/adapter/dockerhub/adapter.go#L157

Added line #L157 was not covered by tests
}
var dur = time.Duration(0)
seconds, err := strconv.ParseInt(clientResp.Header.Get("retry-after"), 10, 64)
if err != nil {
expireTime, err := http.ParseTime(clientResp.Header.Get("retry-after"))
if err != nil {
return nil, errors.New("blocked by dockerhub rate-limit and missing retry-after header")
}
dur = time.Until(expireTime)
} else {
dur = time.Duration(seconds) * time.Second
}
log.Infof("Rate-limit exhausted, sleeping for %.1f seconds", dur.Seconds())
time.Sleep(dur)
log.Info("Sleep finished, resuming operation")
attemptsLeft--

Check warning on line 173 in src/pkg/reg/adapter/dockerhub/adapter.go

View check run for this annotation

Codecov / codecov/patch

src/pkg/reg/adapter/dockerhub/adapter.go#L159-L173

Added lines #L159 - L173 were not covered by tests
}
return nil, errors.New("unable to get past dockerhub rate-limit")

Check warning on line 175 in src/pkg/reg/adapter/dockerhub/adapter.go

View check run for this annotation

Codecov / codecov/patch

src/pkg/reg/adapter/dockerhub/adapter.go#L175

Added line #L175 was not covered by tests
}

// PrepareForPush does the prepare work that needed for pushing/uploading the resource
// eg: create the namespace or repository
func (a *adapter) PrepareForPush(resources []*model.Resource) error {
Expand Down Expand Up @@ -159,7 +210,7 @@
}

func (a *adapter) listNamespaces() ([]string, error) {
resp, err := a.client.Do(http.MethodGet, listNamespacePath, nil)
resp, err := a.limitAwareDo(http.MethodGet, listNamespacePath, nil)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -207,7 +258,7 @@
return err
}

resp, err := a.client.Do(http.MethodPost, createNamespacePath, bytes.NewReader(b))
resp, err := a.limitAwareDo(http.MethodPost, createNamespacePath, bytes.NewReader(b))

Check warning on line 261 in src/pkg/reg/adapter/dockerhub/adapter.go

View check run for this annotation

Codecov / codecov/patch

src/pkg/reg/adapter/dockerhub/adapter.go#L261

Added line #L261 was not covered by tests
if err != nil {
return err
}
Expand All @@ -228,7 +279,7 @@

// getNamespace get namespace from DockerHub, if the namespace not found, two nil would be returned.
func (a *adapter) getNamespace(namespace string) (*model.Namespace, error) {
resp, err := a.client.Do(http.MethodGet, getNamespacePath(namespace), nil)
resp, err := a.limitAwareDo(http.MethodGet, getNamespacePath(namespace), nil)

Check warning on line 282 in src/pkg/reg/adapter/dockerhub/adapter.go

View check run for this annotation

Codecov / codecov/patch

src/pkg/reg/adapter/dockerhub/adapter.go#L282

Added line #L282 was not covered by tests
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -389,7 +440,7 @@
return fmt.Errorf("dockerhub only support repo in format <namespace>/<name>, but got: %s", repository)
}

resp, err := a.client.Do(http.MethodDelete, deleteTagPath(parts[0], parts[1], reference), nil)
resp, err := a.limitAwareDo(http.MethodDelete, deleteTagPath(parts[0], parts[1], reference), nil)

Check warning on line 443 in src/pkg/reg/adapter/dockerhub/adapter.go

View check run for this annotation

Codecov / codecov/patch

src/pkg/reg/adapter/dockerhub/adapter.go#L443

Added line #L443 was not covered by tests
if err != nil {
return err
}
Expand All @@ -410,7 +461,7 @@

// getRepos gets a page of repos from DockerHub
func (a *adapter) getRepos(namespace, name string, page, pageSize int) (*ReposResp, error) {
resp, err := a.client.Do(http.MethodGet, listReposPath(namespace, name, page, pageSize), nil)
resp, err := a.limitAwareDo(http.MethodGet, listReposPath(namespace, name, page, pageSize), nil)
if err != nil {
return nil, err
}
Expand All @@ -437,7 +488,7 @@

// getTags gets a page of tags for a repo from DockerHub
func (a *adapter) getTags(namespace, repo string, page, pageSize int) (*TagsResp, error) {
resp, err := a.client.Do(http.MethodGet, listTagsPath(namespace, repo, page, pageSize), nil)
resp, err := a.limitAwareDo(http.MethodGet, listTagsPath(namespace, repo, page, pageSize), nil)

Check warning on line 491 in src/pkg/reg/adapter/dockerhub/adapter.go

View check run for this annotation

Codecov / codecov/patch

src/pkg/reg/adapter/dockerhub/adapter.go#L491

Added line #L491 was not covered by tests
if err != nil {
return nil, err
}
Expand Down