t8y2/dbx · error

Cannot preserve lease: key does not exist or has no lease

Error message

Cannot preserve lease: key does not exist or has no lease

What it means

Thrown by putPreservingLease (kv.go:320), reached when put is called with preserveLease:true. Before writing, the function GETs the key to learn its current lease; if the key does not exist or exists with no attached lease (Lease <= 0), there is nothing to preserve and this error is returned. Lease preservation only makes sense for keys already owned by an active lease.

Source

Thrown at agents/drivers/etcd-go/kv.go:320

		return response.Header.Revision, nil
	}()
	if err != nil && grantedLeaseID != 0 {
		_, _ = client.Lease.Revoke(context.Background(), grantedLeaseID)
	}
	if err != nil {
		return nil, err
	}
	return map[string]any{"revision": longString(revision)}, nil
}

func putPreservingLease(client *clientv3.Client, ctx context.Context, key, value string) (int64, error) {
	for attempt := 0; attempt < preserveLeaseMaxAttempts; attempt++ {
		existing, err := client.Get(ctx, key)
		if err != nil {
			return 0, err
		}
		if len(existing.Kvs) == 0 || existing.Kvs[0].Lease <= 0 {
			return 0, errors.New("Cannot preserve lease: key does not exist or has no lease")
		}
		current := existing.Kvs[0]
		txn := client.Txn(ctx).
			If(clientv3.Compare(clientv3.ModRevision(key), "=", current.ModRevision)).
			Then(clientv3.OpPut(key, value, clientv3.WithLease(clientv3.LeaseID(current.Lease))))
		response, err := txn.Commit()
		if err != nil {
			return 0, err
		}
		if response.Succeeded {
			return response.Header.Revision, nil
		}
	}
	return 0, errors.New("Cannot preserve lease: key changed concurrently; retry the save")
}

func (s *etcdSession) delete(params map[string]json.RawMessage) (any, error) {
	client, err := s.activeClient()

View on GitHub (pinned to c0390bff16)

Solutions

  1. Ensure the key exists with an active lease before using preserveLease — create it once with { ttl: N } first.
  2. Fall back to a plain put (no preserveLease) when the key is absent, e.g. get first and branch.
  3. Check the key's metadata lease via get before every preserveLease put.
  4. If the lease expired, re-grant it with ttl instead of trying to preserve it.
  5. Add a refresh loop (keepalive) for the lease if keys must outlive the TTL.

Example fix

// before
put(session, key, value, { preserveLease: true }) // key missing -> error
// after
const cur = get(session, key);
if (cur.found && cur.metadata.lease) put(session, key, value, { preserveLease: true });
else put(session, key, value, { ttl: 60 });
Defensive patterns

Strategy: validation

Validate before calling

const cur = get(session, key);
if (!cur.found || !cur.metadata.lease) {
  throw new Error('preserveLease requires an existing key with an active lease');
}

Type guard

function canPreserveLease(keyState) { return keyState.found === true && keyState.metadata != null && keyState.metadata.lease > 0; }

Try / catch

try { return put(session, key, value, { preserveLease: true }); }
catch (e) {
  if (String(e).includes('key does not exist or has no lease'))
    return put(session, key, value, { ttl: 60 }); // bootstrap the lease
  throw e;
}

Prevention

When it happens

Trigger: put(..., { preserveLease: true }) on a key that was never created with a lease/ttl, or on a key whose lease already expired (etcd auto-deleted the key), or on a not-yet-existing key.

Common situations: Using preserveLease as a blanket default for all writes including initial creation; lease TTL expiring between writes so the key vanishes; key previously written without ttl and code assumes it has one; key created by another tool without a lease.

Related errors


AI-assisted analysis of t8y2/dbx@c0390bff16 (2026-09-05). Data as JSON: /api/errors/b02ebbe897b043ae. Report an issue: GitHub.