-
Notifications
You must be signed in to change notification settings - Fork 348
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
feat: email, sms rate limiting new config and refactored rate limiters #1746
base: master
Are you sure you want to change the base?
Conversation
4bc81f3
to
f498771
Compare
Pull Request Test Coverage Report for Build 10597475519Details
💛 - Coveralls |
158f705
to
9d89e34
Compare
internal/conf/rate.go
Outdated
return *r | ||
} | ||
|
||
func (r *Rate) DivideIfDefaultDuration(div float64) *Rate { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove
internal/conf/rate.go
Outdated
return r | ||
} | ||
|
||
func (r *Rate) CreateLimiter() *limiter.Limiter { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove
internal/conf/rate.go
Outdated
return fmt.Sprintf("%f", r.Events) | ||
} | ||
|
||
return fmt.Sprintf("%f/%s", r.Events, r.OverTime.String()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return fmt.Sprintf("%f/%s", r.Events, r.OverTime.String()) | |
return fmt.Sprintf("%d/%s", r.Events, r.OverTime.String()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm - left some comments to improve readability
Events float64 `json:"events,omitempty"` | ||
OverTime time.Duration `json:"over_time,omitempty"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for clarification: Events
keeps track of the number of requests allowed per second while OverTime
is when the bucket gets refilled right? if so, i would just prefer to call OverTime
the ExpirationTTL
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/nit As a follow on suggestion, perhaps we can consider renaming events to AllowedEvents
or similar
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer to not use "TTL" or "Expire" because it's not about expiring, it's describing allowing number of "Events" occurring "OverTime".
How that's implemented in rate limiting can vary.
internal/conf/rate.go
Outdated
func (r *Rate) CreateLimiter() *limiter.Limiter { | ||
overTime := r.OverTime | ||
if int64(overTime) == 0 { | ||
// if r.OverTime is not specified, i.e. the configuration specified just a single float64 number, the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
incomplete comment but i'm guessing you meant we default the overTime to 1 hour
internal/api/ratelimits.go
Outdated
return atomic.AddUint64(&r.counter, events) < uint64(r.Rate.Events) | ||
} | ||
|
||
func (r *SimpleRateLimiter) fillBucket(initialWait time.Duration) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm this function is basically counting down to the bucket expiry and resetting the counter right? should we name this something else instead? naming it fillBucket
confused me a little and i thought this was where we incremented the counter
}) | ||
} | ||
|
||
func (r *Rate) Decode(value string) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we add a comment to describe that decode is necessary for envconfig to unmarshal the value correctly?
RateLimitEmailSent Rate `split_words:"true" default:"30"` | ||
RateLimitSmsSent Rate `split_words:"true" default:"30"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we want to keep the default setting here to 30?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, no breaking changes
internal/conf/rate.go
Outdated
return fmt.Errorf("rate: value does not match rate syntax %q", value) | ||
} | ||
|
||
e, err := strconv.ParseUint(parts[0], 10, 52) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this be set to 52 to account for the mantissa? Consider adding a comment as it might not be immediately obvious
internal/api/ratelimits.go
Outdated
|
||
go r.fillBucket(counterResetsAt.Sub(now)) | ||
|
||
return nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this return the rate limiter?
return nil | |
return r |
Nice work, don't forget to remove |
9d89e34
to
f05a4b7
Compare
internal/api/ratelimits.go
Outdated
|
||
atomic.StoreUint64(&r.counter, 0) | ||
|
||
ticker := time.NewTicker(r.Rate.OverTime) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
defer ticker.Stop()
internal/api/ratelimits.go
Outdated
return atomic.AddUint64(&r.counter, events) < uint64(r.Rate.Events) | ||
} | ||
|
||
func (r *SimpleRateLimiter) fillBucket(initialWait time.Duration) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this runs in a goroutine, is it possible we accept a context here by refactoring the responsibility of token filling? Alternatively this computation can be done at the time of each call to Increment. A reference implementation (an potential candidate for this, which includes context) could be:
internal/api/ratelimits.go
Outdated
|
||
r.counter = uint64(rate.Events * proRate) | ||
|
||
go r.fillBucket(counterResetsAt.Sub(now)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We solve a few potential caveats by moving the responsibility of starting the goroutine into the caller, providing a context to select on along with ticker to prevent goroutine leaks over time. Alternatively as suggested in the other comment we could remove the real-time bucket filling into a task to be done during incrementing.
internal/api/api.go
Outdated
@@ -70,6 +73,11 @@ func (a *API) deprecationNotices() { | |||
func NewAPIWithVersion(globalConfig *conf.GlobalConfiguration, db *storage.Connection, version string) *API { | |||
api := &API{config: globalConfig, db: db, version: version} | |||
|
|||
now := time.Now() | |||
|
|||
api.emailRateLimiter = NewSimpleRateLimiter(now, globalConfig.RateLimitEmailSent.DivideAndSetDuration(6*60, time.Hour)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These could be parameterized in NewAPIWithVersion so it doesn't need to be responsible for starting goroutines, alternatively we provide a context.Context to signal when shutdown has occurred. This would allow us to signal cancelation and synchronize exiting.
atomic.StoreUint64(&r.counter, 0) | ||
|
||
// then keep resetting at regular OverTime intervals | ||
for range r.ticker.C { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You will need another mechanism to signal this goroutine should exit, because r.ticker.C
will not be closed when r.ticker.Stop
is called, leaving this blocked forever.
One option is to add ctx, another is to have a close chan so you can select between the ticker and r.closeCh
here which could be close(r.closeCh)
in r.Stop()
, though you will want to add a mutex at that point to defend against double closes to allow flexibility for callers.
} | ||
} | ||
|
||
func (r *SimpleRateLimiter) Close() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the intention here to have callers of New() (serve_cmd, Reloader, ...) call Close? Workable solution but we may need to change the fillBucket some to ensure it exits, see other comment.
Adds a new config syntax that allows more flexible rate limiting configuration for the
GOTRUE_RATE_LIMIT_EMAIL_SENT
,GOTRUE_RATE_LIMIT_SMS_SENT
configs. It can be added in follow up PRs to the other configs.It's fully backward compatible.
The syntax is:
123
or123.0
expresses 123 events with an unspecified time, this is the legacy case and is on a per-case basis (seeDivideAndSetDuration
)123/24h
expresses 123 emails sent per day (token bucket resets every 24 hours)Additionally the rate limiters for SMS and email are replaced with simple counters, massively simplifying the code paths and decreasing reliance on the tollbooth library which has memory leaks.
You can use any
time.ParseDuration()
value in the second part of the rate config.