From 2822d0e95543a3abc39316463def7614ea6b6818 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 21 Jun 2026 17:58:17 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20review=20#19=20=E2=80=94=20WebhookChanne?= =?UTF-8?q?l=20conversation=20race=20condition=20(FOR=20UPDATE=20transacti?= =?UTF-8?q?on)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Helper/WebhookChannelHelper.php | 56 +++++++++++-------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/source/packages/plg_system_mokosuitesupport/src/Helper/WebhookChannelHelper.php b/source/packages/plg_system_mokosuitesupport/src/Helper/WebhookChannelHelper.php index 2f1c519..80d4791 100644 --- a/source/packages/plg_system_mokosuitesupport/src/Helper/WebhookChannelHelper.php +++ b/source/packages/plg_system_mokosuitesupport/src/Helper/WebhookChannelHelper.php @@ -82,29 +82,41 @@ class WebhookChannelHelper { $db = Factory::getContainer()->get(DatabaseInterface::class); - $db->setQuery($db->getQuery(true) - ->select('id') - ->from('#__mokosuitesupport_conversations') - ->where('channel_user_id = ' . $db->quote($channelUserId)) - ->where($db->quoteName('channel') . ' = ' . $db->quote($channel)) - ->where($db->quoteName('status') . ' IN (' . $db->quote('open') . ',' . $db->quote('assigned') . ')') - ->order('started_at DESC'), 0, 1); - $existingId = (int) $db->loadResult(); + $db->transactionStart(); - if ($existingId) { - return $existingId; + try { + // Lock to prevent duplicate conversation creation from concurrent webhooks + $db->setQuery( + 'SELECT id FROM #__mokosuitesupport_conversations' + . ' WHERE channel_user_id = ' . $db->quote($channelUserId) + . ' AND ' . $db->quoteName('channel') . ' = ' . $db->quote($channel) + . ' AND ' . $db->quoteName('status') . ' IN (' . $db->quote('open') . ',' . $db->quote('assigned') . ')' + . ' ORDER BY started_at DESC LIMIT 1' + . ' FOR UPDATE' + ); + $existingId = (int) $db->loadResult(); + + if ($existingId) { + $db->transactionCommit(); + return $existingId; + } + + $result = ConversationHelper::create($channel, null, $channelUserId); + $convId = $result->conversation_id; + + // Store channel user ID atomically + $db->setQuery($db->getQuery(true) + ->update('#__mokosuitesupport_conversations') + ->set('channel_user_id = ' . $db->quote($channelUserId)) + ->where('id = ' . (int) $convId)); + $db->execute(); + + $db->transactionCommit(); + + return $convId; + } catch (\Throwable $e) { + $db->transactionRollback(); + throw $e; } - - $result = ConversationHelper::create($channel, null, $channelUserId); - $convId = $result->conversation_id; - - // Store channel user ID - $db->setQuery($db->getQuery(true) - ->update('#__mokosuitesupport_conversations') - ->set('channel_user_id = ' . $db->quote($channelUserId)) - ->where('id = ' . (int) $convId)); - $db->execute(); - - return $convId; } }