{"record":{"id":"48b06dfcb9befaea","repo":"crowdsecurity/crowdsec","slug":"unable-to-expire-decisions-for-batch-w","errorCode":null,"errorMessage":"unable to expire decisions for batch: %w","messagePattern":"unable to expire decisions for batch: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/database/allowlists.go","lineNumber":539,"sourceCode":"\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\n\t\tcount, err := c.Ent.Decision.Update().\n\t\t\tSetUntil(now).\n\t\t\tWhere(\n\t\t\t\tdecision.UntilGTE(now),\n\t\t\t\tdecision.IPSizeEQ(ipSize),\n\t\t\t\tdecision.Or(conditions...),\n\t\t\t).\n\t\t\tSave(ctx)\n\n\t\tif err != nil {\n\t\t\treturn totalCount, fmt.Errorf(\"unable to expire decisions for batch: %w\", err)\n\t\t}\n\n\t\ttotalCount += count\n\t\tc.Log.Debugf(\"expired %d decisions for batch of %d allowlist items\", count, len(batch))\n\t}\n\n\treturn totalCount, nil\n}\n","sourceCodeStart":521,"sourceCodeEnd":548,"githubUrl":"https://github.com/crowdsecurity/crowdsec/blob/909b5157986a2b2c2163300fdaef5ed01289f7d2/pkg/database/allowlists.go#L521-L548","documentation":"applyAllowlistBatch expires existing decisions matching a batch of allowlist items via an ent decision Update with OR-ed conditions. If the batched UPDATE fails, this error wraps it and reports the count expired so far (totalCount) - earlier batches may already be committed, so the operation is partially applied.","triggerScenarios":"Calling applyAllowlistBatch via ApplyAllowlistsToExistingDecisions when a decision-update Save fails: SQLite lock contention, IO error, or an overly large OR-condition batch timing out.","commonSituations":"Large allowlists (many batches) against a busy SQLite DB during decision flushes; concurrent bouncer pulls locking decisions table.","solutions":["Check totalCount in the wrapped result to see how far the expiry got before failing","Resolve SQLite lock contention (stop concurrent writers, increase busy_timeout)","Re-run ApplyAllowlistsToExistingDecisions; expiring is idempotent","If batches are too large, reduce the batch size so each UPDATE touches fewer rows"],"exampleFix":"// before\nif err != nil {\n    return totalCount, fmt.Errorf(\"unable to expire decisions for batch: %w\", err)\n}\n// after - retry transient lock errors before giving up\nif err := retryOnLock(ctx, 3, saveFn); err != nil {\n    return totalCount, fmt.Errorf(\"unable to expire decisions for batch: %w\", err)\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"count, err := applyBatch(ctx, items)\nif err != nil {\n    // count = totalCount: batches already committed; expiring is idempotent, safe to retry\n    if isTransientDB(err) {\n        return retryWithBackoff(ctx, 3, func() error { _, err := applyBatch(ctx, items); return err })\n    }\n    return err\n}","preventionTips":["Keep allowlist batches small so each UPDATE stays short-lived","Use a busy_timeout so concurrent bouncer pulls don't abort the expiry update","Re-run on failure: expiring decisions is idempotent"],"tags":["database","allowlist","decisions","sqlite"],"backgroundTag":"database-write-failed","analyzedSha":"909b5157986a2b2c2163300fdaef5ed01289f7d2","analyzedAt":"2026-09-06T12:27:26.012Z","contentChangedAt":"2026-09-06T12:27:26.012Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}