Skip to content

feat: make ClaimVM idempotent with request_id - #106

Merged
richardcase merged 4 commits into
liquidmetal-dev:mainfrom
phoban01:feat/idempotent-claims
Sep 29, 2026
Merged

richardcase merged 4 commits into
liquidmetal-dev:mainfrom
phoban01:feat/idempotent-claims

Conversation

@phoban01

Copy link
Copy Markdown
Contributor

Closes #98. Part 1 of #105.

A client can now retry ClaimVM without losing a VM. The client sets request_id on the request. If a lease with that ID still exists, the manager returns that lease again and does not claim a second VM. This follows the idempotent claims ADR.

Changes

Store:

  • store.Open now applies numbered migrations and records the version in PRAGMA user_version. schema.sql is the version 0 baseline. Each migration runs in its own transaction.
  • Migration 1 adds leases.request_id with a partial unique index. An empty ID is stored as NULL, so claims without an ID never conflict.
  • CreateLease returns ErrDuplicateRequestID when the index rejects a row. GetLeaseByRequestID is new.
  • Open refuses a database that a newer binary has migrated.

API:

  • ClaimVMRequest.request_id and LeaseRecord.request_id are new fields.
  • An ID longer than 255 bytes fails with INVALID_ARGUMENT.
  • A repeat request on the same pool returns the same lease and VM. It does not extend the lease.
  • A repeat request on a different pool fails with INVALID_ARGUMENT.
  • Two identical requests can race. The loser puts its VM back to AVAILABLE and returns the winner's lease.
  • poolmgr_vm_claims_total has a new replayed label.

CLI:

  • poolmgrctl lease claim --request-id sets the field.
  • lease list shows a REQUEST_ID column.

Two cases the ADR does not cover

Both return ABORTED, and the client can retry:

  • A retry arrives while its lease is being released. The VM row is gone, but the lease row is not gone yet.
  • The loser of a race looks up the winner's lease, but that lease has already ended.

In both cases a later retry makes a fresh claim.

Testing

  • go test -race ./... and the e2e tests pass.
  • golangci-lint, buf lint and buf breaking pass.
  • New tests cover each rule in the ADR, an in-place upgrade of an old database, and a deterministic version of the race.

store.Open now reads PRAGMA user_version and applies a numbered list of
migrations on top of the schema.sql baseline, each in its own transaction
with the version bump. Migration 1 adds leases.request_id with a partial
unique index.

ClaimVMRequest and LeaseRecord gain request_id. CreateLease stores it
(empty as NULL) and returns ErrDuplicateRequestID when the index rejects
a row, and the Store gains GetLeaseByRequestID. poolmgrctl lease claim
gains --request-id, and lease list shows a REQUEST_ID column.

The server does not act on request_id yet; replay semantics follow in
the next change.
ClaimVM now rejects a request_id over 255 bytes with INVALID_ARGUMENT
and, when one is set, looks up an existing lease before claiming. A
match on the same pool returns that lease again without extending its
expiry; a match on another pool fails with INVALID_ARGUMENT; no match
claims as before and records request_id on the new lease.

If a concurrent claim with the same request_id commits its lease first,
CreateLease's ErrDuplicateRequestID sends the loser's VM back to
AVAILABLE and returns the winner's lease. The fresh-claim and replay
paths share claimResponse to build the ClaimVMResponse.

poolmgr_vm_claims_total gains a replayed="true|false" label. The ADR is
now Accepted, and the README and design doc describe retrying claims.
Comment thread internal/api/lease.go
Comment thread internal/store/sqlite.go Outdated
EnsureVMDeleted marks a VM DELETING before it calls flintlock. A failed
delete leaves both the VM and lease rows in place. A replay then returned
success for a VM that is being removed. Return ABORTED instead, as we do
once the VM row is gone.
The lease column list and Scan arguments were repeated in six places.
Move them into leaseColumns, scanLease and queryLeases, so a new column
cannot be added to some read paths and missed on others.
@richardcase
richardcase merged commit 4dd479d into liquidmetal-dev:main Sep 29, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[req]: Idempotent ClaimVM

2 participants