The Queue That Grew Up
There is a particular kind of cleanup pass that every solo builder knows. The one you do when the project has been growing fast and you finally let yourself look at the parts you haven’t touched in months. You’re not adding features. You’re just — looking. Tidying. Paying attention to things you’ve been ignoring.
That’s what found the queue.
A single file with a readme
I had been reorganising pkg/katalog. The validate files had
accumulated — validate_serve.go, validate_tokens.go,
validate_sentinels.go, each one growing — and I was inspired by how
Kubernetes itself organises validation into a dedicated sub-package. So
I moved them. pkg/katalog/validate/. Cleaner. More honest about what
the package actually did.
That was satisfying. And then I arrived at pkg/runtime/queue.
One file. A short readme. The entire package could be described in two
sentences: the informer is calling with an event, here is a QueueItem
to hold the key and the GVK, here is a Workqueue to hold the items.
When the queue reached maxDepth, items were dropped and a warning was
logged.
There was even an example to demonstrate the drop behaviour. I had written it proudly at some point. Now I read it and something felt wrong.
Not broken. Just wrong.
The drop that nobody configured
The drop was unconditional. The queue was full? Drop the item. Log a warning. Move on.
I started asking: who decided this was the right behaviour? Nobody did. I had copy-followed a pattern I was familiar with — the workqueue drops when full, that’s how it works — without asking whether Orkestra should expose that decision to the operator author.
What if you didn’t want to drop? What if a dropped item during a critical window was a problem? What if you wanted to drop only outside business hours, or only for certain resource types, or only below a certain priority?
The queue had capacity information. It just wasn’t using it for anything except a binary full/not-full decision.
That led to queue.behaviour. onLimit and onThreshold — two shapes
for saying what the queue should do based on its own depth, with the
same when: and or: conditions available everywhere else in Orkestra.
That part is covered in the gates writing.
But it was this cleanup pass that opened the door.
Reading the source
With the behaviour work done, I was satisfied with the package. But something else had been nagging at me. I had always taken deduplication for granted — the docs said the workqueue handles it, I believed the docs, I never read the implementation.
So I did.
It was simpler than I expected. Not magic. Not complex. A comparison.
If default/my-app is in the queue and another event arrives for
default/my-app, the workqueue asks: is this the same key? Yes. Replace
what’s waiting with the new event — you’re the latest, you’re what
matters. Process once.
Elegantly efficient. And immediately interesting.
Because if the key is the comparison, then changing the key changes the
identity. An item carrying default/my-app + sentinels would not
compare equal to default/my-app + different sentinels. They would be
distinct items. Both would survive.
I had been thinking about sentinel-based gate evaluation for a while.
The reconcileGate could declare sentinels — generationChanged,
labelsChanged — and if those sentinels were present in the event, the
gate would pass. But the gate was receiving events that had potentially
been coalesced. Ten edits, one queue item, the last sentinel context
wins. The first nine were gone.
What if the sentinel map was a pointer? Each event would produce a distinct pointer. The workqueue’s equality check would fail because pointer A ≠ pointer B even if the underlying maps are identical. Every event would survive deduplication as a distinct item.
I added the test. It worked.
The cost and the implicit assumption
Working code isn’t always right code.
The problem was that using sentinel presence to imply event awareness
was implicit. An operator author who declared reconcileGate.sentinels
didn’t necessarily know they were opting into event-per-item queue
semantics. They just wanted the gate to check whether a label had
changed. The fact that this meant ten reconcile cycles for ten rapid
edits instead of one — that was invisible to them.
I thought about how Orkestra handles other things with real cost. The
enrich: capability
— which fetches live resources from the API server
to make things like podCount, hasCrashingPod and other
enrichment targets available in the resolver — is
explicit. You declare the enrich: block and you understand that means
API calls.
The cost is visible because the declaration is visible.
Event awareness needed the same treatment.
The merge that didn’t work
Before landing on the explicit declaration, I tried something else. What if events were merged before the reconcileGate evaluated?
Event 1: generationChanged: true, labelsChanged: false
Event 2: generationChanged: false, labelsChanged: true
A merge would produce: generationChanged: true, labelsChanged: true.
The gate checking for either would pass.
But that gate was now passing for a state that never existed. There was no moment where both were simultaneously true. The merge had created a ghost event — a conjunction of two separate truths that were only true individually, never together.
That was wrong in a specific way. If your gate asks “did the generation change?” the right answer is yes for event 1 and no for event 2. A merged answer of yes for both is not an approximation of the truth. It is a different truth entirely.
Merging didn’t work. The individual events had to remain individual.
Separating item from identity
The solution took a few days to arrive.
The insight was that the QueueItem and the queue identity were the same
thing in the original design, and they didn’t have to be.
type QueueItem struct {
Key string
GVK string
EventID uint64
}
EventID controls identity, not content. When EventID is zero, the
workqueue’s equality check fires on Key and GVK alone — normal
deduplication. When EventID is non-zero, every item is distinct
because every item carries a unique counter value. The sentinel values
live separately, keyed by the full identity including EventID.
The separation meant: sentinel availability does not imply event awareness. You can have sentinels with deduplication. You can have event awareness without sentinels. The two concerns are orthogonal.
type queueItemIdentity struct {
Key string
GVK string
EventID uint64
}
sentinels map[queueItemIdentity]map[string]string
The sentinel values are stored off to the side, indexed by the full
identity. When the informer sees an event QueueItem and asks for its
sentinels, the Workqueue looks up by identity and hands back a copy.
The queue itself stays clean — it never knows what a sentinel means. It
only knows identities.
Six instead of three
The original three enqueue methods became six.
Before: Enqueue, EnqueueWithKey, EnqueueWithSentinels.
After: each of those three now has an event-aware variant —
EnqueueWithEventSentinels, EnqueueWithKeyEventSentinels, and so on.
That doubling looks like complexity but it’s actually clarification. The six methods make the two dimensions explicit: primary vs secondary path (does the informer know the key, or does it need to resolve it?) and event-aware vs coalescing (does each event need individual identity?). Four combinations, six methods because primary non-event-aware doesn’t need both Enqueue and EnqueueWithKey to be event-aware.
The routing through those methods is determined by two things: whether
the informer is handling a primary or secondary event, and what
effectiveBox.reconcileGate.eventAware resolves to. The informer
factory checks both and picks the right path. Neither the informer nor
the queue needs to know what the other is doing. The factory is the
only place that knows both.
The
effectiveBoxbeingoperatorBox.preReconcile.reconcileGate.eventAware.“Effective” because it is target aware. See Orkestra Execution Model.
What the queue became
The queue package went from one file with a readme to the quiet center of a pipeline that three different gates now depend on.
Queue behaviour makes the queue a participant in admission — the first tier of a two-tier evaluation where the queue checks arithmetic and flags items for the informer to evaluate with full resolver context.
Event awareness makes the queue a participant in reconcile gate
evaluation — the eventAware declaration on reconcileGate has no
meaning until the enqueue path honours it by constructing the
appropriate QueueItem identity. The gate declares the policy. The
queue implements it.
And the sentinel context that travels from informer through queue to kordinator to reconcile gate — that journey is only possible because the queue holds sentinel values by identity, separate from the item, and hands them back intact when the kordinator asks.
The queue does none of this by knowing what reconciliation means.
It does it by knowing, precisely, what its job is:
Can and should this work enter the execution pipeline according to the queue’s runtime state and configured behaviour?
Everything downstream is someone else’s responsibility.
That boundary is what allowed the queue to become more without becoming something it shouldn’t be.