You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Two per-subscription costs landed with continuous re-authorization (#1414) and the operation-scoped read authorization restore (#1915). Under an MQTT fan-out workload (~150k+ subscriptions per worker; production Fabric nodes measured at 157,894 subs on one worker) they are significant:
The 30s re-auth sweep scales per subscription.server/liveSubscriptionAuth.ts runs findAndValidateUser + a deep cloneRequestTarget + allowRead serially for every registered subscription, every 30 seconds — even though a fan-out workload collapses to a handful of distinct (resource, user, target) admissions (29 distinct topics in the measured production case). At 150k+ subs/worker that is an estimated 0.5–1.5 cores of steady-state CPU across a 16-worker node, plus allocation churn feeding GC (measured at 26.4% of worker CPU in a production profile).
admittedTarget is retained per subscription. Since Restore operation-scoped read authorization and add explicit row filters #1915 (cc8ebfb), every subscribe deep-clones the RequestTarget and the recheck closure retains it for the life of the subscription. Measured in a local load repro (1,000 WS-MQTT conns × 15 subs): RequestTarget count doubled 15,000 → 30,000, ≈ +0.4 KB/sub (~6 KB/conn, ~7% of subscription heap).
Proposed fix (implemented and verified; full patch attached in the first comment)
Group registrations by a canonical admission identity — resource class (WeakMap'd id) + username + canonical target form. The sweep then runs one findAndValidateUser per username and one allowRead recheck per distinct admission; per-subscription work drops to a sync token-expiry comparison. Duplicate admitted-target clones are discarded at registration, so at most one snapshot is retained per distinct admission. Targets carrying reference-identity values (rowFilter functions, class instances, circular refs) have no safe canonical form and register ungrouped — preserving today's per-subscription behavior for them. Per-entry contexts still get context.user advanced to the fresh user before the group recheck runs.
The patch also includes two small delivery-loop items in server/DurableSubscriptionsSession.ts: yield every 100 delivered messages instead of allocating a promise + macrotask hop per message (3.5× loop overhead in fan-out benchmarks; real backpressure is the awaited listener/socket-drain, the yield is only event-loop fairness), and a shared frozen empty checkPermission object.
RequestTarget 30,000 → 15,029 (the 29 = one snapshot per distinct admission)
5.01 → 4.79 KB/sub worker heap
sweep at a 3s test interval: ~0.1% of worker CPU (20 / 17,771 profile samples); per-sweep work goes from 15,000 × (validate + clone + allowRead) to 1 validate + 29 rechecks
24 new unit tests (grouping, fail-closed on validation/recheck errors, per-entry expiry within a group, teardown/unregister, key canonicalization + separator-collision resistance) and the full integrationTests/security/subscription-revocation.test.ts suite (6/6: drop_user, alter_role, token expiry × WS/MQTT) pass; oxlint + prettier clean.
Note for whoever picks this up: a cross-model review (per engineering guidelines) has not been run on the patch yet.
Filed by an AI agent (Claude Code) from a subscription-path memory/CPU investigation; numbers from a production 5.1.26 Fabric heap snapshot + CPU profile and a local 15k-subscription load repro against this repo.
Problem
Two per-subscription costs landed with continuous re-authorization (#1414) and the operation-scoped read authorization restore (#1915). Under an MQTT fan-out workload (~150k+ subscriptions per worker; production Fabric nodes measured at 157,894 subs on one worker) they are significant:
server/liveSubscriptionAuth.tsrunsfindAndValidateUser+ a deepcloneRequestTarget+allowReadserially for every registered subscription, every 30 seconds — even though a fan-out workload collapses to a handful of distinct (resource, user, target) admissions (29 distinct topics in the measured production case). At 150k+ subs/worker that is an estimated 0.5–1.5 cores of steady-state CPU across a 16-worker node, plus allocation churn feeding GC (measured at 26.4% of worker CPU in a production profile).admittedTargetis retained per subscription. Since Restore operation-scoped read authorization and add explicit row filters #1915 (cc8ebfb), every subscribe deep-clones the RequestTarget and the recheck closure retains it for the life of the subscription. Measured in a local load repro (1,000 WS-MQTT conns × 15 subs): RequestTarget count doubled 15,000 → 30,000, ≈ +0.4 KB/sub (~6 KB/conn, ~7% of subscription heap).Proposed fix (implemented and verified; full patch attached in the first comment)
Group registrations by a canonical admission identity — resource class (WeakMap'd id) + username + canonical target form. The sweep then runs one
findAndValidateUserper username and oneallowReadrecheck per distinct admission; per-subscription work drops to a sync token-expiry comparison. Duplicate admitted-target clones are discarded at registration, so at most one snapshot is retained per distinct admission. Targets carrying reference-identity values (rowFilterfunctions, class instances, circular refs) have no safe canonical form and register ungrouped — preserving today's per-subscription behavior for them. Per-entry contexts still getcontext.useradvanced to the fresh user before the group recheck runs.The patch also includes two small delivery-loop items in
server/DurableSubscriptionsSession.ts: yield every 100 delivered messages instead of allocating a promise + macrotask hop per message (3.5× loop overhead in fan-out benchmarks; real backpressure is the awaited listener/socket-drain, the yield is only event-loop fairness), and a shared frozen emptycheckPermissionobject.Verification (local load repro: 1,000 WS-MQTT connections × 15 subscriptions, 29 topics, QoS 1)
integrationTests/security/subscription-revocation.test.tssuite (6/6: drop_user, alter_role, token expiry × WS/MQTT) pass; oxlint + prettier clean.Note for whoever picks this up: a cross-model review (per engineering guidelines) has not been run on the patch yet.
Filed by an AI agent (Claude Code) from a subscription-path memory/CPU investigation; numbers from a production 5.1.26 Fabric heap snapshot + CPU profile and a local 15k-subscription load repro against this repo.