{"record":{"id":"fe84d41ddc32e2bf","repo":"hibiken/asynq","slug":"rate-newsemaphore-maxtokens-cannot-be-less-than-1","errorCode":null,"errorMessage":"rate.NewSemaphore: maxTokens cannot be less than 1","messagePattern":"rate\\.NewSemaphore: maxTokens cannot be less than 1","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"x/rate/semaphore.go","lineNumber":23,"sourceCode":"\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/hibiken/asynq\"\n\tasynqcontext \"github.com/hibiken/asynq/internal/context\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// NewSemaphore creates a counting Semaphore for the given scope with the given number of tokens.\nfunc NewSemaphore(rco asynq.RedisConnOpt, scope string, maxTokens int) *Semaphore {\n\trc, ok := rco.MakeRedisClient().(redis.UniversalClient)\n\tif !ok {\n\t\tpanic(fmt.Sprintf(\"rate.NewSemaphore: unsupported RedisConnOpt type %T\", rco))\n\t}\n\n\tif maxTokens < 1 {\n\t\tpanic(\"rate.NewSemaphore: maxTokens cannot be less than 1\")\n\t}\n\n\tif len(strings.TrimSpace(scope)) == 0 {\n\t\tpanic(\"rate.NewSemaphore: scope should not be empty\")\n\t}\n\n\treturn &Semaphore{\n\t\trc:        rc,\n\t\tscope:     scope,\n\t\tmaxTokens: maxTokens,\n\t}\n}\n\n// Semaphore is a distributed counting semaphore which can be used to set maxTokens across multiple asynq servers.\ntype Semaphore struct {\n\trc        redis.UniversalClient\n\tmaxTokens int\n\tscope     string","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/hibiken/asynq/blob/d135f1439bee74e989b7f9b41ecd542cc87f024a/x/rate/semaphore.go#L5-L41","documentation":"rate.NewSemaphore panics when maxTokens is less than 1, since a counting semaphore must grant at least one token to be meaningful. This is an eager argument-validation panic raised immediately at construction, before any Redis calls.","triggerScenarios":"Calling rate.NewSemaphore(rco, scope, 0) or rate.NewSemaphore(rco, scope, -1) — typically when maxTokens is computed from config, a multiplier, or a variable that defaults to zero.","commonSituations":"maxTokens sourced from an unset env var or config field defaulting to 0; integer division truncation yielding 0; copy-pasted constructor calls with placeholder values.","solutions":["Pass a positive integer for maxTokens (e.g. 10).","If maxTokens comes from config, default it to a sane positive value when unset (e.g. if v <= 0 { v = defaultMaxTokens }).","Validate the value before calling NewSemaphore and fail with a descriptive error.","Check arithmetic that computes maxTokens (division, multipliers) for zero/negative results."],"exampleFix":"// before\nsem := rate.NewSemaphore(rco, \"api\", cfg.MaxTokens) // MaxTokens = 0\n// after\nif cfg.MaxTokens < 1 { cfg.MaxTokens = 10 }\nsem := rate.NewSemaphore(rco, \"api\", cfg.MaxTokens)","handlingStrategy":"validation","validationCode":"func validateMaxTokens(n int) error {\n    if n < 1 {\n        return fmt.Errorf(\"maxTokens must be >= 1, got %d\", n)\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":"// Go panics are not catchable via try/catch; recover at process boundary if desired:\ndefer func() {\n    if r := recover(); r != nil {\n        log.Fatalf(\"rate.NewSemaphore: %v\", r)\n    }\n}()","preventionTips":["Default maxTokens to a positive value when config is unset.","Check computed values (divisions, multipliers) for zero/negative results before use.","Fail fast at config-load time with a clear message instead of at construction.","Add a test covering the zero/default config path."],"tags":["go","panic","validation","rate-limiting","argument"],"backgroundTag":"invalid-argument-value","analyzedSha":"d135f1439bee74e989b7f9b41ecd542cc87f024a","analyzedAt":"2026-09-07T19:02:34.660Z","contentChangedAt":"2026-09-07T19:02:34.660Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}