crowdsecurity/crowdsec · error · BulkError

bulk creating alert: %w: %w

Error message

bulk creating alert: %w: %w

What it means

saveAlerts performs a single ent CreateBulk insert of all alert builders inside a transaction and wraps any database error with the BulkError sentinel. This is the standard bulk-insert failure path: the database rejected one or more of the alert rows (constraint violation, size limit, connection drop, etc.), so none of the batch is saved and the caller rolls back the transaction.

Source

Thrown at pkg/database/alerts.go:603

func (c *Client) saveAlerts(ctx context.Context, client *ent.Client, batch []alertCreatePlan) ([]string, error) {
	if len(batch) == 0 {
		log.Warningf("no alerts to create, discarded?")
		return nil, nil
	}

	// extract builders in the same order
	builders := make([]*ent.AlertCreate, len(batch))
	for i := range batch {
		if batch[i].builder == nil {
			return nil, fmt.Errorf("nil alert builder at index %d", i)
		}

		builders[i] = batch[i].builder
	}

	alertsCreateBulk, err := client.Alert.CreateBulk(builders...).Save(ctx)
	if err != nil {
		return nil, fmt.Errorf("bulk creating alert: %w: %w", err, BulkError)
	}

	ret := make([]string, len(alertsCreateBulk))
	for i, a := range alertsCreateBulk {
		ret[i] = strconv.Itoa(a.ID)

		d := batch[i].decisions
		if len(d) == 0 {
			continue
		}

		if err := slicetools.Batch(ctx, d, c.decisionBulkSize, func(ctx context.Context, d2 []*ent.Decision) error {
			return retryOnBusy(func() error {
				_, err := client.Alert.Update().Where(alert.IDEQ(a.ID)).AddDecisions(d2...).Save(ctx)
				return err
			})
		}); err != nil {
			return nil, fmt.Errorf("attach decisions to alert %d: %w", a.ID, err)

View on GitHub (pinned to 909b515798)

Solutions

  1. Read the wrapped underlying error (errors.Unwrap / %v of the chain) to identify the specific DB failure — constraints, lock, connection, or context
  2. If it is a duplicate/UUID constraint, stop re-submitting the same alert or deduplicate by UUID before calling CreateAlert
  3. If it is sqlite busy/locked, check for concurrent writers (other crowdsec/cscli processes) and ensure the DB is on a local filesystem, not NFS
  4. If it is a connection failure, verify DB reachability (host/port/credentials in crowdsec.yaml) and retry; the transaction was rolled back so the operation is safe to repeat
  5. If a field is too large, truncate or limit the alert payload (scenario, message) before ingestion

Example fix

// before
ids, err := client.CreateAlert(ctx, machineID, alerts)
if err != nil {
	return err
}

// after
ids, err := client.CreateAlert(ctx, machineID, dedupeByUUID(alerts))
if err != nil {
	if database.IsBulkError(err) {
		log.Errorf("bulk alert insert failed: %v", errors.Unwrap(err))
	}
	return err
}
Defensive patterns

Strategy: retry

Validate before calling

// dedupe and sanity-check alerts before submission
seen := map[string]bool{}
for _, a := range alerts {
	if a.UUID == nil || seen[*a.UUID] {
		return fmt.Errorf("missing or duplicate alert UUID")
	}
	seen[*a.UUID] = true
}

Type guard

func validAlert(a *models.Alert) bool {
	return a != nil && a.UUID != nil && a.Scenario != nil && a.Source != nil && a.Source.Scope != nil && a.Source.Value != nil
}

Try / catch

ids, err := client.CreateAlert(ctx, machineID, alerts)
var bulkErr *database.BulkError
if err != nil {
	if errors.As(err, &bulkErr) || database.IsBulkError(err) {
		log.Errorf("bulk insert rejected: %v", err) // inspect wrapped DB cause before retry
	}
}

Prevention

When it happens

Trigger: client.Alert.CreateBulk(builders...).Save(ctx) returns an error — e.g. a NOT NULL/UNIQUE constraint failure (duplicate alert UUID), a field value exceeding column size, SQLite 'database is locked'/'disk I/O error', MySQL/Postgres connection loss, or context cancellation mid-insert.

Common situations: Duplicate alert UUID when the same alert is submitted twice to a SQLite LAPI database; oversized scenario/message values against a fixed-size column on MySQL; sqlite database file locked by a concurrent cscli command or bouncer writes; disk full on the database volume.

Related errors


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