mirror of
https://github.com/predis/predis.git
synced 2026-08-24 23:19:36 +00:00
382 lines
12 KiB
PHP
382 lines
12 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of the Predis package.
|
|
*
|
|
* (c) 2009-2020 Daniele Alessandri
|
|
* (c) 2021-2025 Till Krüss
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace Predis\Transaction\Strategy;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use Predis\Command\CommandInterface;
|
|
use Predis\Command\Redis\EXEC;
|
|
use Predis\Command\Redis\MULTI;
|
|
use Predis\Command\Redis\SET;
|
|
use Predis\Connection\Cluster\ClusterInterface;
|
|
use Predis\Response\ErrorInterface;
|
|
use Predis\Response\Status;
|
|
use Predis\Transaction\Exception\TransactionException;
|
|
use Predis\Transaction\MultiExecState;
|
|
|
|
class ClusterConnectionStrategyTest extends TestCase
|
|
{
|
|
/**
|
|
* @var ClusterInterface
|
|
*/
|
|
private $mockConnection;
|
|
|
|
/**
|
|
* @var \Predis\Cluster\StrategyInterface
|
|
*/
|
|
private $mockStrategy;
|
|
|
|
/**
|
|
* @var CommandInterface
|
|
*/
|
|
private $mockCommand;
|
|
|
|
/**
|
|
* @return void
|
|
*/
|
|
protected function setUp(): void
|
|
{
|
|
$this->mockConnection = $this->getMockBuilder(ClusterInterface::class)->getMock();
|
|
$this->mockStrategy = $this->getMockBuilder(\Predis\Cluster\StrategyInterface::class)->getMock();
|
|
$this->mockCommand = $this->getMockBuilder(CommandInterface::class)->getMock();
|
|
|
|
$this->mockConnection
|
|
->method('getClusterStrategy')
|
|
->willReturn($this->mockStrategy);
|
|
}
|
|
|
|
/**
|
|
* @group disconnected
|
|
* @return void
|
|
*/
|
|
public function testExecuteCommandThrowsExceptionOnNonInitializedTransactionContext(): void
|
|
{
|
|
$strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState());
|
|
|
|
$this->expectException(TransactionException::class);
|
|
$this->expectExceptionMessage('Transaction context should be initialized first');
|
|
|
|
$strategy->executeCommand($this->mockCommand);
|
|
}
|
|
|
|
/**
|
|
* @group disconnected
|
|
* @return void
|
|
* @throws TransactionException
|
|
*/
|
|
public function testExecuteCommandAssignsHashSlotToSlotlessCommand(): void
|
|
{
|
|
$anotherCommand = new SET();
|
|
|
|
$this->mockStrategy
|
|
->expects($this->exactly(2))
|
|
->method('getSlot')
|
|
->withConsecutive([$this->mockCommand], [$anotherCommand])
|
|
->willReturnOnConsecutiveCalls(10, null);
|
|
|
|
$strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState());
|
|
$strategy->initializeTransaction();
|
|
|
|
$this->assertEquals('QUEUED', $strategy->executeCommand($this->mockCommand));
|
|
$this->assertEquals('QUEUED', $strategy->executeCommand($anotherCommand));
|
|
$this->assertEquals(10, $anotherCommand->getSlot());
|
|
}
|
|
|
|
/**
|
|
* @group disconnected
|
|
* @return void
|
|
* @throws TransactionException
|
|
*/
|
|
public function testExecuteCommandReturnsErrorOnMissMatchingSlots(): void
|
|
{
|
|
$this->mockStrategy
|
|
->expects($this->exactly(2))
|
|
->method('getSlot')
|
|
->with($this->mockCommand)
|
|
->willReturnOnConsecutiveCalls(123, 124);
|
|
|
|
$strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState());
|
|
$strategy->initializeTransaction();
|
|
|
|
$strategy->executeCommand($this->mockCommand);
|
|
$response = $strategy->executeCommand($this->mockCommand);
|
|
|
|
$this->assertInstanceOf(ErrorInterface::class, $response);
|
|
$this->assertSame(
|
|
'To be able to execute a transaction against cluster, all commands should operate on the same hash slot',
|
|
$response->getMessage()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @group disconnected
|
|
* @return void
|
|
* @throws TransactionException
|
|
*/
|
|
public function testExecuteCommandQueueCommandsMappedToTheSameSlot(): void
|
|
{
|
|
$anotherMockCommand = $this->getMockBuilder(CommandInterface::class)->getMock();
|
|
|
|
$this->mockStrategy
|
|
->expects($this->exactly(2))
|
|
->method('getSlot')
|
|
->withConsecutive([$this->mockCommand], [$anotherMockCommand])
|
|
->willReturnOnConsecutiveCalls([123], [123]);
|
|
|
|
$strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState());
|
|
$strategy->initializeTransaction();
|
|
|
|
$resp1 = $strategy->executeCommand($this->mockCommand);
|
|
$resp2 = $strategy->executeCommand($this->mockCommand);
|
|
|
|
$this->assertEquals('QUEUED', $resp1);
|
|
$this->assertEquals('QUEUED', $resp2);
|
|
}
|
|
|
|
/**
|
|
* @group disconnected
|
|
* @return void
|
|
*/
|
|
public function testInitializeTransactionContext(): void
|
|
{
|
|
$strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState());
|
|
$this->assertTrue($strategy->initializeTransaction());
|
|
$this->assertTrue($strategy->initializeTransaction());
|
|
}
|
|
|
|
/**
|
|
* @group disconnected
|
|
* @return void
|
|
* @throws TransactionException
|
|
*/
|
|
public function testExecuteTransactionReturnsNullOnInitializeError(): void
|
|
{
|
|
$this->mockConnection
|
|
->expects($this->once())
|
|
->method('executeCommand')
|
|
->with(new MULTI())
|
|
->willReturn('ERR');
|
|
|
|
$strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState());
|
|
$strategy->initializeTransaction();
|
|
$strategy->executeCommand(new SET());
|
|
|
|
$this->assertNull($strategy->executeTransaction());
|
|
}
|
|
|
|
/**
|
|
* @group disconnected
|
|
* @return void
|
|
* @throws TransactionException
|
|
*/
|
|
public function testExecuteTransactionReturnsNullOnQueueingError(): void
|
|
{
|
|
$this->mockConnection
|
|
->expects($this->exactly(3))
|
|
->method('executeCommand')
|
|
->withConsecutive([new MULTI()], [new SET()], [new SET()])
|
|
->willReturnOnConsecutiveCalls('OK', 'QUEUED', 'ERR');
|
|
|
|
$strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState());
|
|
$strategy->initializeTransaction();
|
|
$strategy->executeCommand(new SET());
|
|
$strategy->executeCommand(new SET());
|
|
$strategy->executeCommand(new SET());
|
|
|
|
$this->assertNull($strategy->executeTransaction());
|
|
}
|
|
|
|
/**
|
|
* @group disconnected
|
|
* @return void
|
|
* @throws TransactionException
|
|
*/
|
|
public function testExecuteTransactionReturnsResultOnSuccessfulTransaction(): void
|
|
{
|
|
$command1 = new SET();
|
|
$command2 = new SET();
|
|
$command3 = new SET();
|
|
$command1->setArguments(['{foo}bar', 'value']);
|
|
$command2->setArguments(['{foo}baz', 'value']);
|
|
$command3->setArguments(['{foo}foo', 'value']);
|
|
|
|
$this->mockConnection
|
|
->expects($this->exactly(5))
|
|
->method('executeCommand')
|
|
->withConsecutive(
|
|
[new MULTI()],
|
|
[$command1],
|
|
[$command2],
|
|
[$command3],
|
|
[new EXEC()]
|
|
)
|
|
->willReturnOnConsecutiveCalls(
|
|
'OK',
|
|
'QUEUED',
|
|
'QUEUED',
|
|
'QUEUED',
|
|
[new Status('OK'), new Status('OK'), new Status('OK')]
|
|
);
|
|
|
|
$strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState());
|
|
$strategy->initializeTransaction();
|
|
$strategy->executeCommand($command1);
|
|
$strategy->executeCommand($command2);
|
|
$strategy->executeCommand($command3);
|
|
|
|
$this->assertEquals(
|
|
[new Status('OK'), new Status('OK'), new Status('OK')],
|
|
$strategy->executeTransaction()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @group disconnected
|
|
* @return void
|
|
* @throws TransactionException
|
|
*/
|
|
public function testMulti(): void
|
|
{
|
|
$this->mockConnection
|
|
->expects($this->once())
|
|
->method('executeCommand')
|
|
->with(new MULTI())
|
|
->willReturnOnConsecutiveCalls('ERR', 'OK');
|
|
|
|
$strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState());
|
|
$this->assertEquals('ERR', $strategy->multi());
|
|
|
|
$this->expectException(TransactionException::class);
|
|
$this->expectExceptionMessage('Transaction context should be initialized first');
|
|
|
|
$strategy->executeCommand(new SET());
|
|
|
|
$this->assertEquals('OK', $strategy->multi());
|
|
$this->assertEquals('QUEUED', $strategy->executeCommand(new SET()));
|
|
}
|
|
|
|
/**
|
|
* @group disconnected
|
|
* @return void
|
|
* @throws TransactionException
|
|
*/
|
|
public function testWatchThrowsExceptionOnKeysPointingToDifferentSlots(): void
|
|
{
|
|
$this->mockStrategy
|
|
->expects($this->once())
|
|
->method('checkSameSlotForKeys')
|
|
->with(['key1', 'key2', 'key3'])
|
|
->willReturn(false);
|
|
|
|
$strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState());
|
|
|
|
$this->expectException(TransactionException::class);
|
|
$this->expectExceptionMessage('WATCHed keys should point to the same hash slot');
|
|
|
|
$strategy->watch(['key1', 'key2', 'key3']);
|
|
}
|
|
|
|
/**
|
|
* @group disconnected
|
|
* @return void
|
|
* @throws TransactionException
|
|
*/
|
|
public function testWatchReturnCorrectResponse(): void
|
|
{
|
|
$this->mockStrategy
|
|
->expects($this->once())
|
|
->method('checkSameSlotForKeys')
|
|
->with(['key1', 'key2', 'key3'])
|
|
->willReturn(true);
|
|
|
|
$this->mockStrategy
|
|
->expects($this->once())
|
|
->method('getSlotByKey')
|
|
->with('key1')
|
|
->willReturn(10);
|
|
|
|
$this->mockConnection
|
|
->expects($this->once())
|
|
->method('executeCommand')
|
|
->withAnyParameters()
|
|
->willReturn(new Status('OK'));
|
|
|
|
$strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState());
|
|
$this->assertTrue($strategy->watch(['key1', 'key2', 'key3']));
|
|
}
|
|
|
|
/**
|
|
* @group disconnected
|
|
* @return void
|
|
* @throws TransactionException
|
|
*/
|
|
public function testWatchAdditionallyInitializeTransactionContextOnCASTransaction(): void
|
|
{
|
|
$this->mockStrategy
|
|
->expects($this->once())
|
|
->method('checkSameSlotForKeys')
|
|
->with(['key1', 'key2', 'key3'])
|
|
->willReturn(true);
|
|
|
|
$this->mockStrategy
|
|
->expects($this->once())
|
|
->method('getSlotByKey')
|
|
->with('key1')
|
|
->willReturn(10);
|
|
|
|
$this->mockConnection
|
|
->expects($this->once())
|
|
->method('executeCommand')
|
|
->withAnyParameters()
|
|
->willReturn(new Status('OK'));
|
|
|
|
$state = new MultiExecState();
|
|
$state->set(MultiExecState::CAS);
|
|
|
|
$strategy = new ClusterConnectionStrategy($this->mockConnection, $state);
|
|
$this->assertTrue($strategy->watch(['key1', 'key2', 'key3']));
|
|
$this->assertEquals('QUEUED', $strategy->executeCommand(new SET()));
|
|
}
|
|
|
|
/**
|
|
* @group disconnected
|
|
* @return void
|
|
*/
|
|
public function testDiscard(): void
|
|
{
|
|
$this->mockConnection
|
|
->expects($this->once())
|
|
->method('executeCommand')
|
|
->withAnyParameters()
|
|
->willReturn(new Status('OK'));
|
|
|
|
$strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState());
|
|
$this->assertEquals('OK', $strategy->discard());
|
|
}
|
|
|
|
/**
|
|
* @group disconnected
|
|
* @return void
|
|
*/
|
|
public function testUnwatch(): void
|
|
{
|
|
$this->mockConnection
|
|
->expects($this->once())
|
|
->method('executeCommand')
|
|
->withAnyParameters()
|
|
->willReturn(new Status('OK'));
|
|
|
|
$strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState());
|
|
$this->assertEquals('OK', $strategy->unwatch());
|
|
}
|
|
}
|