WIP: fix(session): keep the load balancer's affinity cookies so requests and the realtime socket reach one server node #32
Draft
james.maes
wants to merge 2 commits from
fix/load-balancer-affinity-20261110 into main
pull from: fix/load-balancer-affinity-20261110
merge into: QRun-IO:main
QRun-IO:main
QRun-IO:fix/plain-permission-messages
QRun-IO:release/2.0
QRun-IO:fix/2.0-uninstall-survives-held-shortcut-20261111
QRun-IO:feat/2.0-shell-tree-models-20261111
QRun-IO:test/2.0-ui-identifier-baseline-20261111
QRun-IO:feat/2.0-wizard-server-step-20261111
QRun-IO:feat/2.0-companion-management-client-20261111
QRun-IO:docs/2.0-wizard-decisions-and-audit-20261111
QRun-IO:feat/2.0-companion-sign-in-dispatcher-20261110
QRun-IO:refactor/2.0-native-seams-update-service-pipe-20261110
QRun-IO:fix/2.0-contract-and-redaction-findings-20261110
QRun-IO:fix/2.0-one-queue-lookup-20261110
QRun-IO:feat/2.0-journal-schema-version-20261110
QRun-IO:fix/2.0-load-balancer-affinity-20261110
QRun-IO:feat/2.0-certificate-exception-per-origin-20261110
QRun-IO:fix/2.0-cli-piped-input-bom-20261110
QRun-IO:fix/2.0-claim-unpaired-surrogate-20261110
QRun-IO:fix/2.0-worker-recheck-and-in-hand-20261110
QRun-IO:docs/2.0-settlements-and-audit-status-20261110
QRun-IO:refactor/2.0-native-seams-printing-20261110
QRun-IO:refactor/2.0-native-seams-startup-20261110
QRun-IO:fix/2.0-channel-cancel-race-20261110
QRun-IO:test/2.0-runtime-harness-20261110
QRun-IO:refactor/2.0-runtime-seam-20261110
QRun-IO:feat/2.0-management-contract-20261110
QRun-IO:docs/2.0-station-identity-20261110
QRun-IO:refactor/2.0-remove-unreachable-transport-20261110
QRun-IO:feat/2.0-presentation-assembly-on-release-20261110
QRun-IO:fix/2.0-store-open-race-20261110
QRun-IO:docs/2.0-build-decisions-20261110
QRun-IO:docs/2.0-audit-status-20261110
QRun-IO:refactor/2.0-core-hardening-on-release-20261110
QRun-IO:refactor/2.0-cli-cleanup-on-release-20261110
QRun-IO:build/2.0-version-on-release-20261110
QRun-IO:refactor/2.0-legacy-polling-on-release-20261110
QRun-IO:ci/shared-windows
QRun-IO:docs/2.0-design-records-20261110
QRun-IO:fix/enrollment-keeps-pairing-1.2.x-20261110
QRun-IO:fix/enrollment-keeps-pairing-20261110
QRun-IO:ci/msi-fallback-on-shared-windows
QRun-IO:ci/msi-validation-fallback-20261110
QRun-IO:build/2.0-coverage-gate-20261110
QRun-IO:release/1.2.2
QRun-IO:fix/signout-ends-provider-session
QRun-IO:docs/forge-handoff-qbit-112-20261106
QRun-IO:docs/changelog-1.1.1-heading
QRun-IO:docs/forge-handoff-qbit-111-20261106
QRun-IO:docs/release-1.2.1-20261106
QRun-IO:fix/release-edition-splat-20261105
QRun-IO:feat/client-editions-20261105
QRun-IO:fix/native-login-required-nonce
QRun-IO:fix/browser-callback-inbox-20261105
QRun-IO:feat/sign-in-diagnostics-20261105
QRun-IO:docs/printing-release-closeout-20261104
QRun-IO:qualification/windows-shared-original-20261103
QRun-IO:ci/windows-original-packaging-entrypoint-20261103
QRun-IO:qualification/windows6218-printing-20261103
QRun-IO:fix/signing-tool-install-20261102
QRun-IO:release/printing-cohort-20261101
QRun-IO:dependabot/github_actions/actions-1c177510ea
QRun-IO:chore/csharp-documentation-20261101
QRun-IO:chore/script-quality-gates-20261101
QRun-IO:feat/small-label-tests-20261101
QRun-IO:archive/qrun-github-cutover-20261101/e870119e2b83/chore/script-quality-gates-20261101
QRun-IO:archive/qrun-github-cutover-20261101/cc400995f063/dependabot/github_actions/actions-1c177510ea
QRun-IO:migration/cutoff-cc400995f063-dac238
QRun-IO:archive/qrun-github-cutover-20261101/0336d5f888ac/main
QRun-IO:migration/cutoff-0336d5f888ac-0d6e40
QRun-IO:archive/qrun-github-cutover-20261101/3c894733d18c/main
QRun-IO:fix/windows-source-headers-20261101
QRun-IO:archive/qrun-github-cutover-20261101/e5846df71584/main
QRun-IO:archive/qrun-github-cutover-20261101/5d8a542eed61/dependabot/github_actions/actions-df2b3b8582
QRun-IO:docs/installed-native-020-20261031
QRun-IO:dependabot/github_actions/actions-df2b3b8582
QRun-IO:dependabot/github_actions/actions-9120074c41
No Reviewers
Labels
Clear labels
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
secuurity
wontfix
Something isn't working
Improvements or additions to documentation
This issue or pull request already exists
New feature or request
Pull requests that update GitHub Actions code
Good for newcomers
Extra attention is needed
This doesn't seem right
Further information is requested
This will not be worked on
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: QRun-IO/qqq-print-agent#32
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Draft. Do not merge, tag or release from this pull request without the owner's review. A release from it must be signed through the existing release workflow; nothing here was signed or packaged.
Why
A QQQ server that runs as several nodes sits behind a load balancer with cookie stickiness. The session client attaches its cookies by hand (
QqqSessionClient.BuildRequest,ConfigureSocket) and dropped everything but the session, so each request could reach a different node: the realtime socket on one, claims and downloads on another. A server whose claim gate needs the socket on the node that receives the claim (QQQ Printing 1.1.x) then answers 409 to about half the claims. Roaming also exposes the client to a token-refresh race between nodes that a browser, which keeps the cookie, does not see.What changes (Core only)
QqqSessionClient.Affinity.cs(new): keeps the Application Load Balancer's cookies (names beginningAWSALB:AWSALB,AWSALBCORS,AWSALBTG,AWSALBTGCORS,AWSALBAPP-n) from any answer of the session origin, including the answer to the WebSocket upgrade whether accepted or refused. Sends them on every later request to that origin and on the upgrade. Replaces or drops them when the load balancer rewrites or expires them, which is how it moves a client off a node that stopped.Securecookie only over HTTPS. In memory only; never saved with the session; never sent to the identity provider.ReadSessionCookie: cookies under names other thansessionUUID/sessionIdno longer decide whether a session answer is valid. A load balancer adds its own cookies to every answer, and one the strict parser rejects must not fail the request. The session cookie is checked exactly as before.No change to the Windows runtime, the pipe, pairing, the journal or the printing protocol. Nothing changes against a single-node server.
What the client does when its node stops (confirmed by reading the runtime, and by the tests named)
RealtimeReconnectAsync: not ready, claim wave cancelled, reconnect after backoff (1 to 30 s). The load balancer picks a healthy node and rewrites the cookie; after WELCOME and applied state the server announces work again; claimed jobs replay, recorded jobs are not submitted twice (ReconnectReplaysLostSpoolReceiptAndDrainsBufferedJobsWithoutPrintingAgain)AServerErrorKeepsTheSessionAndFollowsTheLoadBalancer). The lane logsPrint lane stopped; the job stays on the server until the next job notificationADroppedConnectionKeepsTheSession)ARefusedUpgradeIsRetryableUnlessAuthorizationWasWithdrawn)Known gap, not changed here: after a failed claim the lane does not wake itself; it waits for the next
JOB_AVAILABLE. QQQ Printing's several-node change (qbit-printing pull request 36) re-announces outstanding work on the next heartbeat, which bounds that wait to one heartbeat interval. qbit-printing 1.1.x also answered 403 to an upgrade when its database could not answer, which blocks this client until a person signs in; pull request 36 changes that to 503.Tests
LoadBalancerAffinityTests(Core.Tests, 34 cases): cookies in the load balancer's real format travel on every later request beside the session and on sign-out; which cookies are kept (20 cases); bounds; rotation and expiry; foreign cookies never invalidate a session answer while a mis-scoped session cookie still does; 5xx / 409 keep the session and follow the rewritten cookie, 401 ends it; a dropped connection; the upgrade carries the cookie and adopts the one on its answer (real loopback WebSocket handshake); a refused upgrade (502, 503, 401).LoopbackWebSocketPeergained optional answer headers and a refusal.Gates (seen)
Recorded in a comment below for the head commit:
./build/Verify.ps1(locked restore, Release x64 solution build with-warnaserror, Core.Tests and the Windows/CLI suite),check-source-headers.py. Gitea runs no checks for this repository yet (pull request 17), and the GitHub shadow was not pushed to.Release
main(the 1.2.x line). It ships in the next 1.2.x tag after merge: 1.2.2 if merged before pull request 19 is tagged, otherwise 1.2.3 (VersionPrefixis not changed here). The stable tag runs the signing workflow on the GitHub bridge; a release must be signed and was not attempted.release/2.0needs the same change ported by hand:Core/Authenticationthere was refactored today (pull request 29:QqqSocketRequest,QqqTransportPolicy), so this does not cherry-pick.Not verified
A real Application Load Balancer: whether it sets
AWSALBon the 101 upgrade answer (the client does not depend on it, because discovery precedes the upgrade), and its behaviour while a target is draining. Load balancers other than AWS's are not recognized. No installer was built and nothing was printed.🤖 Generated with Claude Code
Gates seen locally on head
87e3f18(maintainer workstation; the GitHub shadow was not pushed to and Gitea runs no checks for this repository yet):./build/Verify.ps1 -ProtocolFixtures <fixtures exported from qqq-print-protocol ac835f8, the commit pinned in .github/workflows/ci.yml>: locked restore; Release x64 solution build with-warnaserror, 0 warnings, 0 errors; Core.Tests 231 passed, 0 failed (197 before, 34 new); Windows/CLI suite 377 passed, 0 failed.AgentRuntimeQueuedJobsTests,AgentRuntimeHeartbeatTests,AgentControlConcurrencyTests): each timed out on its own 10 second budget while other builds and test runs were competing for the machine. None of them uses the session client or the loopback peer. The rerun above passed all 377.python .github/scripts/check-source-headers.py: 321 tracked files verified.🤖 Generated with Claude Code
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.