Skip to content

Commit

Permalink
Support cloning RedisClient instance
Browse files Browse the repository at this point in the history
  • Loading branch information
clue committed Feb 15, 2025
1 parent 39aa211 commit a5591b3
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 0 deletions.
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
49 changes: 49 additions & 0 deletions src/RedisClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -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>
*/
Expand Down
42 changes: 42 additions & 0 deletions tests/FunctionalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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->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->ping();

$this->redis->close();

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

0 comments on commit a5591b3

Please sign in to comment.