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

  1. 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
  2. For SQLite lock errors, reduce concurrent writers or switch the DB to a client/server backend (PostgreSQL/MySQL) in crowdsec.db_config
  3. Check disk space and file permissions on the SQLite database file
  4. Verify the payload does not duplicate existing decision UUIDs that would break unique constraints
  5. 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

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


AI-assisted analysis of crowdsecurity/crowdsec@909b515798 (2026-09-06). Data as JSON: /api/errors/a086408bf402133a. Report an issue: GitHub.