REQ-REL-2026-08 AC2 (independent-review finding R2) had no end-to-end proof: an
mqweb outage and the unattended recovery that follows were only covered by unit
and envtest classification tests.
The new Serial spec takes the kind platform's IBM MQ workload down, applies a
Queue spec change while mqweb is unreachable, and asserts the CR falls to
Synced=False with the retryable reason Error (never TerminalError - that is the
23h wedge this requirement fixed). Scaling the workload back up is the only
recovery action: the spec then asserts the Queue returns to Synced=True with
mqObjectExists=true, that metadata.generation is unchanged (so nothing edited
the CR), that the QueueManagerConnection is Ready again, and that the queue
manager really carries the new maxdepth.
Scaling the StatefulSet away is used rather than breaking the Service selector
or adding a NetworkPolicy, because those leave established keep-alive
connections usable and the outage would not be observed.
Why
PR #186 (REQ-REL-2026-08) fixed a P0 wedge: a workload CR sat at
Synced=Falsefor 23h after aqueue-manager restart because a non-transient-looking error returned
(ctrl.Result{}, nil)— norequeue, no log, and nothing left to re-enqueue it. The independent review of that PR raised
finding R2: no outage-recovery e2e. The spec's AC2 —
— was only ever proven by unit and envtest coverage of the error classification. Nothing exercised
a real outage against a real queue manager. This PR closes that gap.
What changed
Test-only.
internal/andcmd/are untouched.test/e2e/mq_outage_e2e_test.go(new) — oneSerialspec,Label("mq", "mq-outage"):QueueManagerConnectionReady,QueueSynced=True,maxdepth=1000verified on the queue manager over mqweb.
Queuespec change (maxdepth=2000) is applied while mqweb is down — this is thetrigger for the failing reconcile, and the last time anything touches the CR.
Synced=Falsewith reasonError, neverTerminalError. This is the regressionitself: a transient outage misclassified as terminal is the 23h wedge.
annotation, no operator restart.
Queuereturns toSynced=True/AvailablewithmqObjectExists=true, thatmetadata.generationis unchanged across recovery (so it self-healed rather than beingre-applied), that the connection is
Readyagain, and that the queue manager really carriesmaxdepth=2000.test/e2e/mq_outage_helpers.go(new) — outage induce/restore helpers plus an independentmqweb reachability probe that builds a fresh client per call, so a pooled keep-alive connection
can never make an outage look healthy.
test/e2e/namespace_helpers.go— addsmkurator-e2e-outageso the spec's CRs are isolatedfrom specs that assume a healthy queue manager, and are covered by the existing suite cleanup.
Two deviations from the original brief, both grounded in the code
Both are documented in a comment at the top of the spec.
The QMC does not go not-Ready during an outage.
QueueManagerConnectionReconciler.fail()returns early — keeping
Ready=Trueand requeueing — when the connection is already Ready at theobserved generation and the error is transient. That is deliberate flap suppression. The spec
therefore asserts QMC readiness only after recovery.
TerminalRetryIntervalis not the path under test. A dial failure against an absent mqweb iswrapped by
mqrest.roundTripasmqadmin.TransientError, so the self-heal path is the transientone that REQ-REL-2026-08 changed:
setSyncedError→TransientRequeueInterval(30s), and/or theworkload watch on
QueueManagerConnectionreadiness, whichever fires first. AC2 only requiresrecovery "within one retry interval", which both paths satisfy — the spec asserts the outcome and
pins the classification (retryable
Error, neverTerminalError) rather than pinning whichtrigger wins.
Correction (independent review): an earlier draft of this section claimed the watch fan-out
"most likely won that race" in the CI run. That is wrong, and the source contradicts it:
connectionWatchPredicates()(reconcile_shared.go:513-520) fires only onconnectionReadyChangedor a generation change, and during the outage the QMC never reconciles or writes status — its
success path returns a bare
ctrl.Result{}andfail()early-returns without a status write — sono QMC update event existed at all. The observed timeline is fully explained by the plain transient
requeue. The QMC-ready watch path is therefore NOT covered by this spec; do not read it as
guarded here.
What to review
selector or adding a NetworkPolicy was rejected: both leave already-established keep-alive
connections usable, so a reconcile reusing an idle connection would still succeed and the spec
would prove nothing. Removing the pod severs every connection and takes the listener away.
reconcile against an unreachable mqweb. The passive alternative (waiting for the drift resync) was
rejected because
DriftResyncAfterjitters between 5 and 10 minutes. Themetadata.generationassertion proves nothing touched the CR during recovery.
mqbut deliberately notslow, so it runs on every PR (the PR filter is(smoke || mq) && !slow). It costs a few minutes per run. If you would rather keep PR e2e lean,adding
slowis a one-word change — but then the regression is only guarded post-merge.Test plan — stated plainly
task test:run(unit + envtest, race, coverage)internal/, 92.5%api/task lint+task arch:linttask verifytask format:checkgo vet -tags e2e ./test/e2e/go test -tags e2e -c ./test/e2e/(tagged build)golangci-lint --build-tags e2eon the new filesKURATOR_E2E_MQ=1 task test:e2elocallye2e (kustomize)on this PRThe e2e suite could not be run on the authoring host.
task cluster:upbrings the kind cluster upfine, but the IBM MQ Helm chart pins
nodeAffinity: kubernetes.io/arch in [amd64, s390x, ppc64le], and the host is Apple Silicon(arm64) —
ibm-mq-0staysPendingwith0/1 nodes are available: 1 node(s) didn't match Pod's node affinity/selector, and the Helm release times out. The proof is therefore this PR's own E2Eworkflow on
ubuntu-latest(amd64), which was watched to completion. Verbatim from that run:Ran 19 of 22 Specs ... SUCCESS! -- 19 Passed | 0 Failed. The outage was real (a refused dial, nota simulated condition), and the whole spec cost ~57s wall clock.
Risks
Serial, and it restores the workload andwaits for mqweb to answer again in
DeferCleanupbefore anything else runs, including on failure.generous existing helper (
qmcWatchRecoveryEventuallyTimeout, 8m) rather than a new constant; therecovery assertion uses
mqSyncedEventuallyTimeout(3m), which comfortably covers the mqrestcircuit breaker's 30s open window plus a 30s transient requeue.
KURATOR_E2E_MQ_STATEFULSET.Out of scope
internal/,cmd/,docs/, or deploy manifests. Shortening--terminal-retry-intervalfor tests would have needed manifest changes owned elsewhere and isnot required, since the path under test is the 30s transient requeue.
MQ returns" is now backed by a test; no text change needed.