├── 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 |

2 |
3 |
4 |
5 |
6 |
7 |
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 |
--------------------------------------------------------------------------------