Skip to content

Make lease requests durable across restart #72

Description

@thymikee

Part of #70. See ADR 0021.

Purpose

Store each lease request before Simlock queues or provisions work.

A client must recover the same result after a disconnect or process restart. A retry must not create a second lease.

Simlock must keep this state in the core registry. A frontend must not own a second request registry.

Proposed API

interface LeaseRequestKey {
  requesterId: string;
  idempotencyKey: string;
}

interface ManagedShapeRequest {
  platform: Platform;
  deviceType: string;
  osVersion?: string;
}

interface CreateLeaseRequest extends LeaseRequestKey {
  spec: ManagedShapeRequest;
  activation: "direct" | "external-fence";
  admission: "wait" | "fail-fast";
}

interface SupersedeLeaseRequest {
  requesterId: string;
  expectedGeneration: number;
  next: Omit<CreateLeaseRequest, "requesterId">;
}

interface WaitOptions {
  signal?: AbortSignal;
  timeoutMs?: number;
}

interface ManagedIdentityStatus {
  deviceId: string;
  generation: number;
  state: "present" | "removing" | "removed";
}

interface SimlockClient {
  requestLease(input: CreateLeaseRequest, wait?: WaitOptions): Promise<LeaseRequestStatus>;
  getLeaseRequest(input: LeaseRequestKey, wait?: WaitOptions): Promise<LeaseRequestStatus>;
  cancelLeaseRequest(input: LeaseRequestKey): Promise<LeaseRequestStatus>;
  supersedeLeaseRequest(input: SupersedeLeaseRequest): Promise<LeaseRequestStatus>;
  getManagedIdentityStatus(input: {
    deviceId: string;
    generation: number;
  }): Promise<ManagedIdentityStatus>;
}

interface SimlockAdminClient {
  confirmLeaseActivation(input: {
    requesterId: string;
    generation: number;
    deviceId: string;
  }): Promise<LeaseRequestStatus>;
  acknowledgeManagedIdentityRemoval(input: {
    deviceId: string;
    generation: number;
  }): Promise<ManagedIdentityStatus>;
}

The status keeps the request result separate from device settlement:

type LeaseRequestDisposition =
  | "open"
  | "canceled"
  | "superseded"
  | "released"
  | "failed";

type DeviceSettlementState =
  | "none"
  | "reserved"
  | "provisioning"
  | "awaiting-external-fence"
  | "leased"
  | "releasing"
  | "deletion-pending"
  | "quarantined"
  | "settled";

interface LeaseRequestStatus extends LeaseRequestKey {
  generation: number;
  spec: ManagedShapeRequest;
  disposition: LeaseRequestDisposition;
  settlement: DeviceSettlementState;
  deviceId?: string;
  lease?: Lease;
  error?: SimlockError;
}

canceled, superseded, or failed does not mean that device cleanup is complete. A reserved or unsettled identity continues to use capacity.

Required behavior

  • Store the request before queue admission or driver work.
  • Use (requesterId, idempotencyKey) as the durable request key.
  • Return the stored result when the client repeats the same request.
  • Return IDEMPOTENCY_CONFLICT when the same key has different immutable input.
  • Keep one nonterminal generation for each requesterId.
  • Treat a stored capacity, disk, validation, or provisioning failure as terminal for that attempt.
  • Never re-evaluate a terminal outcome under the same key.
  • Admit a new key as the next generation without supersession only when the prior disposition is terminal and can no longer grant.
  • Return REQUESTER_BUSY when the prior generation is nonterminal and the new key does not include an authorized supersession.
  • Keep unsettled work from a terminal prior attempt capacity-bearing until cleanup finishes.
  • Make supersession one durable compare-and-set operation.
  • Treat an exact replay of (requesterId, expectedGeneration, next idempotencyKey, immutable next input) as the same supersession.
  • Reject a stale generation or different replacement tuple without mutation.
  • Serialize concurrent supersession attempts so that only one replacement is accepted.
  • Treat disconnect, request timeout, and AbortSignal cancellation as abandonment of only that caller's wait.
  • Install a durable cancellation result only through cancelLeaseRequest.
  • Keep the request status queryable until its device settlement completes.
  • Keep each managed-identity status queryable across restart until Host acknowledges the exact removed generation.
  • Treat an unknown identity as unknown. Never infer removal from a missing record.
  • Authenticate external-fence confirmation and removal acknowledgement through the admin client from Export a supported typed client for the local daemon #71.
  • Reject an ordinary client or invalid admin credential before either mutation starts.
  • Make removal acknowledgement idempotent for the exact identity and generation.
  • Start terminal retention only after settlement, and for an identity only after removal acknowledgement.
  • Share these durable records with all frontends.

For admission: "fail-fast", do not enter the lease queue. Store a terminal failed disposition with a typed capacity refusal and retryAfterMs. The same key always returns that refusal. A retry after the delay uses a new key.

Shape resolution

Simlock resolves { platform, deviceType, osVersion? } against its installed catalog.

  • An omitted OS selects the newest compatible installed runtime.
  • An explicit major selects the newest compatible installed minor.
  • An exact version must match an installed component.
  • A missing component returns a terminal result for that attempt. requestLease must not download it implicitly.

External fence

Android can reuse a local device identity. Host must own an execution claim before Simlock grants that identity.

For activation: "external-fence", Simlock returns awaiting-external-fence with the reserved deviceId.

Host installs its claim and uses the authenticated admin client to call confirmLeaseActivation. Simlock checks the clean baseline, then returns the active lease.

Disconnect and restart must keep the reservation recoverable. Simlock must not publish the lease before confirmation.

The execution claim lasts for the allocator identity's full pool lifetime. A terminal lease or request is not permission to clear it. Host clears the claim only after getManagedIdentityStatus returns removed for the exact generation, then acknowledges that proof through the authenticated admin client.

Existing HTTP work

#68 uses an in-memory request tracker. If #68 merges first, make that tracker use this durable core record.

This issue does not add another network interface.

Completion conditions

  • A lost response followed by a retry returns the first result.
  • The same key returns the same capacity, disk, validation, or provisioning failure after host conditions change.
  • A new key after a terminal capacity refusal can start the next generation without supersession.
  • A terminal prior generation can never publish a grant.
  • A daemon restart keeps pending and terminal requests available.
  • A cancel and grant race produces one durable disposition and no leaked lease.
  • A capacity-one supersession cannot grant the old generation after the new generation starts.
  • Concurrent supersession attempts accept one replacement and refuse the others without mutation.
  • Two requester IDs can hold concurrent leases on one connection.
  • An external-fence request cannot become active before authenticated confirmation.
  • An ordinary client cannot confirm a fence or acknowledge identity removal.
  • An invalid admin credential causes zero mutation.
  • A reusable Android identity is never unclaimed between leases or during reclaim.
  • A request timeout or abort does not cancel durable work.
  • Retention cannot remove identity-removal proof before Host acknowledges it.
  • Tests cover client restart, daemon restart, retention expiry, invalid stale generations, different replay payloads, unsettled cleanup, and removal acknowledgement.

Depends on #71 for the public client methods and admin authentication.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions