Skip to content

perf: Reuse the resolved effective ACL across the passes of a single request - #2182

Draft
jeswr wants to merge 3 commits into
CommunitySolidServer:mainfrom
jeswr:fix/wac-acl-reread-memoization
Draft

perf: Reuse the resolved effective ACL across the passes of a single request#2182
jeswr wants to merge 3 commits into
CommunitySolidServer:mainfrom
jeswr:fix/wac-acl-reread-memoization

Conversation

@jeswr

@jeswr jeswr commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

🚧 DRAFT — for @jeswr to review first (opened by @jeswr's AI coding agent; please hold off reviewing until marked ready).

✍️ Description

A single authenticated GET/HEAD resolves and parses the target's effective .acl twice. The WebAclReader runs once for the authorization decision (shared with the WAC-Allow user pass via the same cached (credentials, requestedModes) pair) and again for the WAC-Allow public pass, which builds a fresh empty-credentials object and so misses the CachedHandler — re-walking the container hierarchy and re-reading + re-parsing the same ACL.

This memoizes the credential-independent part of ACL resolution (the existence walk + read + parse) for the duration of a single request, in a WeakMap keyed by the per-request requestedModes object that ModesExtractor produces once per request. Each pass still evaluates its own credentials against the shared parsed ACL, so the granted permissions and the WAC-Allow header are byte-identical. The cache is request-scoped by construction (a different request has a different key) and so can never serve a stale ACL; failed reads are not memoized.

Added a regression describe block to WebAclReader.test.ts covering the within-request reuse (fails on main), the no-stale-cache guarantee across requests, and the no-memoize-on-failure path.

📈 Performance

Effective-.acl read+parse operations per request (WAC enabled), the load-independent metric asserted by the regression test:

Request before after
Authenticated GET 2 1
Unauthenticated GET 1 1
PUT 1 1

This is the load-independent evidence for a fewer-reads change. The per-request saving is one effective-.acl existence-walk + read + parse on authenticated GETs, so the wall-clock effect is modest. A quick back-to-back wall-clock A/B (baseline main vs this branch, both WAC-enabled, same file backend, constant-WebID extractor so oha could drive authenticated load) did not produce a stable reading on the test box — the build-to-build delta was swamped by background load (1-min load oscillating ~1.6→8.8 mid-run, the faster build flipping run to run). A clean wall-clock confirmation needs a quiet box; the deterministic op-count above is the reproducible evidence.

✅ PR check list

Before this pull request can be merged, a core maintainer will check whether

  • this PR is labeled with the correct semver label
    • semver.patch: Backwards compatible bug fixes.
    • semver.minor: Backwards compatible feature additions.
    • semver.major: Breaking changes. This includes changing interfaces or configuration behaviour.
  • the correct branch is targeted. Patch updates can target main, other changes should target the latest versions/* branch.
  • the RELEASE_NOTES.md document in case of relevant feature or config changes.
  • any relevant documentation was updated to reflect the changes in this PR.

jeswr and others added 2 commits June 25, 2026 12:03
…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>
The added JSDoc/comments were more verbose than the surrounding
authorization source; condense to the load-bearing facts (request-scoped
WeakMap keyed by the per-request requestedModes, failures not memoized).
No behaviour change.

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>
@joachimvh

Copy link
Copy Markdown
Member

Note/reminder (for myself mostly) that if this gets finished I'll probably have to also add these changes in https://github.com/CommunitySolidServer/policy-engine (which is used in v8), if possible.

Comment on lines +97 to +100
const promise = (async(): Promise<Map<Store, ResourceIdentifier[]>> => {
const aclMap = await this.getAclMatches(requestedModes.distinctKeys());
return this.findAuthorizationStatements(aclMap);
})();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const promise = (async(): Promise<Map<Store, ResourceIdentifier[]>> => {
const aclMap = await this.getAclMatches(requestedModes.distinctKeys());
return this.findAuthorizationStatements(aclMap);
})();
const promise = this.getAclMatches(requestedModes.distinctKeys()).then(aclMap =>
this.findAuthorizationStatements(aclMap));

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants