Skip to content

Reuse loaded schema cache in long-running processes - #2804

Open
adilKhan72 wants to merge 2 commits into
nuwave:masterfrom
adilKhan72:fix-octane-schema-cache-memory
Open

adilKhan72 wants to merge 2 commits into
nuwave:masterfrom
adilKhan72:fix-octane-schema-cache-memory

Conversation

@adilKhan72

Copy link
Copy Markdown

Resolves #2436

  • Added or updated tests
  • Documented user facing changes

Changes

In long-running processes such as Laravel Octane, a fresh ASTBuilder is resolved for every request, so ASTCache::fromCacheOrBuild() required the schema cache file on every request. Without OPcache in the worker (opcache.enable_cli=0 is the default for Swoole/RoadRunner), PHP compiled the file and allocated the whole AST array again each time. Because that array was only reachable through the request's app copy, Octane's GC listener could not free it until the following request, so two copies were alive at once.

ASTCache now keeps the loaded cache file contents in memory for the lifetime of the process and reuses them while the file is unchanged (inode, mtime and size, after clearstatcache()). set(), clear() and missing or invalid files reset it, and ASTCache::forgetLoaded() resets everything. The raw array is kept rather than the DocumentAST, so each request still gets its own DocumentAST instance.

Measured with an 18 MB schema cache through Octane's FakeWorker, 200 requests, OPcache off:

real memory per request
before 348 MB 300–670 ms
after 136 MB 8–40 ms

With OPcache enabled, nothing changes (118 MB, 6–10 ms), since the stored array is the immutable one from shared memory.

I couldn't reproduce an unbounded per-request growth; the numbers above level off after ~20 requests, but the double copy plus compile overhead is enough to hit a 512 MB limit with a large schema, which matches the fatal error in the issue (raised while requiring lighthouse-schema.php).

Breaking changes

None.

Changelog

Fixed

  • Reuse the loaded schema cache in long-running processes such as Laravel Octane instead of requiring the cache file on every request

Under Laravel Octane, a fresh ASTBuilder is resolved for every request,
so ASTCache required the schema cache file each time. Without OPcache in
the worker, this compiled the file and allocated the whole array again on
every request, while the previous copy stayed alive until the next cycle
collection.

Keep the loaded cache file contents in memory for the lifetime of the
process and reload them only when the file changes.

Resolves nuwave#2436
@spawnia
spawnia requested a balanced review from Copilot October 25, 2026 11:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Stale OPcache contents can be retained under a new file fingerprint, preventing workers from loading the updated schema.

1 open issue

Review effort: Balanced

return $loaded['astArray'];
}

$astArray = Utils::requireArrayOrForget($this->filesystem, $path);
Within opcache.revalidate_freq, requiring a replaced cache file can return
the previously compiled array. Keeping that in memory under the new file
fingerprint would serve the old schema until the worker restarts.

Ask OPcache to recompile the file when it is newer than the cached script,
and always when this process already loaded an older version.
@adilKhan72

Copy link
Copy Markdown
Author

Good catch from the review, that was a real gap. If the cache file gets replaced within opcache.revalidate_freq, require could still hand back the old compiled array and we'd keep it under the new fingerprint.

Fixed in bc1ac63: before loading the file again we call opcache_invalidate(), forced when this process already had an older version in memory. I added a test that reproduces it with OPcache on (it fails without the fix). It's skipped when OPcache isn't enabled for the CLI, which is the default in CI, so I ran it locally with -d opcache.enable_cli=1.

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.

Schema cache leaks memory with Laravel Octane

2 participants