{"record":{"id":"a086408bf402133a","repo":"crowdsecurity/crowdsec","slug":"s-w-a08640","errorCode":null,"errorMessage":"%s: %w","messagePattern":"%s: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/database/alerts.go","lineNumber":39,"sourceCode":"\t\"github.com/crowdsecurity/crowdsec/pkg/database/ent/decision\"\n\t\"github.com/crowdsecurity/crowdsec/pkg/database/ent/event\"\n\t\"github.com/crowdsecurity/crowdsec/pkg/database/ent/meta\"\n\t\"github.com/crowdsecurity/crowdsec/pkg/models\"\n)\n\nconst (\n\tpaginationSize      = 100 // used to queryAlert to avoid 'too many SQL variable'\n\tdefaultLimit        = 100 // default limit of element to returns when query alerts\n\talertCreateBulkSize = 50  // bulk size when create alerts\n\tmaxLockRetries      = 10  // how many times to retry a bulk operation when sqlite3.ErrBusy is encountered\n)\n\nfunc rollbackOnError(tx *ent.Tx, err error, msg string) error {\n\tif rbErr := tx.Rollback(); rbErr != nil {\n\t\tlog.Errorf(\"rollback error: %v\", rbErr)\n\t}\n\n\treturn fmt.Errorf(\"%s: %w\", msg, err)\n}\n\n// CreateOrUpdateAlert is specific to PAPI : It checks if alert already exists, otherwise inserts it\n// if alert already exists, it checks it associated decisions already exists\n// if some associated decisions are missing (ie. previous insert ended up in error) it inserts them\nfunc (c *Client) CreateOrUpdateAlert(ctx context.Context, machineID string, alertItem *models.Alert) (string, error) {\n\tif alertItem.UUID == \"\" {\n\t\treturn \"\", errors.New(\"alert UUID is empty\")\n\t}\n\n\talerts, err := c.Ent.Alert.Query().Where(alert.UUID(alertItem.UUID)).WithDecisions().All(ctx)\n\tif err != nil && !ent.IsNotFound(err) {\n\t\treturn \"\", fmt.Errorf(\"unable to query alerts for uuid %s: %w\", alertItem.UUID, err)\n\t}\n\n\t// alert wasn't found, insert it (expected hotpath)\n\tif ent.IsNotFound(err) || len(alerts) == 0 {\n\t\talertIDs, err := c.CreateAlert(ctx, machineID, []*models.Alert{alertItem})","sourceCodeStart":21,"sourceCodeEnd":57,"githubUrl":"https://github.com/crowdsecurity/crowdsec/blob/909b5157986a2b2c2163300fdaef5ed01289f7d2/pkg/database/alerts.go#L21-L57","documentation":"rollbackOnError is a helper that rolls back an active ent transaction and wraps the original error with a context message ('%s: %w'). It is thrown whenever a bulk transactional write (alert batch creation, community blocklist update, allowlist add) fails mid-transaction; the returned error carries the caller's message plus the underlying cause. A secondary 'rollback error' may be logged if the rollback itself fails, but the returned error always wraps the original err.","triggerScenarios":"Any failure inside a transactional code path that calls rollbackOnError: CreateAlert/createAlertBatch bulk insert failing (e.g. SQLite 'database is locked' after retries, constraint violation on decision UUID, disk full), UpdateCommunityBlocklist failing mid-insert, or AddToAllowlist hitting a DB constraint/lock error. The transaction is rolled back and the wrapped error returned to the caller.","commonSituations":"SQLite database lock contention when multiple crowdsec components (LAPI, agents) write concurrently; unique-constraint violations on re-sent alerts/decisions; a corrupted or read-only DB file; disk exhaustion on the host running crowdsec.","solutions":["Inspect the wrapped cause (errors.Unwrap / %v of the returned error) to find the real DB error, e.g. 'database is locked' vs constraint violation","For SQLite lock errors, reduce concurrent writers or switch the DB to a client/server backend (PostgreSQL/MySQL) in crowdsec.db_config","Check disk space and file permissions on the SQLite database file","Verify the payload does not duplicate existing decision UUIDs that would break unique constraints","If lock errors are frequent, confirm maxLockRetries behavior in your version and consider upgrading"],"exampleFix":"// caller handling\nif err := createAlertBatch(ctx, alerts); err != nil {\n    // before: log.Fatal(err) hides the cause\n    // after: unwrap and classify\n    if strings.Contains(err.Error(), \"database is locked\") {\n        // retry with backoff\n    }\n    log.Errorf(\"alert tx failed: %v\", err)\n}","handlingStrategy":"try-catch","validationCode":"// pre-check DB writability before batch operations\nif err := c.Ent.Debug().Context(ctx).Ping ?? nil; err != nil { /* not ent: use a trivial query */ }\nif _, err := c.Ent.Alert.Query().Limit(1).All(ctx); err != nil {\n    return fmt.Errorf(\"database not writable/reachable: %w\", err)\n}","typeGuard":"var dbErr *ent.ValidationError\nif errors.As(err, &dbErr) { /* handle schema/constraint issue distinctly */ }","tryCatchPattern":"id, err := doTransactionalWrite(ctx)\nif err != nil {\n    if strings.Contains(err.Error(), \"database is locked\") {\n        // transient: retry with backoff\n    } else if errors.Is(err, context.DeadlineExceeded) {\n        // slow DB: increase timeout\n    }\n    return err\n}","preventionTips":["Avoid many concurrent writers on SQLite; use PostgreSQL/MySQL for busy LAPI deployments","Monitor free disk space on the DB volume","Keep alert/decision payloads free of duplicate UUIDs","Run crowdsec's DB migrations when upgrading before starting the service"],"tags":["database","transaction","sqlite","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-14T00:17:10.932Z"}