HeddleAdmissionControl

class HeddleAdmissionControl(heddle: FairShareExecution, costPerTask: Long = 1, costOf: (TaskDescriptor) -> Long = { costPerTask }, laneToLeaf: (Lane) -> GroupId? = { lane -> defaultLeafOf(lane) }) : AdmissionControl

Binds warp's opaque Lane tags to a weighted fair-share tree and gates warp's free execution path on entitlement — the whole point of the :kuilt-warp-heddle satellite.

Warp answers where a task runs (the consistent-hash ring) and whose turn (the free claim path). It has no notion of how much any lane may take. This adapter supplies that missing dimension without warp core learning a single fair-share type: it implements warp's opaque AdmissionControl, and warp calls it just before running each task. The adapter maps the task's Lane to a fair-share leaf and reserves one run's worth of that leaf's entitlement; when the task finishes warp calls AdmissionTicket.settle, which completes the reservation and charges the ledger once for that run.

Costing is per execution, not per task (design §14.4). Warp's free claim path is at-least-once, so under ring or roster churn one logical task can run on two peers and cost its lane twice. That is the intended semantics: both peers really consumed capacity, and charging once would let a churning lane push its waste onto every other lane. The overshoot is the duplicate-execution rate — zero at stable membership — and design §8.2 states the fairness-error bound over executions for the same reason.

The behaviour that falls out, per warp's contract:

  • Untagged is free. A task on the Lane.ROOT lane (the default) is admitted immediately with a no-op ticket — no reservation, no ledger touch — so an untagged workload is bit-for-bit today's warp.

  • Exhaustion defers, never drops. When a lane's leaf has no spare entitlement, FairShareExecution.reserve returns null; this adapter returns null too, so warp defers the task (leaves it pending) and re-attempts it on a later claim cycle — work resumes when entitlement flows in.

  • Zero consensus on the hot path. reserve / complete are local reads and writes of already-converged holdings; admitting a task adds no consensus round. The heddle's own ledger replication is coordination-free cloth (a Quilter), not consensus.

Entitlement itself flows the ordinary heddle way — a consumer advertises demand and calls HeddleNode.schedule to delegate holdings down the tree by weight. Two lanes weighted 3:1 therefore complete work in a 3:1 ratio: each lane runs exactly as many executions as it was delegated entitlement for.

Parameters

heddle

the fair-share data plane whose holdings this adapter reserves against — the FairShareExecution surface shared by both front doors, so this accepts either a us.tractat.kuilt.heddle.heddleStatic node (no consensus) or an H5 us.tractat.kuilt.heddle.heddleGoverned node (governed) interchangeably. One asymmetry: a governed node must have enrolled itself before it will author any entitlement — until enroll(self) applies, its write gate is closed, so reserve returns null and schedule delegates nothing and this adapter admits no gated task (#1693, design §13.2).

costPerTask

service units reserved and charged per execution. Defaults to 1 — the §14.4 one-unit-per-execution costing; a caller with variable-cost work supplies a per-descriptor cost via costOf.

costOf

per-descriptor cost function; defaults to a flat costPerTask for every descriptor. Charged once per execution, so a task run twice is charged twice.

laneToLeaf

maps a task's Lane to the fair-share leaf GroupId to charge, or null to admit the task un-gated. The default treats Lane.ROOT as un-gated and every other tag as the identically-named leaf (Lane("acme/batch") → GroupId("acme/batch")).

See also

Samples

// 1. Build the adapter — warp's opaque AdmissionControl, backed by the fair-share ledger.
val admission = HeddleAdmissionControl(heddle)
// Pass it to a node:  WarpNode(selfId, seam, roster, scope, clock = …, registry = …,
//                              admissionControl = admission, epoch = <per-boot counter>)

// 2. Tag a task into a lane on the producer side.
val interactive: TaskDescriptor =
    TaskDescriptor(op = OpId("score"), args = "doc-1".encodeToByteArray())
        .inLane("acme/interactive")
check(interactive.lane == Lane("acme/interactive"))

// 3. An untagged task rides the default root lane and is admitted un-gated.
val untagged = TaskDescriptor(op = OpId("score"), args = ByteArray(0))
check(untagged.lane == Lane.ROOT)
check(admission.admit(untagged) === AdmissionTicket.NOOP)

Constructors

Link copied to clipboard
constructor(heddle: FairShareExecution, costPerTask: Long = 1, costOf: (TaskDescriptor) -> Long = { costPerTask }, laneToLeaf: (Lane) -> GroupId? = { lane -> defaultLeafOf(lane) })

Types

Link copied to clipboard
object Companion

Functions

Link copied to clipboard
open override fun admit(descriptor: TaskDescriptor): AdmissionTicket?

Reserve this task's lane entitlement, or defer it.