Skip to content
Registry StackDocsDevelopment (unreleased)

Prove your first booking policy

For the operator

View as Markdown

You will create a booking policy for a registry counter, read what it publishes, and prove with a fixture what callers get when one of its two stations closes. Everything runs offline: no database, no token issuer, no network.

Outcome
A checked booking policy whose fixture proves who gets a station when one is closed.
Time
About 10 minutes
Level
Local evaluation only
Prerequisites
curl 7.76 or laterLinux amd64, Linux arm64, or macOS arm64

schedulingctl is the authoring and operator tool for Registry Scheduling. The installer published with the latest release installs that release, checks each download against the release’s SHA256SUMS, and puts the binaries in ~/.local/bin; keep that directory on your PATH. On Linux amd64 it also installs the scheduling runtime, which this tutorial does not use.

Terminal window
curl -fsSL https://github.com/registrystack/registry-stack/releases/latest/download/scheduling-install.sh | bash
Terminal window
schedulingctl --version
schedulingctl <version>

Create a project from the exact-time template:

Terminal window
schedulingctl init ./scheduling --template standalone-exact-time --format json
{
"command": "init",
"created": [
"scheduling.yaml",
"runtime.example.yaml",
"records.yaml",
"fixtures/counter-stations.yaml",
"fixtures/fold-day-rebooking.yaml"
],
"template": "standalone-exact-time"
}

scheduling.yaml is the policy: what callers may book and when. records.yaml holds the locations and staff stations a running deployment loads separately, and the two fixtures prove the policy offline. You meet runtime.example.yaml only when you deploy.

Terminal window
schedulingctl check ./scheduling --format json
{
"offerings": {
"count": 2,
"ids": [
"registry-update-30",
"fold-day-update-30"
]
},
"projectId": "registry-updates"
}

The report’s effective section also prints a policyDigest, which changes whenever the booking behaviour changes, while databaseAccess and networkAccess are both false: the check touched neither. An offering is one thing a caller can book. Ask schedulingctl what the first one means:

Terminal window
schedulingctl explain ./scheduling --format json
{
"exactTime": {
"bufferAfterMinutes": 5,
"bufferBeforeMinutes": 5,
"durationMinutes": 30,
"horizonDays": 60,
"leadTimeMinutes": 120,
"maximumRecipients": 1,
"pool": "update-stations",
"startIncrementMinutes": 30
},
"id": "registry-update-30",
"location": "bangkok-counter",
"mode": "exact-time"
}

registry-update-30 is a 30-minute appointment at bangkok-counter, starting on the half hour, bookable from two hours to 60 days ahead, with five minutes kept free on each side. Each booking takes one station from update-stations, a resource pool of interchangeable stations. The policy names the pool; records.yaml lists its members:

scheduling/records.yaml
- id: update-stations
members:
- resourceId: station-1
available: true
- resourceId: station-2
available: true

A fixture states a moment in time, the pools as they stand then, and booking requests with the answer each must get. Run both:

Terminal window
schedulingctl test ./scheduling --format json
{
"cases": [
{
"detail": "admitted onto station-1 for 1 unit(s)",
"name": "first-caller-takes-a-station",
"status": "pass"
},
{
"detail": "refused booking.duplicate-active (detailed: booking.duplicate-active)",
"name": "same-key-retry-is-refused",
"status": "pass"
},
{
"detail": "admitted onto station-2 for 1 unit(s)",
"name": "second-caller-gets-the-other-station",
"status": "pass"
},
{
"detail": "refused capacity.exhausted (detailed: capacity.exhausted)",
"name": "both-stations-busy-refuses-another-caller",
"status": "pass"
}
],
"file": "fixtures/counter-stations.yaml",
"status": "passed"
}

The first caller asks for 09:00 Bangkok time (02:00 UTC) and takes station-1. The same caller asking for 09:30 is refused, because the offering allows one active booking per subject. A second caller takes station-2 at 09:00, and a third is refused because both stations are taken. Each case’s duplicateKey names the subject, not the idempotency key: a retry under the same idempotency key returns the first answer instead.

Suppose station-2 is out of service that morning. Say so in the fixture’s facts:

scheduling/fixtures/counter-stations.yaml
- resourceId: station-2
available: true
available: false

Run the fixtures again:

Terminal window
schedulingctl test ./scheduling --format json
{
"detail": "expected admitted onto station-2 for 1 unit(s), actual refused capacity.exhausted (detailed: capacity.exhausted)",
"name": "second-caller-gets-the-other-station",
"status": "fail"
}

The command exits with status 1. With one station open, the second caller is refused like the third. That is the behaviour you want, so the case expectation is what is wrong. Correct it:

scheduling/fixtures/counter-stations.yaml
expect:
type: admitted
units: 1
resource: station-2
type: refused
code: capacity.exhausted
Terminal window
schedulingctl test ./scheduling --format json
{
"ok": true,
"status": "passed",
"proofBoundary": "offline-synthetic"
}

offline-synthetic marks the limit of this proof: the fixtures exercise the policy against facts you wrote, not against a running deployment’s ledger.

How Registry Scheduling works follows a booking through the running service: the hold a caller takes, the appointment it confirms, and the grant that allows both.