Controller Protocol
The Discord-PXE controller is a small Go control plane for assigning global Discord shards to physical PXE nodes.
It does not proxy Discord traffic and it does not replace the bare-metal runtime.
Primary source:
Controller state
The controller tracks:
total global shards
Discord max_concurrency
lease duration
plan generation
live node map
last Identify time per bucket
Each node record contains:
- node ID
- declared capacity
- currently assigned shard IDs
- last check-in time
- plan generation
- draining flag
- READY count
- STARTING count
- BACKOFF count
- FATAL count
This means a check-in carries both capacity and a small health summary.
Node ID validation
Node IDs are bounded and restricted to a safe character set.
Allowed characters are:
A-Z
a-z
0-9
-
_
.
This keeps node identity deterministic and avoids pushing arbitrary strings into logs/routes/state.
Capacity bound
The controller caps one node's declared capacity at 128.
That matches the runtime's local shard-slot architecture.
The global shard count is a separate value and may be larger than one node's capacity.
Lease expiry
On each relevant request, stale nodes are expired if:
now - LastSeen > lease duration
Expired nodes disappear from the live node set before rebalancing.
This is fundamental to ownership safety.
Rebalancing
The current algorithm is deterministic and capacity-weighted.
At a high level:
- sort node IDs
- sum non-draining capacity
- assign each node a proportional target
- cap target by node capacity
- distribute remainder while spare capacity exists
- assign contiguous shard ranges in sorted-node order
- increment generation only if assignments changed
That produces predictable contiguous ownership rather than random shard sets.
Why contiguous ranges?
Contiguous ranges simplify the bare-metal node's local plan:
range_start
range_count
A node can populate local slot 0..N-1 without storing an arbitrary sparse assignment map.
The check-in response still includes the explicit assigned[] list as well.
Check-in contract
Example request:
{
"node_id": "compaq-01",
"capacity": 16,
"ready_shards": 8,
"starting_shards": 2,
"backoff_shards": 1,
"fatal_shards": 0
}
The controller validates:
- JSON shape
- node ID
- capacity 1..128
- shard health counts 0..128
Unknown JSON fields are rejected.
Response
A response includes:
generation
total_shards
assigned[]
range_start
range_count
lease_seconds
max_concurrency
The generation lets a client distinguish plan changes from repeated delivery of the same assignment.
Draining
The node model includes a draining flag.
A draining node receives a target of zero during rebalance, allowing ownership to move elsewhere without pretending the machine disappeared unexpectedly.
That creates a path toward maintenance-aware handoff.
Identify coordination
The controller stores the last Identify time per concurrency bucket.
The Identify claim endpoint exists because Discord's concurrency limit is global to the bot.
The controller can therefore:
- verify the requesting node currently owns the shard
- calculate the shard's concurrency bucket
- determine whether that bucket may Identify now
- return an immediate grant or retry delay
This prevents independent PXE nodes from violating one shared Discord limit.
Guild routing
The routing endpoint accepts a guild ID, calculates the Discord shard, then returns the live node currently leasing it.
The logical flow is:
guild ID
↓
Discord shard formula
↓
global shard ID
↓
current controller plan
↓
physical PXE node
This is the control-plane primitive needed for future guild-scoped routing.
Security boundary
The development controller currently listens on loopback by default.
Before external deployment, the project explicitly calls out the need for:
- authenticated requests
- TLS or equivalent protected transport
- persistent device identity
Shard ownership directly affects which machine is allowed to connect to Discord. A production controller must authenticate node identity and plan updates.
Why controller state is not embedded into Discord-PXE
The bare-metal runtime is optimized around a small hardware-facing environment.
The controller benefits from:
- Go's HTTP/JSON tooling
- maps and sorting
- wall-clock lease handling
- easier observability
- easier policy iteration
Separating control plane from data plane keeps both sides simpler.