В TrueAsync каждый HTTP-запрос выполняется в отдельной корутине внутри одного PHP-воркера. Это означает, что все синглтоны, статические свойства и глобальное состояние разделяются между конкурентными запросами.
Пакет laravel-spawn (ветка feature/scope-per-request) решает часть проблем через:
AsyncApplication— перехват резолва сервисов с проверкойcurrent_context()ScopedServiceProxy— прокси для фасадов, чтобы каждый вызов шёл в контекст текущей корутиныScopedServiceenum — список сервисов, изолированных по корутинам (request,session,auth,auth.driver,cookie)- PDO Pool на уровне C — изоляция соединений к БД без скоупинга
DatabaseManager
| Тип проблемы | Описание |
|---|---|
| Статические свойства / синглтоны | Хранят состояние запроса → утечка между корутинами |
| Кэш фасадов | Facade::$resolvedInstance — статический массив, общий для всех корутин |
| Глобальные переменные | $_GET, $_POST, $_SERVER, $_SESSION — не изолированы (решается на уровне сервера) |
| Блокирующий I/O | Блокирует весь планировщик корутин, если не использовать async I/O |
| Счётчик транзакций | Connection::$transactions — общий между корутинами |
| Пакет | Статус в spawn | Проблема | Решение |
|---|---|---|---|
| Auth | ✅ Scoped | AuthManager::$guards[] кэширует guard-ы с состоянием пользователя. SessionGuard::$user хранит аутентифицированного пользователя |
ScopedService::AUTH + ScopedServiceProxy |
| Session | ✅ Scoped | SessionManager / Store::$attributes хранят данные сессии конкретного запроса |
ScopedService::SESSION + ScopedServiceProxy |
| Cookie | ✅ Scoped | CookieJar::$queued — очередь cookies per-request |
ScopedService::COOKIE (без прокси, т.к. type-hint QueueingFactory) |
| Request | ✅ Scoped | Каждый запрос — уникальный объект с URL, заголовками, телом | ScopedService::REQUEST через current_context() |
| Пакет | Проблема | Решение |
|---|---|---|
| Database / Eloquent | Connection::$transactions — общий счётчик при изолированных PDO-соединениях |
Трейт CoroutineTransactions — счётчик в coroutine_context() |
| View / Blade | View::share('errors') из middleware утекает между корутинами |
AsyncViewFactory — share()/getShared() через current_context() после bootCompleted() |
| Routing | Router::$current / $currentRequest перезаписываются конкурентными запросами |
AsyncRouter — хранение в current_context() после bootCompleted() |
| Translation | Translator::$locale — singleton, setLocale() per-request перезаписывает для всех корутин |
AsyncTranslator — locale в current_context(), $loaded кэш shared для производительности |
| Events / Dispatcher | Event::defer() флаг утекает между корутинами |
Документировано как ограничение — user-space API, не вызывается в Laravel internals |
| Broadcasting | BroadcastManager и драйверы stateless |
Не требует фикса |
| Middleware | Lifecycle — переиспользуются ли экземпляры? | Создаются заново через make() на каждый запрос. Не проблема |
| Пакет | Почему безопасен |
|---|---|
| Cache | CacheManager — синглтон, но store-бэкенды (Redis, File, Memcached) stateless. Подтверждено тестами |
| Queue | QueueManager кэширует соединения, но dispatch stateless. Подтверждено тестами |
MailManager кэширует mailer-ы, но они не хранят per-request состояние. Подтверждено тестами |
|
| Log | Логгирование — side-effect, состояние не хранится |
| Validation | Validator создаётся заново для каждой валидации, Factory::$extensions — read-only |
| Filesystem / Storage | FilesystemManager кэширует disk-и, но они stateless |
| HTTP Client | PendingRequest создаётся заново каждый раз |
| Notifications | Использует Queue/Mail под капотом (безопасны) |
AsyncConfig — per-coroutine overlay для set(), base items shared read-only |
|
| Encryption | Stateless операции с ключом приложения |
| Hashing | Stateless алгоритмы |
| Pagination | Создаётся заново per-request |
- Документация: unsafe-паттерны, safe patterns, адаптация пакетов — см.
ASYNC_ADAPTATION.md - Линтер: PHPStan-правило
MutableStaticPropertyRule— обнаружение mutable static properties (src/PHPStan/) - Адаптеры для сторонних пакетов: spatie/laravel-permission (
AsyncPermissionRegistrar) - Адаптеры для сторонних пакетов: inertia (
AsyncResponseFactory) - Адаптеры для сторонних пакетов: socialite
- Адаптеры:
Translator::setLocale()—AsyncTranslator, locale вcurrent_context(),$loadedкэш shared - Проверить:
terminatingCallbacks[]memory leak (ViewServiceProvider, octane#887)
Популярные third-party пакеты, отсортированные по степени риска.
| Пакет | Установок/мес | Проблема |
|---|---|---|
| barryvdh/laravel-debugbar | ~25M | Singleton-коллекторы (QueryCollector, RouteCollector) копят per-request данные. Memory leak. Данные одного запроса видны в другом |
| laravel/telescope | ~7M | Аналогично — IncomingEntry объекты копятся в памяти. Telescope::$shouldRecord — static флаг, влияет на все корутины |
| Пакет | Установок/мес | Проблема | Решение |
|---|---|---|---|
| spatie/laravel-permission | ~30M | ✅ AsyncPermissionRegistrar — team ID и wildcard index в current_context(), clearPermissionsCollection() no-op в async mode |
src/Permission/AsyncPermissionRegistrar.php |
| livewire/livewire | ~25M | Не поддерживается в async-режиме. LivewireManager — singleton с per-request state. Memory leak (livewire#10009), wire:stream сломан (octane#1022). Filament (Livewire-based) аналогично (filament#19148) |
Использовать Inertia |
| inertiajs/inertia-laravel | ~12M | ✅ AsyncResponseFactory — sharedProps, rootView, version, encryptHistory, urlResolver в current_context() |
src/Inertia/AsyncResponseFactory.php |
| laravel/socialite | ~15M | SocialiteManager кэширует driver-ы в $drivers[] со stale конфигом от предыдущего запроса |
Flush $drivers per-request или скоупить manager |
| Пакет | Установок/мес | Почему безопасен |
|---|---|---|
| laravel/sanctum | ~40M | Использует auth guards (уже scoped). $personalAccessTokenModel — static, read-only |
| laravel/passport | ~15M | Auth guards scoped. Static свойства — boot-time конфигурация |
| laravel/scout | ~8M | EngineManager кэширует engines (Algolia, Meilisearch) — stateless HTTP-клиенты |
| laravel/cashier-stripe | ~7M | Stateless вызовы Stripe API через Eloquent-модели |
| spatie/laravel-medialibrary | ~10M | Model-based, конверсии через queue. Нет singleton state |
| spatie/laravel-activitylog | ~8M | Trait-based логирование. Static конфигурация read-only. Ручной activity() API — создаёт новый logger |
| laravel/horizon | ~10M | Отдельный процесс (horizon:work). Dashboard — stateless чтение из Redis |
| laravel/breeze | ~8M | Scaffolding, не runtime. Риск от Livewire/Inertia под капотом |
Источники: octane best practices, Hypervel porting guide, GitHub issues.
| Проблема | Источник | Статус |
|---|---|---|
Translator::setLocale() per-request |
Hypervel docs — SessionGuard::$user, Translator::$locale |
Проверить — стандартный паттерн SetLocale middleware |
terminatingCallbacks[] memory leak |
octane#887 — ViewServiceProvider добавляет callback при каждом резолве BladeCompiler | Проверить — потенциальный memory leak |
| Проблема | Источник | Наше решение |
|---|---|---|
SessionGuard::$user state leak |
Hypervel docs | ScopedService::AUTH |
Facade::$resolvedInstance shared cache |
Octane docs | ScopedServiceProxy |
View::share() data leak |
Octane docs | AsyncViewFactory |
config()->set() per-request |
Octane docs | Config immutable в нашей модели |
| Проблема | Почему |
|---|---|
$app->singleton(Service, fn($app) => new Service($app)) — stale container |
Larastan OctaneCompatibilityRule ловит это. User-space ошибка, не фреймворк |
| Database connection leak | Наш PDO Pool на уровне C изолирует соединения |
| Swoole-специфичные баги (file hooks, server freeze) | Мы используем TrueAsync, не Swoole |
309 находок в vendor/laravel/framework. Полная классификация:
Не требуют фикса в spawn, но пользователи должны знать:
| Паттерн | Проблема | Безопасная альтернатива |
|---|---|---|
Number::useLocale('de') per-request |
Меняет глобальный static — утечка между корутинами | Передавать locale параметром: Number::format($n, locale: 'de') |
Number::withLocale('de', fn() => ...) |
Безопасен если callback без I/O; опасен если callback содержит yield point (DB, HTTP) | Передавать locale параметром |
once() на singleton-сервисе |
WeakMap кэширует по объекту — singleton = один кэш на все корутины | Не использовать once() с per-request данными на синглтонах (антипаттерн и в Octane) |
Тесты воспроизведения: tests/StaticStateBugsTest.php
| Класс | Свойство | Почему безопасно |
|---|---|---|
Relation |
$constraints |
noConstraints() переключает флаг, но callback внутри — чистый CPU (построение query, нет I/O). В cooperative multitasking нет yield point → другая корутина не может вклиниться |
Relation |
$selfJoinCount |
Монотонный counter для уникальных алиасов — race не вызывает дубликатов |
BladeCompiler |
$componentHashStack |
Используется только при компиляции шаблонов, не рендере. Шаблоны кэшируются после первого запроса |
ManagesLayouts |
$parentPlaceholder |
Детерминированный кэш (hash от имени секции) — одинаковый результат для всех |
View\Component |
$factory |
Container::getInstance()->make('view') — AsyncViewFactory уже корутинно-безопасна |
| Класс | Свойство | Почему |
|---|---|---|
AbstractPaginator |
$currentPageResolver |
$app['request']->input() — request scoped |
AbstractPaginator |
$currentPathResolver |
$app['request']->url() — scoped |
AbstractPaginator |
$queryStringResolver |
$app['request']->query() — scoped |
AbstractCursorPaginator |
$currentCursorResolver |
$app['request']->input() — scoped |
AbstractPaginator |
$viewFactoryResolver |
$app['view'] — один объект, thread-safe |
Uri |
$urlGeneratorResolver |
Boot-time |
76× $macros (Macroable trait) — регистрируются при загрузке, read-only в runtime.
Model static properties (class-level metadata, не per-request):
$resolver, $dispatcher, $booted, $booting, $bootedCallbacks, $traitInitializers,
$globalScopes, $mutatorCache, $attributeMutatorCache, $getAttributeMutatorCache,
$setAttributeMutatorCache, $castTypeCache, $classAttributes, $snakeAttributes,
$primitiveCastTypes, $collectionClass, $manyMethods, $relationResolvers,
$resolvedCollectionClasses, $modelsShouldPreventLazyLoading,
$modelsShouldAutomaticallyEagerLoadRelationships, $modelsShouldPreventSilentlyDiscardingAttributes,
$modelsShouldPreventAccessingMissingAttributes, $lazyLoadingViolationCallback,
$discardedAttributeViolationCallback, $missingAttributeViolationCallback,
$isBroadcasting, $builder, $isSoftDeletable, $isPrunable, $isMassPrunable,
$ignoreOnTouch, $ignoreTimestampsOn, $encrypter, $guardableColumns.
Model::$unguarded — Model::unguard() используется только в seeders (CLI), не в HTTP-запросах. Безопасен.
Model::$recursionCache — WeakMap, ключ = модель-объект. Модели per-request → GC удаляет entry. Безопасен.
Facade: $app, $resolvedInstance, $cached — решено через ScopedServiceProxy.
Routing: Router::$verbs, Route::$validators, ResourceRegistrar::$parameterMap/$singularParameters/$verbs — константы.
Database: Connection::$resolvers, Schema\Builder::$defaultStringLength/$defaultMorphKeyType/$defaultTimePrecision,
PostgresGrammar::$customOperators/$cascadeTruncate, Relation::$morphMap/$requireMorphMap,
Json::$encoder/$decoder, ModelIdentifier::$useMorphMap — boot-time.
Auth/Middleware: $redirectToCallback (4×), $neverEncrypt, $serialize, $neverVerify,
$neverValidate, $neverPrevent, $neverTrim, $skipCallbacks (3×), $alwaysTrust* — boot config.
Support: Container::$instance, AliasLoader::$instance/$facadeNamespace, Env::*,
DateFactory::*, Str::$snakeCache/$camelCache/$studlyCache (детерминированные кэши),
Str::$uuidFactory/$ulidFactory, Pluralizer::*, ServiceProvider::$publishes/*,
Sleep::* (тестовый API), Lottery::$resultFactory, BinaryCodec::$customCodecs,
ConfigurationUrlParser::$driverAliases, EncodedHtmlString::$encodeUsingFactory.
View: Component::$bladeViewCache/$propertyCache/$methodCache/$constructorParametersCache/$ignoredParameterNames (class-level кэши),
DynamicComponent::$compiler/$componentClasses, ManagesLayouts::$parentPlaceholderSalt.
Другое: Encrypter::$supportedCiphers, Mail/Mailable::$viewDataCallback, Markdown::$withSecuredEncoding/$extensions,
Queue::$createPayloadCallbacks, Worker::* (отдельный процесс), Migrator::*, Seeder::$called (CLI),
HandleExceptions::*, Bootstrap::*, Vite::$manifests, Http\Client\*, JsonResource::$wrap/*,
Validation\Rules\*::$defaultCallback, Validator::$placeholderHash, Log\Context\*,
PendingBatch::$batchableClasses, Foundation\Events\*.
DatabaseServiceProvider::boot() устанавливает Model::$resolver = $app['db'] как статическое свойство.
Если db скоупить — после завершения корутины статическая ссылка будет указывать на уничтоженный объект → segfault.
Решение: db остаётся синглтоном, физическая изоляция через PDO Pool.
AuthManager передаёт $app['cookie'] в setCookieJar(QueueingFactory $cookie).
ScopedServiceProxy не реализует QueueingFactory → TypeError.
Решение: Cookie скоупится напрямую через resolve(), без прокси через offsetGet().
// config/async.php
'scoped_services' => [
\SomePackage\Manager::class,
],
// Или программно
$app->scopedSingleton(\SomePackage\Manager::class, fn($app) => new Manager());