Added support for transactions in OSS Cluster (#1497)

* Added support for transactions in OSS Cluster

* Updated test cases

* Updated version restriction

* Updated server version restriction

* Added test skip for Relay

* Added test case for Relay

* Set transaction slot to null in case of failed transaction

* Restrict usage of transaction commands outside of transaction context

* Added handling for Relay responses

* Reverted changes

* Removed unsupported command

* Added dummy arguments

* Codestyle fixes

* Added additional test coverage

* Added CHANGELOG and README entries

* Added missing PR reference

* Missing word

* Fixed README
This commit is contained in:
Vladyslav Vildanov
2025-03-07 10:31:02 +02:00
committed by GitHub
parent 86e6804c80
commit 73e6a03fd8
26 changed files with 1449 additions and 84 deletions
+157
View File
@@ -20,6 +20,7 @@ use Predis\Command\CommandInterface;
use Predis\Connection\NodeConnectionInterface;
use Predis\Connection\Parameters;
use Predis\Response;
use Predis\Transaction\Exception\TransactionException;
use PredisTestCase;
use RuntimeException;
@@ -781,6 +782,7 @@ class MultiExecTest extends PredisTestCase
/**
* @group connected
* @group relay-incompatible
* @requiresRedisVersion >= 2.2.0
*/
public function testIntegrationWritesOnWatchedKeysAbortTransaction(): void
@@ -803,6 +805,31 @@ class MultiExecTest extends PredisTestCase
$this->assertSame('client2', $client1->get('sentinel'));
}
/**
* @group connected
* @group ext-relay
* @requiresRedisVersion >= 2.2.0
*/
public function testRelayIntegrationWritesOnWatchedKeysAbortTransaction(): void
{
$exception = null;
$client1 = $this->getClient();
$client2 = $this->getClient();
try {
$client1->transaction(['watch' => 'sentinel'], function ($tx) use ($client2) {
$tx->set('sentinel', 'client1');
$tx->get('sentinel');
$client2->set('sentinel', 'client2');
});
} catch (Response\ServerException $ex) {
$exception = $ex;
}
$this->assertInstanceOf(Response\ServerException::class, $exception);
$this->assertSame('client2', $client1->get('sentinel'));
}
/**
* @group connected
* @requiresRedisVersion >= 2.2.0
@@ -851,6 +878,136 @@ class MultiExecTest extends PredisTestCase
$this->assertSame([['hijacked!', null]], $responses);
}
/**
* @group connected
* @group cluster
* @return void
* @requiresRedisVersion >= 3.0.0
*/
public function testExecutesTransactionAgainstCluster(): void
{
$redis = $this->getClient();
$response = $redis->transaction(function (MultiExec $tx) {
$tx->set('{foo}foo', 'value');
$tx->set('{foo}bar', 'value');
$tx->set('{foo}baz', 'value');
});
$this->assertEquals(['OK', 'OK', 'OK'], $response);
}
/**
* @group connected
* @group cluster
* @return void
* @requiresRedisVersion >= 3.0.0
*/
public function testThrowsExceptionOnDifferentHashSlots(): void
{
$redis = $this->getClient();
$this->expectException(AbortedMultiExecException::class);
$this->expectExceptionMessage(
'To be able to execute a transaction against cluster, all commands should operate on the same hash slot'
);
$redis->transaction(function (MultiExec $tx) {
$tx->set('foo_bar_baz', 'value');
$tx->set('{foo}bar', 'value');
$tx->set('{foo}baz', 'value');
});
}
/**
* @group connected
* @group cluster
* @return void
* @requiresRedisVersion >= 3.0.0
*/
public function testExecutesCASTransactionAgainstCluster(): void
{
$redis = $this->getClient();
$options = ['cas' => true, 'watch' => ['{foo}foo', '{foo}bar', '{foo}baz']];
$response = $redis->transaction($options, function (MultiExec $tx) {
$tx->multi();
$tx->set('{foo}foo', 'value');
$tx->set('{foo}bar', 'value');
$tx->set('{foo}baz', 'value');
});
$this->assertEquals(['OK', 'OK', 'OK'], $response);
}
/**
* @group connected
* @group cluster
* @return void
* @requiresRedisVersion >= 3.0.0
*/
public function testUNWATCHCurrentlyWATCHedKeys(): void
{
$redis = $this->getClient();
$options = ['cas' => true, 'watch' => ['{foo}foo', '{foo}bar', '{foo}baz']];
$response = $redis->transaction($options, function (MultiExec $tx) {
$tx->multi();
$tx->set('{foo}foo', 'value');
$tx->set('{foo}bar', 'value');
$tx->set('{foo}baz', 'value');
$tx->unwatch();
});
$this->assertEquals(['OK', 'OK', 'OK', 'OK'], $response);
}
/**
* @group connected
* @group cluster
* @return void
* @requiresRedisVersion >= 3.0.0
*/
public function testThrowsExceptionOnWATCHedKeysPointsToDifferentSlots(): void
{
$redis = $this->getClient();
$options = ['cas' => true, 'watch' => ['foo_bar_baz', '{foo}bar', '{foo}baz']];
$this->expectException(TransactionException::class);
$this->expectExceptionMessage('WATCHed keys should point to the same hash slot');
$redis->transaction($options, function (MultiExec $tx) {
$tx->multi();
$tx->set('{foo}foo', 'value');
$tx->set('{foo}bar', 'value');
$tx->set('{foo}baz', 'value');
});
}
/**
* @group connected
* @group cluster
* @return void
* @requiresRedisVersion >= 3.0.0
*/
public function testThrowsExceptionOnTransactionContextPointsToDifferentSlots(): void
{
$redis = $this->getClient();
$options = ['cas' => true, 'watch' => ['{foo}foo', '{foo}bar', '{foo}baz']];
$this->expectException(AbortedMultiExecException::class);
$this->expectExceptionMessage(
'To be able to execute a transaction against cluster, all commands should operate on the same hash slot'
);
$redis->transaction($options, function (MultiExec $tx) {
$tx->multi();
$tx->set('{foo}foo', 'value');
$tx->set('{foo}bar', 'value');
$tx->set('foo_bar_baz', 'value');
});
}
// ******************************************************************** //
// ---- HELPER METHODS ------------------------------------------------ //
// ******************************************************************** //