crowdsecurity/crowdsec · error
%s: %w
Error message
%s: %w
What it means
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.
Source
Thrown at pkg/database/alerts.go:39
"github.com/crowdsecurity/crowdsec/pkg/database/ent/decision"
"github.com/crowdsecurity/crowdsec/pkg/database/ent/event"
"github.com/crowdsecurity/crowdsec/pkg/database/ent/meta"
"github.com/crowdsecurity/crowdsec/pkg/models"
)
const (
paginationSize = 100 // used to queryAlert to avoid 'too many SQL variable'
defaultLimit = 100 // default limit of element to returns when query alerts
alertCreateBulkSize = 50 // bulk size when create alerts
maxLockRetries = 10 // how many times to retry a bulk operation when sqlite3.ErrBusy is encountered
)
func rollbackOnError(tx *ent.Tx, err error, msg string) error {
if rbErr := tx.Rollback(); rbErr != nil {
log.Errorf("rollback error: %v", rbErr)
}
return fmt.Errorf("%s: %w", msg, err)
}
// CreateOrUpdateAlert is specific to PAPI : It checks if alert already exists, otherwise inserts it
// if alert already exists, it checks it associated decisions already exists
// if some associated decisions are missing (ie. previous insert ended up in error) it inserts them
func (c *Client) CreateOrUpdateAlert(ctx context.Context, machineID string, alertItem *models.Alert) (string, error) {
if alertItem.UUID == "" {
return "", errors.New("alert UUID is empty")
}
alerts, err := c.Ent.Alert.Query().Where(alert.UUID(alertItem.UUID)).WithDecisions().All(ctx)
if err != nil && !ent.IsNotFound(err) {
return "", fmt.Errorf("unable to query alerts for uuid %s: %w", alertItem.UUID, err)
}
// alert wasn't found, insert it (expected hotpath)
if ent.IsNotFound(err) || len(alerts) == 0 {
alertIDs, err := c.CreateAlert(ctx, machineID, []*models.Alert{alertItem})View on GitHub (pinned to 909b515798)
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
Example fix
// caller handling
if err := createAlertBatch(ctx, alerts); err != nil {
// before: log.Fatal(err) hides the cause
// after: unwrap and classify
if strings.Contains(err.Error(), "database is locked") {
// retry with backoff
}
log.Errorf("alert tx failed: %v", err)
} Defensive patterns
Strategy: try-catch
Validate before calling
// pre-check DB writability before batch operations
if err := c.Ent.Debug().Context(ctx).Ping ?? nil; err != nil { /* not ent: use a trivial query */ }
if _, err := c.Ent.Alert.Query().Limit(1).All(ctx); err != nil {
return fmt.Errorf("database not writable/reachable: %w", err)
} Type guard
var dbErr *ent.ValidationError
if errors.As(err, &dbErr) { /* handle schema/constraint issue distinctly */ } Try / catch
id, err := doTransactionalWrite(ctx)
if err != nil {
if strings.Contains(err.Error(), "database is locked") {
// transient: retry with backoff
} else if errors.Is(err, context.DeadlineExceeded) {
// slow DB: increase timeout
}
return err
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- unable to expire decisions for list %s : %w
- unable to create alert: %w
- bulk creating alert: %w: %w
- creating alert transaction: %w: %w
- committing alert transaction: %w: %w
AI-assisted analysis of crowdsecurity/crowdsec@909b515798 (2026-09-06).
Data as JSON: /api/errors/a086408bf402133a.
Report an issue: GitHub.