Skip to content

Commit d03d796

Browse files
cstocktonChris Stocktonfadymak
authored
feat: support live reloading of individual rate limits (#2469)
Move API limiter setup out of internal/api and into a dedicated internal/api/apilimiter package, then wire it into serve-time config reloads so rate limit changes are picked up without restarting the service. This change replaces the old LimiterOptions type with an apilimiter.Limiter instance passed through api.WithLimiter(...). API construction now defaults to apilimiter.New(...) when no limiter is injected, and tests are updated to use the new option-based wiring. The new apilimiter package centralizes: - construction of all ratelimit and tollbooth limiters - mapping between config/env vars and limiter fields - copy/update logic for reusing existing limiter state where possible - structured logging for limiter changes during config reload On config reload in serve(): - keep track of the previously active limiter set - call previousLim.Update(...) against the latest config - build the new API with the updated limiter set - store the new API, reload apiworker config, and retain the latest limiter for the next reload cycle This fixes the prior behavior where hot config reload rebuilt the API but kept stale limiter settings, meaning rate-limit changes were not applied until process restart. Additional ratelimit changes: - persist the original parsed conf.Rate value in conf.Rate via val - add GetRateValue() for logging/comparison purposes - extend ratelimit.Limiter with Config() so limiters can expose their backing configuration - add ratelimit.Equal(...) helper to compare limiters, configs, and rate strings consistently - store conf.Rate on BurstLimiter and IntervalLimiter and expose Config() - add String() methods to identify limiter type in tests/debug output - rename IntervalLimiter.limit to events for clarity Behavioral note: - BurstLimiter documentation now matches implementation for non-positive event counts: burst size becomes 0, so no events are allowed Tests: - update API tests to inject limiters through api.WithLimiter - update options tests to validate apilimiter.New - expand ratelimit tests to cover type identification and equality semantics - add dedicated apilimiter tests that verify only the expected fields change when each config/env-backed limiter value is modified --------- Co-authored-by: Chris Stockton <chris.stockton@supabase.io> Co-authored-by: fadymak <dev@fadymak.com>
1 parent e49a3e5 commit d03d796

13 files changed

Lines changed: 826 additions & 154 deletions

File tree

cmd/serve_cmd.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/sirupsen/logrus"
1515
"github.com/spf13/cobra"
1616
"github.com/supabase/auth/internal/api"
17+
"github.com/supabase/auth/internal/api/apilimiter"
1718
"github.com/supabase/auth/internal/api/apiworker"
1819
"github.com/supabase/auth/internal/conf"
1920
"github.com/supabase/auth/internal/mailer/templatemailer"
@@ -63,10 +64,10 @@ func serve(ctx context.Context) {
6364
defer wg.Wait() // Do not return to caller until this goroutine is done.
6465

6566
mrCache := templatemailer.NewCache()
66-
limiterOpts := api.NewLimiterOptions(config)
67+
initialLim := apilimiter.New(config)
6768
initialAPI := api.NewAPIWithVersion(
6869
config, db, utilities.Version,
69-
limiterOpts,
70+
api.WithLimiter(initialLim),
7071
api.WithMailer(templatemailer.FromConfig(config, mrCache)),
7172
)
7273

@@ -130,11 +131,12 @@ func serve(ctx context.Context) {
130131
exitFn("config reloader is exiting")
131132
}()
132133

134+
previousLim := initialLim
133135
fn := func(latestCfg *conf.GlobalConfiguration) {
134136
le.Info("reloading api with new configuration")
135137

136-
// When config is updated we notify the apiworker.
137-
wrk.ReloadConfig(latestCfg)
138+
// Update the previous limiter with the latest config
139+
latestLim := previousLim.Update(le, latestCfg)
138140

139141
// Create a new API version with the updated config.
140142
latestAPI := api.NewAPIWithVersion(
@@ -146,13 +148,17 @@ func serve(ctx context.Context) {
146148
),
147149

148150
// Persist existing rate limiters.
149-
//
150-
// TODO(cstockton): we should consider updating these, if we
151-
// rely on hot config reloads 100% then rate limiter changes
152-
// won't be picked up.
153-
limiterOpts,
151+
api.WithLimiter(latestLim),
154152
)
153+
154+
// Assign this config as the latest configuration
155155
ah.Store(latestAPI)
156+
157+
// When config is updated we notify the apiworker.
158+
wrk.ReloadConfig(latestCfg)
159+
160+
// Update previous limiter
161+
previousLim = latestLim
156162
}
157163

158164
rl := reloader.NewReloader(rc, watchDir)

internal/api/api.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/sebest/xff"
1010
"github.com/sirupsen/logrus"
1111
"github.com/supabase/auth/internal/api/apierrors"
12+
"github.com/supabase/auth/internal/api/apilimiter"
1213
"github.com/supabase/auth/internal/api/apitask"
1314
"github.com/supabase/auth/internal/api/oauthserver"
1415
"github.com/supabase/auth/internal/api/provider"
@@ -54,7 +55,7 @@ type API struct {
5455
// overrideTime can be used to override the clock used by handlers. Should only be used in tests!
5556
overrideTime func() time.Time
5657

57-
limiterOpts *LimiterOptions
58+
limiterOpts *apilimiter.Limiter
5859
}
5960

6061
func (a *API) GetConfig() *conf.GlobalConfiguration { return a.config }
@@ -110,7 +111,7 @@ func NewAPIWithVersion(globalConfig *conf.GlobalConfiguration, db *storage.Conne
110111
api.captchaVerifier = security.NewCaptchaVerifier(&globalConfig.Security.Captcha)
111112
}
112113
if api.limiterOpts == nil {
113-
api.limiterOpts = NewLimiterOptions(globalConfig)
114+
api.limiterOpts = apilimiter.New(globalConfig)
114115
}
115116
if api.hooksMgr == nil {
116117
httpDr := hookshttp.New()

internal/api/api_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88

99
"github.com/stretchr/testify/require"
10+
"github.com/supabase/auth/internal/api/apilimiter"
1011
"github.com/supabase/auth/internal/conf"
1112
"github.com/supabase/auth/internal/crypto"
1213
"github.com/supabase/auth/internal/storage"
@@ -60,8 +61,8 @@ func setupAPIForTestWithCallback(cb func(*conf.GlobalConfiguration, *storage.Con
6061
cb(nil, conn)
6162
}
6263

63-
limiterOpts := NewLimiterOptions(config)
64-
return NewAPIWithVersion(config, conn, apiTestVersion, limiterOpts), config, nil
64+
limiterOpts := apilimiter.New(config)
65+
return NewAPIWithVersion(config, conn, apiTestVersion, WithLimiter(limiterOpts)), config, nil
6566
}
6667

6768
func TestEmailEnabledByDefault(t *testing.T) {

0 commit comments

Comments
 (0)