diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ed05e32..095b6f43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- using throttle/ticket providing system to limit requests per second to the clockify's api to prevent the + error message: `Too Many Requests (code: 429)` + ## [v0.49.0] - 2024-03-29 ### Added diff --git a/api/client.go b/api/client.go index bed5b0ac..a16a0fbb 100644 --- a/api/client.go +++ b/api/client.go @@ -1,6 +1,7 @@ package api import ( + "context" "encoding/hex" "fmt" "net/http" @@ -86,13 +87,17 @@ type Client interface { type client struct { baseURL *url.URL http.Client - debugLogger Logger - infoLogger Logger + debugLogger Logger + infoLogger Logger + requestTickets chan struct{} } // baseURL is the Clockify API base URL const baseURL = "https://api.clockify.me/api" +// REQUEST_RATE_LIMIT maximum number of requests per second +const REQUEST_RATE_LIMIT = 50 + // ErrorMissingAPIKey returned if X-Api-Key is missing var ErrorMissingAPIKey = errors.New("api Key must be informed") @@ -124,6 +129,7 @@ func NewClientFromUrlAndKey( next: http.DefaultTransport, }, }, + requestTickets: startRequestTick(REQUEST_RATE_LIMIT), }, nil } @@ -135,6 +141,39 @@ func NewClient(apiKey string) (Client, error) { ) } +func startRequestTick(limit int) chan struct{} { + ch := make(chan struct{}, limit) + + running := true + release := func() { + i := len(ch) + for i < limit { + if !running { + return + } + + i = i + 1 + ch <- struct{}{} + } + } + + go func() { + release() + for { + select { + case <-time.After(time.Second): + go release() + case <-context.Background().Done(): + running = false + defer close(ch) + return + } + } + }() + + return ch +} + // GetWorkspaces will be used to filter the workspaces type GetWorkspaces struct { Name string @@ -445,18 +484,7 @@ func (c *client) GetUsersHydratedTimeEntries(p GetUserTimeEntriesParam) ([]dto.T return timeEntries, err } - var user dto.User - tries := 0 - for tries < 5 { - tries++ - user, err = c.GetUser(GetUser{p.Workspace, p.UserID}) - if err == nil || !errors.Is(err, ErrorTooManyRequests) { - break - } - - time.Sleep(time.Duration(5)) - } - + user, err := c.GetUser(GetUser{p.Workspace, p.UserID}) if err != nil { return timeEntries, err } diff --git a/api/httpClient.go b/api/httpClient.go index f57d58b7..503d9a37 100644 --- a/api/httpClient.go +++ b/api/httpClient.go @@ -77,6 +77,9 @@ func (c *client) NewRequest(method, uri string, body interface{}) (*http.Request // Do executes a http.Request inside the Clockify's Client func (c *client) Do( req *http.Request, v interface{}, name string) (*http.Response, error) { + + <-c.requestTickets + r, err := c.Client.Do(req) if err != nil { return r, err