Skip to content

Commit e020bac

Browse files
committed
backend: ignore requests for etf2l user 0, add ratelimiting and retries to rgl client
1 parent fd774bd commit e020bac

8 files changed

Lines changed: 114 additions & 9 deletions

File tree

backend/cmd/offi/serve.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func serveAction(ctx context.Context, _ *cli.Command) error {
5454
retryTransport := internalHTTP.Transport(true)
5555

5656
etf2lClient := etf2l.New(retryTransport)
57-
rglClient := rgl.NewClient(defaultTransport)
57+
rglClient := rgl.NewClient(retryTransport)
5858

5959
logsClient := logstf.NewClient(defaultTransport)
6060
demosClient := demostf.NewClient(defaultTransport)

backend/internal/cache/redis_players.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func (r Redis) GetPlayers(ctx context.Context, league string, playerIDs []int64)
5858
} else {
5959
var player Player
6060
if err = json.Unmarshal([]byte(result.(string)), &player); err != nil {
61-
return nil, fmt.Errorf("unmarshalling player: %w", err)
61+
return nil, fmt.Errorf("unmarshaling player: %w", err)
6262
}
6363

6464
players[playerIDs[i]] = &player

backend/internal/etf2l/etf2l.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func New(rt http.RoundTripper) *Client {
2020
return &Client{
2121
apiURL: "https://api-v2.etf2l.org",
2222
httpClient: &http.Client{Transport: rt},
23-
limiter: rate.NewLimiter(rate.Every(time.Second), 6),
23+
limiter: rate.NewLimiter(rate.Every(time.Second), 5),
2424
tracer: otel.Tracer("etf2l"),
2525
}
2626
}

backend/internal/http/transport.go

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package http
22

33
import (
4+
"log/slog"
5+
"math"
46
"net/http"
57
info "offi/internal/build_info"
68
"offi/internal/tracing"
9+
"strconv"
10+
"time"
711

812
"github.com/go-chi/transport"
913
)
@@ -17,8 +21,87 @@ func Transport(withRetries bool) http.RoundTripper {
1721
}
1822

1923
if withRetries {
20-
base = append(base, transport.Retry(transport.Chain(http.DefaultTransport, uaTransport), 3))
24+
base = append(base, Retry(transport.Chain(http.DefaultTransport, uaTransport), 3))
2125
}
2226

2327
return transport.Chain(http.DefaultTransport, base...)
2428
}
29+
30+
func Retry(baseTransport http.RoundTripper, maxRetries int) func(http.RoundTripper) http.RoundTripper {
31+
return func(next http.RoundTripper) http.RoundTripper {
32+
return transport.RoundTripFunc(func(req *http.Request) (resp *http.Response, err error) {
33+
defer func() {
34+
if isRetryable(resp) {
35+
for i := 1; i <= maxRetries; i++ {
36+
wait := backOff(resp, i)
37+
38+
timer := time.NewTimer(wait)
39+
40+
slog.DebugContext(req.Context(), "waiting before retrying request", "wait_time", wait.String())
41+
42+
select {
43+
case <-req.Context().Done():
44+
timer.Stop()
45+
err = req.Context().Err()
46+
break
47+
case <-timer.C:
48+
}
49+
50+
resp, err = baseTransport.RoundTrip(req)
51+
if !isRetryable(resp) {
52+
break
53+
}
54+
55+
slog.InfoContext(req.Context(), "retrying request",
56+
"target_host", req.URL.Host,
57+
"attempt", i,
58+
)
59+
}
60+
}
61+
}()
62+
63+
return next.RoundTrip(req)
64+
})
65+
}
66+
}
67+
68+
func backOff(resp *http.Response, attempt int) time.Duration {
69+
minDuration := 1 * time.Second
70+
maxDuration := 16 * time.Second
71+
72+
if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode == http.StatusServiceUnavailable {
73+
if s, ok := resp.Header["Retry-After"]; ok {
74+
if sleep, err := strconv.ParseInt(s[0], 10, 64); err == nil {
75+
return time.Second * time.Duration(sleep)
76+
}
77+
}
78+
}
79+
80+
// simple exp. backoff
81+
mult := math.Pow(2, float64(attempt)) * float64(minDuration)
82+
sleep := time.Duration(mult)
83+
if float64(sleep) != mult || sleep > maxDuration {
84+
sleep = maxDuration
85+
}
86+
return sleep
87+
}
88+
89+
func isRetryable(resp *http.Response) bool {
90+
if resp == nil {
91+
return false
92+
}
93+
94+
// 429 Too Many Requests is recoverable. Sometimes the server puts
95+
// Retry-After response header to indicate when the server is will be available again
96+
if resp.StatusCode == http.StatusTooManyRequests {
97+
return true
98+
}
99+
100+
// We retry on 500-range responses to allow the server time to recover, as 500's are typically not permanent
101+
// errors and may relate to outages on the server side.
102+
if resp.StatusCode == 0 || (resp.StatusCode > 500 && resp.StatusCode != http.StatusNotImplemented) {
103+
return true
104+
}
105+
106+
return false
107+
}

backend/internal/rgl/client.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,25 @@ import (
77
"fmt"
88
"net/http"
99
"strconv"
10+
"time"
1011

1112
"go.opentelemetry.io/otel"
13+
"go.opentelemetry.io/otel/attribute"
1214
"go.opentelemetry.io/otel/trace"
15+
"golang.org/x/time/rate"
1316
)
1417

1518
type Client struct {
16-
client *http.Client
17-
tracer trace.Tracer
19+
client *http.Client
20+
limiter *rate.Limiter
21+
tracer trace.Tracer
1822
}
1923

2024
func NewClient(rt http.RoundTripper) *Client {
2125
return &Client{
22-
client: &http.Client{Transport: rt},
23-
tracer: otel.Tracer("rgl"),
26+
client: &http.Client{Transport: rt},
27+
limiter: rate.NewLimiter(rate.Every(time.Second), 5),
28+
tracer: otel.Tracer("rgl"),
2429
}
2530
}
2631

@@ -33,14 +38,22 @@ func (c *Client) GetPlayers(ctx context.Context, playerIDs []int64) ([]Player, e
3338
ctx, span := c.tracer.Start(ctx, "rgl.GetPlayers")
3439
defer span.End()
3540

41+
t := time.Now()
42+
43+
if err := c.limiter.Wait(ctx); err != nil {
44+
return nil, err
45+
}
46+
47+
span.SetAttributes(attribute.Float64("rate_limit_wait", float64(time.Since(t).Milliseconds())))
48+
3649
stringPlayerIDs := make([]string, len(playerIDs))
3750
for i, id := range playerIDs {
3851
stringPlayerIDs[i] = strconv.FormatInt(id, 10)
3952
}
4053

4154
reqBytes, err := json.Marshal(stringPlayerIDs)
4255
if err != nil {
43-
return nil, fmt.Errorf("marshalling player IDs: %w", err)
56+
return nil, fmt.Errorf("marshaling player IDs: %w", err)
4457
}
4558

4659
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.rgl.gg/v0/profile/getmany", bytes.NewReader(reqBytes))

backend/internal/service/get_etf2l_players.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ func (s *Service) getETF2LPlayers(ctx context.Context, playerIDs []int64, withRe
2626
var players []gen.ETF2LPlayer
2727

2828
for _, playerID := range playerIDs {
29+
if playerID == 0 {
30+
continue
31+
}
32+
2933
player, err := s.cache.GetPlayer(ctx, cache.LeagueETF2L, playerID)
3034
switch {
3135
case errors.Is(err, redis.Nil):

backend/internal/service/get_logs_for_match.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ func (s *Service) GetLogsForMatch(ctx context.Context, params gen.GetLogsForMatc
6363
}
6464

6565
func (s *Service) getLogsForMatch(ctx context.Context, matchID int) (logs []db.Log, err error) {
66+
// TODO: use cache for storing information that match exists
6667
exists, err := s.db.MatchExists(ctx, matchID)
6768
if err != nil {
6869
return nil, fmt.Errorf("failed to get match %d from cache: %w", matchID, err)

backend/internal/service/get_players.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ func (s *Service) getPlayers(ctx context.Context, playerIDs []int64, withRecruit
2929
var players []gen.Player
3030

3131
for _, playerID := range playerIDs {
32+
if playerID == 0 {
33+
continue
34+
}
35+
3236
player, err := s.cache.GetPlayer(ctx, cache.LeagueETF2L, playerID)
3337
switch {
3438
case errors.Is(err, redis.Nil):

0 commit comments

Comments
 (0)