{"record":{"id":"d217b52cb69e0788","repo":"crowdsecurity/crowdsec","slug":"expire-decisions-with-provided-filter-w-w","errorCode":null,"errorMessage":"expire decisions with provided filter: %w: %w","messagePattern":"expire decisions with provided filter: %w: %w","errorType":"exception","errorClass":"DeleteFail","httpStatus":null,"severity":"error","filePath":"pkg/database/decisions.go","lineNumber":273,"sourceCode":"\t\tdefault:\n\t\t\treturn 0, nil, fmt.Errorf(\"'%s' doesn't exist: %w\", param, InvalidFilter)\n\t\t}\n\t}\n\n\tdecisions, err = decisionIPFilter(decisions, contains, rng)\n\tif err != nil {\n\t\treturn 0, nil, err\n\t}\n\n\tdecisionsToDelete, err := decisions.All(ctx)\n\tif err != nil {\n\t\tc.Log.Warningf(\"ExpireDecisionsWithFilter : %s\", err)\n\t\treturn 0, nil, fmt.Errorf(\"expire decisions with provided filter: %w\", DeleteFail)\n\t}\n\n\tcount, err := c.ExpireDecisions(ctx, decisionsToDelete)\n\tif err != nil {\n\t\treturn 0, nil, fmt.Errorf(\"expire decisions with provided filter: %w: %w\", err, DeleteFail)\n\t}\n\n\treturn count, decisionsToDelete, err\n}\n\nfunc decisionIDs(decisions []*ent.Decision) []int {\n\tids := make([]int, len(decisions))\n\tfor i, d := range decisions {\n\t\tids[i] = d.ID\n\t}\n\n\treturn ids\n}\n\n// expireDecisionBatch expires the decisions as a single operation.\nfunc (c *Client) expireDecisionBatch(ctx context.Context, batch []*ent.Decision, now time.Time) (int, error) {\n\tids := decisionIDs(batch)\n","sourceCodeStart":255,"sourceCodeEnd":291,"githubUrl":"https://github.com/crowdsecurity/crowdsec/blob/909b5157986a2b2c2163300fdaef5ed01289f7d2/pkg/database/decisions.go#L255-L291","documentation":"After the candidate decisions are fetched, ExpireDecisionsWithFilter calls ExpireDecisions to set their until_ts to now. If that bulk update fails, the underlying error is wrapped with the DeleteFail sentinel and this message, meaning the filter matched rows but the expiry write itself failed.","triggerScenarios":"ExpireDecisions returning an error: DB connection dropped between query and update, write lock contention on decision table, context cancelled mid-update.","commonSituations":"Concurrent LAPI writers contending on the decisions table; SQLite 'database is locked' under parallel cscli commands; DB going away during long maintenance jobs.","solutions":["Retry the operation; the failure is at the write stage and the filter query already succeeded","Check DB connectivity and lock contention (show processlist / SQLite busy_timeout)","Verify disk space and DB write permissions for the crowdsec process","Check errors.Is(err, database.DeleteFail) to map to a 500 in API handlers"],"exampleFix":"// before\ncount, _, err := client.ExpireDecisionsWithFilter(ctx, filter)\nif err != nil { return err }\n// after\ncount, _, err := client.ExpireDecisionsWithFilter(ctx, filter)\nif errors.Is(err, database.DeleteFail) {\n    return fmt.Errorf(\"decision expiry failed, check database: %w\", err)\n}","handlingStrategy":"retry","validationCode":"// ensure DB is reachable and writable before the bulk update\nif err := entClient.Ping(ctx); err != nil { return err }","typeGuard":null,"tryCatchPattern":"if _, _, err := client.ExpireDecisionsWithFilter(ctx, filter); err != nil {\n    if errors.Is(err, database.DeleteFail) {\n        // retry once after short backoff; check inner error for lock/conn issues\n    }\n    return err\n}","preventionTips":["Avoid many concurrent writers doing bulk expiry on the same table","For SQLite, set busy_timeout to tolerate lock contention","Ensure adequate disk space on the DB volume"],"tags":["go","database","write","ent"],"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-14T00:17:10.932Z"}