Skip to content

Commit 76f6583

Browse files
committed
[dbal] Use concurrent fetch message approach (no transaction, no pessimistic lock)
1 parent 2e9fb0b commit 76f6583

1 file changed

Lines changed: 71 additions & 49 deletions

File tree

‎pkg/dbal/DbalConsumerHelperTrait.php‎

Lines changed: 71 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111

1212
trait DbalConsumerHelperTrait
1313
{
14+
private $redeliverMessagesLastExecutedAt;
15+
16+
private $removeExpiredMessagesLastExecutedAt;
17+
1418
abstract protected function getContext(): DbalContext;
1519

1620
abstract protected function getConnection(): Connection;
@@ -20,66 +24,72 @@ protected function fetchMessage(array $queues, int $redeliveryDelay): ?array
2024
$now = time();
2125
$deliveryId = (string) Uuid::uuid1();
2226

23-
$this->getConnection()->beginTransaction();
24-
25-
try {
26-
$query = $this->getConnection()->createQueryBuilder()
27-
->select('*')
28-
->from($this->getContext()->getTableName())
29-
->andWhere('delivery_id IS NULL')
30-
->andWhere('delayed_until IS NULL OR delayed_until <= :delayedUntil')
31-
->andWhere('queue IN (:queues)')
32-
->addOrderBy('priority', 'desc')
33-
->addOrderBy('published_at', 'asc')
34-
->setMaxResults(1);
35-
36-
// select for update
37-
$message = $this->getConnection()->executeQuery(
38-
$query->getSQL().' '.$this->getConnection()->getDatabasePlatform()->getWriteLockSQL(),
39-
['delayedUntil' => $now, 'queues' => array_values($queues)],
40-
['delayedUntil' => ParameterType::INTEGER, 'queues' => Connection::PARAM_STR_ARRAY]
41-
)->fetch();
42-
43-
if (!$message) {
44-
$this->getConnection()->commit();
27+
$endAt = microtime(true) + 0.2; // add 200ms
28+
29+
$select = $this->getConnection()->createQueryBuilder()
30+
->select('id')
31+
->from($this->getContext()->getTableName())
32+
->andWhere('delivery_id IS NULL')
33+
->andWhere('delayed_until IS NULL OR delayed_until <= :delayedUntil')
34+
->andWhere('queue IN (:queues)')
35+
->addOrderBy('priority', 'desc')
36+
->addOrderBy('published_at', 'asc')
37+
->setParameter('delayedUntil', $now, ParameterType::INTEGER)
38+
->setParameter('queues', array_values($queues), Connection::PARAM_STR_ARRAY)
39+
->setMaxResults(1);
40+
41+
$update = $this->getConnection()->createQueryBuilder()
42+
->update($this->getContext()->getTableName())
43+
->set('delivery_id', ':deliveryId')
44+
->set('redeliver_after', ':redeliverAfter')
45+
->andWhere('id = :messageId')
46+
->andWhere('delivery_id IS NULL')
47+
->setParameter('deliveryId', $deliveryId, Type::STRING)
48+
->setParameter('redeliverAfter', $now + $redeliveryDelay, Type::BIGINT)
49+
;
4550

51+
while (microtime() < $endAt) {
52+
$result = $select->execute()->fetch();
53+
if (empty($result)) {
4654
return null;
4755
}
4856

49-
// mark message as delivered to consumer
50-
$this->getConnection()->createQueryBuilder()
51-
->andWhere('id = :id')
52-
->update($this->getContext()->getTableName())
53-
->set('delivery_id', ':deliveryId')
54-
->set('redeliver_after', ':redeliverAfter')
55-
->setParameter('id', $message['id'], Type::GUID)
56-
->setParameter('deliveryId', $deliveryId, Type::STRING)
57-
->setParameter('redeliverAfter', $now + $redeliveryDelay, Type::BIGINT)
58-
->execute()
57+
$update
58+
->setParameter('messageId', $result['id'], Type::GUID)
5959
;
6060

61-
$this->getConnection()->commit();
62-
63-
$deliveredMessage = $this->getConnection()->createQueryBuilder()
64-
->select('*')
65-
->from($this->getContext()->getTableName())
66-
->andWhere('delivery_id = :deliveryId')
67-
->setParameter('deliveryId', $deliveryId, Type::STRING)
68-
->setMaxResults(1)
69-
->execute()
70-
->fetch()
71-
;
72-
73-
return $deliveredMessage ?: null;
74-
} catch (\Exception $e) {
75-
$this->getConnection()->rollBack();
76-
77-
throw $e;
61+
if ($update->execute()) {
62+
$deliveredMessage = $this->getConnection()->createQueryBuilder()
63+
->select('*')
64+
->from($this->getContext()->getTableName())
65+
->andWhere('delivery_id = :deliveryId')
66+
->setParameter('deliveryId', $deliveryId, Type::STRING)
67+
->setMaxResults(1)
68+
->execute()
69+
->fetch()
70+
;
71+
72+
if (false == $deliveredMessage) {
73+
throw new \LogicException('There must be a message at all times at this stage but there is no a message.');
74+
}
75+
76+
return $deliveredMessage;
77+
}
7878
}
79+
80+
return null;
7981
}
8082

8183
protected function redeliverMessages(): void
8284
{
85+
if (null === $this->redeliverMessagesLastExecutedAt) {
86+
$this->redeliverMessagesLastExecutedAt = microtime(true);
87+
}
88+
89+
if ((microtime(true) - $this->redeliverMessagesLastExecutedAt) < 1) {
90+
return;
91+
}
92+
8393
$this->getConnection()->createQueryBuilder()
8494
->update($this->getContext()->getTableName())
8595
->set('delivery_id', ':deliveryId')
@@ -91,16 +101,28 @@ protected function redeliverMessages(): void
91101
->setParameter('redelivered', true, Type::BOOLEAN)
92102
->execute()
93103
;
104+
105+
$this->redeliverMessagesLastExecutedAt = microtime(true);
94106
}
95107

96108
protected function removeExpiredMessages(): void
97109
{
110+
if (null === $this->removeExpiredMessagesLastExecutedAt) {
111+
$this->removeExpiredMessagesLastExecutedAt = microtime(true);
112+
}
113+
114+
if ((microtime(true) - $this->removeExpiredMessagesLastExecutedAt) < 1) {
115+
return;
116+
}
117+
98118
$this->getConnection()->createQueryBuilder()
99119
->delete($this->getContext()->getTableName())
100120
->andWhere('(time_to_live IS NOT NULL) AND (time_to_live < :now)')
101121
->setParameter(':now', (int) time(), Type::BIGINT)
102122
->setParameter('redelivered', false, Type::BOOLEAN)
103123
->execute()
104124
;
125+
126+
$this->removeExpiredMessagesLastExecutedAt = microtime(true);
105127
}
106128
}

0 commit comments

Comments
 (0)