feat: Refresh Sushi connection on each Octane request via event listener#131
Open
tkaratug wants to merge 3 commits intocalebporzio:mainfrom
Open
feat: Refresh Sushi connection on each Octane request via event listener#131tkaratug wants to merge 3 commits intocalebporzio:mainfrom
tkaratug wants to merge 3 commits intocalebporzio:mainfrom
Conversation
* Add `shouldRefreshDataOnEachRequest()` hook and wire it to Octane's `RequestReceived` event. On each new request, `clearSushiConnection()` nulls out `$sushiConnection` so the next query re-runs `getRows()` and rebuilds the in-memory table. * Under FPM the listener is never registered and `$sushiConnection` is never nulled, so there is zero overhead for non-Octane deployments.
* Updated `test_model_without_refresh_does_not_rerun_get_rows_on_new_request` to `test_model_without_refresh_does_not_register_octane_listener`. * Adjusted assertions to verify that the Octane listener is not registered when `shouldRefreshDataOnEachRequest()` is false.
…sertions * Updated the test to set the `sushiOctaneListenerRegistered` property as accessible. * This change allows for proper validation of the listener registration state in the tests.
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.
Related work
PR #128 by @alimorgaan previously addressed the same problem by tracking the current request ID inside
resolveConnection()usingspl_object_hash(app('request')). That PR was not merged, and the subsequent v2.5.4 release introduced Laravel 13 compatibility changes that altered the booting mechanism (adding thewhenBooteddeferral). This PR reopens the feature request with a different implementation approach that is compatible with v2.5.4 and avoids the issues described below.Problem
When running under Laravel Octane, a single application process handles many HTTP requests. Sushi's SQLite connection (and the data behind it) is configured once at boot and reused for the lifetime of that process. This is the right default, but it means models whose
getRows()returns dynamic data (e.g. pulled from cache or a database) will serve stale rows for every request after the first.Solution
This PR adds an opt-in
shouldRefreshDataOnEachRequest()hook. When a model overrides it to returntrue, Sushi registers a listener on Octane'sRequestReceivedevent at boot time. On every new request the listener callsclearSushiConnection(), nulling out the static connection. The next query triggers a lazyconfigureSushiConnection()call inresolveConnection(), which re-runsgetRows()and rebuilds the in-memory SQLite table.Under traditional FPM deployments (or any environment where the
Laravel\Octane\Events\RequestReceivedclass does not exist) the listener is never registered,$sushiConnectionis never nulled, and there is zero overhead compared to the current behaviour.Why not the request-ID approach from PR #128?
The request-ID approach had two problems:
app('request')) on every single query, adding overhead even for models that never refresh.spl_object_hash()can return the same hash for different request objects, and theREQUEST_TIME_FLOATfallback introduces a race condition under high traffic.The event listener approach moves request detection entirely out of the hot path. The listener fires once per request at the Octane level, and
resolveConnection()only pays a=== nullcheck — which is alwaysfalsefor non-refreshing models after boot.Usage
What changed
src/Sushi.phpprotected static $sushiOctaneListenerRegistered = falseto prevent duplicate listener registration when a model is booted more than once (e.g. afterclearBootedModels()).protected static function shouldRefreshDataOnEachRequest(): bool— returnsfalseby default, making this entirely opt-in and backwards-compatible.resolveConnection()to lazily callconfigureSushiConnection()when the connection isnulland the model has opted in to per-request refresh. The double condition (=== null && shouldRefreshDataOnEachRequest()) short-circuits immediately for the common case (refresh disabled), so there is no per-query overhead for standard models.bootSushi()to callregisterOctaneRefreshListener()afterconfigureSushiConnection(), in both thewhenBootedand the legacy path.protected static function registerOctaneRefreshListener(): void— guards with three early returns (refresh disabled / already registered / Octane not present), then registers a static closure onapp('events')that callsclearSushiConnection()for this model class.public static function clearSushiConnection(): void— sets$sushiConnection = null, making it available as a public API so callers (and tests) can simulate an Octane request boundary without depending on the event system.tests/SushiTest.phpOctaneRefreshModel— a test model that overridesshouldRefreshDataOnEachRequest()and counts how many timesgetRows()has been called, returning one extra row per call.test_model_with_refresh_reruns_get_rows_after_connection_is_cleared— verifies that callingclearSushiConnection()(what the Octane event listener does) causes the next query to re-rungetRows()and return fresh data.test_model_without_refresh_does_not_register_octane_listener— verifies that$sushiOctaneListenerRegisteredremainsfalseafter boot for a model with the defaultshouldRefreshDataOnEachRequest() = false, proving that the Octane listener is never registered and the connection is therefore never cleared between requests.resetStatics()onFoo,Bar, andOctaneRefreshModelto reset$sushiOctaneListenerRegisteredbetween tests.setUp()to resetBar::$hasBeenAccessedBeforeunconditionally (it was only reset inside individual tests, which could leave state across test runs).README.md