Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Doctrine][Messenger] Make the messenger doctrine transport scalable #50972

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

Closed
wants to merge 11 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\MariaDBPlatform;
use Doctrine\DBAL\Platforms\MySQL57Platform;
use Doctrine\DBAL\Platforms\MySQL80Platform;
use Doctrine\DBAL\Platforms\OraclePlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Platforms\SQLServer2012Platform;
use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
use Doctrine\DBAL\Query\QueryBuilder;
use Doctrine\DBAL\Result;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
Expand All @@ -36,11 +39,19 @@ public function testGetAMessageWillChangeItsStatus()
{
$queryBuilder = $this->getQueryBuilderMock();
$driverConnection = $this->getDBALConnectionMock();
$stmt = $this->getResultMock([
'id' => 1,
'body' => '{"message":"Hi"}',
'headers' => json_encode(['type' => DummyMessage::class]),
]);
$resultMock = [
[
'id' => 1,
'body' => '{"message":"Hi"}',
'headers' => json_encode(['type' => DummyMessage::class]),
],
[
'id' => 2,
'body' => '{"message":"There"}',
'headers' => json_encode(['type' => DummyMessage::class]),
],
];
$stmt = $this->getResultMockForGet($resultMock);

$driverConnection
->method('createQueryBuilder')
Expand All @@ -62,17 +73,19 @@ public function testGetAMessageWillChangeItsStatus()
->willReturn(1);

$connection = new Connection([], $driverConnection);
$doctrineEnvelope = $connection->get();
$this->assertEquals(1, $doctrineEnvelope['id']);
$this->assertEquals('{"message":"Hi"}', $doctrineEnvelope['body']);
$this->assertEquals(['type' => DummyMessage::class], $doctrineEnvelope['headers']);
$doctrineEnvelopeList = $connection->get();
foreach ($doctrineEnvelopeList as $key => $doctrineEnvelope) {
$this->assertEquals($resultMock[$key]['id'], $doctrineEnvelope['id']);
$this->assertEquals($resultMock[$key]['body'], $doctrineEnvelope['body']);
$this->assertEquals(json_decode($resultMock[$key]['headers'], true), $doctrineEnvelope['headers']);
}
}

public function testGetWithNoPendingMessageWillReturnNull()
public function testGetWithNoPendingMessageWillReturnEmptyList()
{
$queryBuilder = $this->getQueryBuilderMock();
$driverConnection = $this->getDBALConnectionMock();
$stmt = $this->getResultMock(false);
$stmt = $this->getResultMockForGet([]);

$queryBuilder
->method('getParameters')
Expand All @@ -90,8 +103,9 @@ public function testGetWithNoPendingMessageWillReturnNull()
->willReturn($stmt);

$connection = new Connection([], $driverConnection);
$doctrineEnvelope = $connection->get();
$this->assertNull($doctrineEnvelope);
$doctrineEnvelopeList = $connection->get();
$this->assertIsArray($doctrineEnvelopeList);
$this->assertEmpty($doctrineEnvelopeList);
}

public function testItThrowsATransportExceptionIfItCannotAcknowledgeMessage()
Expand Down Expand Up @@ -151,11 +165,27 @@ private function getQueryBuilderMock()
$queryBuilder->method('setMaxResults')->willReturn($queryBuilder);
$queryBuilder->method('setParameter')->willReturn($queryBuilder);
$queryBuilder->method('setParameters')->willReturn($queryBuilder);
$queryBuilder->method('addOrderBy')->willReturn($queryBuilder);

$expressionBuilderMock = $this->createMock(ExpressionBuilder::class);
$expressionBuilderMock->method('in')->willReturn('');
$queryBuilder->method('expr')->willReturn($expressionBuilderMock);

return $queryBuilder;
}

private function getResultMock($expectedResult)
private function getResultMockForGet($expectedResult)
{
$stmt = $this->createMock(class_exists(Result::class) ? Result::class : ResultStatement::class);

$stmt->expects($this->once())
->method(class_exists(Result::class) ? 'fetchAllAssociative' : 'fetchAll')
->willReturn($expectedResult);

return $stmt;
}

private function getResultMockForFind($expectedResult)
{
$stmt = $this->createMock(class_exists(Result::class) ? Result::class : ResultStatement::class);

Expand Down Expand Up @@ -255,14 +285,14 @@ public static function buildConfigurationProvider(): iterable
public function testItThrowsAnExceptionIfAnExtraOptionsInDefined()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Unknown option found: [new_option]. Allowed options are [table_name, queue_name, redeliver_timeout, auto_setup]');
$this->expectExceptionMessage('Unknown option found: [new_option]. Allowed options are [table_name, queue_name, batch_size, redeliver_timeout, auto_setup]');
Connection::buildConfiguration('doctrine://default', ['new_option' => 'woops']);
}

public function testItThrowsAnExceptionIfAnExtraOptionsInDefinedInDSN()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Unknown option found in DSN: [new_option]. Allowed options are [table_name, queue_name, redeliver_timeout, auto_setup]');
$this->expectExceptionMessage('Unknown option found in DSN: [new_option]. Allowed options are [table_name, queue_name, batch_size, redeliver_timeout, auto_setup]');
Connection::buildConfiguration('doctrine://default?new_option=woops');
}

Expand All @@ -271,7 +301,7 @@ public function testFind()
$queryBuilder = $this->getQueryBuilderMock();
$driverConnection = $this->getDBALConnectionMock();
$id = 1;
$stmt = $this->getResultMock([
$stmt = $this->getResultMockForFind([
'id' => $id,
'body' => '{"message":"Hi"}',
'headers' => json_encode(['type' => DummyMessage::class]),
Expand Down Expand Up @@ -355,7 +385,7 @@ public function testFindAll()
/**
* @dataProvider providePlatformSql
*/
public function testGeneratedSql(AbstractPlatform $platform, string $expectedSql)
public function testGeneratedSql(AbstractPlatform $platform, int $batchSize, string $expectedSql)
{
$driverConnection = $this->createMock(DBALConnection::class);
$driverConnection->method('getDatabasePlatform')->willReturn($platform);
Expand All @@ -378,33 +408,84 @@ public function testGeneratedSql(AbstractPlatform $platform, string $expectedSql
;
$driverConnection->expects($this->once())->method('commit');

$connection = new Connection([], $driverConnection);
$connection = new Connection(['batch_size' => $batchSize], $driverConnection);
$connection->get();
}

public static function providePlatformSql(): iterable
{
yield 'MySQL' => [
yield 'MySQL57|batch_size=1' => [
new MySQL57Platform(),
1,
'SELECT m.* FROM messenger_messages m WHERE (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) AND (m.queue_name = ?) ORDER BY available_at ASC LIMIT 1 FOR UPDATE',
];

yield 'MySQL57|batch_size=50' => [
new MySQL57Platform(),
50,
'SELECT m.* FROM messenger_messages m WHERE (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) AND (m.queue_name = ?) ORDER BY available_at ASC LIMIT 50 FOR UPDATE',
];

yield 'MySQL8|batch_size=1' => [
new MySQL80Platform(),
1,
'SELECT m.* FROM messenger_messages m WHERE (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) AND (m.queue_name = ?) ORDER BY available_at ASC LIMIT 1 FOR UPDATE SKIP LOCKED',
];

yield 'MySQL8|batch_size=50' => [
new MySQL80Platform(),
50,
'SELECT m.* FROM messenger_messages m WHERE (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) AND (m.queue_name = ?) ORDER BY available_at ASC LIMIT 50 FOR UPDATE SKIP LOCKED',
];

yield 'Postgres|batch_size=1' => [
new PostgreSQLPlatform(),
1,
'SELECT m.* FROM messenger_messages m WHERE (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) AND (m.queue_name = ?) ORDER BY available_at ASC LIMIT 1 FOR UPDATE SKIP LOCKED',
];

yield 'Postgres|batch_size=50' => [
new PostgreSQLPlatform(),
50,
'SELECT m.* FROM messenger_messages m WHERE (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) AND (m.queue_name = ?) ORDER BY available_at ASC LIMIT 50 FOR UPDATE SKIP LOCKED',
];

if (class_exists(MariaDBPlatform::class)) {
yield 'MariaDB' => [
yield 'MariaDB|batch_size=1' => [
new MariaDBPlatform(),
1,
'SELECT m.* FROM messenger_messages m WHERE (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) AND (m.queue_name = ?) ORDER BY available_at ASC LIMIT 1 FOR UPDATE',
];
yield 'MariaDB|batch_size=50' => [
new MariaDBPlatform(),
50,
'SELECT m.* FROM messenger_messages m WHERE (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) AND (m.queue_name = ?) ORDER BY available_at ASC LIMIT 50 FOR UPDATE',
];
}

yield 'SQL Server' => [
yield 'SQL Server|batch_size=1' => [
new SQLServer2012Platform(),
'SELECT m.* FROM messenger_messages m WITH (UPDLOCK, ROWLOCK) WHERE (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) AND (m.queue_name = ?) ORDER BY available_at ASC OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY ',
1,
'SELECT m.* FROM messenger_messages m WITH (UPDLOCK, ROWLOCK, READPAST) WHERE (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) AND (m.queue_name = ?) ORDER BY available_at ASC OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY ',
];

yield 'Oracle' => [
yield 'SQL Server|batch_size=50' => [
new SQLServer2012Platform(),
50,
'SELECT m.* FROM messenger_messages m WITH (UPDLOCK, ROWLOCK, READPAST) WHERE (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) AND (m.queue_name = ?) ORDER BY available_at ASC OFFSET 0 ROWS FETCH NEXT 50 ROWS ONLY ',
];

yield 'Oracle|batch_size=1' => [
new OraclePlatform(),
1,
'SELECT w.id AS "id", w.body AS "body", w.headers AS "headers", w.queue_name AS "queue_name", w.created_at AS "created_at", w.available_at AS "available_at", w.delivered_at AS "delivered_at" FROM messenger_messages w WHERE w.id IN (SELECT a.id FROM (SELECT m.id FROM messenger_messages m WHERE (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) AND (m.queue_name = ?) ORDER BY available_at ASC) a WHERE ROWNUM <= 1) FOR UPDATE',
];

yield 'Oracle|batch_size=50' => [
new OraclePlatform(),
50,
'SELECT w.id AS "id", w.body AS "body", w.headers AS "headers", w.queue_name AS "queue_name", w.created_at AS "created_at", w.available_at AS "available_at", w.delivered_at AS "delivered_at" FROM messenger_messages w WHERE w.id IN (SELECT a.id FROM (SELECT m.id FROM messenger_messages m WHERE (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) AND (m.queue_name = ?) ORDER BY available_at ASC) a WHERE ROWNUM <= 50) FOR UPDATE',
];
}

public function testConfigureSchema()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ protected function tearDown(): void
public function testConnectionSendAndGet()
{
$this->connection->send('{"message": "Hi"}', ['type' => DummyMessage::class]);
$encoded = $this->connection->get();
$this->assertEquals('{"message": "Hi"}', $encoded['body']);
$this->assertEquals(['type' => DummyMessage::class], $encoded['headers']);
$encodedList = $this->connection->get()[0];
$this->assertEquals('{"message": "Hi"}', $encodedList['body']);
$this->assertEquals(['type' => DummyMessage::class], $encodedList['headers']);
}

public function testSendWithDelay()
Expand Down Expand Up @@ -108,7 +108,7 @@ public function testItRetrieveTheFirstAvailableMessage()
'available_at' => $this->formatDateTime(new \DateTimeImmutable('2019-03-15 12:30:00', new \DateTimeZone('UTC'))),
]);

$encoded = $this->connection->get();
$encoded = $this->connection->get()[0];
$this->assertEquals('{"message": "Hi available"}', $encoded['body']);
}

Expand Down Expand Up @@ -173,18 +173,20 @@ public function testItRetrieveTheMessageThatIsOlderThanRedeliverTimeout()
'available_at' => $this->formatDateTime(new \DateTimeImmutable('2019-03-15 12:30:00', new \DateTimeZone('UTC'))),
]);

$next = $this->connection->get();
$next = $this->connection->get()[0];
$this->assertEquals('{"message": "Hi requeued"}', $next['body']);
$this->connection->reject($next['id']);
}

public function testTheTransportIsSetupOnGet()
{
$this->assertFalse($this->createSchemaManager()->tablesExist(['messenger_messages']));
$this->assertNull($this->connection->get());
$result = $this->connection->get();
$this->assertIsArray($result);
$this->assertEmpty($result);

$this->connection->send('the body', ['my' => 'header']);
$envelope = $this->connection->get();
$envelope = $this->connection->get()[0];
$this->assertEquals('the body', $envelope['body']);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,11 @@ public function testPostgreSqlConnectionSendAndGet()
{
$this->connection->send('{"message": "Hi"}', ['type' => DummyMessage::class]);

$encoded = $this->connection->get();
$encoded = $this->connection->get()[0];
$this->assertEquals('{"message": "Hi"}', $encoded['body']);
$this->assertEquals(['type' => DummyMessage::class], $encoded['headers']);

$this->assertNull($this->connection->get());
$this->assertEmpty($this->connection->get());
}

private function createSchemaManager(): AbstractSchemaManager
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\Messenger\Bridge\Doctrine\Tests\Fixtures\DummyMessage;
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\Connection;
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineReceivedStamp;
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineReceiver;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Exception\MessageDecodingFailedException;
Expand All @@ -38,7 +37,7 @@ public function testItReturnsTheDecodedMessageToTheHandler()

$doctrineEnvelope = $this->createDoctrineEnvelope();
$connection = $this->createMock(Connection::class);
$connection->method('get')->willReturn($doctrineEnvelope);
$connection->method('get')->willReturn([$doctrineEnvelope]);

$receiver = new DoctrineReceiver($connection, $serializer);
$actualEnvelopes = $receiver->get();
Expand All @@ -47,11 +46,6 @@ public function testItReturnsTheDecodedMessageToTheHandler()
$actualEnvelope = $actualEnvelopes[0];
$this->assertEquals(new DummyMessage('Hi'), $actualEnvelopes[0]->getMessage());

/** @var DoctrineReceivedStamp $doctrineReceivedStamp */
$doctrineReceivedStamp = $actualEnvelope->last(DoctrineReceivedStamp::class);
$this->assertNotNull($doctrineReceivedStamp);
$this->assertSame('1', $doctrineReceivedStamp->getId());

/** @var TransportMessageIdStamp $transportMessageIdStamp */
$transportMessageIdStamp = $actualEnvelope->last(TransportMessageIdStamp::class);
$this->assertNotNull($transportMessageIdStamp);
Expand All @@ -66,7 +60,7 @@ public function testItRejectTheMessageIfThereIsAMessageDecodingFailedException()

$doctrineEnvelop = $this->createDoctrineEnvelope();
$connection = $this->createMock(Connection::class);
$connection->method('get')->willReturn($doctrineEnvelop);
$connection->method('get')->willReturn([$doctrineEnvelop]);
$connection->expects($this->once())->method('reject');

$receiver = new DoctrineReceiver($connection, $serializer);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public function testReceivesMessages()
];

$serializer->method('decode')->with(['body' => 'body', 'headers' => ['my' => 'header']])->willReturn(new Envelope($decodedMessage));
$connection->method('get')->willReturn($doctrineEnvelope);
$connection->method('get')->willReturn([$doctrineEnvelope]);

$envelopes = $transport->get();
$this->assertSame($decodedMessage, $envelopes[0]->getMessage());
Expand Down
Loading