├── src ├── Commands │ ├── stubs │ │ └── rr.yaml │ ├── Command.php │ ├── Concerns │ │ ├── InteractsWithTerminal.php │ │ ├── InteractsWithEnvironmentVariables.php │ │ ├── InteractsWithServers.php │ │ ├── InstallsRoadRunnerDependencies.php │ │ └── InteractsWithIO.php │ ├── StatusCommand.php │ ├── ReloadCommand.php │ ├── StopCommand.php │ ├── InstallCommand.php │ ├── StartCommand.php │ └── StartSwooleCommand.php ├── Exceptions │ ├── ServerShutdownException.php │ ├── ValueTooLargeForColumnException.php │ ├── WorkerException.php │ ├── TaskTimeoutException.php │ ├── DdException.php │ ├── TaskException.php │ └── TaskExceptionResult.php ├── Swoole │ ├── TaskResult.php │ ├── Handlers │ │ ├── OnManagerStart.php │ │ ├── OnServerStart.php │ │ └── OnWorkerStart.php │ ├── Actions │ │ ├── EnsureRequestsDontExceedMaxExecutionTime.php │ │ └── ConvertSwooleRequestToIlluminateRequest.php │ ├── SignalDispatcher.php │ ├── SwooleCoroutineDispatcher.php │ ├── SwooleExtension.php │ ├── InvokeTickCallable.php │ ├── ServerProcessInspector.php │ ├── ServerStateFile.php │ ├── SwooleTaskDispatcher.php │ └── SwooleHttpTaskDispatcher.php ├── Events │ ├── WorkerStarting.php │ ├── WorkerStopping.php │ ├── TickReceived.php │ ├── WorkerErrorOccurred.php │ ├── TaskReceived.php │ ├── RequestReceived.php │ ├── RequestHandled.php │ ├── TickTerminated.php │ ├── TaskTerminated.php │ ├── HasApplicationAndSandbox.php │ └── RequestTerminated.php ├── Contracts │ ├── StoppableClient.php │ ├── DispatchesCoroutines.php │ ├── OperationTerminated.php │ ├── ServerProcessInspector.php │ ├── ServesStaticFiles.php │ ├── DispatchesTasks.php │ ├── Worker.php │ └── Client.php ├── OctaneResponse.php ├── Exec.php ├── Listeners │ ├── GiveNewApplicationInstanceToSessionManager.php │ ├── FlushStrCache.php │ ├── CreateConfigurationSandbox.php │ ├── FlushArrayCache.php │ ├── GiveNewRequestInstanceToPaginator.php │ ├── DisconnectFromDatabases.php │ ├── GiveNewRequestInstanceToApplication.php │ ├── FlushQueuedCookies.php │ ├── GiveNewApplicationInstanceToHttpKernel.php │ ├── CollectGarbage.php │ ├── GiveNewApplicationInstanceToRouter.php │ ├── EnsureUploadedFilesAreValid.php │ ├── FlushSessionState.php │ ├── EnforceRequestScheme.php │ ├── FlushDatabaseQueryLog.php │ ├── StopWorkerIfNecessary.php │ ├── GiveNewApplicationInstanceToAuthorizationGate.php │ ├── EnsureUploadedFilesCanBeMoved.php │ ├── FlushDatabaseRecordModificationState.php │ ├── GiveNewApplicationInstanceToQueueManager.php │ ├── GiveNewApplicationInstanceToValidationFactory.php │ ├── EnsureRequestServerPortMatchesScheme.php │ ├── GiveNewApplicationInstanceToFilesystemManager.php │ ├── GiveNewApplicationInstanceToPipelineHub.php │ ├── GiveNewApplicationInstanceToViewFactory.php │ ├── GiveNewApplicationInstanceToMailManager.php │ ├── PrepareInertiaForNextOperation.php │ ├── PrepareLivewireForNextOperation.php │ ├── FlushTranslatorCache.php │ ├── GiveNewApplicationInstanceToDatabaseManager.php │ ├── FlushMonologState.php │ ├── GiveNewApplicationInstanceToCacheManager.php │ ├── PrepareScoutForNextOperation.php │ ├── FlushLogContext.php │ ├── FlushUploadedFiles.php │ ├── FlushAuthenticationState.php │ ├── GiveNewApplicationInstanceToNotificationChannelManager.php │ ├── PrepareSocialiteForNextOperation.php │ ├── FlushTemporaryContainerInstances.php │ ├── GiveNewApplicationInstanceToDatabaseSessionHandler.php │ ├── GiveNewApplicationInstanceToBroadcastManager.php │ ├── RefreshQueryDurationHandling.php │ ├── ReportException.php │ └── FlushLocaleState.php ├── PosixExtension.php ├── DispatchesEvents.php ├── SequentialCoroutineDispatcher.php ├── SymfonyProcessFactory.php ├── CurrentApplication.php ├── Cache │ ├── OctaneArrayStore.php │ └── OctaneStore.php ├── WorkerExceptionInspector.php ├── RoadRunner │ ├── Concerns │ │ └── FindsRoadRunnerBinary.php │ ├── ServerStateFile.php │ ├── ServerProcessInspector.php │ └── RoadRunnerClient.php ├── Testing │ └── Fakes │ │ ├── FakeWorker.php │ │ └── FakeClient.php ├── Facades │ └── Octane.php ├── Tables │ ├── OpenSwooleTable.php │ ├── TableFactory.php │ ├── Concerns │ │ └── EnsuresColumnSizes.php │ └── SwooleTable.php ├── RequestContext.php ├── Concerns │ ├── RegistersTickHandlers.php │ ├── ProvidesRouting.php │ ├── ProvidesConcurrencySupport.php │ └── ProvidesDefaultConfigurationOptions.php ├── Octane.php ├── SequentialTaskDispatcher.php ├── ApplicationGateway.php ├── Stream.php ├── MarshalsPsr7RequestsAndResponses.php ├── ApplicationFactory.php └── Worker.php ├── fixes ├── fix-symfony-file-moving.php └── fix-symfony-file-validation.php ├── bin ├── WorkerState.php ├── file-watcher.cjs ├── createSwooleCacheTable.php ├── createSwooleTimerTable.php ├── createSwooleServer.php ├── createSwooleTables.php ├── bootstrap.php ├── roadrunner-worker └── swoole-server ├── LICENSE.md ├── README.md ├── composer.json ├── art └── logo.svg └── config └── octane.php /src/Commands/stubs/rr.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Exceptions/ServerShutdownException.php: -------------------------------------------------------------------------------- 1 | sandbox->instance('config', clone $event->sandbox['config']); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Exceptions/WorkerException.php: -------------------------------------------------------------------------------- 1 | file = $file; 14 | $this->line = $line; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Events/RequestHandled.php: -------------------------------------------------------------------------------- 1 | sandbox->make('cache')->store('array')->flush(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Events/TickTerminated.php: -------------------------------------------------------------------------------- 1 | sandbox); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Contracts/OperationTerminated.php: -------------------------------------------------------------------------------- 1 | sandbox->make('db')->getConnections() as $connection) { 15 | $connection->disconnect(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Listeners/GiveNewRequestInstanceToApplication.php: -------------------------------------------------------------------------------- 1 | app->instance('request', $event->request); 15 | $event->sandbox->instance('request', $event->request); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Listeners/FlushQueuedCookies.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('cookie')) { 15 | return; 16 | } 17 | 18 | $event->sandbox->make('cookie')->flushQueuedCookies(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Listeners/GiveNewApplicationInstanceToHttpKernel.php: -------------------------------------------------------------------------------- 1 | sandbox->make(Kernel::class)->setApplication($event->sandbox); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Contracts/ServerProcessInspector.php: -------------------------------------------------------------------------------- 1 | app->make('config')->get('octane.garbage'); 15 | 16 | if ($garbage && (memory_get_usage() / 1024 / 1024) > $garbage) { 17 | gc_collect_cycles(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Listeners/GiveNewApplicationInstanceToRouter.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('router')) { 15 | return; 16 | } 17 | 18 | $event->sandbox->make('router')->setContainer($event->sandbox); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Listeners/EnsureUploadedFilesAreValid.php: -------------------------------------------------------------------------------- 1 | console.log('File added...')) 13 | .on('change', () => console.log('File changed...')) 14 | .on('unlink', () => console.log('File deleted...')) 15 | .on('unlinkDir', () => console.log('Directory deleted...')); 16 | -------------------------------------------------------------------------------- /src/Listeners/FlushSessionState.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('session')) { 15 | return; 16 | } 17 | 18 | $driver = $event->sandbox->make('session')->driver(); 19 | 20 | $driver->flush(); 21 | $driver->regenerate(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Listeners/EnforceRequestScheme.php: -------------------------------------------------------------------------------- 1 | sandbox->make('config')->get('octane.https')) { 15 | return; 16 | } 17 | 18 | $event->sandbox->make('url')->forceScheme('https'); 19 | 20 | $event->request->server->set('HTTPS', 'on'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Listeners/FlushDatabaseQueryLog.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('db')) { 15 | return; 16 | } 17 | 18 | foreach ($event->sandbox->make('db')->getConnections() as $connection) { 19 | $connection->flushQueryLog(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/DispatchesEvents.php: -------------------------------------------------------------------------------- 1 | bound(Dispatcher::class)) { 18 | $app[Dispatcher::class]->dispatch($event); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Events/HasApplicationAndSandbox.php: -------------------------------------------------------------------------------- 1 | app; 15 | } 16 | 17 | /** 18 | * Get the sandbox version of the application instance. 19 | */ 20 | public function sandbox(): Application 21 | { 22 | return $this->sandbox; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Listeners/StopWorkerIfNecessary.php: -------------------------------------------------------------------------------- 1 | sandbox->make(Client::class); 18 | 19 | if ($client instanceof StoppableClient) { 20 | $client->stop(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Contracts/ServesStaticFiles.php: -------------------------------------------------------------------------------- 1 | mapWithKeys( 15 | fn ($coroutine, $key) => [$key => $coroutine()] 16 | )->all(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Listeners/GiveNewApplicationInstanceToAuthorizationGate.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved(Gate::class)) { 17 | return; 18 | } 19 | 20 | $event->sandbox->make(Gate::class)->setContainer($event->sandbox); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Listeners/EnsureUploadedFilesCanBeMoved.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('db')) { 15 | return; 16 | } 17 | 18 | foreach ($event->sandbox->make('db')->getConnections() as $connection) { 19 | $connection->forgetRecordModificationState(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Events/RequestTerminated.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('queue')) { 15 | return; 16 | } 17 | 18 | with($event->sandbox->make('queue'), function ($manager) use ($event) { 19 | $manager->setApplication($event->sandbox); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /bin/createSwooleCacheTable.php: -------------------------------------------------------------------------------- 1 | column('value', Table::TYPE_STRING, $serverState['octaneConfig']['cache']['bytes'] ?? 10000); 14 | $cacheTable->column('expiration', Table::TYPE_INT); 15 | 16 | $cacheTable->create(); 17 | 18 | return $cacheTable; 19 | } 20 | -------------------------------------------------------------------------------- /src/Listeners/GiveNewApplicationInstanceToValidationFactory.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('validator')) { 15 | return; 16 | } 17 | 18 | with($event->sandbox->make('validator'), function ($factory) use ($event) { 19 | $factory->setContainer($event->sandbox); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /bin/createSwooleTimerTable.php: -------------------------------------------------------------------------------- 1 | 0) { 9 | $timerTable = TableFactory::make($serverState['octaneConfig']['max_timer_table_size'] ?? 250); 10 | 11 | $timerTable->column('worker_pid', Table::TYPE_INT); 12 | $timerTable->column('time', Table::TYPE_INT); 13 | $timerTable->column('fd', Table::TYPE_INT); 14 | 15 | $timerTable->create(); 16 | 17 | return $timerTable; 18 | } 19 | 20 | return null; 21 | -------------------------------------------------------------------------------- /src/Listeners/EnsureRequestServerPortMatchesScheme.php: -------------------------------------------------------------------------------- 1 | request->getPort(); 15 | 16 | if (is_null($port) || $port === '') { 17 | $event->request->server->set( 18 | 'SERVER_PORT', 19 | $event->request->getScheme() === 'https' ? 443 : 80 20 | ); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Listeners/GiveNewApplicationInstanceToFilesystemManager.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('filesystem')) { 15 | return; 16 | } 17 | 18 | with($event->sandbox->make('filesystem'), function ($manager) use ($event) { 19 | $manager->setApplication($event->sandbox); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/SymfonyProcessFactory.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved(Hub::class)) { 17 | return; 18 | } 19 | 20 | with($event->sandbox->make(Hub::class), function ($hub) use ($event) { 21 | $hub->setContainer($event->sandbox); 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/CurrentApplication.php: -------------------------------------------------------------------------------- 1 | instance('app', $app); 17 | $app->instance(Container::class, $app); 18 | 19 | Container::setInstance($app); 20 | 21 | Facade::clearResolvedInstances(); 22 | Facade::setFacadeApplication($app); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Listeners/GiveNewApplicationInstanceToViewFactory.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('view')) { 15 | return; 16 | } 17 | 18 | with($event->sandbox->make('view'), function ($view) use ($event) { 19 | $view->setContainer($event->sandbox); 20 | 21 | $view->share('app', $event->sandbox); 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Listeners/GiveNewApplicationInstanceToMailManager.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('mail.manager')) { 15 | return; 16 | } 17 | 18 | with($event->sandbox->make('mail.manager'), function ($manager) use ($event) { 19 | $manager->setApplication($event->sandbox); 20 | $manager->forgetMailers(); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Listeners/PrepareInertiaForNextOperation.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved(ResponseFactory::class)) { 17 | return; 18 | } 19 | 20 | $factory = $event->sandbox->make(ResponseFactory::class); 21 | 22 | if (method_exists($factory, 'flushShared')) { 23 | $factory->flushShared(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Listeners/PrepareLivewireForNextOperation.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved(LivewireManager::class)) { 17 | return; 18 | } 19 | 20 | $manager = $event->sandbox->make(LivewireManager::class); 21 | 22 | if (method_exists($manager, 'flushState')) { 23 | $manager->flushState(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Listeners/FlushTranslatorCache.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('translator')) { 18 | return; 19 | } 20 | 21 | $translator = $event->sandbox->make('translator'); 22 | 23 | if ($translator instanceof NamespacedItemResolver) { 24 | $translator->flushParsedKeys(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Listeners/GiveNewApplicationInstanceToDatabaseManager.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('db') || 15 | ! method_exists($event->sandbox->make('db'), 'setApplication')) { 16 | return; 17 | } 18 | 19 | with($event->sandbox->make('db'), function ($manager) use ($event) { 20 | $manager->setApplication($event->sandbox); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /bin/createSwooleServer.php: -------------------------------------------------------------------------------- 1 | set(array_merge( 21 | $serverState['defaultServerOptions'], 22 | $config['swoole']['options'] ?? [] 23 | )); 24 | 25 | return $server; 26 | -------------------------------------------------------------------------------- /src/Listeners/FlushMonologState.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('log')) { 17 | return; 18 | } 19 | 20 | collect($event->sandbox->make('log')->getChannels()) 21 | ->map->getLogger() 22 | ->filter(function ($logger) { 23 | return $logger instanceof ResettableInterface; 24 | })->each->reset(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Listeners/GiveNewApplicationInstanceToCacheManager.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('cache')) { 15 | return; 16 | } 17 | 18 | with($event->sandbox->make('cache'), function ($manager) use ($event) { 19 | if (method_exists($manager, 'setApplication')) { 20 | $manager->setApplication($event->sandbox); 21 | } 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Listeners/PrepareScoutForNextOperation.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved(EngineManager::class)) { 17 | return; 18 | } 19 | 20 | $factory = $event->sandbox->make(EngineManager::class); 21 | 22 | if (! method_exists($factory, 'forgetEngines')) { 23 | return; 24 | } 25 | 26 | $factory->forgetEngines(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Listeners/FlushLogContext.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('log')) { 15 | return; 16 | } 17 | 18 | if (method_exists($event->sandbox['log'], 'flushSharedContext')) { 19 | $event->sandbox['log']->flushSharedContext(); 20 | } 21 | 22 | if (method_exists($event->sandbox['log']->driver(), 'withoutContext')) { 23 | $event->sandbox['log']->withoutContext(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Swoole/Handlers/OnManagerStart.php: -------------------------------------------------------------------------------- 1 | shouldSetProcessName) { 24 | $this->extension->setProcessName($this->appName, 'manager process'); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Listeners/FlushUploadedFiles.php: -------------------------------------------------------------------------------- 1 | request->files->all() as $file) { 17 | if (! $file instanceof SplFileInfo || 18 | ! is_string($path = $file->getRealPath())) { 19 | continue; 20 | } 21 | 22 | clearstatcache(true, $path); 23 | 24 | if (is_file($path)) { 25 | unlink($path); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Listeners/FlushAuthenticationState.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('auth.driver')) { 15 | $event->sandbox->forgetInstance('auth.driver'); 16 | } 17 | 18 | if ($event->sandbox->resolved('auth')) { 19 | with($event->sandbox->make('auth'), function ($auth) use ($event) { 20 | $auth->setApplication($event->sandbox); 21 | $auth->forgetGuards(); 22 | }); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Cache/OctaneArrayStore.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved(ChannelManager::class)) { 17 | return; 18 | } 19 | 20 | with($event->sandbox->make(ChannelManager::class), function ($manager) use ($event) { 21 | $manager->setContainer($event->sandbox); 22 | $manager->forgetDrivers(); 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Listeners/PrepareSocialiteForNextOperation.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved(Factory::class)) { 17 | return; 18 | } 19 | 20 | $factory = $event->sandbox->make(Factory::class); 21 | 22 | if (! method_exists($factory, 'forgetDrivers')) { 23 | return; 24 | } 25 | 26 | $factory->forgetDrivers(); 27 | $factory->setContainer($event->sandbox); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Listeners/FlushTemporaryContainerInstances.php: -------------------------------------------------------------------------------- 1 | app, 'resetScope')) { 15 | $event->app->resetScope(); 16 | } 17 | 18 | if (method_exists($event->app, 'forgetScopedInstances')) { 19 | $event->app->forgetScopedInstances(); 20 | } 21 | 22 | foreach ($event->sandbox->make('config')->get('octane.flush', []) as $binding) { 23 | $event->app->forgetInstance($binding); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Contracts/DispatchesTasks.php: -------------------------------------------------------------------------------- 1 | $columns) { 11 | $table = TableFactory::make(explode(':', $name)[1] ?? 1000); 12 | 13 | foreach ($columns ?? [] as $columnName => $column) { 14 | $table->column($columnName, match (explode(':', $column)[0] ?? 'string') { 15 | 'string' => Table::TYPE_STRING, 16 | 'int' => Table::TYPE_INT, 17 | 'float' => Table::TYPE_FLOAT, 18 | }, explode(':', $column)[1] ?? 1000); 19 | } 20 | 21 | $table->create(); 22 | 23 | $tables[explode(':', $name)[0]] = $table; 24 | } 25 | 26 | return $tables; 27 | -------------------------------------------------------------------------------- /src/Contracts/Client.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('session')) { 17 | return; 18 | } 19 | 20 | $handler = $event->sandbox->make('session')->driver()->getHandler(); 21 | 22 | if (! $handler instanceof DatabaseSessionHandler || 23 | ! method_exists($handler, 'setContainer')) { 24 | return; 25 | } 26 | 27 | $handler->setContainer($event->sandbox); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Commands/Concerns/InteractsWithTerminal.php: -------------------------------------------------------------------------------- 1 | terminalWidth == null) { 24 | $this->terminalWidth = (new Terminal)->getWidth(); 25 | 26 | $this->terminalWidth = $this->terminalWidth >= 30 27 | ? $this->terminalWidth 28 | : 30; 29 | } 30 | 31 | return $this->terminalWidth; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Listeners/GiveNewApplicationInstanceToBroadcastManager.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved(BroadcastManager::class)) { 17 | return; 18 | } 19 | 20 | with($event->sandbox->make(BroadcastManager::class), function ($manager) use ($event) { 21 | $manager->setApplication($event->sandbox); 22 | 23 | // Forgetting drivers will flush all channel routes which is unwanted... 24 | // $manager->forgetDrivers(); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/WorkerExceptionInspector.php: -------------------------------------------------------------------------------- 1 | class; 23 | } 24 | 25 | /** 26 | * Get the worker exception trace. 27 | * 28 | * @param \Throwable $throwable 29 | * @return array 30 | */ 31 | public function getTrace($throwable) 32 | { 33 | return $this->trace; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/RoadRunner/Concerns/FindsRoadRunnerBinary.php: -------------------------------------------------------------------------------- 1 | find('rr', null, [base_path()]))) { 22 | if (! Str::contains($roadRunnerBinary, 'vendor/bin/rr')) { 23 | return $roadRunnerBinary; 24 | } 25 | } 26 | 27 | return null; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Listeners/RefreshQueryDurationHandling.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('db')) { 15 | return; 16 | } 17 | 18 | foreach ($event->sandbox->make('db')->getConnections() as $connection) { 19 | if ( 20 | method_exists($connection, 'resetTotalQueryDuration') 21 | && method_exists($connection, 'allowQueryDurationHandlersToRunAgain') 22 | ) { 23 | $connection->resetTotalQueryDuration(); 24 | $connection->allowQueryDurationHandlersToRunAgain(); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Testing/Fakes/FakeWorker.php: -------------------------------------------------------------------------------- 1 | client->requests as $request) { 13 | [$request, $context] = $this->client->marshalRequest( 14 | new RequestContext(['request' => $request]) 15 | ); 16 | 17 | $this->handle($request, $context); 18 | } 19 | } 20 | 21 | public function runTasks() 22 | { 23 | return collect($this->client->requests)->map(fn ($data) => $this->handleTask($data))->all(); 24 | } 25 | 26 | public function runTicks() 27 | { 28 | return collect($this->client->requests)->map(fn () => $this->handleTick())->all(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Facades/Octane.php: -------------------------------------------------------------------------------- 1 | columns[$name] = [$type, $size]; 24 | 25 | return parent::column($name, $type, $size); 26 | } 27 | 28 | /** 29 | * Update a row of the table. 30 | */ 31 | public function set(string $key, array $values): bool 32 | { 33 | collect($values) 34 | ->each($this->ensureColumnsSize()); 35 | 36 | return parent::set($key, $values); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Listeners/ReportException.php: -------------------------------------------------------------------------------- 1 | exception) { 19 | tap($event->sandbox, function ($sandbox) use ($event) { 20 | if ($event->exception instanceof DdException) { 21 | return; 22 | } 23 | 24 | if ($sandbox->environment('local', 'testing')) { 25 | Stream::throwable($event->exception); 26 | } 27 | 28 | $sandbox[ExceptionHandler::class]->report($event->exception); 29 | }); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Listeners/FlushLocaleState.php: -------------------------------------------------------------------------------- 1 | sandbox->make('config'); 17 | 18 | tap($event->sandbox->make('translator'), function ($translator) use ($config) { 19 | $translator->setLocale($config->get('app.locale')); 20 | $translator->setFallback($config->get('app.fallback_locale')); 21 | }); 22 | 23 | $provider = tap(new CarbonServiceProvider($event->app))->updateLocale(); 24 | 25 | collect($event->sandbox->getProviders($provider)) 26 | ->values() 27 | ->whenNotEmpty(fn ($providers) => $providers->first()->setAppGetter(fn () => $event->sandbox)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Tables/TableFactory.php: -------------------------------------------------------------------------------- 1 | message = json_encode($vars); 15 | } 16 | 17 | /** 18 | * Get the evaluated contents of the object. 19 | * 20 | * @return string 21 | */ 22 | public function render() 23 | { 24 | $dump = function ($var) { 25 | $data = (new VarCloner())->cloneVar($var)->withMaxDepth(3); 26 | 27 | return (string) (new HtmlDumper(false))->dump($data, true, [ 28 | 'maxDepth' => 3, 29 | 'maxStringLength' => 160, 30 | ]); 31 | }; 32 | 33 | return collect($this->vars)->map($dump)->implode(''); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Exceptions/TaskException.php: -------------------------------------------------------------------------------- 1 | class = $class; 31 | $this->file = $file; 32 | $this->line = $line; 33 | } 34 | 35 | /** 36 | * Returns the original throwable class name. 37 | * 38 | * @return string 39 | */ 40 | public function getClass() 41 | { 42 | return $this->class; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Tables/Concerns/EnsuresColumnSizes.php: -------------------------------------------------------------------------------- 1 | columns, $column)) { 20 | return; 21 | } 22 | 23 | [$type, $size] = $this->columns[$column]; 24 | 25 | if ($type == Table::TYPE_STRING && strlen($value) > $size) { 26 | throw new ValueTooLargeForColumnException(sprintf( 27 | 'Value [%s...] is too large for [%s] column.', 28 | substr($value, 0, 20), 29 | $column, 30 | )); 31 | } 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/RequestContext.php: -------------------------------------------------------------------------------- 1 | data[$offset]); 17 | } 18 | 19 | #[\ReturnTypeWillChange] 20 | public function offsetGet($offset) 21 | { 22 | return $this->data[$offset]; 23 | } 24 | 25 | #[\ReturnTypeWillChange] 26 | public function offsetSet($offset, $value) 27 | { 28 | $this->data[$offset] = $value; 29 | } 30 | 31 | #[\ReturnTypeWillChange] 32 | public function offsetUnset($offset) 33 | { 34 | unset($this->data[$offset]); 35 | } 36 | 37 | public function __get($key) 38 | { 39 | return $this->data[$key]; 40 | } 41 | 42 | public function __set($key, $value) 43 | { 44 | $this->data[$key] = $value; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Concerns/RegistersTickHandlers.php: -------------------------------------------------------------------------------- 1 | listen( 30 | TickReceived::class, 31 | $listener 32 | ); 33 | 34 | return $listener; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Commands/Concerns/InteractsWithEnvironmentVariables.php: -------------------------------------------------------------------------------- 1 | addPath(app()->environmentPath()) 24 | ->addName(app()->environmentFile()) 25 | ->make() 26 | ->read(); 27 | 28 | foreach ((new Parser())->parse($content) as $entry) { 29 | $variables->push($entry->getName()); 30 | } 31 | } catch (InvalidPathException $e) { 32 | // .. 33 | } 34 | 35 | $variables->each(fn ($name) => Env::getRepository()->clear($name)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Taylor Otwell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Testing/Fakes/FakeClient.php: -------------------------------------------------------------------------------- 1 | request, $context]; 27 | } 28 | 29 | public function respond(RequestContext $context, OctaneResponse $octaneResponse): void 30 | { 31 | $this->responses[] = $octaneResponse->response; 32 | } 33 | 34 | public function error(Throwable $e, Application $app, Request $request, RequestContext $context): void 35 | { 36 | $message = $app->make('config')->get('app.debug') ? (string) $e : 'Internal server error.'; 37 | 38 | $this->errors[] = $message; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Octane.php: -------------------------------------------------------------------------------- 1 | bound(Server::class)) { 24 | throw new Exception('Tables may only be accessed when using the Swoole server.'); 25 | } 26 | 27 | $tables = app(WorkerState::class)->tables; 28 | 29 | if (! isset($tables[$table])) { 30 | throw new Exception("Swoole table [{$table}] has not been configured."); 31 | } 32 | 33 | return $tables[$table]; 34 | } 35 | 36 | /** 37 | * Format an exception to a string that should be returned to the client. 38 | */ 39 | public static function formatExceptionForClient(Throwable $e, bool $debug = false): string 40 | { 41 | return $debug ? (string) $e : 'Internal server error.'; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Concerns/ProvidesRouting.php: -------------------------------------------------------------------------------- 1 | routes[$method.$uri] = $callback; 24 | } 25 | 26 | /** 27 | * Determine if a route exists for the given method and URI. 28 | */ 29 | public function hasRouteFor(string $method, string $uri): bool 30 | { 31 | return isset($this->routes[$method.$uri]); 32 | } 33 | 34 | /** 35 | * Invoke the route for the given method and URI. 36 | */ 37 | public function invokeRoute(Request $request, string $method, string $uri): Response 38 | { 39 | return call_user_func($this->routes[$method.$uri], $request); 40 | } 41 | 42 | /** 43 | * Get the registered Octane routes. 44 | */ 45 | public function getRoutes(): array 46 | { 47 | return $this->routes; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Swoole/Actions/EnsureRequestsDontExceedMaxExecutionTime.php: -------------------------------------------------------------------------------- 1 | timerTable as $workerId => $row) { 27 | if ((time() - $row['time']) > $this->maxExecutionTime) { 28 | $this->timerTable->del($workerId); 29 | 30 | $this->extension->dispatchProcessSignal($row['worker_pid'], SIGKILL); 31 | 32 | if ($this->server instanceof Server) { 33 | $response = Response::create($this->server, $row['fd']); 34 | 35 | if ($response) { 36 | $response->status(408); 37 | $response->end(); 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Swoole/SignalDispatcher.php: -------------------------------------------------------------------------------- 1 | signal($processId, 0); 17 | } 18 | 19 | /** 20 | * Send a SIGTERM signal to the given process. 21 | */ 22 | public function terminate(int $processId, int $wait = 0): bool 23 | { 24 | $this->extension->dispatchProcessSignal($processId, SIGTERM); 25 | 26 | if ($wait) { 27 | $start = time(); 28 | 29 | do { 30 | if (! $this->canCommunicateWith($processId)) { 31 | return true; 32 | } 33 | 34 | $this->extension->dispatchProcessSignal($processId, SIGTERM); 35 | 36 | sleep(1); 37 | } while (time() < $start + $wait); 38 | } 39 | 40 | return false; 41 | } 42 | 43 | /** 44 | * Send a signal to the given process. 45 | */ 46 | public function signal(int $processId, int $signal): bool 47 | { 48 | return $this->extension->dispatchProcessSignal($processId, $signal); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Swoole/SwooleCoroutineDispatcher.php: -------------------------------------------------------------------------------- 1 | $callback) { 26 | Coroutine::create(function () use ($key, $callback, $waitGroup, &$results) { 27 | $waitGroup->add(); 28 | 29 | $results[$key] = $callback(); 30 | 31 | $waitGroup->done(); 32 | }); 33 | } 34 | 35 | $waitGroup->wait($waitSeconds); 36 | }; 37 | 38 | if (! $this->withinCoroutineContext) { 39 | Coroutine\run($callback); 40 | } else { 41 | $callback(); 42 | } 43 | 44 | return $results; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Swoole/SwooleExtension.php: -------------------------------------------------------------------------------- 1 | mapWithKeys( 23 | fn ($task, $key) => [$key => (function () use ($task) { 24 | try { 25 | return $task(); 26 | } catch (Throwable $e) { 27 | report($e); 28 | 29 | return TaskExceptionResult::from($e); 30 | } 31 | })()] 32 | )->each(function ($result) { 33 | if ($result instanceof TaskExceptionResult) { 34 | throw $result->getOriginal(); 35 | } 36 | })->all(); 37 | } 38 | 39 | /** 40 | * Concurrently dispatch the given callbacks via background tasks. 41 | */ 42 | public function dispatch(array $tasks): void 43 | { 44 | try { 45 | $this->resolve($tasks); 46 | } catch (Throwable) { 47 | // .. 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Swoole/Handlers/OnServerStart.php: -------------------------------------------------------------------------------- 1 | serverStateFile->writeProcessIds( 32 | $server->master_pid, 33 | $server->manager_pid 34 | ); 35 | 36 | if ($this->shouldSetProcessName) { 37 | $this->extension->setProcessName($this->appName, 'master process'); 38 | } 39 | 40 | if ($this->shouldTick) { 41 | Timer::tick(1000, function () use ($server) { 42 | $server->task('octane-tick'); 43 | }); 44 | } 45 | 46 | if ($this->maxExecutionTime > 0) { 47 | Timer::tick(1000, function () use ($server) { 48 | (new EnsureRequestsDontExceedMaxExecutionTime( 49 | $this->extension, $this->timerTable, $this->maxExecutionTime, $server 50 | ))(); 51 | }); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Concerns/ProvidesConcurrencySupport.php: -------------------------------------------------------------------------------- 1 | tasks()->resolve($tasks, $waitMilliseconds); 27 | } 28 | 29 | /** 30 | * Get the task dispatcher. 31 | * 32 | * @return \Laravel\Octane\Contracts\DispatchesTasks 33 | */ 34 | public function tasks() 35 | { 36 | return match (true) { 37 | app()->bound(DispatchesTasks::class) => app(DispatchesTasks::class), 38 | app()->bound(Server::class) => new SwooleTaskDispatcher, 39 | class_exists(Server::class) => (fn (array $serverState) => new SwooleHttpTaskDispatcher( 40 | $serverState['state']['host'] ?? '127.0.0.1', 41 | $serverState['state']['port'] ?? '8000', 42 | new SequentialTaskDispatcher 43 | ))(app(ServerStateFile::class)->read()), 44 | default => new SequentialTaskDispatcher, 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /bin/bootstrap.php: -------------------------------------------------------------------------------- 1 | cache->get('tick-'.$this->key); 29 | 30 | if (! is_null($lastInvokedAt) && 31 | (Carbon::now()->getTimestamp() - $lastInvokedAt) < $this->seconds) { 32 | return; 33 | } 34 | 35 | $this->cache->forever('tick-'.$this->key, Carbon::now()->getTimestamp()); 36 | 37 | if (is_null($lastInvokedAt) && ! $this->immediate) { 38 | return; 39 | } 40 | 41 | try { 42 | call_user_func($this->callback); 43 | } catch (Throwable $e) { 44 | $this->exceptionHandler->report($e); 45 | } 46 | } 47 | 48 | /** 49 | * Indicate how often the listener should be invoked. 50 | * 51 | * @return $this 52 | */ 53 | public function seconds(int $seconds) 54 | { 55 | $this->seconds = $seconds; 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * Indicate that the listener should be invoked on the first tick after the server starts. 62 | * 63 | * @return $this 64 | */ 65 | public function immediate() 66 | { 67 | $this->immediate = true; 68 | 69 | return $this; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Exceptions/TaskExceptionResult.php: -------------------------------------------------------------------------------- 1 | getFile(), ClosureStream::STREAM_PROTO.'://') 27 | ? collect($throwable->getTrace())->whereNotNull('file')->first() 28 | : null; 29 | 30 | return new static( 31 | $throwable::class, 32 | $throwable->getMessage(), 33 | (int) $throwable->getCode(), 34 | $fallbackTrace['file'] ?? $throwable->getFile(), 35 | $fallbackTrace['line'] ?? (int) $throwable->getLine(), 36 | ); 37 | } 38 | 39 | /** 40 | * Gets the original throwable. 41 | * 42 | * @return \Laravel\Octane\Exceptions\TaskException|\Laravel\Octane\Exceptions\DdException 43 | */ 44 | public function getOriginal() 45 | { 46 | if ($this->class == DdException::class) { 47 | return new DdException( 48 | json_decode($this->message, true) 49 | ); 50 | } 51 | 52 | return new TaskException( 53 | $this->class, 54 | $this->message, 55 | (int) $this->code, 56 | $this->file, 57 | (int) $this->line, 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Logo Laravel Octane

2 | 3 |

4 | Build Status 5 | Total Downloads 6 | Latest Stable Version 7 | License 8 |

9 | 10 | ## Introduction 11 | 12 | Laravel Octane supercharges your application's performance by serving your application using high-powered application servers, including [Open Swoole](https://openswoole.com), [Swoole](https://github.com/swoole/swoole-src), and [RoadRunner](https://roadrunner.dev). Octane boots your application once, keeps it in memory, and then feeds it requests at supersonic speeds. 13 | 14 | ## Official Documentation 15 | 16 | Documentation for Octane can be found on the [Laravel website](https://laravel.com/docs/octane). 17 | 18 | ## Contributing 19 | 20 | Thank you for considering contributing to Octane! You can read the contribution guide [here](.github/CONTRIBUTING.md). 21 | 22 | ## Code of Conduct 23 | 24 | In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). 25 | 26 | ## Security Vulnerabilities 27 | 28 | Please review [our security policy](https://github.com/laravel/octane/security/policy) on how to report security vulnerabilities. 29 | 30 | ## License 31 | 32 | Laravel Octane is open-sourced software licensed under the [MIT license](LICENSE.md). 33 | -------------------------------------------------------------------------------- /src/ApplicationGateway.php: -------------------------------------------------------------------------------- 1 | dispatchEvent($this->sandbox, new RequestReceived($this->app, $this->sandbox, $request)); 29 | 30 | if (Octane::hasRouteFor($request->getMethod(), '/'.$request->path())) { 31 | return Octane::invokeRoute($request, $request->getMethod(), '/'.$request->path()); 32 | } 33 | 34 | return tap($this->sandbox->make(Kernel::class)->handle($request), function ($response) use ($request) { 35 | $this->dispatchEvent($this->sandbox, new RequestHandled($this->sandbox, $request, $response)); 36 | }); 37 | } 38 | 39 | /** 40 | * "Shut down" the application after a request. 41 | */ 42 | public function terminate(Request $request, Response $response): void 43 | { 44 | $this->sandbox->make(Kernel::class)->terminate($request, $response); 45 | 46 | $this->dispatchEvent($this->sandbox, new RequestTerminated($this->app, $this->sandbox, $request, $response)); 47 | 48 | $route = $request->route(); 49 | 50 | if ($route instanceof Route && method_exists($route, 'flushController')) { 51 | $route->flushController(); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Swoole/ServerProcessInspector.php: -------------------------------------------------------------------------------- 1 | $masterProcessId, 24 | 'managerProcessId' => $managerProcessId 25 | ] = $this->serverStateFile->read(); 26 | 27 | return $managerProcessId 28 | ? $masterProcessId && $managerProcessId && $this->dispatcher->canCommunicateWith((int) $managerProcessId) 29 | : $masterProcessId && $this->dispatcher->canCommunicateWith((int) $masterProcessId); 30 | } 31 | 32 | /** 33 | * Reload the Swoole workers. 34 | */ 35 | public function reloadServer(): void 36 | { 37 | [ 38 | 'masterProcessId' => $masterProcessId, 39 | ] = $this->serverStateFile->read(); 40 | 41 | $this->dispatcher->signal((int) $masterProcessId, SIGUSR1); 42 | } 43 | 44 | /** 45 | * Stop the Swoole server. 46 | */ 47 | public function stopServer(): bool 48 | { 49 | [ 50 | 'masterProcessId' => $masterProcessId, 51 | 'managerProcessId' => $managerProcessId 52 | ] = $this->serverStateFile->read(); 53 | 54 | $workerProcessIds = $this->exec->run('pgrep -P '.$managerProcessId); 55 | 56 | foreach ([$masterProcessId, $managerProcessId, ...$workerProcessIds] as $processId) { 57 | $this->dispatcher->signal((int) $processId, SIGKILL); 58 | } 59 | 60 | return true; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /bin/roadrunner-worker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | waitRequest()) { 40 | try { 41 | $worker = $worker ?: tap((new Worker( 42 | new ApplicationFactory($basePath), $roadRunnerClient 43 | )))->boot(); 44 | } catch (Throwable $e) { 45 | Stream::shutdown($e); 46 | 47 | exit(1); 48 | } 49 | 50 | if (! $psr7Request instanceof ServerRequestInterface) { 51 | break; 52 | } 53 | 54 | [$request, $context] = $roadRunnerClient->marshalRequest(new RequestContext([ 55 | 'psr7Request' => $psr7Request 56 | ])); 57 | 58 | $worker->handle($request, $context); 59 | } 60 | 61 | if (! is_null($worker)) { 62 | $worker->terminate(); 63 | } 64 | -------------------------------------------------------------------------------- /src/Tables/SwooleTable.php: -------------------------------------------------------------------------------- 1 | = 50000) { 8 | class SwooleTable extends Table 9 | { 10 | use Concerns\EnsuresColumnSizes; 11 | 12 | /** 13 | * The table columns. 14 | * 15 | * @var array 16 | */ 17 | protected $columns; 18 | 19 | /** 20 | * Set the data type and size of the columns. 21 | */ 22 | public function column(string $name, int $type, int $size = 0): bool 23 | { 24 | $this->columns[$name] = [$type, $size]; 25 | 26 | return parent::column($name, $type, $size); 27 | } 28 | 29 | /** 30 | * Update a row of the table. 31 | */ 32 | public function set(string $key, array $values): bool 33 | { 34 | collect($values) 35 | ->each($this->ensureColumnsSize()); 36 | 37 | return parent::set($key, $values); 38 | } 39 | } 40 | } else { 41 | class SwooleTable extends Table 42 | { 43 | use Concerns\EnsuresColumnSizes; 44 | 45 | /** 46 | * The table columns. 47 | * 48 | * @var array 49 | */ 50 | protected $columns; 51 | 52 | /** 53 | * Set the data type and size of the columns. 54 | * 55 | * @param string $name 56 | * @param int $type 57 | * @param int $size 58 | * @return void 59 | */ 60 | public function column($name, $type, $size = 0) 61 | { 62 | $this->columns[$name] = [$type, $size]; 63 | 64 | parent::column($name, $type, $size); 65 | } 66 | 67 | /** 68 | * Update a row of the table. 69 | * 70 | * @param string $key 71 | * @return void 72 | */ 73 | public function set($key, array $values) 74 | { 75 | collect($values) 76 | ->each($this->ensureColumnsSize()); 77 | 78 | parent::set($key, $values); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Stream.php: -------------------------------------------------------------------------------- 1 | 'request', 19 | 'method' => $method, 20 | 'url' => $url, 21 | 'memory' => memory_get_usage(), 22 | 'statusCode' => $statusCode, 23 | 'duration' => $duration, 24 | ])."\n"); 25 | } 26 | 27 | /** 28 | * Stream the given throwable to stderr. 29 | * 30 | * @return void 31 | */ 32 | public static function throwable(Throwable $throwable) 33 | { 34 | $fallbackTrace = str_starts_with($throwable->getFile(), ClosureStream::STREAM_PROTO.'://') 35 | ? collect($throwable->getTrace())->whereNotNull('file')->first() 36 | : null; 37 | 38 | fwrite(STDERR, json_encode([ 39 | 'type' => 'throwable', 40 | 'class' => $throwable::class, 41 | 'code' => $throwable->getCode(), 42 | 'file' => $fallbackTrace['file'] ?? $throwable->getFile(), 43 | 'line' => $fallbackTrace['line'] ?? (int) $throwable->getLine(), 44 | 'message' => $throwable->getMessage(), 45 | 'trace' => array_slice($throwable->getTrace(), 0, 2), 46 | ])."\n"); 47 | } 48 | 49 | /** 50 | * Stream the given shutdown throwable to stderr. 51 | * 52 | * @return void 53 | */ 54 | public static function shutdown(Throwable $throwable) 55 | { 56 | fwrite(STDERR, json_encode([ 57 | 'type' => 'shutdown', 58 | 'class' => $throwable::class, 59 | 'code' => $throwable->getCode(), 60 | 'file' => $throwable->getFile(), 61 | 'line' => $throwable->getLine(), 62 | 'message' => $throwable->getMessage(), 63 | 'trace' => array_slice($throwable->getTrace(), 0, 2), 64 | ])."\n"); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/RoadRunner/ServerStateFile.php: -------------------------------------------------------------------------------- 1 | path) 19 | ? json_decode(file_get_contents($this->path), true) 20 | : []; 21 | 22 | return [ 23 | 'masterProcessId' => $state['masterProcessId'] ?? null, 24 | 'state' => $state['state'] ?? [], 25 | ]; 26 | } 27 | 28 | /** 29 | * Write the given process ID to the server state file. 30 | */ 31 | public function writeProcessId(int $masterProcessId): void 32 | { 33 | if (! is_writable($this->path) && ! is_writable(dirname($this->path))) { 34 | throw new RuntimeException('Unable to write to process ID file.'); 35 | } 36 | 37 | file_put_contents($this->path, json_encode( 38 | array_merge($this->read(), ['masterProcessId' => $masterProcessId]), 39 | JSON_PRETTY_PRINT 40 | )); 41 | } 42 | 43 | /** 44 | * Write the given state array to the server state file. 45 | */ 46 | public function writeState(array $newState): void 47 | { 48 | if (! is_writable($this->path) && ! is_writable(dirname($this->path))) { 49 | throw new RuntimeException('Unable to write to process ID file.'); 50 | } 51 | 52 | file_put_contents($this->path, json_encode( 53 | array_merge($this->read(), ['state' => $newState]), 54 | JSON_PRETTY_PRINT 55 | )); 56 | } 57 | 58 | /** 59 | * Delete the process ID file. 60 | */ 61 | public function delete(): bool 62 | { 63 | if (is_writable($this->path)) { 64 | return unlink($this->path); 65 | } 66 | 67 | return false; 68 | } 69 | 70 | /** 71 | * Get the path to the process ID file. 72 | */ 73 | public function path(): string 74 | { 75 | return $this->path; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/RoadRunner/ServerProcessInspector.php: -------------------------------------------------------------------------------- 1 | $masterProcessId, 30 | ] = $this->serverStateFile->read(); 31 | 32 | return $masterProcessId && $this->posix->kill($masterProcessId, 0); 33 | } 34 | 35 | /** 36 | * Reload the RoadRunner workers. 37 | */ 38 | public function reloadServer(): void 39 | { 40 | [ 41 | 'state' => [ 42 | 'host' => $host, 43 | 'rpcPort' => $rpcPort, 44 | ], 45 | ] = $this->serverStateFile->read(); 46 | 47 | tap($this->processFactory->createProcess([ 48 | $this->findRoadRunnerBinary(), 49 | 'reset', 50 | '-o', "rpc.listen=tcp://$host:$rpcPort", 51 | '-s', 52 | ], base_path()))->start()->waitUntil(function ($type, $buffer) { 53 | if ($type === Process::ERR) { 54 | throw new RuntimeException('Cannot reload RoadRunner: '.$buffer); 55 | } 56 | 57 | return true; 58 | }); 59 | } 60 | 61 | /** 62 | * Stop the RoadRunner server. 63 | */ 64 | public function stopServer(): bool 65 | { 66 | [ 67 | 'masterProcessId' => $masterProcessId, 68 | ] = $this->serverStateFile->read(); 69 | 70 | return (bool) $this->posix->kill($masterProcessId, SIGTERM); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Commands/StatusCommand.php: -------------------------------------------------------------------------------- 1 | option('server') ?: config('octane.server'); 32 | 33 | $isRunning = match ($server) { 34 | 'swoole' => $this->isSwooleServerRunning(), 35 | 'roadrunner' => $this->isRoadRunnerServerRunning(), 36 | default => $this->invalidServer($server), 37 | }; 38 | 39 | return ! tap($isRunning, function ($isRunning) { 40 | $isRunning 41 | ? $this->info('Octane server is running.') 42 | : $this->info('Octane server is not running.'); 43 | }); 44 | } 45 | 46 | /** 47 | * Check if the Swoole server is running. 48 | * 49 | * @return bool 50 | */ 51 | protected function isSwooleServerRunning() 52 | { 53 | return app(SwooleServerProcessInspector::class) 54 | ->serverIsRunning(); 55 | } 56 | 57 | /** 58 | * Check if the RoadRunner server is running. 59 | * 60 | * @return bool 61 | */ 62 | protected function isRoadRunnerServerRunning() 63 | { 64 | return app(RoadRunnerServerProcessInspector::class) 65 | ->serverIsRunning(); 66 | } 67 | 68 | /** 69 | * Inform the user that the server type is invalid. 70 | * 71 | * @return bool 72 | */ 73 | protected function invalidServer(string $server) 74 | { 75 | $this->error("Invalid server: {$server}."); 76 | 77 | return false; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/RoadRunner/RoadRunnerClient.php: -------------------------------------------------------------------------------- 1 | toHttpFoundationRequest($context->psr7Request), 33 | $context, 34 | ]; 35 | } 36 | 37 | /** 38 | * Send the response to the server. 39 | */ 40 | public function respond(RequestContext $context, OctaneResponse $octaneResponse): void 41 | { 42 | if ($octaneResponse->outputBuffer && 43 | ! $octaneResponse->response instanceof StreamedResponse && 44 | ! $octaneResponse->response instanceof BinaryFileResponse) { 45 | $octaneResponse->response->setContent( 46 | $octaneResponse->outputBuffer.$octaneResponse->response->getContent() 47 | ); 48 | } 49 | 50 | $this->client->respond($this->toPsr7Response($octaneResponse->response)); 51 | } 52 | 53 | /** 54 | * Send an error message to the server. 55 | */ 56 | public function error(Throwable $e, Application $app, Request $request, RequestContext $context): void 57 | { 58 | $this->client->getWorker()->error(Octane::formatExceptionForClient( 59 | $e, 60 | $app->make('config')->get('app.debug') 61 | )); 62 | } 63 | 64 | /** 65 | * Stop the underlying server / worker. 66 | */ 67 | public function stop(): void 68 | { 69 | $this->client->getWorker()->stop(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Swoole/ServerStateFile.php: -------------------------------------------------------------------------------- 1 | path) 19 | ? json_decode(file_get_contents($this->path), true) 20 | : []; 21 | 22 | return [ 23 | 'masterProcessId' => $state['masterProcessId'] ?? null, 24 | 'managerProcessId' => $state['managerProcessId'] ?? null, 25 | 'state' => $state['state'] ?? [], 26 | ]; 27 | } 28 | 29 | /** 30 | * Write the given process IDs to the server state file. 31 | */ 32 | public function writeProcessIds(int $masterProcessId, int $managerProcessId): void 33 | { 34 | if (! is_writable($this->path) && ! is_writable(dirname($this->path))) { 35 | throw new RuntimeException('Unable to write to process ID file.'); 36 | } 37 | 38 | file_put_contents($this->path, json_encode( 39 | array_merge($this->read(), [ 40 | 'masterProcessId' => $masterProcessId, 41 | 'managerProcessId' => $managerProcessId, 42 | ]), 43 | JSON_PRETTY_PRINT 44 | )); 45 | } 46 | 47 | /** 48 | * Write the given state array to the server state file. 49 | */ 50 | public function writeState(array $newState): void 51 | { 52 | if (! is_writable($this->path) && ! is_writable(dirname($this->path))) { 53 | throw new RuntimeException('Unable to write to process ID file.'); 54 | } 55 | 56 | file_put_contents($this->path, json_encode( 57 | array_merge($this->read(), ['state' => $newState]), 58 | JSON_PRETTY_PRINT 59 | )); 60 | } 61 | 62 | /** 63 | * Delete the process ID file. 64 | */ 65 | public function delete(): bool 66 | { 67 | if (is_writable($this->path)) { 68 | return unlink($this->path); 69 | } 70 | 71 | return false; 72 | } 73 | 74 | /** 75 | * Get the path to the process ID file. 76 | */ 77 | public function path(): string 78 | { 79 | return $this->path; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Commands/ReloadCommand.php: -------------------------------------------------------------------------------- 1 | option('server') ?: config('octane.server'); 32 | 33 | return match ($server) { 34 | 'swoole' => $this->reloadSwooleServer(), 35 | 'roadrunner' => $this->reloadRoadRunnerServer(), 36 | default => $this->invalidServer($server), 37 | }; 38 | } 39 | 40 | /** 41 | * Reload the Swoole server for Octane. 42 | * 43 | * @return int 44 | */ 45 | protected function reloadSwooleServer() 46 | { 47 | $inspector = app(SwooleServerProcessInspector::class); 48 | 49 | if (! $inspector->serverIsRunning()) { 50 | $this->error('Octane server is not running.'); 51 | 52 | return 1; 53 | } 54 | 55 | $this->info('Reloading workers...'); 56 | 57 | $inspector->reloadServer(); 58 | 59 | return 0; 60 | } 61 | 62 | /** 63 | * Reload the RoadRunner server for Octane. 64 | * 65 | * @return int 66 | */ 67 | protected function reloadRoadRunnerServer() 68 | { 69 | $inspector = app(RoadRunnerServerProcessInspector::class); 70 | 71 | if (! $inspector->serverIsRunning()) { 72 | $this->error('Octane server is not running.'); 73 | 74 | return 1; 75 | } 76 | 77 | $this->info('Reloading workers...'); 78 | 79 | $inspector->reloadServer(); 80 | 81 | return 0; 82 | } 83 | 84 | /** 85 | * Inform the user that the server type is invalid. 86 | * 87 | * @return int 88 | */ 89 | protected function invalidServer(string $server) 90 | { 91 | $this->error("Invalid server: {$server}."); 92 | 93 | return 1; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/octane", 3 | "description": "Supercharge your Laravel application's performance.", 4 | "keywords": ["laravel", "octane", "roadrunner", "swoole"], 5 | "license": "MIT", 6 | "support": { 7 | "issues": "https://github.com/laravel/octane/issues", 8 | "source": "https://github.com/laravel/octane" 9 | }, 10 | "authors": [ 11 | { 12 | "name": "Taylor Otwell", 13 | "email": "taylor@laravel.com" 14 | } 15 | ], 16 | "require": { 17 | "php": "^8.1.0", 18 | "laravel/framework": "^10.10.1", 19 | "laminas/laminas-diactoros": "^3.0.0", 20 | "laravel/serializable-closure": "^1.3.0", 21 | "nesbot/carbon": "^2.66.0", 22 | "symfony/psr-http-message-bridge": "^2.2.0" 23 | }, 24 | "require-dev": { 25 | "guzzlehttp/guzzle": "^7.6.1", 26 | "inertiajs/inertia-laravel": "^0.6.9", 27 | "laravel/scout": "^10.2.1", 28 | "laravel/socialite": "^5.6.1", 29 | "livewire/livewire": "^2.12.3", 30 | "mockery/mockery": "^1.5.1", 31 | "nunomaduro/collision": "^6.4.0|^7.5.2", 32 | "orchestra/testbench": "^8.5.2", 33 | "phpstan/phpstan": "^1.10.15", 34 | "phpunit/phpunit": "^10.1.3", 35 | "spiral/roadrunner-http": "^3.0.1", 36 | "spiral/roadrunner-cli": "^2.5.0" 37 | }, 38 | "bin": [ 39 | "bin/roadrunner-worker", 40 | "bin/swoole-server" 41 | ], 42 | "conflict": { 43 | "spiral/roadrunner": "<2023.1.0", 44 | "spiral/roadrunner-cli": "<2.5.0", 45 | "spiral/roadrunner-http": "<3.0.1" 46 | }, 47 | "autoload": { 48 | "psr-4": { 49 | "Laravel\\Octane\\": "src" 50 | } 51 | }, 52 | "autoload-dev": { 53 | "psr-4": { 54 | "Laravel\\Octane\\Tests\\": "tests" 55 | } 56 | }, 57 | "scripts": { 58 | "post-autoload-dump": [ 59 | "@php vendor/bin/testbench package:discover --ansi" 60 | ] 61 | }, 62 | "extra": { 63 | "branch-alias": { 64 | "dev-master": "2.x-dev" 65 | }, 66 | "laravel": { 67 | "providers": [ 68 | "Laravel\\Octane\\OctaneServiceProvider" 69 | ], 70 | "aliases": { 71 | "Octane": "Laravel\\Octane\\Facades\\Octane" 72 | } 73 | } 74 | }, 75 | "config": { 76 | "sort-packages": true 77 | }, 78 | "minimum-stability": "stable", 79 | "prefer-stable": true 80 | } 81 | -------------------------------------------------------------------------------- /src/Swoole/SwooleTaskDispatcher.php: -------------------------------------------------------------------------------- 1 | bound(Server::class)) { 27 | throw new InvalidArgumentException('Tasks can only be resolved within a Swoole server context / web request.'); 28 | } 29 | 30 | $results = app(Server::class)->taskWaitMulti(collect($tasks)->mapWithKeys(function ($task, $key) { 31 | return [$key => $task instanceof Closure 32 | ? new SerializableClosure($task) 33 | : $task, ]; 34 | })->all(), $waitMilliseconds / 1000); 35 | 36 | if ($results === false) { 37 | throw TaskTimeoutException::after($waitMilliseconds); 38 | } 39 | 40 | $i = 0; 41 | 42 | foreach ($tasks as $key => $task) { 43 | if (isset($results[$i])) { 44 | if ($results[$i] instanceof TaskExceptionResult) { 45 | throw $results[$i]->getOriginal(); 46 | } 47 | 48 | $tasks[$key] = $results[$i]->result; 49 | } else { 50 | $tasks[$key] = false; 51 | } 52 | 53 | $i++; 54 | } 55 | 56 | return $tasks; 57 | } 58 | 59 | /** 60 | * Concurrently dispatch the given callbacks via background tasks. 61 | */ 62 | public function dispatch(array $tasks): void 63 | { 64 | if (! app()->bound(Server::class)) { 65 | throw new InvalidArgumentException('Tasks can only be dispatched within a Swoole server context / web request.'); 66 | } 67 | 68 | $server = app(Server::class); 69 | 70 | collect($tasks)->each(function ($task) use ($server) { 71 | $server->task($task instanceof Closure ? new SerializableClosure($task) : $task); 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/MarshalsPsr7RequestsAndResponses.php: -------------------------------------------------------------------------------- 1 | httpFoundationRequestFactory()->createRequest($request)); 40 | } 41 | 42 | /** 43 | * Convert the given HttpFoundation response into a PSR-7 response. 44 | */ 45 | protected function toPsr7Response(Response $response): ResponseInterface 46 | { 47 | return $this->psr7ResponseFactory()->createResponse($response); 48 | } 49 | 50 | /** 51 | * Create the Symfony HttpFoundation factory. 52 | * 53 | * This instance can turn a PSR-7 request into an HttpFoundation request. 54 | */ 55 | protected function httpFoundationRequestFactory(): HttpFoundationFactoryInterface 56 | { 57 | return $this->httpFoundationFactory ?: ( 58 | $this->httpFoundationFactory = new HttpFoundationFactory 59 | ); 60 | } 61 | 62 | /** 63 | * Create the Symfony PSR-7 factory. 64 | * 65 | * This instance can turn an HTTP Foundation response into a PSR-7 response. 66 | */ 67 | protected function psr7ResponseFactory(): HttpMessageFactoryInterface 68 | { 69 | return $this->psrHttpFactory ?: ($this->psrHttpFactory = new PsrHttpFactory( 70 | new ServerRequestFactory, 71 | new StreamFactory, 72 | new UploadedFileFactory, 73 | new ResponseFactory 74 | )); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Swoole/SwooleHttpTaskDispatcher.php: -------------------------------------------------------------------------------- 1 | mapWithKeys(function ($task, $key) { 36 | return [$key => $task instanceof Closure 37 | ? new SerializableClosure($task) 38 | : $task, ]; 39 | })->all(); 40 | 41 | try { 42 | $response = Http::timeout(($waitMilliseconds / 1000) + 5)->post("http://{$this->host}:{$this->port}/octane/resolve-tasks", [ 43 | 'tasks' => Crypt::encryptString(serialize($tasks)), 44 | 'wait' => $waitMilliseconds, 45 | ]); 46 | 47 | return match ($response->status()) { 48 | 200 => unserialize($response), 49 | 504 => throw TaskTimeoutException::after($waitMilliseconds), 50 | default => throw TaskExceptionResult::from( 51 | new Exception('Invalid response from task server.'), 52 | )->getOriginal(), 53 | }; 54 | } catch (ConnectionException) { 55 | return $this->fallbackDispatcher->resolve($tasks, $waitMilliseconds); 56 | } 57 | } 58 | 59 | /** 60 | * Concurrently dispatch the given callbacks via background tasks. 61 | */ 62 | public function dispatch(array $tasks): void 63 | { 64 | $tasks = collect($tasks)->mapWithKeys(function ($task, $key) { 65 | return [$key => $task instanceof Closure 66 | ? new SerializableClosure($task) 67 | : $task, ]; 68 | })->all(); 69 | 70 | try { 71 | Http::post("http://{$this->host}:{$this->port}/octane/dispatch-tasks", [ 72 | 'tasks' => Crypt::encryptString(serialize($tasks)), 73 | ]); 74 | } catch (ConnectionException) { 75 | $this->fallbackDispatcher->dispatch($tasks); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Commands/StopCommand.php: -------------------------------------------------------------------------------- 1 | option('server') ?: config('octane.server'); 34 | 35 | return match ($server) { 36 | 'swoole' => $this->stopSwooleServer(), 37 | 'roadrunner' => $this->stopRoadRunnerServer(), 38 | default => $this->invalidServer($server), 39 | }; 40 | } 41 | 42 | /** 43 | * Stop the Swoole server for Octane. 44 | * 45 | * @return int 46 | */ 47 | protected function stopSwooleServer() 48 | { 49 | $inspector = app(SwooleServerProcessInspector::class); 50 | 51 | if (! $inspector->serverIsRunning()) { 52 | app(SwooleServerStateFile::class)->delete(); 53 | 54 | $this->error('Swoole server is not running.'); 55 | 56 | return 1; 57 | } 58 | 59 | $this->info('Stopping server...'); 60 | 61 | if (! $inspector->stopServer()) { 62 | $this->error('Failed to stop Swoole server.'); 63 | 64 | return 1; 65 | } 66 | 67 | app(SwooleServerStateFile::class)->delete(); 68 | 69 | return 0; 70 | } 71 | 72 | /** 73 | * Stop the RoadRunner server for Octane. 74 | * 75 | * @return int 76 | */ 77 | protected function stopRoadRunnerServer() 78 | { 79 | $inspector = app(RoadRunnerServerProcessInspector::class); 80 | 81 | if (! $inspector->serverIsRunning()) { 82 | app(RoadRunnerServerStateFile::class)->delete(); 83 | 84 | $this->error('RoadRunner server is not running.'); 85 | 86 | return 1; 87 | } 88 | 89 | $this->info('Stopping server...'); 90 | 91 | $inspector->stopServer(); 92 | 93 | app(RoadRunnerServerStateFile::class)->delete(); 94 | 95 | return 0; 96 | } 97 | 98 | /** 99 | * Inform the user that the server type is invalid. 100 | * 101 | * @return int 102 | */ 103 | protected function invalidServer(string $server) 104 | { 105 | $this->error("Invalid server: {$server}."); 106 | 107 | return 1; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/ApplicationFactory.php: -------------------------------------------------------------------------------- 1 | basePath.'/.laravel/app.php', 25 | $this->basePath.'/bootstrap/app.php', 26 | ]; 27 | 28 | foreach ($paths as $path) { 29 | if (file_exists($path)) { 30 | return $this->warm($this->bootstrap(require $path, $initialInstances)); 31 | } 32 | } 33 | 34 | throw new RuntimeException("Application bootstrap file not found in 'bootstrap' or '.laravel' directory."); 35 | } 36 | 37 | /** 38 | * Bootstrap the given application. 39 | */ 40 | public function bootstrap(Application $app, array $initialInstances = []): Application 41 | { 42 | foreach ($initialInstances as $key => $value) { 43 | $app->instance($key, $value); 44 | } 45 | 46 | $app->bootstrapWith($this->getBootstrappers($app)); 47 | 48 | $app->loadDeferredProviders(); 49 | 50 | return $app; 51 | } 52 | 53 | /** 54 | * Get the application's HTTP kernel bootstrappers. 55 | */ 56 | protected function getBootstrappers(Application $app): array 57 | { 58 | $method = (new ReflectionObject( 59 | $kernel = $app->make(HttpKernelContract::class) 60 | ))->getMethod('bootstrappers'); 61 | 62 | $method->setAccessible(true); 63 | 64 | return $this->injectBootstrapperBefore( 65 | RegisterProviders::class, 66 | SetRequestForConsole::class, 67 | $method->invoke($kernel) 68 | ); 69 | } 70 | 71 | /** 72 | * Inject a given bootstrapper before another bootstrapper. 73 | */ 74 | protected function injectBootstrapperBefore(string $before, string $inject, array $bootstrappers): array 75 | { 76 | $injectIndex = array_search($before, $bootstrappers, true); 77 | 78 | if ($injectIndex !== false) { 79 | array_splice($bootstrappers, $injectIndex, 0, [$inject]); 80 | } 81 | 82 | return $bootstrappers; 83 | } 84 | 85 | /** 86 | * Warm the application with pre-resolved, cached services that persist across requests. 87 | */ 88 | public function warm(Application $app, array $services = []): Application 89 | { 90 | foreach ($services ?: $app->make('config')->get('octane.warm', []) as $service) { 91 | if (is_string($service) && $app->bound($service)) { 92 | $app->make($service); 93 | } 94 | } 95 | 96 | return $app; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Swoole/Actions/ConvertSwooleRequestToIlluminateRequest.php: -------------------------------------------------------------------------------- 1 | prepareServerVariables( 19 | $swooleRequest->server ?? [], 20 | $swooleRequest->header ?? [], 21 | $phpSapi 22 | ); 23 | 24 | $request = new SymfonyRequest( 25 | $swooleRequest->get ?? [], 26 | $swooleRequest->post ?? [], 27 | [], 28 | $swooleRequest->cookie ?? [], 29 | $swooleRequest->files ?? [], 30 | $serverVariables, 31 | $swooleRequest->rawContent(), 32 | ); 33 | 34 | if (str_starts_with((string) $request->headers->get('CONTENT_TYPE'), 'application/x-www-form-urlencoded') && 35 | in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'PATCH', 'DELETE'])) { 36 | parse_str($request->getContent(), $data); 37 | 38 | $request->request = new ParameterBag($data); 39 | } 40 | 41 | return Request::createFromBase($request); 42 | } 43 | 44 | /** 45 | * Parse the "server" variables and headers into a single array of $_SERVER variables. 46 | */ 47 | protected function prepareServerVariables(array $server, array $headers, string $phpSapi): array 48 | { 49 | $results = []; 50 | 51 | foreach ($server as $key => $value) { 52 | $results[strtoupper($key)] = $value; 53 | } 54 | 55 | $results = array_merge( 56 | $results, 57 | $this->formatHttpHeadersIntoServerVariables($headers) 58 | ); 59 | 60 | if (isset($results['REQUEST_URI'], $results['QUERY_STRING']) && 61 | strlen($results['QUERY_STRING']) > 0 && 62 | strpos($results['REQUEST_URI'], '?') === false) { 63 | $results['REQUEST_URI'] .= '?'.$results['QUERY_STRING']; 64 | } 65 | 66 | return $phpSapi === 'cli-server' 67 | ? $this->correctHeadersSetIncorrectlyByPhpDevServer($results) 68 | : $results; 69 | } 70 | 71 | /** 72 | * Format the given HTTP headers into properly formatted $_SERVER variables. 73 | */ 74 | protected function formatHttpHeadersIntoServerVariables(array $headers): array 75 | { 76 | $results = []; 77 | 78 | foreach ($headers as $key => $value) { 79 | $key = strtoupper(str_replace('-', '_', $key)); 80 | 81 | if (! in_array($key, ['HTTPS', 'REMOTE_ADDR', 'SERVER_PORT'])) { 82 | $key = 'HTTP_'.$key; 83 | } 84 | 85 | $results[$key] = $value; 86 | } 87 | 88 | return $results; 89 | } 90 | 91 | /** 92 | * Correct headers set incorrectly by built-in PHP development server. 93 | */ 94 | protected function correctHeadersSetIncorrectlyByPhpDevServer(array $headers): array 95 | { 96 | if (array_key_exists('HTTP_CONTENT_LENGTH', $headers)) { 97 | $headers['CONTENT_LENGTH'] = $headers['HTTP_CONTENT_LENGTH']; 98 | } 99 | 100 | if (array_key_exists('HTTP_CONTENT_TYPE', $headers)) { 101 | $headers['CONTENT_TYPE'] = $headers['HTTP_CONTENT_TYPE']; 102 | } 103 | 104 | return $headers; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Swoole/Handlers/OnWorkerStart.php: -------------------------------------------------------------------------------- 1 | clearOpcodeCache(); 34 | 35 | $this->workerState->server = $server; 36 | $this->workerState->workerId = $workerId; 37 | $this->workerState->workerPid = posix_getpid(); 38 | $this->workerState->worker = $this->bootWorker($server); 39 | 40 | $this->dispatchServerTickTaskEverySecond($server); 41 | $this->streamRequestsToConsole($server); 42 | 43 | if ($this->shouldSetProcessName) { 44 | $isTaskWorker = $workerId >= $server->setting['worker_num']; 45 | 46 | $this->extension->setProcessName( 47 | $this->serverState['appName'], 48 | $isTaskWorker ? 'task worker process' : 'worker process', 49 | ); 50 | } 51 | } 52 | 53 | /** 54 | * Boot the Octane worker and application. 55 | * 56 | * @param \Swoole\Http\Server $server 57 | * @return \Laravel\Octane\Worker|null 58 | */ 59 | protected function bootWorker($server) 60 | { 61 | try { 62 | return tap(new Worker( 63 | new ApplicationFactory($this->basePath), 64 | $this->workerState->client = new SwooleClient 65 | ))->boot([ 66 | 'octane.cacheTable' => $this->workerState->cacheTable, 67 | Server::class => $server, 68 | WorkerState::class => $this->workerState, 69 | ]); 70 | } catch (Throwable $e) { 71 | Stream::shutdown($e); 72 | 73 | $server->shutdown(); 74 | } 75 | } 76 | 77 | /** 78 | * Start the Octane server tick to dispatch the tick task every second. 79 | * 80 | * @param \Swoole\Http\Server $server 81 | * @return void 82 | */ 83 | protected function dispatchServerTickTaskEverySecond($server) 84 | { 85 | // ... 86 | } 87 | 88 | /** 89 | * Register the request handled listener that will output request information per request. 90 | * 91 | * @param \Swoole\Http\Server $server 92 | * @return void 93 | */ 94 | protected function streamRequestsToConsole($server) 95 | { 96 | $this->workerState->worker->onRequestHandled(function ($request, $response, $sandbox) { 97 | if (! $sandbox->environment('local', 'testing')) { 98 | return; 99 | } 100 | 101 | Stream::request( 102 | $request->getMethod(), 103 | $request->fullUrl(), 104 | $response->getStatusCode(), 105 | (microtime(true) - $this->workerState->lastRequestTime) * 1000, 106 | ); 107 | }); 108 | } 109 | 110 | /** 111 | * Clear the APCu and Opcache caches. 112 | * 113 | * @return void 114 | */ 115 | protected function clearOpcodeCache() 116 | { 117 | foreach (['apcu_clear_cache', 'opcache_reset'] as $function) { 118 | if (function_exists($function)) { 119 | $function(); 120 | } 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Commands/InstallCommand.php: -------------------------------------------------------------------------------- 1 | option('server') ?: $this->choice( 36 | 'Which application server you would like to use?', 37 | ['roadrunner', 'swoole'], 38 | ); 39 | 40 | return (int) ! tap(match ($server) { 41 | 'swoole' => $this->installSwooleServer(), 42 | 'roadrunner' => $this->installRoadRunnerServer(), 43 | default => $this->invalidServer($server), 44 | }, function ($installed) use ($server) { 45 | if ($installed) { 46 | $this->updateEnvironmentFile($server); 47 | 48 | $this->callSilent('vendor:publish', ['--tag' => 'octane-config', '--force' => true]); 49 | 50 | $this->info('Octane installed successfully.'); 51 | $this->newLine(); 52 | } 53 | }); 54 | } 55 | 56 | /** 57 | * Updates the environment file with the given server. 58 | * 59 | * @param string $server 60 | * @return void 61 | */ 62 | public function updateEnvironmentFile($server) 63 | { 64 | if (File::exists($env = app()->environmentFile())) { 65 | $contents = File::get($env); 66 | 67 | if (! Str::contains($contents, 'OCTANE_SERVER=')) { 68 | File::append( 69 | $env, 70 | PHP_EOL.'OCTANE_SERVER='.$server.PHP_EOL, 71 | ); 72 | } else { 73 | $this->warn('Please adjust the `OCTANE_SERVER` environment variable.'); 74 | } 75 | } 76 | } 77 | 78 | /** 79 | * Install the RoadRunner dependencies. 80 | * 81 | * @return bool 82 | */ 83 | public function installRoadRunnerServer() 84 | { 85 | if (! $this->ensureRoadRunnerPackageIsInstalled()) { 86 | return false; 87 | } 88 | 89 | if (File::exists(base_path('.gitignore'))) { 90 | collect(['rr', '.rr.yaml']) 91 | ->each(function ($file) { 92 | $contents = File::get(base_path('.gitignore')); 93 | if (! Str::contains($contents, $file.PHP_EOL)) { 94 | File::append( 95 | base_path('.gitignore'), 96 | $file.PHP_EOL 97 | ); 98 | } 99 | }); 100 | } 101 | 102 | return $this->ensureRoadRunnerBinaryIsInstalled(); 103 | } 104 | 105 | /** 106 | * Install the Swoole dependencies. 107 | * 108 | * @return bool 109 | */ 110 | public function installSwooleServer() 111 | { 112 | if (! resolve(SwooleExtension::class)->isInstalled()) { 113 | $this->warn('The Swoole extension is missing.'); 114 | } 115 | 116 | return true; 117 | } 118 | 119 | /** 120 | * Inform the user that the server type is invalid. 121 | * 122 | * @return bool 123 | */ 124 | protected function invalidServer(string $server) 125 | { 126 | $this->error("Invalid server: {$server}."); 127 | 128 | return false; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Commands/StartCommand.php: -------------------------------------------------------------------------------- 1 | option('server') ?: config('octane.server'); 45 | 46 | return match ($server) { 47 | 'swoole' => $this->startSwooleServer(), 48 | 'roadrunner' => $this->startRoadRunnerServer(), 49 | default => $this->invalidServer($server), 50 | }; 51 | } 52 | 53 | /** 54 | * Start the Swoole server for Octane. 55 | * 56 | * @return int 57 | */ 58 | protected function startSwooleServer() 59 | { 60 | return $this->call('octane:swoole', [ 61 | '--host' => $this->getHost(), 62 | '--port' => $this->getPort(), 63 | '--workers' => $this->option('workers'), 64 | '--task-workers' => $this->option('task-workers'), 65 | '--max-requests' => $this->option('max-requests'), 66 | '--watch' => $this->option('watch'), 67 | '--poll' => $this->option('poll'), 68 | ]); 69 | } 70 | 71 | /** 72 | * Start the RoadRunner server for Octane. 73 | * 74 | * @return int 75 | */ 76 | protected function startRoadRunnerServer() 77 | { 78 | return $this->call('octane:roadrunner', [ 79 | '--host' => $this->getHost(), 80 | '--port' => $this->getPort(), 81 | '--rpc-host' => $this->option('rpc-host'), 82 | '--rpc-port' => $this->option('rpc-port'), 83 | '--workers' => $this->option('workers'), 84 | '--max-requests' => $this->option('max-requests'), 85 | '--rr-config' => $this->option('rr-config'), 86 | '--watch' => $this->option('watch'), 87 | '--poll' => $this->option('poll'), 88 | '--log-level' => $this->option('log-level'), 89 | ]); 90 | } 91 | 92 | /** 93 | * Inform the user that the server type is invalid. 94 | * 95 | * @return int 96 | */ 97 | protected function invalidServer(string $server) 98 | { 99 | $this->error("Invalid server: {$server}."); 100 | 101 | return 1; 102 | } 103 | 104 | /** 105 | * Stop the server. 106 | * 107 | * @return void 108 | */ 109 | protected function stopServer() 110 | { 111 | $server = $this->option('server') ?: config('octane.server'); 112 | 113 | $this->callSilent('octane:stop', [ 114 | '--server' => $server, 115 | ]); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Concerns/ProvidesDefaultConfigurationOptions.php: -------------------------------------------------------------------------------- 1 | isStarted()) { 23 | sleep(1); 24 | } 25 | 26 | $this->writeServerRunningMessage(); 27 | 28 | $watcher = $this->startServerWatcher(); 29 | 30 | try { 31 | while ($server->isRunning()) { 32 | $this->writeServerOutput($server); 33 | 34 | if ($watcher->isRunning() && 35 | $watcher->getIncrementalOutput()) { 36 | $this->info('Application change detected. Restarting workers…'); 37 | 38 | $inspector->reloadServer(); 39 | } elseif ($watcher->isTerminated()) { 40 | $this->error( 41 | 'Watcher process has terminated. Please ensure Node and chokidar are installed.'.PHP_EOL. 42 | $watcher->getErrorOutput() 43 | ); 44 | 45 | return 1; 46 | } 47 | 48 | usleep(500 * 1000); 49 | } 50 | 51 | $this->writeServerOutput($server); 52 | } catch (ServerShutdownException) { 53 | return 1; 54 | } finally { 55 | $this->stopServer(); 56 | } 57 | 58 | return $server->getExitCode(); 59 | } 60 | 61 | /** 62 | * Start the watcher process for the server. 63 | * 64 | * @return \Symfony\Component\Process\Process|object 65 | */ 66 | protected function startServerWatcher() 67 | { 68 | if (! $this->option('watch')) { 69 | return new class 70 | { 71 | public function __call($method, $parameters) 72 | { 73 | return null; 74 | } 75 | }; 76 | } 77 | 78 | if (empty($paths = config('octane.watch'))) { 79 | throw new InvalidArgumentException( 80 | 'List of directories/files to watch not found. Please update your "config/octane.php" configuration file.', 81 | ); 82 | } 83 | 84 | return tap(new Process([ 85 | (new ExecutableFinder)->find('node'), 86 | 'file-watcher.cjs', 87 | json_encode(collect(config('octane.watch'))->map(fn ($path) => base_path($path))), 88 | $this->option('poll'), 89 | ], realpath(__DIR__.'/../../../bin'), null, null, null))->start(); 90 | } 91 | 92 | /** 93 | * Write the server start "message" to the console. 94 | * 95 | * @return void 96 | */ 97 | protected function writeServerRunningMessage() 98 | { 99 | $this->info('Server running…'); 100 | 101 | $this->output->writeln([ 102 | '', 103 | ' Local: http://'.$this->getHost().':'.$this->getPort().' ', 104 | '', 105 | ' Press Ctrl+C to stop the server', 106 | '', 107 | ]); 108 | } 109 | 110 | /** 111 | * Retrieve the given server output and flush it. 112 | * 113 | * @return array 114 | */ 115 | protected function getServerOutput($server) 116 | { 117 | return tap([ 118 | $server->getIncrementalOutput(), 119 | $server->getIncrementalErrorOutput(), 120 | ], fn () => $server->clearOutput()->clearErrorOutput()); 121 | } 122 | 123 | /** 124 | * Get the Octane HTTP server host IP to bind on. 125 | * 126 | * @return string 127 | */ 128 | protected function getHost() 129 | { 130 | return $this->option('host') ?? config('octane.host') ?? $_ENV['OCTANE_HOST'] ?? '127.0.0.1'; 131 | } 132 | 133 | /** 134 | * Get the Octane HTTP server port. 135 | * 136 | * @return string 137 | */ 138 | protected function getPort() 139 | { 140 | return $this->option('port') ?? config('octane.port') ?? $_ENV['OCTANE_PORT'] ?? '8000'; 141 | } 142 | 143 | /** 144 | * Returns the list of signals to subscribe. 145 | */ 146 | public function getSubscribedSignals(): array 147 | { 148 | return [SIGINT, SIGTERM]; 149 | } 150 | 151 | /** 152 | * The method will be called when the application is signaled. 153 | */ 154 | public function handleSignal(int $signal): void 155 | { 156 | $this->stopServer(); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /art/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Commands/Concerns/InstallsRoadRunnerDependencies.php: -------------------------------------------------------------------------------- 1 | isRoadRunnerInstalled()) { 44 | return true; 45 | } 46 | 47 | if (! $this->confirm('Octane requires "spiral/roadrunner-http:^3.0.1" and "spiral/roadrunner-cli:^2.5.0". Do you wish to install them as a dependencies?')) { 48 | $this->error('Octane requires "spiral/roadrunner-http" and "spiral/roadrunner-cli".'); 49 | 50 | return false; 51 | } 52 | 53 | $command = $this->findComposer().' require spiral/roadrunner-http:^3.0.1 spiral/roadrunner-cli:^2.5.0 --with-all-dependencies'; 54 | 55 | $process = Process::fromShellCommandline($command, null, null, null, null); 56 | 57 | if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) { 58 | try { 59 | $process->setTty(true); 60 | } catch (RuntimeException $e) { 61 | $this->output->writeln('Warning: '.$e->getMessage()); 62 | } 63 | } 64 | 65 | try { 66 | $process->run(function ($type, $line) { 67 | $this->output->write($line); 68 | }); 69 | } catch (ProcessSignaledException $e) { 70 | if (extension_loaded('pcntl') && $e->getSignal() !== SIGINT) { 71 | throw $e; 72 | } 73 | } 74 | 75 | return true; 76 | } 77 | 78 | /** 79 | * Get the composer command for the environment. 80 | * 81 | * @return string 82 | */ 83 | protected function findComposer() 84 | { 85 | $composerPath = getcwd().'/composer.phar'; 86 | 87 | $phpPath = (new PhpExecutableFinder)->find(); 88 | 89 | if (! file_exists($composerPath)) { 90 | $composerPath = (new ExecutableFinder())->find('composer'); 91 | } 92 | 93 | return '"'.$phpPath.'" '.$composerPath; 94 | } 95 | 96 | /** 97 | * Ensure the RoadRunner binary is installed into the project. 98 | */ 99 | protected function ensureRoadRunnerBinaryIsInstalled(): string 100 | { 101 | if (! is_null($roadRunnerBinary = $this->findRoadRunnerBinary())) { 102 | return $roadRunnerBinary; 103 | } 104 | 105 | if ($this->confirm('Unable to locate RoadRunner binary. Should Octane download the binary for your operating system?', true)) { 106 | $this->downloadRoadRunnerBinary(); 107 | 108 | copy(__DIR__.'/../stubs/rr.yaml', base_path('.rr.yaml')); 109 | } 110 | 111 | return base_path('rr'); 112 | } 113 | 114 | /** 115 | * Ensure the RoadRunner binary installed in your project meets Octane requirements. 116 | * 117 | * @param string $roadRunnerBinary 118 | * @return void 119 | */ 120 | protected function ensureRoadRunnerBinaryMeetsRequirements($roadRunnerBinary) 121 | { 122 | $version = tap(new Process([$roadRunnerBinary, '--version'], base_path())) 123 | ->run() 124 | ->getOutput(); 125 | 126 | if (! Str::startsWith($version, 'rr version')) { 127 | return $this->warn( 128 | 'Unable to determine the current RoadRunner binary version. Please report this issue: https://github.com/laravel/octane/issues/new.' 129 | ); 130 | } 131 | 132 | $version = explode(' ', $version)[2]; 133 | 134 | if (version_compare($version, $this->requiredVersion, '>=')) { 135 | return; 136 | } 137 | 138 | $this->warn("Your RoadRunner binary version ($version) may be incompatible with Octane."); 139 | 140 | if ($this->confirm('Should Octane download the latest RoadRunner binary version for your operating system?', true)) { 141 | rename($roadRunnerBinary, "$roadRunnerBinary.backup"); 142 | 143 | try { 144 | $this->downloadRoadRunnerBinary(); 145 | } catch (Throwable $e) { 146 | report($e); 147 | 148 | rename("$roadRunnerBinary.backup", $roadRunnerBinary); 149 | 150 | return $this->warn('Unable to download RoadRunner binary. The HTTP request exception has been logged.'); 151 | } 152 | 153 | unlink("$roadRunnerBinary.backup"); 154 | } 155 | } 156 | 157 | /** 158 | * Download the latest version of the RoadRunner binary. 159 | * 160 | * @return void 161 | */ 162 | protected function downloadRoadRunnerBinary() 163 | { 164 | $installed = false; 165 | 166 | tap(new Process(array_filter([ 167 | (new PhpExecutableFinder)->find(), 168 | './vendor/bin/rr', 169 | 'get-binary', 170 | '-n', 171 | '--ansi', 172 | ]), base_path(), null, null, null))->mustRun(function (string $type, string $buffer) use (&$installed) { 173 | if (! $installed) { 174 | $this->output->write($buffer); 175 | 176 | $installed = str_contains($buffer, 'has been installed into'); 177 | } 178 | }); 179 | 180 | chmod(base_path('rr'), 0755); 181 | 182 | $this->line(''); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /bin/swoole-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | require __DIR__.'/bootstrap.php'; 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Create The Swoole Server 23 | |-------------------------------------------------------------------------- 24 | | 25 | | First, we will load the server state file from disk. This file contains 26 | | various information we need to boot Swoole such as the configuration 27 | | and application name. We can use this data to start up our server. 28 | | 29 | */ 30 | 31 | $serverState = json_decode(file_get_contents( 32 | $serverStateFile = $_SERVER['argv'][1] 33 | ), true)['state']; 34 | 35 | $server = require __DIR__.'/createSwooleServer.php'; 36 | 37 | $timerTable = require __DIR__.'/createSwooleTimerTable.php'; 38 | 39 | /* 40 | |-------------------------------------------------------------------------- 41 | | Handle Server & Manager Start 42 | |-------------------------------------------------------------------------- 43 | | 44 | | The following callbacks manage the master process and manager process 45 | | start events. These handlers primarily are responsible for writing 46 | | the process ID to the server state file so we can remember them. 47 | | 48 | */ 49 | 50 | $server->on('start', fn (Server $server) => $bootstrap($serverState) && (new OnServerStart( 51 | new ServerStateFile($serverStateFile), 52 | new SwooleExtension, 53 | $serverState['appName'], 54 | $serverState['octaneConfig']['max_execution_time'] ?? 0, 55 | $timerTable, 56 | $serverState['octaneConfig']['tick'] ?? true 57 | ))($server)); 58 | 59 | $server->on('managerstart', function () use ($serverState) { 60 | // Don't bootstrap entire application before server / worker start. Otherwise, files can't be gracefully reloaded... #632 61 | require_once __DIR__.'/../src/Swoole/Handlers/OnManagerStart.php'; 62 | require_once __DIR__.'/../src/Swoole/SwooleExtension.php'; 63 | 64 | (new OnManagerStart( 65 | new SwooleExtension, $serverState['appName'] 66 | ))(); 67 | }); 68 | 69 | /* 70 | |-------------------------------------------------------------------------- 71 | | Handle Worker Start 72 | |-------------------------------------------------------------------------- 73 | | 74 | | Swoole will start multiple worker processes and the following callback 75 | | will handle their state events. When a worker starts we will create 76 | | a new Octane worker and inform it to start handling our requests. 77 | | 78 | | We will also create a "workerState" variable which will maintain state 79 | | and allow us to access the worker and client from the callback that 80 | | will handle incoming requests. Basically this works like a cache. 81 | | 82 | */ 83 | 84 | require_once __DIR__.'/WorkerState.php'; 85 | 86 | $workerState = new WorkerState; 87 | 88 | $workerState->cacheTable = require __DIR__.'/createSwooleCacheTable.php'; 89 | $workerState->timerTable = $timerTable; 90 | $workerState->tables = require __DIR__.'/createSwooleTables.php'; 91 | 92 | $server->on('workerstart', fn (Server $server, $workerId) => 93 | (fn ($basePath) => (new OnWorkerStart( 94 | new SwooleExtension, $basePath, $serverState, $workerState 95 | ))($server, $workerId))($bootstrap($serverState)) 96 | ); 97 | 98 | /* 99 | |-------------------------------------------------------------------------- 100 | | Handle Incoming Requests 101 | |-------------------------------------------------------------------------- 102 | | 103 | | The following callback will handle all incoming requests plus send them 104 | | the worker. The worker will send the request through the application 105 | | and ask the client to send the response back to the Swoole server. 106 | | 107 | */ 108 | 109 | $server->on('request', function ($request, $response) use ($server, $workerState, $serverState) { 110 | $workerState->lastRequestTime = microtime(true); 111 | 112 | if ($workerState->timerTable) { 113 | $workerState->timerTable->set($workerState->workerId, [ 114 | 'worker_pid' => $workerState->workerPid, 115 | 'time' => time(), 116 | 'fd' => $request->fd, 117 | ]); 118 | } 119 | 120 | $workerState->worker->handle(...$workerState->client->marshalRequest(new RequestContext([ 121 | 'swooleRequest' => $request, 122 | 'swooleResponse' => $response, 123 | 'publicPath' => $serverState['publicPath'], 124 | 'octaneConfig' => $serverState['octaneConfig'], 125 | ]))); 126 | 127 | if ($workerState->timerTable) { 128 | $workerState->timerTable->del($workerState->workerId); 129 | } 130 | }); 131 | 132 | /* 133 | |-------------------------------------------------------------------------- 134 | | Handle Tasks 135 | |-------------------------------------------------------------------------- 136 | | 137 | | Swoole tasks can be used to offload concurrent work onto a group of 138 | | background processes which handle the work in isolation and with 139 | | separate application state. We should handle these tasks below. 140 | | 141 | */ 142 | 143 | $server->on('task', fn (Server $server, int $taskId, int $fromWorkerId, $data) => 144 | $data === 'octane-tick' 145 | ? $workerState->worker->handleTick() 146 | : $workerState->worker->handleTask($data) 147 | ); 148 | 149 | $server->on('finish', fn (Server $server, int $taskId, $result) => $result); 150 | 151 | /* 152 | |-------------------------------------------------------------------------- 153 | | Handle Worker & Server Shutdown 154 | |-------------------------------------------------------------------------- 155 | | 156 | | The following callbacks handle the master and worker shutdown events so 157 | | we can clean up any state, including the server state file. An event 158 | | will be dispatched by the worker so the developer can take action. 159 | | 160 | */ 161 | 162 | $server->on('workerstop', function () use ($workerState) { 163 | if ($workerState->tickTimerId) { 164 | Timer::clear($workerState->tickTimerId); 165 | } 166 | 167 | $workerState->worker->terminate(); 168 | }); 169 | 170 | $server->start(); 171 | -------------------------------------------------------------------------------- /src/Commands/StartSwooleCommand.php: -------------------------------------------------------------------------------- 1 | isInstalled()) { 56 | $this->error('The Swoole extension is missing.'); 57 | 58 | return 1; 59 | } 60 | 61 | if ($inspector->serverIsRunning()) { 62 | $this->error('Server is already running.'); 63 | 64 | return 1; 65 | } 66 | 67 | if (config('octane.swoole.ssl', false) === true && ! defined('SWOOLE_SSL')) { 68 | $this->error('You must configure Swoole with `--enable-openssl` to support ssl.'); 69 | 70 | return 1; 71 | } 72 | 73 | $this->writeServerStateFile($serverStateFile, $extension); 74 | 75 | $this->forgetEnvironmentVariables(); 76 | 77 | $server = tap(new Process([ 78 | (new PhpExecutableFinder)->find(), 79 | ...config('octane.swoole.php_options', []), 80 | config('octane.swoole.command', 'swoole-server'), 81 | $serverStateFile->path(), 82 | ], realpath(__DIR__.'/../../bin'), [ 83 | 'APP_ENV' => app()->environment(), 84 | 'APP_BASE_PATH' => base_path(), 85 | 'LARAVEL_OCTANE' => 1, 86 | ]))->start(); 87 | 88 | return $this->runServer($server, $inspector, 'swoole'); 89 | } 90 | 91 | /** 92 | * Write the Swoole server state file. 93 | * 94 | * @return void 95 | */ 96 | protected function writeServerStateFile( 97 | ServerStateFile $serverStateFile, 98 | SwooleExtension $extension 99 | ) { 100 | $serverStateFile->writeState([ 101 | 'appName' => config('app.name', 'Laravel'), 102 | 'host' => $this->getHost(), 103 | 'port' => $this->getPort(), 104 | 'workers' => $this->workerCount($extension), 105 | 'taskWorkers' => $this->taskWorkerCount($extension), 106 | 'maxRequests' => $this->option('max-requests'), 107 | 'publicPath' => public_path(), 108 | 'storagePath' => storage_path(), 109 | 'defaultServerOptions' => $this->defaultServerOptions($extension), 110 | 'octaneConfig' => config('octane'), 111 | ]); 112 | } 113 | 114 | /** 115 | * Get the default Swoole server options. 116 | * 117 | * @return array 118 | */ 119 | protected function defaultServerOptions(SwooleExtension $extension) 120 | { 121 | return [ 122 | 'enable_coroutine' => false, 123 | 'daemonize' => false, 124 | 'log_file' => storage_path('logs/swoole_http.log'), 125 | 'log_level' => app()->environment('local') ? SWOOLE_LOG_INFO : SWOOLE_LOG_ERROR, 126 | 'max_request' => $this->option('max-requests'), 127 | 'package_max_length' => 10 * 1024 * 1024, 128 | 'reactor_num' => $this->workerCount($extension), 129 | 'send_yield' => true, 130 | 'socket_buffer_size' => 10 * 1024 * 1024, 131 | 'task_max_request' => $this->option('max-requests'), 132 | 'task_worker_num' => $this->taskWorkerCount($extension), 133 | 'worker_num' => $this->workerCount($extension), 134 | ]; 135 | } 136 | 137 | /** 138 | * Get the number of workers that should be started. 139 | * 140 | * @return int 141 | */ 142 | protected function workerCount(SwooleExtension $extension) 143 | { 144 | return $this->option('workers') === 'auto' 145 | ? $extension->cpuCount() 146 | : $this->option('workers'); 147 | } 148 | 149 | /** 150 | * Get the number of task workers that should be started. 151 | * 152 | * @return int 153 | */ 154 | protected function taskWorkerCount(SwooleExtension $extension) 155 | { 156 | return $this->option('task-workers') === 'auto' 157 | ? $extension->cpuCount() 158 | : $this->option('task-workers'); 159 | } 160 | 161 | /** 162 | * Write the server process output ot the console. 163 | * 164 | * @param \Symfony\Component\Process\Process $server 165 | * @return void 166 | */ 167 | protected function writeServerOutput($server) 168 | { 169 | [$output, $errorOutput] = $this->getServerOutput($server); 170 | 171 | Str::of($output) 172 | ->explode("\n") 173 | ->filter() 174 | ->each(fn ($output) => is_array($stream = json_decode($output, true)) 175 | ? $this->handleStream($stream) 176 | : $this->info($output) 177 | ); 178 | 179 | Str::of($errorOutput) 180 | ->explode("\n") 181 | ->filter() 182 | ->groupBy(fn ($output) => $output) 183 | ->each(function ($group) { 184 | is_array($stream = json_decode($output = $group->first(), true)) && isset($stream['type']) 185 | ? $this->handleStream($stream) 186 | : $this->raw($output); 187 | }); 188 | } 189 | 190 | /** 191 | * Stop the server. 192 | * 193 | * @return void 194 | */ 195 | protected function stopServer() 196 | { 197 | $this->callSilent('octane:stop', [ 198 | '--server' => 'swoole', 199 | ]); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/Cache/OctaneStore.php: -------------------------------------------------------------------------------- 1 | table->get($key); 41 | 42 | if (! $this->recordIsFalseOrExpired($record)) { 43 | return unserialize($record['value']); 44 | } 45 | 46 | if (in_array($key, $this->intervals) && 47 | ! is_null($interval = $this->getInterval($key))) { 48 | return $interval['resolver'](); 49 | } 50 | } 51 | 52 | /** 53 | * Retrieve an interval item from the cache. 54 | * 55 | * @param string $key 56 | * @return array|null 57 | */ 58 | protected function getInterval($key) 59 | { 60 | $interval = $this->get('interval-'.$key); 61 | 62 | return $interval ? unserialize($interval) : null; 63 | } 64 | 65 | /** 66 | * Retrieve multiple items from the cache by key. 67 | * 68 | * Items not found in the cache will have a null value. 69 | * 70 | * @return array 71 | */ 72 | public function many(array $keys) 73 | { 74 | return collect($keys)->mapWithKeys(fn ($key) => [$key => $this->get($key)])->all(); 75 | } 76 | 77 | /** 78 | * Store an item in the cache for a given number of seconds. 79 | * 80 | * @param string $key 81 | * @param mixed $value 82 | * @param int $seconds 83 | * @return bool 84 | */ 85 | public function put($key, $value, $seconds) 86 | { 87 | return $this->table->set($key, [ 88 | 'value' => serialize($value), 89 | 'expiration' => Carbon::now()->getTimestamp() + $seconds, 90 | ]); 91 | } 92 | 93 | /** 94 | * Store multiple items in the cache for a given number of seconds. 95 | * 96 | * @param int $seconds 97 | * @return bool 98 | */ 99 | public function putMany(array $values, $seconds) 100 | { 101 | foreach ($values as $key => $value) { 102 | $this->put($key, $value, $seconds); 103 | } 104 | 105 | return true; 106 | } 107 | 108 | /** 109 | * Increment the value of an item in the cache. 110 | * 111 | * @param string $key 112 | * @param mixed $value 113 | * @return int|bool 114 | */ 115 | public function increment($key, $value = 1) 116 | { 117 | $record = $this->table->get($key); 118 | 119 | if ($this->recordIsFalseOrExpired($record)) { 120 | return tap($value, fn ($value) => $this->put($key, $value, static::ONE_YEAR)); 121 | } 122 | 123 | return tap((int) (unserialize($record['value']) + $value), function ($value) use ($key, $record) { 124 | $this->put($key, $value, $record['expiration'] - Carbon::now()->getTimestamp()); 125 | }); 126 | } 127 | 128 | /** 129 | * Decrement the value of an item in the cache. 130 | * 131 | * @param string $key 132 | * @param mixed $value 133 | * @return int|bool 134 | */ 135 | public function decrement($key, $value = 1) 136 | { 137 | return $this->increment($key, $value * -1); 138 | } 139 | 140 | /** 141 | * Store an item in the cache indefinitely. 142 | * 143 | * @param string $key 144 | * @param mixed $value 145 | * @return bool 146 | */ 147 | public function forever($key, $value) 148 | { 149 | return $this->put($key, $value, static::ONE_YEAR); 150 | } 151 | 152 | /** 153 | * Register a cache key that should be refreshed at a given interval (in minutes). 154 | * 155 | * @param string $key 156 | * @param int $seconds 157 | * @return void 158 | */ 159 | public function interval($key, Closure $resolver, $seconds) 160 | { 161 | if (! is_null($this->getInterval($key))) { 162 | $this->intervals[] = $key; 163 | 164 | return; 165 | } 166 | 167 | $this->forever('interval-'.$key, serialize([ 168 | 'resolver' => new SerializableClosure($resolver), 169 | 'lastRefreshedAt' => null, 170 | 'refreshInterval' => $seconds, 171 | ])); 172 | 173 | $this->intervals[] = $key; 174 | } 175 | 176 | /** 177 | * Refresh all of the applicable interval caches. 178 | * 179 | * @return void 180 | */ 181 | public function refreshIntervalCaches() 182 | { 183 | foreach ($this->intervals as $key) { 184 | if (! $this->intervalShouldBeRefreshed($interval = $this->getInterval($key))) { 185 | continue; 186 | } 187 | 188 | try { 189 | $this->forever('interval-'.$key, serialize(array_merge( 190 | $interval, ['lastRefreshedAt' => Carbon::now()->getTimestamp()], 191 | ))); 192 | 193 | $this->forever($key, $interval['resolver']()); 194 | } catch (Throwable $e) { 195 | report($e); 196 | } 197 | } 198 | } 199 | 200 | /** 201 | * Determine if the given interval record should be refreshed. 202 | * 203 | * @return bool 204 | */ 205 | protected function intervalShouldBeRefreshed(array $interval) 206 | { 207 | return is_null($interval['lastRefreshedAt']) || 208 | (Carbon::now()->getTimestamp() - $interval['lastRefreshedAt']) >= $interval['refreshInterval']; 209 | } 210 | 211 | /** 212 | * Remove an item from the cache. 213 | * 214 | * @param string $key 215 | * @return bool 216 | */ 217 | public function forget($key) 218 | { 219 | return $this->table->del($key); 220 | } 221 | 222 | /** 223 | * Remove all items from the cache. 224 | * 225 | * @return bool 226 | */ 227 | public function flush() 228 | { 229 | foreach ($this->table as $key => $record) { 230 | if (str_starts_with($key, 'interval-')) { 231 | continue; 232 | } 233 | 234 | $this->forget($key); 235 | } 236 | 237 | return true; 238 | } 239 | 240 | /** 241 | * Determine if the record is missing or expired. 242 | * 243 | * @param array|null $record 244 | * @return bool 245 | */ 246 | protected function recordIsFalseOrExpired($record) 247 | { 248 | return $record === false || $record['expiration'] <= Carbon::now()->getTimestamp(); 249 | } 250 | 251 | /** 252 | * Get the cache key prefix. 253 | * 254 | * @return string 255 | */ 256 | public function getPrefix() 257 | { 258 | return ''; 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /config/octane.php: -------------------------------------------------------------------------------- 1 | env('OCTANE_SERVER', 'roadrunner'), 40 | 41 | /* 42 | |-------------------------------------------------------------------------- 43 | | Force HTTPS 44 | |-------------------------------------------------------------------------- 45 | | 46 | | When this configuration value is set to "true", Octane will inform the 47 | | framework that all absolute links must be generated using the HTTPS 48 | | protocol. Otherwise your links may be generated using plain HTTP. 49 | | 50 | */ 51 | 52 | 'https' => env('OCTANE_HTTPS', false), 53 | 54 | /* 55 | |-------------------------------------------------------------------------- 56 | | Octane Listeners 57 | |-------------------------------------------------------------------------- 58 | | 59 | | All of the event listeners for Octane's events are defined below. These 60 | | listeners are responsible for resetting your application's state for 61 | | the next request. You may even add your own listeners to the list. 62 | | 63 | */ 64 | 65 | 'listeners' => [ 66 | WorkerStarting::class => [ 67 | EnsureUploadedFilesAreValid::class, 68 | EnsureUploadedFilesCanBeMoved::class, 69 | ], 70 | 71 | RequestReceived::class => [ 72 | ...Octane::prepareApplicationForNextOperation(), 73 | ...Octane::prepareApplicationForNextRequest(), 74 | // 75 | ], 76 | 77 | RequestHandled::class => [ 78 | // 79 | ], 80 | 81 | RequestTerminated::class => [ 82 | // FlushUploadedFiles::class, 83 | ], 84 | 85 | TaskReceived::class => [ 86 | ...Octane::prepareApplicationForNextOperation(), 87 | // 88 | ], 89 | 90 | TaskTerminated::class => [ 91 | // 92 | ], 93 | 94 | TickReceived::class => [ 95 | ...Octane::prepareApplicationForNextOperation(), 96 | // 97 | ], 98 | 99 | TickTerminated::class => [ 100 | // 101 | ], 102 | 103 | OperationTerminated::class => [ 104 | FlushTemporaryContainerInstances::class, 105 | // DisconnectFromDatabases::class, 106 | // CollectGarbage::class, 107 | ], 108 | 109 | WorkerErrorOccurred::class => [ 110 | ReportException::class, 111 | StopWorkerIfNecessary::class, 112 | ], 113 | 114 | WorkerStopping::class => [ 115 | // 116 | ], 117 | ], 118 | 119 | /* 120 | |-------------------------------------------------------------------------- 121 | | Warm / Flush Bindings 122 | |-------------------------------------------------------------------------- 123 | | 124 | | The bindings listed below will either be pre-warmed when a worker boots 125 | | or they will be flushed before every new request. Flushing a binding 126 | | will force the container to resolve that binding again when asked. 127 | | 128 | */ 129 | 130 | 'warm' => [ 131 | ...Octane::defaultServicesToWarm(), 132 | ], 133 | 134 | 'flush' => [ 135 | // 136 | ], 137 | 138 | /* 139 | |-------------------------------------------------------------------------- 140 | | Octane Cache Table 141 | |-------------------------------------------------------------------------- 142 | | 143 | | While using Swoole, you may leverage the Octane cache, which is powered 144 | | by a Swoole table. You may set the maximum number of rows as well as 145 | | the number of bytes per row using the configuration options below. 146 | | 147 | */ 148 | 149 | 'cache' => [ 150 | 'rows' => 1000, 151 | 'bytes' => 10000, 152 | ], 153 | 154 | /* 155 | |-------------------------------------------------------------------------- 156 | | Octane Swoole Tables 157 | |-------------------------------------------------------------------------- 158 | | 159 | | While using Swoole, you may define additional tables as required by the 160 | | application. These tables can be used to store data that needs to be 161 | | quickly accessed by other workers on the particular Swoole server. 162 | | 163 | */ 164 | 165 | 'tables' => [ 166 | 'example:1000' => [ 167 | 'name' => 'string:1000', 168 | 'votes' => 'int', 169 | ], 170 | ], 171 | 172 | /* 173 | |-------------------------------------------------------------------------- 174 | | File Watching 175 | |-------------------------------------------------------------------------- 176 | | 177 | | The following list of files and directories will be watched when using 178 | | the --watch option offered by Octane. If any of the directories and 179 | | files are changed, Octane will automatically reload your workers. 180 | | 181 | */ 182 | 183 | 'watch' => [ 184 | 'app', 185 | 'bootstrap', 186 | 'config', 187 | 'database', 188 | 'public/**/*.php', 189 | 'resources/**/*.php', 190 | 'routes', 191 | 'composer.lock', 192 | '.env', 193 | ], 194 | 195 | /* 196 | |-------------------------------------------------------------------------- 197 | | Garbage Collection Threshold 198 | |-------------------------------------------------------------------------- 199 | | 200 | | When executing long-lived PHP scripts such as Octane, memory can build 201 | | up before being cleared by PHP. You can force Octane to run garbage 202 | | collection if your application consumes this amount of megabytes. 203 | | 204 | */ 205 | 206 | 'garbage' => 50, 207 | 208 | /* 209 | |-------------------------------------------------------------------------- 210 | | Maximum Execution Time 211 | |-------------------------------------------------------------------------- 212 | | 213 | | The following setting configures the maximum execution time for requests 214 | | being handled by Octane. You may set this value to 0 to indicate that 215 | | there isn't a specific time limit on Octane request execution time. 216 | | 217 | */ 218 | 219 | 'max_execution_time' => 30, 220 | 221 | ]; 222 | -------------------------------------------------------------------------------- /src/Commands/Concerns/InteractsWithIO.php: -------------------------------------------------------------------------------- 1 | ignoreMessages)) { 46 | $this->output instanceof OutputStyle 47 | ? fwrite(STDERR, $string."\n") 48 | : $this->output->writeln($string); 49 | } 50 | } 51 | 52 | /** 53 | * Write a string as information output. 54 | * 55 | * @param string $string 56 | * @param int|string|null $verbosity 57 | * @return void 58 | */ 59 | public function info($string, $verbosity = null) 60 | { 61 | $this->label($string, $verbosity, 'INFO', 'blue', 'white'); 62 | } 63 | 64 | /** 65 | * Write a string as error output. 66 | * 67 | * @param string $string 68 | * @param int|string|null $verbosity 69 | * @return void 70 | */ 71 | public function error($string, $verbosity = null) 72 | { 73 | $this->label($string, $verbosity, 'ERROR', 'red', 'white'); 74 | } 75 | 76 | /** 77 | * Write a string as warning output. 78 | * 79 | * @param string $string 80 | * @param int|string|null $verbosity 81 | * @return void 82 | */ 83 | public function warn($string, $verbosity = null) 84 | { 85 | $this->label($string, $verbosity, 'WARN', 'yellow', 'black'); 86 | } 87 | 88 | /** 89 | * Write a string as label output. 90 | * 91 | * @param string $string 92 | * @param int|string|null $verbosity 93 | * @param string $level 94 | * @param string $background 95 | * @param string $foreground 96 | * @return void 97 | */ 98 | public function label($string, $verbosity, $level, $background, $foreground) 99 | { 100 | if (! empty($string) && ! Str::startsWith($string, $this->ignoreMessages)) { 101 | $this->output->writeln([ 102 | '', 103 | " $level $string", 104 | ], $this->parseVerbosity($verbosity)); 105 | } 106 | } 107 | 108 | /** 109 | * Write information about a request to the console. 110 | * 111 | * @param array $request 112 | * @param int|string|null $verbosity 113 | * @return void 114 | */ 115 | public function requestInfo($request, $verbosity = null) 116 | { 117 | $terminalWidth = $this->getTerminalWidth(); 118 | 119 | $url = parse_url($request['url'], PHP_URL_PATH) ?: '/'; 120 | $duration = number_format(round($request['duration'], 2), 2, '.', ''); 121 | 122 | $memory = isset($request['memory']) 123 | ? (number_format($request['memory'] / 1024 / 1024, 2, '.', '').' mb ') 124 | : ''; 125 | 126 | ['method' => $method, 'statusCode' => $statusCode] = $request; 127 | 128 | $dots = str_repeat('.', max($terminalWidth - strlen($method.$url.$duration.$memory) - 16, 0)); 129 | 130 | if (empty($dots) && ! $this->output->isVerbose()) { 131 | $url = substr($url, 0, $terminalWidth - strlen($method.$duration.$memory) - 15 - 3).'...'; 132 | } else { 133 | $dots .= ' '; 134 | } 135 | 136 | $this->output->writeln(sprintf( 137 | ' %s %s %s %s%s%s ms', 138 | match (true) { 139 | $statusCode >= 500 => 'red', 140 | $statusCode >= 400 => 'yellow', 141 | $statusCode >= 300 => 'cyan', 142 | $statusCode >= 100 => 'green', 143 | default => 'white', 144 | }, 145 | $statusCode, 146 | $method, 147 | $url, 148 | $dots, 149 | $memory, 150 | $duration, 151 | ), $this->parseVerbosity($verbosity)); 152 | } 153 | 154 | /** 155 | * Write information about a dd to the console. 156 | * 157 | * @param array $throwable 158 | * @param int|string|null $verbosity 159 | * @return void 160 | */ 161 | public function ddInfo($throwable, $verbosity = null) 162 | { 163 | collect(json_decode($throwable['message'], true)) 164 | ->each(fn ($var) => VarDumper::dump($var)); 165 | } 166 | 167 | /** 168 | * Write information about a throwable to the console. 169 | * 170 | * @param array $throwable 171 | * @param int|string|null $verbosity 172 | * @return void 173 | */ 174 | public function throwableInfo($throwable, $verbosity = null) 175 | { 176 | if ($throwable['class'] == DdException::class) { 177 | return $this->ddInfo($throwable, $verbosity); 178 | } 179 | 180 | if (! class_exists('NunoMaduro\Collision\Writer')) { 181 | $this->label($throwable['message'], $verbosity, $throwable['class'], 'red', 'white'); 182 | 183 | $this->newLine(); 184 | 185 | $outputTrace = function ($trace, $number) { 186 | $number++; 187 | 188 | if (isset($trace['line'])) { 189 | ['line' => $line, 'file' => $file] = $trace; 190 | 191 | $this->line(" $number $file:$line"); 192 | } 193 | }; 194 | 195 | $outputTrace($throwable, -1); 196 | 197 | return collect($throwable['trace'])->each($outputTrace); 198 | } 199 | 200 | (new Writer(null, $this->output))->write( 201 | new WorkerExceptionInspector( 202 | new WorkerException( 203 | $throwable['message'], 204 | (int) $throwable['code'], 205 | $throwable['file'], 206 | (int) $throwable['line'], 207 | ), 208 | $throwable['class'], 209 | $throwable['trace'], 210 | ), 211 | ); 212 | } 213 | 214 | /** 215 | * Write information about a "shutdown" throwable to the console. 216 | * 217 | * @param array $throwable 218 | * @param int|string|null $verbosity 219 | * @return void 220 | */ 221 | public function shutdownInfo($throwable, $verbosity = null) 222 | { 223 | $this->throwableInfo($throwable, $verbosity); 224 | 225 | throw new ServerShutdownException; 226 | } 227 | 228 | /** 229 | * Handle stream information from the worker. 230 | * 231 | * @param array $stream 232 | * @param int|string|null $verbosity 233 | * @return void 234 | */ 235 | public function handleStream($stream, $verbosity = null) 236 | { 237 | match ($stream['type'] ?? null) { 238 | 'request' => $this->requestInfo($stream, $verbosity), 239 | 'throwable' => $this->throwableInfo($stream, $verbosity), 240 | 'shutdown' => $this->shutdownInfo($stream, $verbosity), 241 | default => $this->info(json_encode($stream), $verbosity) 242 | }; 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/Worker.php: -------------------------------------------------------------------------------- 1 | app = $app = $this->appFactory->createApplication( 52 | array_merge( 53 | $initialInstances, 54 | [Client::class => $this->client], 55 | ) 56 | ); 57 | 58 | $this->dispatchEvent($app, new WorkerStarting($app)); 59 | } 60 | 61 | /** 62 | * Handle an incoming request and send the response to the client. 63 | * 64 | * @param \Laravel\Octane\RequestContext $context 65 | */ 66 | public function handle(Request $request, RequestContext $context): void 67 | { 68 | if ($this->client instanceof ServesStaticFiles && 69 | $this->client->canServeRequestAsStaticFile($request, $context)) { 70 | $this->client->serveStaticFile($request, $context); 71 | 72 | return; 73 | } 74 | 75 | // We will clone the application instance so that we have a clean copy to switch 76 | // back to once the request has been handled. This allows us to easily delete 77 | // certain instances that got resolved / mutated during a previous request. 78 | CurrentApplication::set($sandbox = clone $this->app); 79 | 80 | $gateway = new ApplicationGateway($this->app, $sandbox); 81 | 82 | try { 83 | $responded = false; 84 | 85 | ob_start(); 86 | 87 | $response = $gateway->handle($request); 88 | 89 | $output = ob_get_contents(); 90 | 91 | ob_end_clean(); 92 | 93 | // Here we will actually hand the incoming request to the Laravel application so 94 | // it can generate a response. We'll send this response back to the client so 95 | // it can be returned to a browser. This gateway will also dispatch events. 96 | $this->client->respond( 97 | $context, 98 | $octaneResponse = new OctaneResponse($response, $output), 99 | ); 100 | 101 | $responded = true; 102 | 103 | $this->invokeRequestHandledCallbacks($request, $response, $sandbox); 104 | 105 | $gateway->terminate($request, $response); 106 | } catch (Throwable $e) { 107 | $this->handleWorkerError($e, $sandbox, $request, $context, $responded); 108 | } finally { 109 | $sandbox->flush(); 110 | 111 | $this->app->make('view.engine.resolver')->forget('blade'); 112 | $this->app->make('view.engine.resolver')->forget('php'); 113 | 114 | // After the request handling process has completed we will unset some variables 115 | // plus reset the current application state back to its original state before 116 | // it was cloned. Then we will be ready for the next worker iteration loop. 117 | unset($gateway, $sandbox, $request, $response, $octaneResponse, $output); 118 | 119 | CurrentApplication::set($this->app); 120 | } 121 | } 122 | 123 | /** 124 | * Handle an incoming task. 125 | * 126 | * @param mixed $data 127 | * @return mixed 128 | */ 129 | public function handleTask($data) 130 | { 131 | $result = false; 132 | 133 | // We will clone the application instance so that we have a clean copy to switch 134 | // back to once the request has been handled. This allows us to easily delete 135 | // certain instances that got resolved / mutated during a previous request. 136 | CurrentApplication::set($sandbox = clone $this->app); 137 | 138 | try { 139 | $this->dispatchEvent($sandbox, new TaskReceived($this->app, $sandbox, $data)); 140 | 141 | $result = $data(); 142 | 143 | $this->dispatchEvent($sandbox, new TaskTerminated($this->app, $sandbox, $data, $result)); 144 | } catch (Throwable $e) { 145 | $this->dispatchEvent($sandbox, new WorkerErrorOccurred($e, $sandbox)); 146 | 147 | return TaskExceptionResult::from($e); 148 | } finally { 149 | $sandbox->flush(); 150 | 151 | // After the request handling process has completed we will unset some variables 152 | // plus reset the current application state back to its original state before 153 | // it was cloned. Then we will be ready for the next worker iteration loop. 154 | unset($sandbox); 155 | 156 | CurrentApplication::set($this->app); 157 | } 158 | 159 | return new TaskResult($result); 160 | } 161 | 162 | /** 163 | * Handle an incoming tick. 164 | */ 165 | public function handleTick(): void 166 | { 167 | CurrentApplication::set($sandbox = clone $this->app); 168 | 169 | try { 170 | $this->dispatchEvent($sandbox, new TickReceived($this->app, $sandbox)); 171 | $this->dispatchEvent($sandbox, new TickTerminated($this->app, $sandbox)); 172 | } catch (Throwable $e) { 173 | $this->dispatchEvent($sandbox, new WorkerErrorOccurred($e, $sandbox)); 174 | } finally { 175 | $sandbox->flush(); 176 | 177 | unset($sandbox); 178 | 179 | CurrentApplication::set($this->app); 180 | } 181 | } 182 | 183 | /** 184 | * Handle an uncaught exception from the worker. 185 | * 186 | * @param \Laravel\Octane\RequestContext $context 187 | */ 188 | protected function handleWorkerError( 189 | Throwable $e, 190 | Application $app, 191 | Request $request, 192 | RequestContext $context, 193 | bool $hasResponded 194 | ): void { 195 | if (! $hasResponded) { 196 | $this->client->error($e, $app, $request, $context); 197 | } 198 | 199 | $this->dispatchEvent($app, new WorkerErrorOccurred($e, $app)); 200 | } 201 | 202 | /** 203 | * Invoke the request handled callbacks. 204 | * 205 | * @param \Illuminate\Http\Request $request 206 | * @param \Symfony\Component\HttpFoundation\Response $response 207 | * @param \Illuminate\Foundation\Application $sandbox 208 | */ 209 | protected function invokeRequestHandledCallbacks($request, $response, $sandbox): void 210 | { 211 | foreach ($this->requestHandledCallbacks as $callback) { 212 | $callback($request, $response, $sandbox); 213 | } 214 | } 215 | 216 | /** 217 | * Register a closure to be invoked when requests are handled. 218 | * 219 | * @return $this 220 | */ 221 | public function onRequestHandled(Closure $callback) 222 | { 223 | $this->requestHandledCallbacks[] = $callback; 224 | 225 | return $this; 226 | } 227 | 228 | /** 229 | * Get the application instance being used by the worker. 230 | */ 231 | public function application(): Application 232 | { 233 | if (! $this->app) { 234 | throw new RuntimeException('Worker has not booted. Unable to access application.'); 235 | } 236 | 237 | return $this->app; 238 | } 239 | 240 | /** 241 | * Terminate the worker. 242 | */ 243 | public function terminate(): void 244 | { 245 | $this->dispatchEvent($this->app, new WorkerStopping($this->app)); 246 | } 247 | } 248 | --------------------------------------------------------------------------------