diff --git a/src/Stache/ServiceProvider.php b/src/Stache/ServiceProvider.php index b54b619e676..1fa4fbe88f4 100644 --- a/src/Stache/ServiceProvider.php +++ b/src/Stache/ServiceProvider.php @@ -50,6 +50,8 @@ public function boot() $this->registerStores($stache); $this->resetMemoizedStateBetweenJobs($stache); + + $this->resetMemoizedStateBetweenOctaneRequests($stache); } private function registerStores($stache) @@ -88,6 +90,22 @@ private function resetMemoizedStateBetweenJobs($stache) }); } + /** + * The stores are made when the app boots, so under Octane a worker uses the same ones + * for every request it handles. What they remember (their paths, for one) has to be + * read again for each request, or the worker never sees what another process saves. + */ + private function resetMemoizedStateBetweenOctaneRequests($stache) + { + // Listened for by name, since Octane isn't necessarily installed. + Event::listen('Laravel\Octane\Events\RequestReceived', function () use ($stache) { + $stache->stores()->each->resetMemoizedState(); + + // Octane handles each request in its own copy of the container. + app('stache.indexes')->each->resetMemoizedState(); + }); + } + private function locks() { if (config('statamic.stache.lock.enabled', true)) { diff --git a/tests/Stache/ServiceProviderTest.php b/tests/Stache/ServiceProviderTest.php index f9c882732c1..23961f42b87 100644 --- a/tests/Stache/ServiceProviderTest.php +++ b/tests/Stache/ServiceProviderTest.php @@ -56,6 +56,36 @@ public function it_resets_memoized_store_and_index_state_between_jobs_when_runni $this->assertEquals(2, $hits->filter(fn ($key) => $key === $idIndexCacheKey)->count()); } + #[Test] + public function it_resets_memoized_store_and_index_state_when_an_octane_request_arrives() + { + $store = (new ServiceProviderTestStore)->directory('/path/to/directory'); + Stache::registerStore($store); + + $pathsCacheKey = "stache::indexes::{$store->key()}::path"; + $idIndexCacheKey = "stache::indexes::{$store->key()}::id"; + + Cache::put($pathsCacheKey, ['alfa' => 'alfa.md']); + Cache::put($idIndexCacheKey, ['alfa' => 'alfa']); + + // A request to a worker uses the store, which is the same one for every request it handles. + $this->assertEquals(['alfa'], $store->paths()->keys()->all()); + $this->assertEquals(['alfa'], $store->index('id')->keys()); + + // Another process saves an item. + Cache::put($pathsCacheKey, ['alfa' => 'alfa.md', 'bravo' => 'bravo.md']); + Cache::put($idIndexCacheKey, ['alfa' => 'alfa', 'bravo' => 'bravo']); + + $this->assertEquals(['alfa'], $store->paths()->keys()->all()); + $this->assertEquals(['alfa'], $store->index('id')->keys()); + + // Octane isn't a dependency, so its event is fired by name, as the listener is registered. + Event::dispatch('Laravel\Octane\Events\RequestReceived'); + + $this->assertEquals(['alfa', 'bravo'], $store->paths()->keys()->all()); + $this->assertEquals(['alfa', 'bravo'], $store->index('id')->keys()); + } + #[Test] public function it_does_not_reset_memoized_state_for_jobs_processed_outside_a_worker() {