Repository navigation
Fix dispatching events that have listeners registered against an interface - #241
Conversation
…t application server Under PHP-FPM every request gets a fresh process, so state left on a long-lived object never hurts anyone. Under Laravel Octane the same process serves many requests, and anything derived from one request stays visible to the next. This adds the primitives the companion Winter changes call. Fires listeners registered against the interfaces an event carries, not only against its class name. Laravel dispatches several events by interface and Octane depends on it: RequestTerminated carries OperationTerminated, and Octane registers its cleanup against the interface. With the previous dispatcher, Octane's own state handling sat inactive for the whole request lifecycle and nothing errored. This applies everywhere rather than only under Octane, and is the widest reaching change here. Adds Application::runningInApplicationServer(), which reports whether a request-serving worker is active by testing for the Laravel\Octane\Contracts\Client binding. Octane binds it in workers and nowhere else, so installing the package does not make an ordinary artisan command answer true. Boot-time gates need this because runningInConsole() is true under Octane, where the SAPI really is CLI. Adds reset primitives to the services that cache per-request values, named by the new Winter\Storm\Contracts\ResetsWorkerState contract so Winter can find them. Auth\Manager held the resolved user and impersonation stack, Halcyon\MemoryCacheManager and Halcyon\Model held parsed template records, Exception\ErrorHandler held custom handler masks, and ExtendableTrait held per-request extension bindings. Implementations must be idempotent, and must leave boot-time registrations alone, since those are built once per worker and discarding them degrades the worker for good. Applies the console root URL override in RegisterWinter only when no request is bound. Under Octane the SAPI is CLI, so it previously fired for every request a worker served and overwrote the URL derived from the real request. Documents how flushRequestCache differs from flushDuplicateCache, which are easy to confuse. Tests: 736 green, each verified failing before its fix. Also exercised end to end against a Winter based application of about thirty plugins running on Octane and FrankenPHP, whose Cypress suite of 36 groups passes on Octane and PHP-FPM alike.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The worker-state reset ran before the trusted-proxy middleware, so an eager Request::ip() there recorded the load balancer's address — and could still be reading the previous operation's request. resetWorkerState() now leaves the address unresolved and resolveIpAddress() derives it on first use, which under throttling happens mid-request, after the proxy chain is trusted. Also corrects the runningInApplicationServer() docblock: Octane's worker entrypoints set APP_RUNNING_IN_CONSOLE=false, so runningInConsole() answers false inside a worker, not true as previously claimed.
Coalescing a null Request::ip() to '0.0.0.0' changed throttle semantics for queue jobs and artisan commands: findThrottleByUserId() scopes its lookup to the given address when one is present, so the placeholder address missed existing ban rows recorded under real client IPs and minted spurious '0.0.0.0' rows. A null address means 'match on the user alone', which is how console contexts found bans before this branch. The resolver is now public (getIpAddress) so external readers have a resolving access path instead of observing the property's unresolved null window.
init() still captured Request::ip() at singleton construction, which under plain PHP-FPM can happen before the trusted-proxy middleware runs — anything resolving auth early froze the load balancer's address for the whole request, and getIpAddress() never re-derives once a non-null value is stored. The address now starts unresolved everywhere and is derived at first actual use.
3ad24fb to
18d39fe
Compare
The worker-state and detection tests carried multi-paragraph docblocks narrating production rationale that belongs in the production code, plus one claim the branch itself later corrected (that a worker answers true to runningInConsole). Cut each comment to what the test name does not already say, and correct that claim where it was repeated.
|
This pull request will be closed and archived in 3 days, as there has been no activity in the last 60 days. |
The auth, error handler, Halcyon cache and ResetsWorkerState changes move to the Winter.Octane plugin, which clears the same state from outside. The Auth\Manager::once() and ExtendableTrait fixes move to their own PRs. runningInApplicationServer() is no longer needed: the Winter gates check Octane's client binding directly, and Octane's entry points already make runningInConsole() false, so the Halcyon request cache is on in a worker without a change.
Octane's worker entry points set APP_RUNNING_IN_CONSOLE to false, so runningInConsole() is false in a worker and the console root URL override never runs there. The Winter.Octane suite passes without the guard.
…eset methods (#1) The core PRs were reduced to the changes a plugin cannot make (wintercms/winter#1511 and wintercms/storm#241), so the plugin now clears the manager, auth, error mask and Halcyon state itself: - MANAGER_STATE lists the request-derived properties on the navigation, widget, settings, mail, component and back-end auth managers, cleared by reflection. - ErrorHandler's masks join STATIC_CACHES, and the Halcyon in-memory cache is flushed on every resolved store. - ResolveAuthIpAddress records the client address on the auth manager after the trusted-proxy middleware. - The ResetsWorkerState contract moves to Winter\Octane\Contracts. - Registration now checks only that Storm dispatches interface listeners, and boot() logs a warning when a worker has no back-end navigation (Winter without #1511). - The auth reset also turns sessions back on, covering Storm without the once() fix.
|
@LukeTowers I reduced this to the interface-listener fix only. The rest moved to wn-octane-plugin or the split-out PRs (#261, #262). |
Summary
Working toward Laravel Octane support. This PR is now only the change that has to live in core; the rest moved to the Winter.Octane plugin or to separate PRs.
Winter\Storm\Events\Dispatcherinherits Laravel'saddInterfaceListeners(), which expects listeners stored aslisteners[event][]. Winter's dispatcher wraps listeners as it registers them and stores them by priority, aslisteners[event][priority][], so the inherited method passes each priority bucket tomakeListener()as if it were a listener. Dispatching any event that has a listener registered against one of its interfaces then throws. Octane registers its post-operation cleanup against theOperationTerminatedinterface, so everyRequestTerminateddispatch fails. The override merges the sorted listeners for each interface instead.This also affects PHP-FPM: any application or package that listens on an interface hits the same exception today.
What moved out
These changes moved to the Winter.Octane plugin, which clears the same state from outside core:
ResetsWorkerStatecontract and the auth manager resetErrorHandler::resetMaskState()and the Halcyon request cache flushThese were dropped:
Application::runningInApplicationServer(): Register back-end navigation, widgets and settings in a Laravel Octane worker winter#1511 now checks Octane's client binding directlyMemoryCacheManager::isEnabled()change and theRegisterWinterrequest guard: Octane's worker entry points setAPP_RUNNING_IN_CONSOLEto false, sorunningInConsole()is already false in a workerThese are bugs outside Octane too, so each is now its own PR:
Auth\Manager::once()leaves sessions disabled when authentication throwsTesting
tests/Events/InterfaceListenerTest.phpcovers interface listeners alone, alongside class listeners in priority order, class-string listeners resolved through the container, and events with no interface listeners. The full Storm suite passes (872 tests).