{"record":{"id":"6a655895e756115e","repo":"crowdsecurity/crowdsec","slug":"attach-decisions-to-alert-d-w","errorCode":null,"errorMessage":"attach decisions to alert %d: %w","messagePattern":"attach decisions to alert (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/database/alerts.go","lineNumber":621,"sourceCode":"\t\treturn nil, fmt.Errorf(\"bulk creating alert: %w: %w\", err, BulkError)\n\t}\n\n\tret := make([]string, len(alertsCreateBulk))\n\tfor i, a := range alertsCreateBulk {\n\t\tret[i] = strconv.Itoa(a.ID)\n\n\t\td := batch[i].decisions\n\t\tif len(d) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := slicetools.Batch(ctx, d, c.decisionBulkSize, func(ctx context.Context, d2 []*ent.Decision) error {\n\t\t\treturn retryOnBusy(func() error {\n\t\t\t\t_, err := client.Alert.Update().Where(alert.IDEQ(a.ID)).AddDecisions(d2...).Save(ctx)\n\t\t\t\treturn err\n\t\t\t})\n\t\t}); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"attach decisions to alert %d: %w\", a.ID, err)\n\t\t}\n\t}\n\n\treturn ret, nil\n}\n\ntype alertCreatePlan struct {\n\tbuilder   *ent.AlertCreate\n\tdecisions []*ent.Decision\n}\n\nfunc (c *Client) createAlertBatch(ctx context.Context, machineID string, owner *ent.Machine, alerts []*models.Alert) ([]string, error) {\n\ttx, err := c.Ent.Tx(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating alert transaction: %w: %w\", err, BulkError)\n\t}\n\n\ttxEnt := tx.Client()","sourceCodeStart":603,"sourceCodeEnd":639,"githubUrl":"https://github.com/crowdsecurity/crowdsec/blob/909b5157986a2b2c2163300fdaef5ed01289f7d2/pkg/database/alerts.go#L603-L639","documentation":"After the alerts are bulk-inserted, saveAlerts attaches each alert's pre-created decision rows via Alert.Update().AddDecisions(), batched and retried on SQLite busy. This error wraps any failure of that update: the alert row exists (a.ID is printed) but the decision-linking write failed, causing the whole transaction to roll back.","triggerScenarios":"The AddDecisions update fails for any reason: SQLite 'database is locked' persisting beyond maxLockRetries (\"exceeded N busy retries\"), a foreign-key/unique constraint on decision rows, the update statement hitting a dropped connection, or context cancellation during the batches.","commonSituations":"High-concurrency setups on SQLite where LAPI writes and cscli queries contend and the busy-retry loop is exhausted; very large alert batches with many decisions exceeding the configured decisionBulkSize across many batches while another writer holds the lock; DB connectivity blips on MySQL/Postgres backends.","solutions":["Check the wrapped error: if it is 'exceeded N busy retries', reduce concurrent writers on the SQLite DB or switch to MySQL/Postgres for the crowdsec database","Retry the whole CreateAlert call — the transaction rolled back, so alerts were not persisted and it is safe to resubmit","Increase throughput headroom: enable WAL mode for SQLite, or ensure the DB file is on local disk, not a network mount","If it is a constraint error on decisions, inspect the decision payloads for duplicate IDs or invalid references before submitting","Verify DB connectivity and timeouts in the database config if on a network backend"],"exampleFix":"// before\nids, err := client.CreateAlert(ctx, machineID, alerts)\nif err != nil {\n\treturn fmt.Errorf(\"create failed: %w\", err)\n}\n\n// after\nids, err := client.CreateAlert(ctx, machineID, alerts)\nif err != nil {\n\tif strings.Contains(err.Error(), \"busy retries\") {\n\t\ttime.Sleep(2 * time.Second)\n\t\tids, err = client.CreateAlert(ctx, machineID, alerts) // tx rolled back: safe retry\n\t}\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create failed: %w\", err)\n\t}\n}","handlingStrategy":"retry","validationCode":"// bound the work: cap decisions per alert before submission\nif len(alert.Decisions) > maxDecisionsPerAlert {\n\treturn fmt.Errorf(\"alert %s has too many decisions (%d)\", *alert.UUID, len(alert.Decisions))\n}","typeGuard":"null","tryCatchPattern":"ids, err := client.CreateAlert(ctx, machineID, alerts)\nif err != nil && strings.Contains(err.Error(), \"attach decisions\") {\n\t// tx rolled back; back off and retry whole batch\n\ttime.Sleep(backoff)\n\tids, err = client.CreateAlert(ctx, machineID, alerts)\n}","preventionTips":["Reduce concurrent writers on SQLite (bouncers/cscli/LAPI) or switch to MySQL/Postgres","Enable WAL mode on the SQLite database","Retry idempotently: the transaction rolls back on this error, so resubmission is safe","Keep alert decision counts reasonable; tune decisionBulkSize if batches are huge","Watch for 'exceeded N busy retries' in crowdsec logs as an early sign of lock contention"],"tags":["database","sqlite-busy","foreign-keys","crowdsec"],"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"}