jaegertracing/jaeger · warning

failed to forfeit resource lock: %w

Error message

failed to forfeit resource lock: %w

What it means

Lock.Forfeit returns this when the conditional DELETE was applied=false, i.e. the lock exists but is owned by a different tenant. It always wraps errLockOwnership ('this host does not own the resource lock'), so errors.Is(err, errLockOwnership) matches.

Source

Thrown at internal/storage/distributedlock/cassandra/lock.go:76

			return false, fmt.Errorf("failed to extend lease on resource lock: %w", err)
		}
		return true, nil
	}
	return false, nil
}

// Forfeit forfeits an existing lease around a given resource.
func (l *Lock) Forfeit(resource string) (bool, error) {
	var name, owner string
	applied, err := l.session.Query(cqlDeleteLock, resource, l.tenantID).ScanCAS(&name, &owner)
	if err != nil {
		return false, fmt.Errorf("failed to forfeit resource lock due to cassandra error: %w", err)
	}
	if applied {
		// The lock was successfully deleted
		return true, nil
	}
	return false, fmt.Errorf("failed to forfeit resource lock: %w", errLockOwnership)
}

// extendLease will attempt to extend the lease of an existing lock on a given resource.
func (l *Lock) extendLease(resource string, ttl time.Duration) error {
	ttlSec := int(ttl.Seconds())
	var owner string
	applied, err := l.session.Query(cqlUpdateLock, ttlSec, l.tenantID, resource, l.tenantID).ScanCAS(&owner)
	if err != nil {
		return err
	}
	if applied {
		return nil
	}
	return errLockOwnership
}

View on GitHub (pinned to 806f444784)

Solutions

  1. Check errors.Is(err, errLockOwnership) and treat as 'not owner' — don't crash, just skip release.
  2. Ensure all instances that contend for the same resource use the same tenantID.
  3. Wait for the lease TTL to expire so the lock becomes free, then reacquire.
  4. Verify you only call Forfeit for resources you successfully acquired in the same process/config.
  5. Use unique resource names per logical job to avoid cross-tenant contention.

Example fix

// before
if _, err := lock.Forfeit("index-cleaner"); err != nil {
    return err // fails when another peer holds the lock
}
// after
if _, err := lock.Forfeit("index-cleaner"); err != nil {
    if errors.Is(err, errLockOwnership) {
        log.Info("lock held by another peer; skipping forfeit")
        return nil
    }
    return err
}
Defensive patterns

Strategy: type-guard

Validate before calling

// only forfeit resources this process acquired and still owns
if ownedResources[resource] != l.tenantID {
    return nil // skip forfeit entirely
}

Type guard

func isNotOwner(err error) bool {
    return errors.Is(err, errLockOwnership) // wraps 'failed to forfeit resource lock: %w'
}

Try / catch

_, err := lock.Forfeit(resource)
switch {
case err == nil:
    // released
case errors.Is(err, errLockOwnership):
    // held by another peer — expected, log and continue
default:
    return fmt.Errorf("forfeit failed: %w", err)
}

Prevention

When it happens

Trigger: Calling Forfeit(resource) when another tenantID currently holds the lease on that resource (or the row's owner value doesn't match this Lock's tenantID). Not a Cassandra failure — the CAS simply didn't match.

Common situations: Two jobs using different tenant configs contending for the same resource name; a stale lock held by a crashed peer that had a different tenantID; caller attempting to release a lock it never acquired.

Related errors


AI-assisted analysis of jaegertracing/jaeger@806f444784 (2026-09-01). Data as JSON: /api/errors/ae75bb0e389f695a. Report an issue: GitHub.