hashicorp/nomad · warning

lock release: %w

Error message

lock release: %w

What it means

This error is produced by LockLeaser.Start when the underlying locker.Release call fails while cleaning up after the protected functions finish. It wraps whatever Release returned — most commonly 'release conflict' (ErrLockConflict) from losing the lock. Start collects it via errors.Join, so the returned error can combine a release failure with other errors.

Source

Thrown at api/locks.go:269

	return ll
}

// Start wraps the start function in charge of executing the protected
// function and maintain the lease but is in charge of releasing the
// lock before exiting. It is a blocking function.
func (ll *LockLeaser) Start(ctx context.Context, protectedFuncs ...func(ctx context.Context) error) error {
	var mErr []error

	err := ll.start(ctx, protectedFuncs...)
	if err != nil {
		mErr = append(mErr, err)
	}

	if ll.locked {
		err = ll.locker.Release(ctx)
		if err != nil {
			mErr = append(mErr, fmt.Errorf("lock release: %w", err))
		}
	}

	return errors.Join(mErr...)
}

// start starts the process of maintaining the lease and executes the protected
// function on an independent go routine. It is a blocking function, it
// will return once the protected function is done or an execution error
// arises.
func (ll *LockLeaser) start(ctx context.Context, protectedFuncs ...func(ctx context.Context) error) error {
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	// errChannel is used track execution errors
	errChannel := make(chan error, 1)
	defer close(errChannel)

View on GitHub (pinned to 482b49bf1a)

Solutions

  1. Inspect the wrapped cause with errors.Is/errors.As: if errors.Is(err, api.ErrLockConflict), the lock was already lost, so the release failure is expected and can be ignored.
  2. Make the protected function resilient: check ctx cancellation / lock health and abort promptly when the lease is lost, before Release is attempted.
  3. Increase the lock TTL or shorten the protected work so the lease does not expire before Start returns.
  4. If the cause is not ErrLockConflict, check network/API connectivity and retry the Start call.
  5. Log and continue: Start still returns the protected function's result via errors.Join; handle each joined error individually.

Example fix

// before
if err := leaser.Start(ctx, work); err != nil {
    log.Fatal(err)
}
// after
if err := leaser.Start(ctx, work); err != nil {
    if errors.Is(err, api.ErrLockConflict) {
        log.Println("lock was lost before release; ignoring cleanup conflict")
    } else {
        log.Fatal(err)
    }
}
Defensive patterns

Strategy: type-guard

Validate before calling

// preflight: validate the lock TTL so the lease can survive the protected work
ttl, err := time.ParseDuration(v.Lock.TTL)
if err != nil || ttl <= 0 {
    return fmt.Errorf("invalid lock TTL before starting leaser: %q", v.Lock.TTL)
}

Type guard

func IsLockConflict(err error) bool {
    return errors.Is(err, api.ErrLockConflict)
}

Try / catch

err := leaser.Start(ctx, work)
if err != nil {
    if errors.Is(err, api.ErrLockConflict) {
        // joined release failure is a lost-lock cleanup conflict; log and continue
        log.Printf("lock already lost: %v", err)
        return nil
    }
    return err
}

Prevention

When it happens

Trigger: LockLeaser.Start finishing while the lock was lost mid-run (TTL expired, another instance took over) so Release gets 409 Conflict; Release failing due to network/API errors after the protected function completes; the protected function taking longer than the TTL so the lease lapses before the automatic Release.

Common situations: Batch jobs whose runtime exceeds DefaultLockTTL (15s) without renewal succeeding; two replicas contending for the same lock variable; tests like TestFailedRenewal exercising renewal failures then cleanup; a Renew returning ErrLockConflict that propagates through maintainLease and start's error channel before Release runs on cleanup.

Related errors


AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04). Data as JSON: /api/errors/9a1779a44ed384a5. Report an issue: GitHub.