Skip to content

fix: event-based cache invalidation for mid-request view clears#107

Closed
erhanurgun wants to merge 3 commits into
livewire:mainfrom
erhanurgun:fix/event-based-cache-invalidation-96
Closed

fix: event-based cache invalidation for mid-request view clears#107
erhanurgun wants to merge 3 commits into
livewire:mainfrom
erhanurgun:fix/event-based-cache-invalidation-96

Conversation

@erhanurgun

@erhanurgun erhanurgun commented Mar 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Details

The original issue: calling Artisan::call('optimize:clear') (or view:clear) mid-request deletes compiled view files, but BlazeRuntime::$compiled still marks them as compiled. Subsequent resolve() calls skip recompilation and require_once fails on the missing file.

The previous fix added file_exists() to the ensureCompiled() guard, but this introduced a stat() syscall on every component resolution -- benchmarks showed ~40% regression (33ms vs 20ms).

New approach: Register a CommandFinished event listener in BlazeServiceProvider::boot(). When view:clear finishes:

  1. BlazeRuntime::clearCompiled() resets the $compiled array so resolve() goes through the normal compilation flow
  2. Component::flushCache() clears Laravel's static $bladeViewCache so createBladeViewFromString() recreates temporary source files that view:clear deleted

Scope: This fixes same-process Artisan::call() scenarios (mid-request calls, test suites, queue workers). Cross-process invalidation (terminal commands during an active request) is an inherent race condition in any in-memory cache approach and requires zero-downtime deployment strategies instead.

Rebased onto current main (post-#95 DI refactoring).

Test plan

  • New integration test: render, view:clear, re-render (previously crashed with FileNotFoundException)
  • All 164 existing tests pass (5 skipped -- FluxPro license tests)
  • CI benchmark confirms no performance regression vs main (19.66ms vs 19.65ms baseline)

Fixes #96

@github-actions

github-actions Bot commented Mar 3, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results

Attempt Blade Blaze Improvement
#1 * 352.67ms 19.79ms 94.4%
#2 356.14ms 19.43ms 94.5%
#3 342.95ms 19.25ms 94.4%
#4 351.64ms 19.59ms 94.4%
#5 353.31ms 19.45ms 94.5%
#6 345.21ms 19.35ms 94.4%
#7 346.36ms 19.34ms 94.4%
#8 345.83ms 19.36ms 94.4%
#9 341.79ms 19.48ms 94.3%
#10 355.34ms 19.42ms 94.5%
Snapshot 351.96ms 19.61ms 94.4%
Result 346.36ms (~) 19.42ms (~) 94.4% (~)

Median of 10 attempts (* = outlier, excluded from result), 5000 iterations x 10 rounds, 46.34s total

@juliuskiekbusch

Copy link
Copy Markdown

@erhanurgun I don't think this would work, as the Command is run in a different PHP-Process, which uses their own static variables and event-handler.

@erhanurgun

Copy link
Copy Markdown
Contributor Author

@erhanurgun I don't think this would work, as the Command is run in a different PHP-Process, which uses their own static variables and event-handler.

If you know how it shouldn't work, you should also know how it should work... How should we fix this problem? Do you have any suggestions?

@juliuskiekbusch

Copy link
Copy Markdown

My recommendation is to use Zero-Downtime Deployments, so the files are not deleted during a request. The request can still finish in the old Directory, while new requests are handled in the new Directory.

I wonder why this problem only occurs with blaze and not with a normal blade. 🤔

Though I don't have any idea for a performant approach. Process to Process Communcation would create a lot of different Problems in my opinion.

@juliuskiekbusch

Copy link
Copy Markdown

Okay it seems I didn't catch the mid-request Part and assumed that another process would clear the files.

@ganyicz

ganyicz commented Mar 4, 2026

Copy link
Copy Markdown
Collaborator

@erhanurgun I'm not sure if we can reliably prevent this. This PR only fixes the issue when you run Artisan::call('view:clear') from the same script that's doing the compilation. If a compiled file gets deleted otherwise or you call php artisan view:clear from the terminal you'll hit the same thing mid request.

For now I'm leaning towards leaving it as it is. The file_exists check is a good solution but too slow unfortunately.

It's probably not recommended to run Artisan::call('view:clear') in a Livewire request in production anyway, I would consider rethinking the core issue of why you need to do this.

You could hit this during deployment while running optimize and also serving requests, but as mentioned the proper solution there is zero downtime deployment.

Let me know what do you think but unless there is a more performant solution or a more convincing reason that justifies 30% slowdown I don't think we'll go through with this one.

Instead of adding a file_exists() check to the hot path (which caused
~40% performance regression), listen for the view:clear command via
CommandFinished event and reset both BlazeRuntime's in-memory compilation
cache and Laravel's Component view string cache when compiled files are
actually deleted.

This preserves zero-overhead in-memory caching during normal requests
while correctly handling mid-request cache clears (e.g. optimize:clear).

Fixes livewire#96
@erhanurgun

Copy link
Copy Markdown
Contributor Author

@juliuskiekbusch, Issue:
The BlazeRuntime::ensureCompiled() in-memory cache remains stale after a mid-request view:clear. Compiler.php (lines 101-102) executes ensureCompiled() + require_once sequentially in the template code it generates. When the cache is stale, ensureCompiled() returns without recompiling, and require_once fails to open the deleted file and throws an error...

@erhanurgun erhanurgun force-pushed the fix/event-based-cache-invalidation-96 branch from b2fdeb8 to 203e093 Compare March 5, 2026 03:15
@erhanurgun

erhanurgun commented Mar 5, 2026

Copy link
Copy Markdown
Contributor Author

@ganyicz Thanks for the detailed feedback -- really appreciate the thoughtful analysis.

I think there might be a mix-up between this PR and the reverted #97, so I want to clarify the performance point first:

This PR has zero overhead -- not 30%.

The 30% regression came from #97, which added file_exists() to the hot path of ensureCompiled(). This PR is fundamentally different: it registers a CommandFinished event listener that only fires when an Artisan command finishes. During normal rendering, there are no extra syscalls, no filesystem checks, nothing touches the hot path. CI benchmark: 19.66ms vs 19.65ms baseline (statistical noise).

This is the "more performant solution" you mentioned -- it costs literally nothing during normal operation.

On scope and use cases:

You're absolutely right that calling view:clear inside a Livewire action isn't recommended for production. But same-process Artisan::call() comes up in other legitimate scenarios:

  • This project's own test suite -- beforeEach(fn () => Artisan::call('view:clear')) in IntegrationTest.php. Without WithConsoleEvents, the event listener wouldn't fire and mid-test cache staleness could occur.
  • optimize:clear internally calls view:clear. Users running Artisan::call('optimize:clear') in deployment scripts, queue workers, or scheduled tasks can trigger this unintentionally.
  • Any application code that runs Artisan commands and then continues rendering.

Cross-process invalidation (terminal php artisan view:clear during an HTTP request) is indeed a race condition that no in-memory cache can solve without per-call syscalls -- this applies equally to standard Blade. Zero-downtime deployment is the right answer there, agreed.

Given this costs nothing at runtime and fixes a real, reproducible edge case, I believe it's worth including. That said, I don't have an alternative approach beyond what's already been explored (#97's file_exists() and this event-based approach). If this still doesn't align with the project's direction, feel free to close the PR and issue -- completely understand.

@ganyicz

ganyicz commented Mar 5, 2026

Copy link
Copy Markdown
Collaborator

I understand this PR is overhead-free but it doesn't fix the underlying issue, the file_exists check would, but unfortunately that's slow.

I thought I understand the need for this in tests but as you pointed out we're also running Artisan::call('view:clear') in our test suite and we never hit this. So if I understand correctly this only happens if you first render a view and then call view:clear inside the same test. This is a very narrow use case and again something you probably shouldn't do.

A better solution here would be to fix the core issue, which is Blaze failing when a compiled file gets deleted mid-request. If we had that, we wouldn't need this workaround. I'll see if I can come up with an overhead-free approach.

@ganyicz ganyicz closed this Mar 5, 2026
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.

ensureCompiled() in-memory cache causes require_once failure when compiled views are cleared mid-request

3 participants