fix: throttle student answers and attempt starts per user

WR-03: every new or changed open answer in a learning quiz with a
grader is a paid grading call. Add per-user buckets golem15.quizzes.answer
(60/min) and golem15.quizzes.start (10/min), configurable under
golem15.quizzes.throttle.*.
This commit is contained in:
Jakub Zych
2026-11-09 22:48:50 +01:00
parent 34b0966819
commit bb18242af9
5 changed files with 61 additions and 5 deletions

View File

@@ -52,6 +52,36 @@ func TestGraceFrom(cfg *compass.Config) time.Duration {
return min(max(grace, 0), maxTestGrace)
}
// Default per-user rate limits of the student write routes, per minute. In
// a learning quiz with a grader every new or changed open answer is a paid
// grading call, so answering and starting attempts are throttled (WR-03).
const (
DefaultAnswersPerMinute = 60
DefaultAttemptsPerMinute = 10
)
// maxPerMinute caps a configured per-minute limit.
const maxPerMinute = 10000
// PerMinuteFrom reads a per-minute rate limit at key (for example
// golem15.quizzes.throttle.answers_per_minute). A nil config, a missing
// key or a value that is not a number of at least 1 give def; the value
// is truncated to a whole number and capped at 10000.
func PerMinuteFrom(cfg *compass.Config, key string, def int) int {
if cfg == nil {
return def
}
v, ok := cfg.Lookup(key)
if !ok {
return def
}
n, ok := number(v)
if !ok || n < 1 || math.IsInf(n, 0) {
return def
}
return int(min(n, maxPerMinute))
}
// number is v as a float when it is a number or a numeric string.
func number(v any) (float64, bool) {
switch n := v.(type) {