-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Messenger] Reduce lock time when using MySQL for transport #60207
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
base: 7.4
Are you sure you want to change the base?
Changes from all commits
bc1fbeb
ef84582
110629a
4259f68
d5db0bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -11,6 +11,7 @@ | |||||
|
||||||
namespace Symfony\Component\Messenger\Bridge\Doctrine\Transport; | ||||||
|
||||||
use Doctrine\DBAL\ArrayParameterType; | ||||||
use Doctrine\DBAL\Connection as DBALConnection; | ||||||
use Doctrine\DBAL\Driver\Exception as DriverException; | ||||||
use Doctrine\DBAL\Exception as DBALException; | ||||||
|
@@ -158,15 +159,17 @@ public function send(string $body, array $headers, int $delay = 0): string | |||||
|
||||||
public function get(): ?array | ||||||
{ | ||||||
if ($this->doMysqlCleanup && $this->driverConnection->getDatabasePlatform() instanceof AbstractMySQLPlatform) { | ||||||
if ($this->driverConnection->getDatabasePlatform() instanceof AbstractMySQLPlatform) { | ||||||
if ($this->doMysqlCleanup) { | ||||||
$this->deleteDeliveredMessageForMySQLPlatform(); | ||||||
} | ||||||
try { | ||||||
$this->driverConnection->delete($this->configuration['table_name'], ['delivered_at' => '9999-12-31 23:59:59']); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this made an exclusive lock on more than a row or record level. this is problematic since it blocks all other processes |
||||||
$this->doMysqlCleanup = false; | ||||||
} catch (DriverException $e) { | ||||||
// Ignore the exception | ||||||
} catch (TableNotFoundException $e) { | ||||||
return $this->getMessageForMySQLPlatform(); | ||||||
} catch (TableNotFoundException) { | ||||||
if ($this->autoSetup) { | ||||||
$this->setup(); | ||||||
|
||||||
return $this->getMessageForMySQLPlatform(); | ||||||
} | ||||||
} | ||||||
} | ||||||
|
@@ -627,4 +630,111 @@ private function fallBackToForUpdate(QueryBuilder $query, string $sql): string | |||||
return $sql; | ||||||
} | ||||||
} | ||||||
|
||||||
private function getMessageForMySQLPlatform(): ?array | ||||||
{ | ||||||
$possibleIdsToClaim = $this->createAvailableMessagesQueryBuilder() | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fetch only ids till a message is claimed to not load all the payloads unnecessary (they can be huge) |
||||||
->select('id') | ||||||
->orderBy('available_at', 'ASC') | ||||||
->setMaxResults(100)->fetchFirstColumn(); | ||||||
|
||||||
if (!$possibleIdsToClaim) { | ||||||
$this->queueEmptiedAt = microtime(true) * 1000; | ||||||
|
||||||
return null; | ||||||
} | ||||||
$this->queueEmptiedAt = null; | ||||||
|
||||||
$claimedId = null; | ||||||
foreach ($possibleIdsToClaim as $id) { | ||||||
if (null === $claimedId = $this->claimMessage($id)) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
break; | ||||||
} | ||||||
} | ||||||
|
||||||
if (!null === $claimedId) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Typo i gues |
||||||
// all open messages already have been claimed by other workers. | ||||||
return null; | ||||||
} | ||||||
|
||||||
$messageData = $this->createQueryBuilder() | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. load the data only for the claimed message |
||||||
->andWhere('m.id = :id') | ||||||
->setParameter(':id', $claimedId) | ||||||
->fetchAssociative(); | ||||||
|
||||||
return $this->decodeEnvelopeHeaders($messageData); | ||||||
} | ||||||
|
||||||
/** | ||||||
* @throws DBALException | ||||||
*/ | ||||||
private function deleteDeliveredMessageForMySQLPlatform(): void | ||||||
{ | ||||||
try { | ||||||
$ids = $this->selectMessageIdsToDelete(); | ||||||
$this->driverConnection->createQueryBuilder() | ||||||
->delete($this->configuration['table_name']) | ||||||
->where('id IN (:ids)') | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. row/record level exclusive lock to not interfere with the selecting part There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would it work to replace this by a subquery instead of doing a roundtrip to get the ids? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. id rather not use subqueries. they are very often a cause of performance flaws |
||||||
->setParameter('ids', $ids, ArrayParameterType::INTEGER) | ||||||
->executeQuery() | ||||||
; | ||||||
$this->doMysqlCleanup = false; | ||||||
} catch (DriverException $e) { | ||||||
// Ignore the exception | ||||||
} catch (TableNotFoundException $e) { | ||||||
if ($this->autoSetup) { | ||||||
$this->setup(); | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
/** | ||||||
* @return array<int, mixed> | ||||||
* | ||||||
* @throws DBALException | ||||||
*/ | ||||||
private function selectMessageIdsToDelete(): array | ||||||
{ | ||||||
return $this->driverConnection->createQueryBuilder() | ||||||
->select('m.id') | ||||||
->from($this->configuration['table_name'], 'm') | ||||||
->where('m.queue_name = ?') | ||||||
->andWhere('m.delivered_at = ?') | ||||||
->setParameters([ | ||||||
$this->configuration['queue_name'], | ||||||
'9999-12-31 23:59:59', | ||||||
], [ | ||||||
Types::STRING, | ||||||
Types::STRING, | ||||||
]) | ||||||
->setMaxResults(5_000) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what if there are more? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no problem. each time one message gets claimed 5k are being deleted. |
||||||
->fetchFirstColumn(); | ||||||
} | ||||||
|
||||||
private function claimMessage(mixed $id): mixed | ||||||
{ | ||||||
$now = new \DateTimeImmutable('UTC'); | ||||||
$redeliverLimit = $now->modify(\sprintf('-%d seconds', $this->configuration['redeliver_timeout'])); | ||||||
|
||||||
$claimed = 0 < $this->driverConnection->createQueryBuilder() | ||||||
->update($this->configuration['table_name']) | ||||||
->set('delivered_at', ':now') | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this will invoke an exclusive lock on row/record level to ensure we wont have race conditions and multiple workers handling the same message. either we can update the message, that means the message id has not been updated (delivered_at) |
||||||
->andWhere('id = :id') | ||||||
->andWhere('queue_name = :queue_name') | ||||||
->andWhere('delivered_at is null OR delivered_at < :redeliver_imit') | ||||||
->setParameters([ | ||||||
'now' => $now, | ||||||
'id' => $id, | ||||||
'queue_name' => $this->configuration['queue_name'], | ||||||
'redeliver_imit' => $redeliverLimit, | ||||||
], [ | ||||||
'now' => Types::DATETIME_IMMUTABLE, | ||||||
'id' => Types::INTEGER, | ||||||
'queue_name' => Types::STRING, | ||||||
'redeliver_imit' => Types::DATETIME_IMMUTABLE, | ||||||
]) | ||||||
->executeStatement(); | ||||||
|
||||||
return $claimed ? $id : null; | ||||||
} | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it'd be nice to reduce the diff on this file, doable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tbh no. i changed the algorithm and the queries. i tried to, but that was the best i could do here on the unit test