perf: Reuse the resolved effective ACL across the passes of a single request#2182
Draft
jeswr wants to merge 2 commits into
Draft
perf: Reuse the resolved effective ACL across the passes of a single request#2182jeswr wants to merge 2 commits into
jeswr wants to merge 2 commits into
Conversation
…request A single authenticated GET/HEAD invokes the WebAclReader several times with the same requestedModes but different credentials: once for the authorization decision (sharing its (credentials, requestedModes) object pair with the WAC-Allow user-permission pass) and once more for the WAC-Allow public pass, which uses a fresh empty credentials literal and therefore misses the CachedHandler. Each missing pass re-walks the container hierarchy and re-reads and re-parses the same effective .acl, even though the resolved ACL is identical for all of them. Memoize the credential-independent part of ACL resolution (the effective-ACL existence walk plus the read and parse of the relevant authorization statements) for the duration of a single request, keyed by the requestedModes AccessMap object in a WeakMap. The per-credential permission evaluation still runs for every call against the shared, read-only parsed ACL, so the granted permissions and the resulting authorization decision are unchanged. The cache is request-scoped by construction: the ModesExtractor produces the requestedModes object exactly once per request and shares it across the passes, so a different request always carries a different key and can never hit an earlier request's entry, and the WeakMap entry is collected once the request's AccessMap is unreachable. This mirrors the request-scoping already used by CachedResourceSet. Failed reads are not memoized. Measured effective-.acl read+parse counts (WAC enabled): an authenticated GET drops from 2 to 1, while the unauthenticated GET and PUT stay at 1. Model: claude-opus-4-8 Provenance: Opus 4.8 (Fable unavailable) — re-review/upgrade candidate Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gent webId) The new regression tests had two TS errors only surfaced by tsc -p test (the CI test-unit typecheck), not by build:ts (src-only): an AccessMap built with the wrong generic (IdentifierSetMultiMap<ResourceIdentifier> -> <AccessMode>), and an un-narrowed credentials.agent.webId access. Test-only; the fix is unchanged. Model: claude-opus-4-8 Provenance: Opus 4.8 (Fable unavailable) -- re-review/upgrade candidate Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🚧 DRAFT — not ready for review. This PR was opened by @jeswr's AI coding agent and is intentionally in draft so that @jeswr can review it first, before it is submitted for CommunitySolidServer maintainer review. Please hold off reviewing until it's marked ready.
Problem
A single authenticated
GET/HEADresolves the target's effective.aclmore than once, even though the effective ACL is identical for every resolution within the request. The resolution is expensive: an existence walk up the container hierarchy (WebAclReader.getAclRecursive→ResourceSet.hasResource) plus a read and parse of the ACL document (ResourceStore.getRepresentation→readableToQuads).The reader is invoked three times per authenticated GET:
AuthorizingHttpHandler— the authorization decision, with(credentials, requestedModes).WacAllowHttpHandler— theWAC-Allowuser-permission pass, with the same(credentials, requestedModes).WacAllowHttpHandler— theWAC-Allowpublic-permission pass, with(credentials: {}, requestedModes).The
CachedHandlerin front of thePermissionReaderis keyed by the[credentials, requestedModes]object pair. BecauseModesExtractoris itself cached on theoperation, passes (1) and (2) share the same(credentials, requestedModes)objects and already hit that cache. But pass (3) builds a fresh{}credentials literal on every request, so it is always a cache miss — and re-reads + re-parses the same ACL.Measured
.aclread counts on current upstreammain(WAC enabled)I instrumented
ResourceStore.getRepresentation(the ACL read+parse) andResourceSet.hasResource(the existence walk) and drove real requests through the fullAuthorizingHttpHandler → WacAllowHttpHandlerchain (with the realCachedHandler/WebAclReader/PermissionBasedAuthorizer):So on current
mainthe redundant work is one extra full ACL read + parse + existence walk per authenticated GET — the surviving duplicate is theWAC-Allowpublic pass. (Passes (1) and (2) are already deduplicated by the existingCachedHandler; the unauthenticated case reuses the user result and PUT skipsWacAllowHttpHandlerentirely.)Fix
Memoize the credential-independent part of ACL resolution — the effective-ACL existence walk and the read + parse of the relevant authorization statements (
getAclMatches+findAuthorizationStatements) — for the duration of a single request, insideWebAclReader. Each call still evaluates its own(credentials)question (findPermissions) against the shared parsed ACL, so the granted permissions are unchanged — only the redundant read/parse/walk is avoided.Measured after the fix
The
WAC-Allowpublic pass now reuses the parsed ACL from the authorization/user pass. OutputWAC-Allow(user=…,public=…) is byte-identical before and after.Why this is WAC-safe
This is security-critical, so the memoization is deliberately conservative:
WeakMapkeyed by therequestedModesAccessMapobject.ModesExtractorproduces that object exactly once per request (it is itself aCachedHandleron theoperation) and shares it across all three passes above. A different request always carries a differentAccessMapobject, so it cannot hit an earlier request's entry, and once a request'sAccessMapis unreachable theWeakMapentry becomes eligible for GC. It can never serve a stale ACL to a later request. This mirrors the request-scoping already used byCachedResourceSet(aWeakMapkeyed by theResourceIdentifierobject).findPermissions/determinePermissionsstill run per call against the shared, read-only parsed quad store, so the computedPermissionMap(and hence the allow/deny decision and theWAC-Allowheader) is identical. This is backed by the unchangedWebAclReader,WacAllowHttpHandler,AuthorizingHttpHandler,PermissionBasedAuthorizerunit suites and the fullLdpHandlerWithAuth+PermissionTableintegration suites (426 end-to-end WAC cases) all passing unchanged.Tests
Added a regression
describeblock toWebAclReader.test.ts:resolves the effective ACL once when reused for different credentials— callshandle()with the user credentials and then the empty ({}, public) credentials, sharing the samerequestedModesobject (the within-request pattern), and assertsgetRepresentation+hasResourceare each called once while both permission results stay correct. This test fails onmain(Received number of calls: 2).re-resolves the effective ACL for a different request (no stale cache)— a second request with a freshrequestedModesobject re-reads the ACL (asserts 2 reads), guarding the request-scoping / no-stale-serve property.does not memoize a failed ACL read— a failing read is not cached; the retry re-reads../srcstays at 100% coverage.— 🤖 PSS agent — @jeswr's coding agent for
prod-solid-server/ the Solid app + Pod-Manager suite. Opened in draft for @jeswr's review.