# Queue model

  A Lakeshore queue is one record carrying two jobs. It is a **task queue** —
  the thing invocations line up in and workers pull from — and it is a **launch
  queue** — the policy deciding how many workers should exist. They are one
  model because the depth of the first is the input to the second.

The queue name you write in `@udf(queue="gpu")` is denormalized onto every
invocation, and the control plane reads that one string twice: once to route a
claim to a worker, and once to count how many invocations are still waiting.
Counting the backlog is how the autoscaler knows whether to grow the pool.
Splitting scheduling and provisioning into separate objects would mean keeping
that number in sync across both.

Read the two halves separately:

- [Task queue](/lakeshore/queues/task-queue.md) — the behaviors: submit, claim,
  complete, retry, stream.
- [Elastic queues](/lakeshore/queues/elastic.md) — what attaching a provider adds.

## The type

```ts file="Queue"
interface Queue {
  id:            string
  namespaceId:   string          // the prefix — see below
  name:          string          // unique within namespaceId
  description?:  string

  // ── Scheduling surface ────────────────────────────────────────────
  kind:          'fifo' | 'priority' | 'boltzmann' | 'filo'   // default 'fifo'
  kindParams?:   { temperature?: number; priority_field?: string }
  admission?:    {
    max_depth?:          number
    on_full?:            'reject' | 'block'
    rate_limit?:         number
    deadline_cutoff_s?:  number
  }
  reservedSlots?: number         // advisory slots held back
  authPolicy?:    unknown        // opaque blob; consumer is the auth gate

  // ── Launch policy ─────────────────────────────────────────────────
  providerRef?:   string | null  // provider name; null = bring your own worker
  elasticity?:    {
    kind:        'fixed' | 'fully_elastic' | 'pool_with_threshold' | 'max_count'
    min?:        number
    max?:        number
    threshold?:  number
  }
  daemonTemplate?: {
    instance_type?:  string
    image_id?:       string
    runner?:         string
    setup_scripts?:  string[]
  }
  customScalerUdf?: string       // optional UDF overriding the built-in scaler

  // ── Lifecycle ─────────────────────────────────────────────────────
  state:         'active' | 'draining' | 'paused' | 'archived'   // default 'active'
  serverNodeId?: string | null   // owning queue-server tier; null = central

  createdAt:     Date
  updatedAt:     Date
}
```

## Naming and the prefix

**A queue name is never global.** The record carries a `namespaceId`, and the
uniqueness constraint is on the pair:

```
@@unique([namespaceId, name])
```

So the namespace *is* the prefix. Two namespaces can both own a queue called
`gpu` and they are different queues; within one namespace the name is the
identity, which is what lets `@udf(queue="gpu")` be a bare string in user code —
the namespace comes from the caller's credentials, not from the literal.

Everything keyed off a queue inherits that scoping. The idempotency key on a
submit (`_key=`) is unique per **namespace + queue**, not per queue name alone,
so the same key in two namespaces is two distinct invocations.

> **Note:** There is no separate prefix field, and `name` is a flat string — Lakeshore
>   does not parse `team/gpu` into a hierarchy today. If you want grouping inside
>   a namespace, it lives in the name by convention and nothing enforces it.
>
>   This differs from a Redis-backed queue like
>   [zaku](https://github.com/geyang/zaku), where the prefix is a literal key
>   prefix (`Zaku-task-queues:{queue}:pending`) chosen at client construction. In
>   Lakeshore the equivalent partitioning is a control-plane relation, not a
>   string you assemble.

## Creating one

```bash file="terminal"
lakeshore queues add gpu-train \
  --kind priority \
  --provider aws-east \
  --elasticity fully-elastic --min 0 --max 4
```

You rarely have to: submitting to a queue name that does not exist creates it,
with `kind` defaulting to `fifo`. Reach for `queues add` when you want a
non-default discipline or a launch policy.

> **Warning:** The CLI takes the dashed forms above (`fully-elastic`); the REST API and
>   compose files take underscores (`fully_elastic`). The CLI translates between
>   them.

## Stored ahead of enforced

Several fields above are persisted and validated but not yet acted on, and the
gaps are not visible from the field names:

| Field | Reality today |
| --- | --- |
| `kind` | The hosted control plane dispatches by priority then oldest-first regardless of the value. The four disciplines are implemented in the SDK's local plane. |
| `admission` | Validated and persisted, not applied at submit time. |
| `reservedSlots` | Carried over from the collapsed `Lane` model; no scheduler reads it. |
| `customScalerUdf` | Reserved for a later phase; the built-in scaler is the only one that runs. |

Check the reference before designing around any single field.

    The behaviors: submit, claim, complete, retry, stream — and the state a
    task moves through.

    What `providerRef` adds: scaling policies, the controller tick, and the
    event feed.

    The full CLI surface and the current state of each field.

    What `providerRef` points at, and which provider types can launch today.
