plandex-ai/plandex · error

error inserting new lock: %v

Error message

error inserting new lock: %v

What it means

lockRepoDB wraps any failure of the INSERT INTO repo_locks ... ON CONFLICT DO NOTHING statement (other than the no-rows conflict case) into 'error inserting new lock'. It signals that the lock row could not be created in Postgres, so the plan-level repo lock was not acquired. The original database error is embedded in the message via %v.

Source

Thrown at app/server/db/locks.go:355

		newLock.PlanBuildId,
		newLock.Scope,
		newLock.Branch,
	).Scan(&insertedId)
	if err != nil {
		if err == sql.ErrNoRows {
			// Means ON CONFLICT DO NOTHING prevented insertion
			// => concurrency conflict => backoff & retry
			return retryWithExponentialBackoff(params.Ctx,
				errors.New("lock conflict: row not inserted"),
				numRetry,
				func(nextAttempt int) (string, error) {
					return lockRepoDB(params, nextAttempt)
				},
			)
		}

		log.Printf("[Lock][%d] error inserting new lock: %v | reason: %s", goroutineID, err, params.Reason)
		return "", fmt.Errorf("error inserting new lock: %v", err)
	}

	if insertedId.Valid {
		newLock.Id = insertedId.String
	} else {
		if locksVerboseLogging {
			log.Printf("no rows returned from insert query, means there was a conflict")
		}
		return retryWithExponentialBackoff(params.Ctx, err, numRetry, func(nextAttempt int) (string, error) {
			return lockRepoDB(params, nextAttempt)
		})
	}

	if locksVerboseLogging {
		log.Printf("[Lock][%d] INSERT took %v | reason: %s",
			goroutineID, time.Since(insertStart), params.Reason)
	}

View on GitHub (pinned to e2d772072e)

Solutions

  1. Inspect the wrapped cause in the message (e.g. pq code 40001/40P01) — transient errors are retried by retryWithExponentialBackoff, so persistent ones usually mean a non-transient DB issue
  2. Verify the repo_locks schema matches the insert columns (org_id, user_id, plan_id, plan_build_id, scope, branch) and FK targets exist
  3. Check Postgres health: connection limits (max_connections), pool settings, and logs for deadlocks or cancellations at the same timestamp
  4. Ensure the caller's context deadline is generous enough for the insert under normal load

Example fix

// before
id, err := lockRepoDB(ctx, planId, orgId, scope)
if err != nil { log.Fatal(err) }
// after
id, err := lockRepoDB(ctx, planId, orgId, scope)
if err != nil {
    var pqErr *pq.Error
    if errors.As(err, &pqErr) && (pqErr.Code == "40001" || pqErr.Code == "40P01") {
        // transient: rely on retry/backoff instead of failing hard
    }
    return fmt.Errorf("acquiring repo lock for plan %s: %w", planId, err)
}
Defensive patterns

Strategy: try-catch

Validate before calling

if err := Conn.PingContext(ctx); err != nil {
    return fmt.Errorf("db unavailable, skip lock acquisition: %w", err)
}

Type guard

var pqErr *pq.Error
if errors.As(err, &pqErr) {
    transient := pqErr.Code == "40001" || pqErr.Code == "40P01"
}

Try / catch

id, err := lockRepoDB(ctx, orgId, planId, reason)
if err != nil {
    var pqErr *pq.Error
    if errors.As(err, &pqErr) && (pqErr.Code == "40001" || pqErr.Code == "40P01") {
        // transient: safe to retry later
    }
    return fmt.Errorf("acquire lock plan %s: %w", planId, err)
}

Prevention

When it happens

Trigger: The INSERT returns a real DB error rather than sql.ErrNoRows: e.g. connection dropped mid-query, NOT NULL/FK violation on org_id, serialization failure (40001) or deadlock (40P01) under REPEATABLE READ, or context deadline exceeded on params.Ctx while the query runs.

Common situations: Postgres restarted or connection pool exhausted under load; concurrent plans deadlocking on the repo_locks unique index; invalid orgId referencing a missing orgs row; query canceled because the caller's context timed out before insert completed.

Related errors


AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05). Data as JSON: /api/errors/2daab962d66cd581. Report an issue: GitHub.