Merge pull request #523 from beyondcode/feature/pcntl
[feature] PCNTL signals into soft-close of connections for Redis horizontal replication
This commit is contained in:
commit
a1d7d974d8
|
|
@ -56,6 +56,9 @@
|
||||||
"orchestra/database": "^4.0|^5.0|^6.0",
|
"orchestra/database": "^4.0|^5.0|^6.0",
|
||||||
"phpunit/phpunit": "^8.0|^9.0"
|
"phpunit/phpunit": "^8.0|^9.0"
|
||||||
},
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-pcntl": "Running the server needs pcntl to listen to command signals and soft-shutdown."
|
||||||
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"BeyondCode\\LaravelWebSockets\\": "src/"
|
"BeyondCode\\LaravelWebSockets\\": "src/"
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,13 @@ class LocalChannelManager implements ChannelManager
|
||||||
*/
|
*/
|
||||||
protected $users = [];
|
protected $users = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wether the current instance accepts new connections.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
protected $acceptsNewConnections = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new channel manager instance.
|
* Create a new channel manager instance.
|
||||||
*
|
*
|
||||||
|
|
@ -71,6 +78,28 @@ class LocalChannelManager implements ChannelManager
|
||||||
return $this->channels[$appId][$channel];
|
return $this->channels[$appId][$channel];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the local connections, regardless of the channel
|
||||||
|
* they are connected to.
|
||||||
|
*
|
||||||
|
* @return \React\Promise\PromiseInterface
|
||||||
|
*/
|
||||||
|
public function getLocalConnections(): PromiseInterface
|
||||||
|
{
|
||||||
|
$connections = collect($this->channels)
|
||||||
|
->map(function ($channelsWithConnections, $appId) {
|
||||||
|
return collect($channelsWithConnections)->values();
|
||||||
|
})
|
||||||
|
->values()->collapse()
|
||||||
|
->map(function ($channel) {
|
||||||
|
return collect($channel->getConnections());
|
||||||
|
})
|
||||||
|
->values()->collapse()
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
return new FulfilledPromise($connections);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all channels for a specific app
|
* Get all channels for a specific app
|
||||||
* for the current instance.
|
* for the current instance.
|
||||||
|
|
@ -313,6 +342,50 @@ class LocalChannelManager implements ChannelManager
|
||||||
return new FulfilledPromise($results);
|
return new FulfilledPromise($results);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep tracking the connections availability when they pong.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function connectionPonged(ConnectionInterface $connection): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the obsolete connections that didn't ponged in a while.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function removeObsoleteConnections(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the current instance as unable to accept new connections.
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function declineNewConnections()
|
||||||
|
{
|
||||||
|
$this->acceptsNewConnections = false;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the current server instance
|
||||||
|
* accepts new connections.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function acceptsNewConnections(): bool
|
||||||
|
{
|
||||||
|
return $this->acceptsNewConnections;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the channel class by the channel name.
|
* Get the channel class by the channel name.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,13 @@
|
||||||
namespace BeyondCode\LaravelWebSockets\ChannelManagers;
|
namespace BeyondCode\LaravelWebSockets\ChannelManagers;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Channels\Channel;
|
use BeyondCode\LaravelWebSockets\Channels\Channel;
|
||||||
|
use BeyondCode\LaravelWebSockets\Helpers;
|
||||||
|
use BeyondCode\LaravelWebSockets\Server\MockableConnection;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Clue\React\Redis\Client;
|
use Clue\React\Redis\Client;
|
||||||
use Clue\React\Redis\Factory;
|
use Clue\React\Redis\Factory;
|
||||||
|
use Illuminate\Cache\RedisLock;
|
||||||
|
use Illuminate\Support\Facades\Redis;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
use React\EventLoop\LoopInterface;
|
use React\EventLoop\LoopInterface;
|
||||||
|
|
@ -41,6 +46,21 @@ class RedisChannelManager extends LocalChannelManager
|
||||||
*/
|
*/
|
||||||
protected $subscribeClient;
|
protected $subscribeClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Redis manager instance.
|
||||||
|
*
|
||||||
|
* @var \Illuminate\Redis\RedisManager
|
||||||
|
*/
|
||||||
|
protected $redis;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The lock name to use on Redis to avoid multiple
|
||||||
|
* actions that might lead to multiple processings.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected static $redisLockName = 'laravel-websockets:channel-manager:lock';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new channel manager instance.
|
* Create a new channel manager instance.
|
||||||
*
|
*
|
||||||
|
|
@ -52,6 +72,10 @@ class RedisChannelManager extends LocalChannelManager
|
||||||
{
|
{
|
||||||
$this->loop = $loop;
|
$this->loop = $loop;
|
||||||
|
|
||||||
|
$this->redis = Redis::connection(
|
||||||
|
config('websockets.replication.modes.redis.connection', 'default')
|
||||||
|
);
|
||||||
|
|
||||||
$connectionUri = $this->getConnectionUri();
|
$connectionUri = $this->getConnectionUri();
|
||||||
|
|
||||||
$factoryClass = $factoryClass ?: Factory::class;
|
$factoryClass = $factoryClass ?: Factory::class;
|
||||||
|
|
@ -67,6 +91,17 @@ class RedisChannelManager extends LocalChannelManager
|
||||||
$this->serverId = Str::uuid()->toString();
|
$this->serverId = Str::uuid()->toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the local connections, regardless of the channel
|
||||||
|
* they are connected to.
|
||||||
|
*
|
||||||
|
* @return \React\Promise\PromiseInterface
|
||||||
|
*/
|
||||||
|
public function getLocalConnections(): PromiseInterface
|
||||||
|
{
|
||||||
|
return parent::getLocalConnections();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all channels for a specific app
|
* Get all channels for a specific app
|
||||||
* for the current instance.
|
* for the current instance.
|
||||||
|
|
@ -108,9 +143,9 @@ class RedisChannelManager extends LocalChannelManager
|
||||||
$connection, $channel, new stdClass
|
$connection, $channel, new stdClass
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
})->then(function () use ($connection) {
|
||||||
|
parent::unsubscribeFromAllChannels($connection);
|
||||||
});
|
});
|
||||||
|
|
||||||
parent::unsubscribeFromAllChannels($connection);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -130,6 +165,8 @@ class RedisChannelManager extends LocalChannelManager
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$this->addConnectionToSet($connection);
|
||||||
|
|
||||||
$this->addChannelToSet(
|
$this->addChannelToSet(
|
||||||
$connection->app->id, $channelName
|
$connection->app->id, $channelName
|
||||||
);
|
);
|
||||||
|
|
@ -156,8 +193,14 @@ class RedisChannelManager extends LocalChannelManager
|
||||||
if ($count === 0) {
|
if ($count === 0) {
|
||||||
$this->unsubscribeFromTopic($connection->app->id, $channelName);
|
$this->unsubscribeFromTopic($connection->app->id, $channelName);
|
||||||
|
|
||||||
|
$this->removeUserData(
|
||||||
|
$connection->app->id, $channelName, $connection->socketId
|
||||||
|
);
|
||||||
|
|
||||||
$this->removeChannelFromSet($connection->app->id, $channelName);
|
$this->removeChannelFromSet($connection->app->id, $channelName);
|
||||||
|
|
||||||
|
$this->removeConnectionFromSet($connection);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,7 +211,13 @@ class RedisChannelManager extends LocalChannelManager
|
||||||
if ($count < 1) {
|
if ($count < 1) {
|
||||||
$this->unsubscribeFromTopic($connection->app->id, $channelName);
|
$this->unsubscribeFromTopic($connection->app->id, $channelName);
|
||||||
|
|
||||||
|
$this->removeUserData(
|
||||||
|
$connection->app->id, $channelName, $connection->socketId
|
||||||
|
);
|
||||||
|
|
||||||
$this->removeChannelFromSet($connection->app->id, $channelName);
|
$this->removeChannelFromSet($connection->app->id, $channelName);
|
||||||
|
|
||||||
|
$this->removeConnectionFromSet($connection);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -293,12 +342,8 @@ class RedisChannelManager extends LocalChannelManager
|
||||||
{
|
{
|
||||||
return $this->publishClient
|
return $this->publishClient
|
||||||
->hgetall($this->getRedisKey($appId, $channel, ['users']))
|
->hgetall($this->getRedisKey($appId, $channel, ['users']))
|
||||||
->then(function ($members) {
|
->then(function ($list) {
|
||||||
[$keys, $values] = collect($members)->partition(function ($value, $key) {
|
return collect(Helpers::redisListToArray($list))
|
||||||
return $key % 2 === 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
return collect(array_combine($keys->all(), $values->all()))
|
|
||||||
->map(function ($user) {
|
->map(function ($user) {
|
||||||
return json_decode($user);
|
return json_decode($user);
|
||||||
})
|
})
|
||||||
|
|
@ -344,6 +389,43 @@ class RedisChannelManager extends LocalChannelManager
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep tracking the connections availability when they pong.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function connectionPonged(ConnectionInterface $connection): bool
|
||||||
|
{
|
||||||
|
// This will update the score with the current timestamp.
|
||||||
|
$this->addConnectionToSet($connection);
|
||||||
|
|
||||||
|
return parent::connectionPonged($connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the obsolete connections that didn't ponged in a while.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function removeObsoleteConnections(): bool
|
||||||
|
{
|
||||||
|
$this->lock()->get(function () {
|
||||||
|
$this->getConnectionsFromSet(0, now()->subMinutes(2)->format('U'))
|
||||||
|
->then(function ($connections) {
|
||||||
|
foreach ($connections as $connection => $score) {
|
||||||
|
[$appId, $socketId] = explode(':', $connection);
|
||||||
|
|
||||||
|
$this->unsubscribeFromAllChannels(
|
||||||
|
$this->fakeConnectionForApp($appId, $socketId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return parent::removeObsoleteConnections();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle a message received from Redis on a specific channel.
|
* Handle a message received from Redis on a specific channel.
|
||||||
*
|
*
|
||||||
|
|
@ -462,6 +544,57 @@ class RedisChannelManager extends LocalChannelManager
|
||||||
return $this->incrementSubscriptionsCount($appId, $channel, $increment * -1);
|
return $this->incrementSubscriptionsCount($appId, $channel, $increment * -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the connection to the sorted list.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @param \DateTime|string|null $moment
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addConnectionToSet(ConnectionInterface $connection, $moment = null)
|
||||||
|
{
|
||||||
|
$this->getPublishClient()
|
||||||
|
->zadd(
|
||||||
|
$this->getRedisKey(null, null, ['sockets']),
|
||||||
|
Carbon::parse($moment)->format('U'), "{$connection->app->id}:{$connection->socketId}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the connection from the sorted list.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function removeConnectionFromSet(ConnectionInterface $connection)
|
||||||
|
{
|
||||||
|
$this->getPublishClient()
|
||||||
|
->zrem(
|
||||||
|
$this->getRedisKey(null, null, ['sockets']),
|
||||||
|
"{$connection->app->id}:{$connection->socketId}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the connections from the sorted list, with last
|
||||||
|
* connection between certain timestamps.
|
||||||
|
*
|
||||||
|
* @param int $start
|
||||||
|
* @param int $stop
|
||||||
|
* @return PromiseInterface
|
||||||
|
*/
|
||||||
|
public function getConnectionsFromSet(int $start = 0, int $stop = 0)
|
||||||
|
{
|
||||||
|
return $this->getPublishClient()
|
||||||
|
->zrange(
|
||||||
|
$this->getRedisKey(null, null, ['sockets']),
|
||||||
|
$start, $stop, 'withscores'
|
||||||
|
)
|
||||||
|
->then(function ($list) {
|
||||||
|
return Helpers::redisListToArray($list);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a channel to the set list.
|
* Add a channel to the set list.
|
||||||
*
|
*
|
||||||
|
|
@ -555,11 +688,11 @@ class RedisChannelManager extends LocalChannelManager
|
||||||
* Get the Redis Keyspace name to handle subscriptions
|
* Get the Redis Keyspace name to handle subscriptions
|
||||||
* and other key-value sets.
|
* and other key-value sets.
|
||||||
*
|
*
|
||||||
* @param mixed $appId
|
* @param string|int|null $appId
|
||||||
* @param string|null $channel
|
* @param string|null $channel
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function getRedisKey($appId, string $channel = null, array $suffixes = []): string
|
public function getRedisKey($appId = null, string $channel = null, array $suffixes = []): string
|
||||||
{
|
{
|
||||||
$prefix = config('database.redis.options.prefix', null);
|
$prefix = config('database.redis.options.prefix', null);
|
||||||
|
|
||||||
|
|
@ -577,4 +710,28 @@ class RedisChannelManager extends LocalChannelManager
|
||||||
|
|
||||||
return $hash;
|
return $hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a new RedisLock instance to avoid race conditions.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Cache\CacheLock
|
||||||
|
*/
|
||||||
|
protected function lock()
|
||||||
|
{
|
||||||
|
return new RedisLock($this->redis, static::$redisLockName, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a fake connection for app that will mimick a connection
|
||||||
|
* by app ID and Socket ID to be able to be passed to the methods
|
||||||
|
* that accepts a connection class.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @param string $socketId
|
||||||
|
* @return ConnectionInterface
|
||||||
|
*/
|
||||||
|
public function fakeConnectionForApp($appId, string $socketId)
|
||||||
|
{
|
||||||
|
return new MockableConnection($appId, $socketId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ class StartServer extends Command
|
||||||
{--disable-statistics : Disable the statistics tracking.}
|
{--disable-statistics : Disable the statistics tracking.}
|
||||||
{--statistics-interval= : The amount of seconds to tick between statistics saving.}
|
{--statistics-interval= : The amount of seconds to tick between statistics saving.}
|
||||||
{--debug : Forces the loggers to be enabled and thereby overriding the APP_DEBUG setting.}
|
{--debug : Forces the loggers to be enabled and thereby overriding the APP_DEBUG setting.}
|
||||||
{--test : Prepare the server, but do not start it.}
|
{--loop : Programatically inject the loop.}
|
||||||
';
|
';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -79,6 +79,10 @@ class StartServer extends Command
|
||||||
|
|
||||||
$this->configureRoutes();
|
$this->configureRoutes();
|
||||||
|
|
||||||
|
$this->configurePcntlSignal();
|
||||||
|
|
||||||
|
$this->configurePongTracker();
|
||||||
|
|
||||||
$this->startServer();
|
$this->startServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,7 +145,7 @@ class StartServer extends Command
|
||||||
|
|
||||||
$this->loop->addPeriodicTimer(10, function () {
|
$this->loop->addPeriodicTimer(10, function () {
|
||||||
if ($this->getLastRestart() !== $this->lastRestart) {
|
if ($this->getLastRestart() !== $this->lastRestart) {
|
||||||
$this->loop->stop();
|
$this->triggerSoftShutdown();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -156,6 +160,46 @@ class StartServer extends Command
|
||||||
WebSocketRouter::routes();
|
WebSocketRouter::routes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the PCNTL signals for soft shutdown.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function configurePcntlSignal()
|
||||||
|
{
|
||||||
|
// When the process receives a SIGTERM or a SIGINT
|
||||||
|
// signal, it should mark the server as unavailable
|
||||||
|
// to receive new connections, close the current connections,
|
||||||
|
// then stopping the loop.
|
||||||
|
|
||||||
|
$this->loop->addSignal(SIGTERM, function () {
|
||||||
|
$this->line('Closing existing connections...');
|
||||||
|
|
||||||
|
$this->triggerSoftShutdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->loop->addSignal(SIGINT, function () {
|
||||||
|
$this->line('Closing existing connections...');
|
||||||
|
|
||||||
|
$this->triggerSoftShutdown();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the tracker that will delete
|
||||||
|
* from the store the connections that.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function configurePongTracker()
|
||||||
|
{
|
||||||
|
$this->loop->addPeriodicTimer(10, function () {
|
||||||
|
$this->laravel
|
||||||
|
->make(ChannelManager::class)
|
||||||
|
->removeObsoleteConnections();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configure the HTTP logger class.
|
* Configure the HTTP logger class.
|
||||||
*
|
*
|
||||||
|
|
@ -209,14 +253,6 @@ class StartServer extends Command
|
||||||
|
|
||||||
$this->buildServer();
|
$this->buildServer();
|
||||||
|
|
||||||
// For testing, just boot up the server, run it
|
|
||||||
// but exit after the next tick.
|
|
||||||
if ($this->option('test')) {
|
|
||||||
$this->loop->futureTick(function () {
|
|
||||||
$this->loop->stop();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->server->run();
|
$this->server->run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -231,6 +267,10 @@ class StartServer extends Command
|
||||||
$this->option('host'), $this->option('port')
|
$this->option('host'), $this->option('port')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if ($loop = $this->option('loop')) {
|
||||||
|
$this->loop = $loop;
|
||||||
|
}
|
||||||
|
|
||||||
$this->server = $this->server
|
$this->server = $this->server
|
||||||
->setLoop($this->loop)
|
->setLoop($this->loop)
|
||||||
->withRoutes(WebSocketRouter::getRoutes())
|
->withRoutes(WebSocketRouter::getRoutes())
|
||||||
|
|
@ -249,4 +289,29 @@ class StartServer extends Command
|
||||||
'beyondcode:websockets:restart', 0
|
'beyondcode:websockets:restart', 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger a soft shutdown for the process.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function triggerSoftShutdown()
|
||||||
|
{
|
||||||
|
$channelManager = $this->laravel->make(ChannelManager::class);
|
||||||
|
|
||||||
|
// Close the new connections allowance on this server.
|
||||||
|
$channelManager->declineNewConnections();
|
||||||
|
|
||||||
|
// Get all local connections and close them. They will
|
||||||
|
// be automatically be unsubscribed from all channels.
|
||||||
|
$channelManager->getLocalConnections()
|
||||||
|
->then(function ($connections) {
|
||||||
|
foreach ($connections as $connection) {
|
||||||
|
$connection->close();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->then(function () {
|
||||||
|
$this->loop->stop();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,14 @@ interface ChannelManager
|
||||||
*/
|
*/
|
||||||
public function findOrCreate($appId, string $channel);
|
public function findOrCreate($appId, string $channel);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the local connections, regardless of the channel
|
||||||
|
* they are connected to.
|
||||||
|
*
|
||||||
|
* @return \React\Promise\PromiseInterface
|
||||||
|
*/
|
||||||
|
public function getLocalConnections(): PromiseInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all channels for a specific app
|
* Get all channels for a specific app
|
||||||
* for the current instance.
|
* for the current instance.
|
||||||
|
|
@ -177,4 +185,19 @@ interface ChannelManager
|
||||||
* @return \React\Promise\PromiseInterface
|
* @return \React\Promise\PromiseInterface
|
||||||
*/
|
*/
|
||||||
public function getChannelsMembersCount($appId, array $channelNames): PromiseInterface;
|
public function getChannelsMembersCount($appId, array $channelNames): PromiseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep tracking the connections availability when they pong.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function connectionPonged(ConnectionInterface $connection): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the obsolete connections that didn't ponged in a while.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function removeObsoleteConnections(): bool;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets;
|
||||||
|
|
||||||
|
class Helpers
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the Redis' list of key after value
|
||||||
|
* to key-value pairs.
|
||||||
|
*
|
||||||
|
* @param array $list
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function redisListToArray(array $list)
|
||||||
|
{
|
||||||
|
// Redis lists come into a format where the keys are on even indexes
|
||||||
|
// and the values are on odd indexes. This way, we know which
|
||||||
|
// ones are keys and which ones are values and their get combined
|
||||||
|
// later to form the key => value array.
|
||||||
|
[$keys, $values] = collect($list)->partition(function ($value, $key) {
|
||||||
|
return $key % 2 === 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return array_combine($keys->all(), $values->all());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -34,6 +34,8 @@ class PusherChannelProtocolMessage extends PusherClientMessage
|
||||||
$connection->send(json_encode([
|
$connection->send(json_encode([
|
||||||
'event' => 'pusher:pong',
|
'event' => 'pusher:pong',
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
$this->channelManager->connectionPonged($connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Server;
|
||||||
|
|
||||||
|
use Ratchet\ConnectionInterface;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
class MockableConnection implements ConnectionInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a new Mockable connection.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @param string $socketId
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct($appId, string $socketId)
|
||||||
|
{
|
||||||
|
$this->app = new stdClass;
|
||||||
|
|
||||||
|
$this->app->id = $appId;
|
||||||
|
$this->socketId = $socketId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send data to the connection.
|
||||||
|
*
|
||||||
|
* @param string $data
|
||||||
|
* @return \Ratchet\ConnectionInterface
|
||||||
|
*/
|
||||||
|
public function send($data)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the connection.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function close()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -39,6 +39,10 @@ class WebSocketHandler implements MessageComponentInterface
|
||||||
*/
|
*/
|
||||||
public function onOpen(ConnectionInterface $connection)
|
public function onOpen(ConnectionInterface $connection)
|
||||||
{
|
{
|
||||||
|
if (! $this->connectionCanBeMade($connection)) {
|
||||||
|
return $connection->close();
|
||||||
|
}
|
||||||
|
|
||||||
$this->verifyAppKey($connection)
|
$this->verifyAppKey($connection)
|
||||||
->verifyOrigin($connection)
|
->verifyOrigin($connection)
|
||||||
->limitConcurrentConnections($connection)
|
->limitConcurrentConnections($connection)
|
||||||
|
|
@ -69,6 +73,10 @@ class WebSocketHandler implements MessageComponentInterface
|
||||||
*/
|
*/
|
||||||
public function onMessage(ConnectionInterface $connection, MessageInterface $message)
|
public function onMessage(ConnectionInterface $connection, MessageInterface $message)
|
||||||
{
|
{
|
||||||
|
if (! isset($connection->app)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Messages\PusherMessageFactory::createForMessage(
|
Messages\PusherMessageFactory::createForMessage(
|
||||||
$message, $connection, $this->channelManager
|
$message, $connection, $this->channelManager
|
||||||
)->respond();
|
)->respond();
|
||||||
|
|
@ -113,6 +121,18 @@ class WebSocketHandler implements MessageComponentInterface
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the connection can be made for the
|
||||||
|
* current server instance.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function connectionCanBeMade(ConnectionInterface $connection): bool
|
||||||
|
{
|
||||||
|
return $this->channelManager->acceptsNewConnections();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify the app key validity.
|
* Verify the app key validity.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Statistics\Collectors;
|
namespace BeyondCode\LaravelWebSockets\Statistics\Collectors;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Helpers;
|
||||||
use BeyondCode\LaravelWebSockets\Statistics\Statistic;
|
use BeyondCode\LaravelWebSockets\Statistics\Statistic;
|
||||||
use Illuminate\Cache\RedisLock;
|
use Illuminate\Cache\RedisLock;
|
||||||
use Illuminate\Support\Facades\Redis;
|
use Illuminate\Support\Facades\Redis;
|
||||||
|
|
@ -30,7 +31,7 @@ class RedisCollector extends MemoryCollector
|
||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected static $redisLockName = 'laravel-websockets:lock';
|
protected static $redisLockName = 'laravel-websockets:collector:lock';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the logger.
|
* Initialize the logger.
|
||||||
|
|
@ -178,7 +179,7 @@ class RedisCollector extends MemoryCollector
|
||||||
}
|
}
|
||||||
|
|
||||||
$statistic = $this->arrayToStatisticInstance(
|
$statistic = $this->arrayToStatisticInstance(
|
||||||
$appId, $this->redisListToArray($list)
|
$appId, Helpers::redisListToArray($list)
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->createRecord($statistic, $appId);
|
$this->createRecord($statistic, $appId);
|
||||||
|
|
@ -229,7 +230,7 @@ class RedisCollector extends MemoryCollector
|
||||||
->hgetall($this->channelManager->getRedisKey($appId, null, ['stats']))
|
->hgetall($this->channelManager->getRedisKey($appId, null, ['stats']))
|
||||||
->then(function ($list) use ($appId, &$appsWithStatistics) {
|
->then(function ($list) use ($appId, &$appsWithStatistics) {
|
||||||
$appsWithStatistics[$appId] = $this->arrayToStatisticInstance(
|
$appsWithStatistics[$appId] = $this->arrayToStatisticInstance(
|
||||||
$appId, $this->redisListToArray($list)
|
$appId, Helpers::redisListToArray($list)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -251,7 +252,7 @@ class RedisCollector extends MemoryCollector
|
||||||
->hgetall($this->channelManager->getRedisKey($appId, null, ['stats']))
|
->hgetall($this->channelManager->getRedisKey($appId, null, ['stats']))
|
||||||
->then(function ($list) use ($appId) {
|
->then(function ($list) use ($appId) {
|
||||||
return $this->arrayToStatisticInstance(
|
return $this->arrayToStatisticInstance(
|
||||||
$appId, $this->redisListToArray($list)
|
$appId, Helpers::redisListToArray($list)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -361,26 +362,6 @@ class RedisCollector extends MemoryCollector
|
||||||
return new RedisLock($this->redis, static::$redisLockName, 0);
|
return new RedisLock($this->redis, static::$redisLockName, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform the Redis' list of key after value
|
|
||||||
* to key-value pairs.
|
|
||||||
*
|
|
||||||
* @param array $list
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
protected function redisListToArray(array $list)
|
|
||||||
{
|
|
||||||
// Redis lists come into a format where the keys are on even indexes
|
|
||||||
// and the values are on odd indexes. This way, we know which
|
|
||||||
// ones are keys and which ones are values and their get combined
|
|
||||||
// later to form the key => value array.
|
|
||||||
[$keys, $values] = collect($list)->partition(function ($value, $key) {
|
|
||||||
return $key % 2 === 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
return array_combine($keys->all(), $values->all());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform a key-value pair to a Statistic instance.
|
* Transform a key-value pair to a Statistic instance.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,43 @@ class StartServerTest extends TestCase
|
||||||
{
|
{
|
||||||
public function test_does_not_fail_if_building_up()
|
public function test_does_not_fail_if_building_up()
|
||||||
{
|
{
|
||||||
$this->artisan('websockets:serve', ['--test' => true, '--debug' => true]);
|
$this->loop->futureTick(function () {
|
||||||
|
$this->loop->stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->artisan('websockets:serve', ['--loop' => $this->loop, '--debug' => true, '--port' => 6001]);
|
||||||
|
|
||||||
|
$this->assertTrue(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_pcntl_sigint_signal()
|
||||||
|
{
|
||||||
|
$this->loop->futureTick(function () {
|
||||||
|
$this->newActiveConnection(['public-channel']);
|
||||||
|
$this->newActiveConnection(['public-channel']);
|
||||||
|
|
||||||
|
posix_kill(posix_getpid(), SIGINT);
|
||||||
|
|
||||||
|
$this->loop->stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->artisan('websockets:serve', ['--loop' => $this->loop, '--debug' => true, '--port' => 6002]);
|
||||||
|
|
||||||
|
$this->assertTrue(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_pcntl_sigterm_signal()
|
||||||
|
{
|
||||||
|
$this->loop->futureTick(function () {
|
||||||
|
$this->newActiveConnection(['public-channel']);
|
||||||
|
$this->newActiveConnection(['public-channel']);
|
||||||
|
|
||||||
|
posix_kill(posix_getpid(), SIGTERM);
|
||||||
|
|
||||||
|
$this->loop->stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->artisan('websockets:serve', ['--loop' => $this->loop, '--debug' => true, '--port' => 6003]);
|
||||||
|
|
||||||
$this->assertTrue(true);
|
$this->assertTrue(true);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,4 +108,21 @@ class ConnectionTest extends TestCase
|
||||||
->assertSentEvent('pusher:error', ['data' => ['message' => 'Over capacity', 'code' => 4100]])
|
->assertSentEvent('pusher:error', ['data' => ['message' => 'Over capacity', 'code' => 4100]])
|
||||||
->assertClosed();
|
->assertClosed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_close_all_new_connections_after_stating_the_server_does_not_accept_new_connections()
|
||||||
|
{
|
||||||
|
$this->newActiveConnection(['test-channel'])
|
||||||
|
->assertSentEvent('pusher:connection_established')
|
||||||
|
->assertSentEvent('pusher_internal:subscription_succeeded');
|
||||||
|
|
||||||
|
$this->channelManager->declineNewConnections();
|
||||||
|
|
||||||
|
$this->assertFalse(
|
||||||
|
$this->channelManager->acceptsNewConnections()
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->newActiveConnection(['test-channel'])
|
||||||
|
->assertNothingSent()
|
||||||
|
->assertClosed();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,18 @@ class Connection implements ConnectionInterface
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that no events occured within the connection.
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function assertNothingSent()
|
||||||
|
{
|
||||||
|
PHPUnit::assertEquals([], $this->sentData);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert the connection is closed.
|
* Assert the connection is closed.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace BeyondCode\LaravelWebSockets\Test;
|
namespace BeyondCode\LaravelWebSockets\Test;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
use BeyondCode\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
||||||
|
use Ratchet\ConnectionInterface;
|
||||||
|
|
||||||
class PresenceChannelTest extends TestCase
|
class PresenceChannelTest extends TestCase
|
||||||
{
|
{
|
||||||
|
|
@ -185,4 +186,22 @@ class PresenceChannelTest extends TestCase
|
||||||
], $statistic->toArray());
|
], $statistic->toArray());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_local_connections_for_private_channels()
|
||||||
|
{
|
||||||
|
$this->newPresenceConnection('presence-channel', ['user_id' => 1]);
|
||||||
|
$this->newPresenceConnection('presence-channel-2', ['user_id' => 2]);
|
||||||
|
|
||||||
|
$this->channelManager
|
||||||
|
->getLocalConnections()
|
||||||
|
->then(function ($connections) {
|
||||||
|
$this->assertCount(2, $connections);
|
||||||
|
|
||||||
|
foreach ($connections as $connection) {
|
||||||
|
$this->assertInstanceOf(
|
||||||
|
ConnectionInterface::class, $connection
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace BeyondCode\LaravelWebSockets\Test;
|
namespace BeyondCode\LaravelWebSockets\Test;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
use BeyondCode\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
||||||
|
use Ratchet\ConnectionInterface;
|
||||||
|
|
||||||
class PrivateChannelTest extends TestCase
|
class PrivateChannelTest extends TestCase
|
||||||
{
|
{
|
||||||
|
|
@ -138,4 +139,22 @@ class PrivateChannelTest extends TestCase
|
||||||
], $statistic->toArray());
|
], $statistic->toArray());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_local_connections_for_private_channels()
|
||||||
|
{
|
||||||
|
$this->newPrivateConnection('private-channel');
|
||||||
|
$this->newPrivateConnection('private-channel-2');
|
||||||
|
|
||||||
|
$this->channelManager
|
||||||
|
->getLocalConnections()
|
||||||
|
->then(function ($connections) {
|
||||||
|
$this->assertCount(2, $connections);
|
||||||
|
|
||||||
|
foreach ($connections as $connection) {
|
||||||
|
$this->assertInstanceOf(
|
||||||
|
ConnectionInterface::class, $connection
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Test;
|
namespace BeyondCode\LaravelWebSockets\Test;
|
||||||
|
|
||||||
|
use Ratchet\ConnectionInterface;
|
||||||
|
|
||||||
class PublicChannelTest extends TestCase
|
class PublicChannelTest extends TestCase
|
||||||
{
|
{
|
||||||
public function test_connect_to_public_channel()
|
public function test_connect_to_public_channel()
|
||||||
|
|
@ -114,4 +116,22 @@ class PublicChannelTest extends TestCase
|
||||||
], $statistic->toArray());
|
], $statistic->toArray());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_local_connections_for_public_channels()
|
||||||
|
{
|
||||||
|
$this->newActiveConnection(['public-channel']);
|
||||||
|
$this->newActiveConnection(['public-channel-2']);
|
||||||
|
|
||||||
|
$this->channelManager
|
||||||
|
->getLocalConnections()
|
||||||
|
->then(function ($connections) {
|
||||||
|
$this->assertCount(2, $connections);
|
||||||
|
|
||||||
|
foreach ($connections as $connection) {
|
||||||
|
$this->assertInstanceOf(
|
||||||
|
ConnectionInterface::class, $connection
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,4 +32,106 @@ class ReplicationTest extends TestCase
|
||||||
'data' => ['channel' => 'public-channel', 'test' => 'yes'],
|
'data' => ['channel' => 'public-channel', 'test' => 'yes'],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_not_ponged_connections_do_get_removed_for_public_channels()
|
||||||
|
{
|
||||||
|
$this->runOnlyOnRedisReplication();
|
||||||
|
|
||||||
|
$connection = $this->newActiveConnection(['public-channel']);
|
||||||
|
|
||||||
|
// Make the connection look like it was lost 1 day ago.
|
||||||
|
$this->channelManager->addConnectionToSet($connection, now()->subDays(1));
|
||||||
|
|
||||||
|
$this->channelManager
|
||||||
|
->getConnectionsFromSet(0, now()->subMinutes(2)->format('U'))
|
||||||
|
->then(function ($expiredConnections) {
|
||||||
|
$this->assertCount(1, $expiredConnections);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->channelManager->removeObsoleteConnections();
|
||||||
|
|
||||||
|
$this->channelManager
|
||||||
|
->getGlobalConnectionsCount('1234', 'public-channel')
|
||||||
|
->then(function ($count) {
|
||||||
|
$this->assertEquals(0, $count);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->channelManager
|
||||||
|
->getConnectionsFromSet(0, now()->subMinutes(2)->format('U'))
|
||||||
|
->then(function ($expiredConnections) {
|
||||||
|
$this->assertCount(0, $expiredConnections);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_not_ponged_connections_do_get_removed_for_private_channels()
|
||||||
|
{
|
||||||
|
$this->runOnlyOnRedisReplication();
|
||||||
|
|
||||||
|
$connection = $this->newPrivateConnection('private-channel');
|
||||||
|
|
||||||
|
// Make the connection look like it was lost 1 day ago.
|
||||||
|
$this->channelManager->addConnectionToSet($connection, now()->subDays(1));
|
||||||
|
|
||||||
|
$this->channelManager
|
||||||
|
->getConnectionsFromSet(0, now()->subMinutes(2)->format('U'))
|
||||||
|
->then(function ($expiredConnections) {
|
||||||
|
$this->assertCount(1, $expiredConnections);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->channelManager->removeObsoleteConnections();
|
||||||
|
|
||||||
|
$this->channelManager
|
||||||
|
->getGlobalConnectionsCount('1234', 'private-channel')
|
||||||
|
->then(function ($count) {
|
||||||
|
$this->assertEquals(0, $count);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->channelManager
|
||||||
|
->getConnectionsFromSet(0, now()->subMinutes(2)->format('U'))
|
||||||
|
->then(function ($expiredConnections) {
|
||||||
|
$this->assertCount(0, $expiredConnections);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_not_ponged_connections_do_get_removed_for_presence_channels()
|
||||||
|
{
|
||||||
|
$this->runOnlyOnRedisReplication();
|
||||||
|
|
||||||
|
$connection = $this->newPresenceConnection('presence-channel');
|
||||||
|
|
||||||
|
// Make the connection look like it was lost 1 day ago.
|
||||||
|
$this->channelManager->addConnectionToSet($connection, now()->subDays(1));
|
||||||
|
|
||||||
|
$this->channelManager
|
||||||
|
->getConnectionsFromSet(0, now()->subMinutes(2)->format('U'))
|
||||||
|
->then(function ($expiredConnections) {
|
||||||
|
$this->assertCount(1, $expiredConnections);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->channelManager
|
||||||
|
->getChannelMembers('1234', 'presence-channel')
|
||||||
|
->then(function ($members) {
|
||||||
|
$this->assertCount(1, $members);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->channelManager->removeObsoleteConnections();
|
||||||
|
|
||||||
|
$this->channelManager
|
||||||
|
->getGlobalConnectionsCount('1234', 'private-channel')
|
||||||
|
->then(function ($count) {
|
||||||
|
$this->assertEquals(0, $count);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->channelManager
|
||||||
|
->getConnectionsFromSet(0, now()->subMinutes(2)->format('U'))
|
||||||
|
->then(function ($expiredConnections) {
|
||||||
|
$this->assertCount(0, $expiredConnections);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->channelManager
|
||||||
|
->getChannelMembers('1234', 'presence-channel')
|
||||||
|
->then(function ($members) {
|
||||||
|
$this->assertCount(0, $members);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue