Skip to content

Support cloning RedisClient instance #172

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

Merged
merged 1 commit into from
Feb 22, 2025
Merged
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
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -46,6 +46,7 @@ It enables you to set and query its data or use its PubSub topics to react to in
* [API](#api)
* [RedisClient](#redisclient)
* [__construct()](#__construct)
* [__clone()](#__clone)
* [__call()](#__call)
* [callAsync()](#callasync)
* [end()](#end)
@@ -415,6 +416,42 @@ $connector = new React\Socket\Connector([
$redis = new Clue\React\Redis\RedisClient('localhost', $connector);
```

#### __clone()

The `__clone()` method is a magic method in PHP that is called
automatically when a `RedisClient` instance is being cloned:

```php
$original = new Clue\React\Redis\RedisClient($uri);
$redis = clone $original;
```

This method ensures the cloned client is created in a "fresh" state and
any connection state is reset on the clone, matching how a new instance
would start after returning from its constructor. Accordingly, the clone
will always start in an unconnected and unclosed state, with no event
listeners attached and ready to accept commands. Invoking any of the
[commands](#commands) will establish a new connection as usual:

```php
$redis = clone $original;
$redis->set('name', 'Alice');
```

This can be especially useful if the original connection is used for a
[PubSub subscription](#pubsub) or when using blocking commands or similar
and you need a control connection that is not affected by any of this.
Both instances will not be directly affected by any operations performed,
for example you can [`close()`](#close) either instance without also
closing the other. Similarly, you can also clone a fresh instance from a
closed state or overwrite a dead connection:

```php
$redis->close();
$redis = clone $redis;
$redis->set('name', 'Alice');
```

#### __call()

The `__call(string $name, list<string|int|float> $args): PromiseInterface<mixed>` method can be used to
49 changes: 49 additions & 0 deletions src/RedisClient.php
Original file line number Diff line number Diff line change
@@ -90,6 +90,55 @@ public function __construct(string $uri, ?ConnectorInterface $connector = null)
$this->factory = new Factory($connector);
}

/**
* The `__clone()` method is a magic method in PHP that is called
* automatically when a `RedisClient` instance is being cloned:
*
* ```php
* $original = new Clue\React\Redis\RedisClient($uri);
* $redis = clone $original;
* ```
*
* This method ensures the cloned client is created in a "fresh" state and
* any connection state is reset on the clone, matching how a new instance
* would start after returning from its constructor. Accordingly, the clone
* will always start in an unconnected and unclosed state, with no event
* listeners attached and ready to accept commands. Invoking any of the
* [commands](#commands) will establish a new connection as usual:
*
* ```php
* $redis = clone $original;
* $redis->set('name', 'Alice');
* ```
*
* This can be especially useful if the original connection is used for a
* [PubSub subscription](#pubsub) or when using blocking commands or similar
* and you need a control connection that is not affected by any of this.
* Both instances will not be directly affected by any operations performed,
* for example you can [`close()`](#close) either instance without also
* closing the other. Similarly, you can also clone a fresh instance from a
* closed state or overwrite a dead connection:
*
* ```php
* $redis->close();
* $redis = clone $redis;
* $redis->set('name', 'Alice');
* ```
*
* @return void
* @throws void
*/
public function __clone()
{
$this->closed = false;
$this->promise = null;
$this->idleTimer = null;
$this->pending = 0;
$this->subscribed = [];
$this->psubscribed = [];
$this->removeAllListeners();
}

/**
* @return PromiseInterface<StreamingClient>
*/
42 changes: 42 additions & 0 deletions tests/FunctionalTest.php
Original file line number Diff line number Diff line change
@@ -176,4 +176,46 @@ public function testClose(): void

$redis->get('willBeRejectedRightAway')->then(null, $this->expectCallableOnce());
}

public function testCloneWhenOriginalIsIdleReturnsClientThatWillCloseIndependently(): void
{
$prefix = 'test:' . mt_rand() . ':';
$original = new RedisClient($this->uri);

$this->assertNull(await($original->callAsync('GET', $prefix . 'doesnotexist')));

$redis = clone $original;

$this->assertNull(await($redis->callAsync('GET', $prefix . 'doesnotexist')));
}

public function testCloneWhenOriginalIsPendingReturnsClientThatWillCloseIndependently(): void
{
$prefix = 'test:' . mt_rand() . ':';
$original = new RedisClient($this->uri);

$this->assertNull(await($original->callAsync('GET', $prefix . 'doesnotexist')));
$promise = $original->callAsync('GET', $prefix . 'doesnotexist');

$redis = clone $original;

$this->assertNull(await($redis->callAsync('GET', $prefix . 'doesnotexist')));
$this->assertNull(await($promise));
}

public function testCloneReturnsClientNotAffectedByPubSubSubscriptions(): void
{
$prefix = 'test:' . mt_rand() . ':';
$consumer = new RedisClient($this->uri);

$consumer->on('message', $this->expectCallableNever());
$consumer->on('pmessage', $this->expectCallableNever());
await($consumer->callAsync('SUBSCRIBE', $prefix . 'demo'));
await($consumer->callAsync('PSUBSCRIBE', $prefix . '*'));

$redis = clone $consumer;
$consumer->close();

$this->assertNull(await($redis->callAsync('GET', $prefix . 'doesnotexist')));
}
}
32 changes: 32 additions & 0 deletions tests/RedisClientTest.php
Original file line number Diff line number Diff line change
@@ -836,4 +836,36 @@ public function testBlpopWillRejectWhenUnderlyingClientClosesWhileWaitingForResp

$promise->then(null, $this->expectCallableOnceWith($e));
}

public function testCloneClosedClientReturnsClientThatWillCreateNewConnectionForFirstCommand(): void
{
$this->redis->close();

$redis = clone $this->redis;

$deferred = new Deferred($this->expectCallableNever());
$this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise());

$promise = $redis->callAsync('PING');

$promise->then($this->expectCallableNever(), $this->expectCallableNever());
}

public function testCloneClientReturnsClientThatWillNotBeAffectedByOldClientClosing(): void
{
$this->redis->on('close', $this->expectCallableOnce());

$redis = clone $this->redis;

$this->assertEquals([], $redis->listeners());

$deferred = new Deferred($this->expectCallableNever());
$this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise());

$promise = $redis->callAsync('PING');

$this->redis->close();

$promise->then($this->expectCallableNever(), $this->expectCallableNever());
}
}