Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1.x] Adds support for reconnecting to Redis if disconnected by server #281

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"illuminate/support": "^10.47|^11.0|^12.0",
"laravel/prompts": "^0.1.15|^0.2.0|^0.3.0",
"pusher/pusher-php-server": "^7.2",
"ratchet/rfc6455": "^0.3.1",
"ratchet/rfc6455": "^0.4",
"react/promise-timer": "^1.10",
"react/socket": "^1.14",
"symfony/console": "^6.0|^7.0",
Expand Down
1 change: 1 addition & 0 deletions config/reverb.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'database' => env('REDIS_DB', '0'),
'timeout' => env('REDIS_TIMEOUT', 60),
],
],
'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15),
Expand Down
16 changes: 16 additions & 0 deletions src/Exceptions/RedisConnectionException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Laravel\Reverb\Exceptions;

use Exception;

class RedisConnectionException extends Exception
{
/**
* Timeout while attempting to connect to Redis.
*/
public static function failedAfter(string $name, int $timeout): self
{
return new static("Failed to connect to Redis connection [{$name}] after retrying for {$timeout}s.");
}
}
1 change: 0 additions & 1 deletion src/Servers/Reverb/Console/Commands/StartServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ protected function ensureHorizontalScalability(LoopInterface $loop): void
{
if ($this->laravel->make(ServerProviderManager::class)->driver('reverb')->subscribesToEvents()) {
$this->laravel->make(PubSubProvider::class)->connect($loop);
$this->laravel->make(PubSubProvider::class)->subscribe();
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/Servers/Reverb/Http/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Laravel\Reverb\Servers\Reverb\Http;

use Closure;
use GuzzleHttp\Psr7\HttpFactory;
use GuzzleHttp\Psr7\Message;
use Illuminate\Support\Arr;
use Laravel\Reverb\Servers\Reverb\Concerns\ClosesConnections;
Expand Down Expand Up @@ -31,7 +32,7 @@ class Router
*/
public function __construct(protected UrlMatcherInterface $matcher)
{
$this->negotiator = new ServerNegotiator(new RequestVerifier);
$this->negotiator = new ServerNegotiator(new RequestVerifier, new HttpFactory);
}

/**
Expand Down
6 changes: 5 additions & 1 deletion src/Servers/Reverb/Http/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ public function __construct(protected ServerInterface $socket, protected Router
*/
public function start(): void
{
$this->loop->run();
try {
$this->loop->run();
} catch (Throwable $e) {
Log::error($e->getMessage());
}
}

/**
Expand Down
219 changes: 219 additions & 0 deletions src/Servers/Reverb/Publishing/RedisClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
<?php

namespace Laravel\Reverb\Servers\Reverb\Publishing;

use Clue\React\Redis\Client;
use Exception;
use Illuminate\Support\Arr;
use Illuminate\Support\ConfigurationUrlParser;
use Illuminate\Support\Facades\Config;
use Laravel\Reverb\Exceptions\RedisConnectionException;
use Laravel\Reverb\Loggers\Log;
use React\EventLoop\LoopInterface;

class RedisClient
{
/**
* Redis connection client.
*
* @var \Clue\React\Redis\Client
*/
protected $client;

/**
* The name of the Redis connection.
*/
protected string $name = 'redis';

/**
* Determine if the client should attempt to reconnect when disconnected from the server.
*/
protected bool $shouldRetry = true;

/**
* Number of seconds the elapsed since attempting to reconnect.
*/
protected int $retryTimer = 0;

/**
* Create a new instance of the Redis client.
*
* @param callable|null $onConnect
*/
public function __construct(
protected LoopInterface $loop,
protected RedisClientFactory $clientFactory,
protected string $channel,
protected array $server,
protected $onConnect = null
) {
//
}

/**
* Create a new connetion to the Redis server.
*/
public function connect(): void
{
$this->clientFactory->make($this->loop, $this->redisUrl())->then(
fn (Client $client) => $this->onConnection($client),
fn (Exception $exception) => $this->onFailedConnection($exception),
);
}

/**
* Attempt to reconnect to the Redis server.
*/
public function reconnect(): void
{
if (! $this->shouldRetry) {
return;
}

$this->loop->addTimer(1, fn () => $this->attemptReconnection());
}

/**
* Disconnect from the Redis server.
*/
public function disconnect(): void
{
$this->shouldRetry = false;

$this->client?->close();
}

/**
* Listen for a given event.
*/
public function on(string $event, callable $callback): void
{
$this->client->on($event, $callback);
}

/**
* Determine if the client is currently connected to the server.
*/
public function isConnected(): bool
{
return (bool) $this->client === true && $this->client instanceof Client;
}

/**
* Handle a connection failure to the Redis server.
*/
protected function configureClientErrorHandler(): void
{
$this->client->on('close', function () {
$this->client = null;

Log::info('Disconnected from Redis', "<fg=red>{$this->name}</>");

$this->reconnect();
});
}

/**
* Handle a successful connection to the Redis server.
*/
protected function onConnection(Client $client): void
{
$this->client = $client;

$this->resetRetryTimer();
$this->configureClientErrorHandler();

if ($this->onConnect) {
call_user_func($this->onConnect, $client);
}

Log::info('Redis connection established', "<fg=green>{$this->name}</>");
}

/**
* Handle a failed connection to the Redis server.
*/
protected function onFailedConnection(Exception $exception): void
{
$this->client = null;

Log::error($exception->getMessage());

$this->reconnect();
}

/**
* Attempt to reconnect to the Redis server until the timeout is reached.
*/
protected function attemptReconnection(): void
{
$this->retryTimer++;

if ($this->retryTimer >= $this->retryTimeout()) {
$exception = RedisConnectionException::failedAfter($this->name, $this->retryTimeout());

Log::error($exception->getMessage());

throw $exception;
}

Log::info('Attempting reconnection to Redis', "<fg=yellow>{$this->name}</>");

$this->connect();
}

/**
* Determine the configured reconnection timeout.
*/
protected function retryTimeout(): int
{
return (int) ($this->server['timeout'] ?? 60);
}

/**
* Reset the retry connection timer.
*/
protected function resetRetryTimer(): void
{
$this->retryTimer = 0;
}

/**
* Get the connection URL for Redis.
*/
protected function redisUrl(): string
{
$config = empty($this->server) ? Config::get('database.redis.default') : $this->server;

$parsed = (new ConfigurationUrlParser)->parseConfiguration($config);

$driver = strtolower($parsed['driver'] ?? '');

if (in_array($driver, ['tcp', 'tls'])) {
$parsed['scheme'] = $driver;
}

[$host, $port, $protocol, $query] = [
$parsed['host'],
$parsed['port'] ?: 6379,
Arr::get($parsed, 'scheme') === 'tls' ? 's' : '',
[],
];

if ($parsed['username'] ?? false) {
$query['username'] = $parsed['username'];
}

if ($parsed['password'] ?? false) {
$query['password'] = $parsed['password'];
}

if ($parsed['database'] ?? false) {
$query['db'] = $parsed['database'];
}

$query = http_build_query($query);

return "redis{$protocol}://{$host}:{$port}".($query ? "?{$query}" : '');
}
}
6 changes: 3 additions & 3 deletions src/Servers/Reverb/Publishing/RedisClientFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@

namespace Laravel\Reverb\Servers\Reverb\Publishing;

use Clue\React\Redis\Client;
use Clue\React\Redis\Factory;
use React\EventLoop\LoopInterface;
use React\Promise\PromiseInterface;

class RedisClientFactory
{
/**
* Create a new Redis client.
*/
public function make(LoopInterface $loop, string $redisUrl): Client
public function make(LoopInterface $loop, string $redisUrl): PromiseInterface
{
return (new Factory($loop))->createLazyClient(
return (new Factory($loop))->createClient(
$redisUrl
);
}
Expand Down
Loading