From 8a4cecdaed5ab94965c28a059b16e35fefea1e84 Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Thu, 4 Jan 2024 20:21:48 +0000 Subject: [PATCH] add restart command --- src/Concerns/InteractsWithServerState.php | 54 ++++++++++++++ .../Reverb/Console/Commands/RestartServer.php | 69 +++++++++++++++++ .../Reverb/Console/Commands/StartServer.php | 74 +++++++++++++++++-- src/Servers/Reverb/ReverbProvider.php | 2 + 4 files changed, 194 insertions(+), 5 deletions(-) create mode 100644 src/Concerns/InteractsWithServerState.php create mode 100644 src/Servers/Reverb/Console/Commands/RestartServer.php diff --git a/src/Concerns/InteractsWithServerState.php b/src/Concerns/InteractsWithServerState.php new file mode 100644 index 00000000..51898a17 --- /dev/null +++ b/src/Concerns/InteractsWithServerState.php @@ -0,0 +1,54 @@ +key); + } + + /** + * Get the current state of the running server. + * + * @return null|array{HOST: string, PORT: int, DEBUG: bool, RESTART: bool} + */ + protected function getState(): ?array + { + return Cache::get($this->key); + } + + /** + * Set the state of the running server. + */ + protected function setState(string $host, int $port, bool $debug, bool $restart = false): void + { + Cache::forever($this->key, [ + 'HOST' => $host, + 'PORT' => $port, + 'DEBUG' => $debug ??= false, + 'RESTART' => $restart, + ]); + } + + /** + * Destroy the server state. + */ + protected function destroyState(): void + { + Cache::forget($this->key); + } +} diff --git a/src/Servers/Reverb/Console/Commands/RestartServer.php b/src/Servers/Reverb/Console/Commands/RestartServer.php new file mode 100644 index 00000000..fdbd9b00 --- /dev/null +++ b/src/Servers/Reverb/Console/Commands/RestartServer.php @@ -0,0 +1,69 @@ +getState()) { + $this->error('No Reverb server running.'); + + return; + } + + $this->sendStopSignal($state); + + $this->waitForServerToStop(fn () => $this->call('reverb:start', [ + '--host' => $state['HOST'], + '--port' => $state['PORT'], + '--debug' => $state['DEBUG'], + ])); + } + + /** + * Send the stop signal to the running server. + * + * @param array{HOST: string, PORT: int, DEBUG: bool, RESTART: bool} $state + */ + protected function sendStopSignal(array $state): void + { + $this->components->info('Sending stop signal to Reverb server.'); + + $this->setState($state['HOST'], $state['PORT'], $state['DEBUG'], true); + } + + /** + * Run the callback when the server has stopped. + */ + protected function waitForServerToStop(callable $callback): void + { + while ($this->serverIsRunning()) { + usleep(1000); + } + + $callback(); + } +} diff --git a/src/Servers/Reverb/Console/Commands/StartServer.php b/src/Servers/Reverb/Console/Commands/StartServer.php index 2d231ead..02d2bf9d 100644 --- a/src/Servers/Reverb/Console/Commands/StartServer.php +++ b/src/Servers/Reverb/Console/Commands/StartServer.php @@ -3,18 +3,25 @@ namespace Laravel\Reverb\Servers\Reverb\Console\Commands; use Illuminate\Console\Command; +use Laravel\Reverb\Application; use Laravel\Reverb\Concerns\InteractsWithAsyncRedis; +use Laravel\Reverb\Concerns\InteractsWithServerState; +use Laravel\Reverb\Contracts\ApplicationProvider; use Laravel\Reverb\Contracts\Logger; use Laravel\Reverb\Jobs\PingInactiveConnections; use Laravel\Reverb\Jobs\PruneStaleConnections; use Laravel\Reverb\Loggers\CliLogger; +use Laravel\Reverb\Protocols\Pusher\Channels\ChannelConnection; +use Laravel\Reverb\Protocols\Pusher\Contracts\ChannelManager; use Laravel\Reverb\Servers\Reverb\Factory as ServerFactory; +use Laravel\Reverb\Servers\Reverb\Http\Server; use React\EventLoop\Loop; use React\EventLoop\LoopInterface; +use Symfony\Component\Console\Command\SignalableCommandInterface; -class StartServer extends Command +class StartServer extends Command implements SignalableCommandInterface { - use InteractsWithAsyncRedis; + use InteractsWithAsyncRedis, InteractsWithServerState; /** * The name and signature of the console command. @@ -38,7 +45,7 @@ class StartServer extends Command */ public function handle(): void { - if ($this->option('debug')) { + if ($debug = $this->option('debug')) { $this->laravel->instance(Logger::class, new CliLogger($this->output)); } @@ -48,17 +55,36 @@ public function handle(): void $loop = Loop::get(); + $server = ServerFactory::make($host, $port, loop: $loop); + $this->bindRedis($loop); $this->subscribeToRedis($loop); $this->scheduleCleanup($loop); - - $server = ServerFactory::make($host, $port, loop: $loop); + $this->checkForRestartSignal($server, $loop); + $this->setState($host, $port, $debug ??= false); $this->components->info("Starting server on {$host}:{$port}"); $server->start(); } + /** + * Get the list of signals handled by the command. + */ + public function getSubscribedSignals(): array + { + return [SIGINT, SIGTERM]; + } + + /** + * Handle the signals sent to the server. + */ + public function handleSignal(int $signal): void + { + $this->gracefullyDisconnect(); + $this->destroyState(); + } + /** * Use the event loop to schedule periodic cleanup of connections. */ @@ -70,4 +96,42 @@ protected function scheduleCleanup(LoopInterface $loop): void PingInactiveConnections::dispatch(); }); } + + /** + * Check to see whether the restart signal has been sent. + */ + protected function checkForRestartSignal(Server $server, LoopInterface $loop): void + { + $loop->addPeriodicTimer(5, function () use ($server) { + $state = $this->getState(); + + if (! $state['RESTART']) { + return; + } + + $this->components->info('Stopping Reverb server.'); + + $this->gracefullyDisconnect(); + + $server->stop(); + + $this->destroyState(); + }); + } + + /** + * Gracefully disconnect all connections. + */ + protected function gracefullyDisconnect(): void + { + $this->laravel->make(ApplicationProvider::class) + ->all() + ->each(function (Application $application) { + collect( + $this->laravel->make(ChannelManager::class) + ->for($application) + ->connections() + )->each(fn (ChannelConnection $connection) => $connection->disconnect()); + }); + } } diff --git a/src/Servers/Reverb/ReverbProvider.php b/src/Servers/Reverb/ReverbProvider.php index fbcb9838..2f35f01b 100644 --- a/src/Servers/Reverb/ReverbProvider.php +++ b/src/Servers/Reverb/ReverbProvider.php @@ -9,6 +9,7 @@ use Laravel\Reverb\Concerns\InteractsWithAsyncRedis; use Laravel\Reverb\Contracts\ServerProvider; use Laravel\Reverb\Protocols\Pusher\EventDispatcher; +use Laravel\Reverb\Servers\Reverb\Console\Commands\RestartServer; use Laravel\Reverb\Servers\Reverb\Console\Commands\StartServer; use React\EventLoop\LoopInterface; @@ -40,6 +41,7 @@ public function boot(): void Artisan::starting(function ($artisan) { $artisan->resolveCommands([ StartServer::class, + RestartServer::class, ]); }); }