Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/matomo-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ jobs:
database:
- { engine: 'Mysql', version: '8.0' }
- { engine: 'Mariadb', version: '10.6' }
# added by hand, generate:test-action has no adapter option for plugins
include:
- php: 'matomo6_max_php'
target: 'maximum_supported_matomo'
database: { engine: 'Mysql', version: '8.0', adapter: 'MYSQLI' }
steps:
- uses: actions/checkout@v3
with:
Expand All @@ -55,6 +60,7 @@ jobs:
plugin-name: 'QueuedTracking'
php-version: ${{ matrix.php }}
test-type: 'PluginTests'
mysql-driver: ${{ matrix.database.adapter || 'PDO_MYSQL' }}
mysql-engine: ${{ matrix.database.engine }}
mysql-version: ${{ matrix.database.version }}
matomo-test-branch: ${{ matrix.target }}
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## Changelog
6.0.3
- Fixed existing QueuedTracking settings being cleared when the plugin is installed.
- Fixed database deadlocks, lock wait timeouts and snapshot isolation conflicts not being retried when Matomo uses the MYSQLI database adapter

6.0.2 - 2026-10-20
- Fixed queue processing failing with a fatal error, and some hits being counted twice, after a database deadlock or a MariaDB snapshot isolation conflict; requests hit by a snapshot isolation conflict are now retried
Expand Down
12 changes: 10 additions & 2 deletions Queue/Processor/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Piwik\Exception\UnexpectedWebsiteFoundException;
use Piwik\Plugins\QueuedTracking\Configuration;
use Piwik\Tracker;
use Piwik\Tracker\Db\DbException;
use Piwik\Tracker\RequestSet;
use Exception;

Expand Down Expand Up @@ -147,9 +148,16 @@ public function commit()
*/
private function isRetryableTransactionError(Exception $e): bool
{
$db = $this->getDb();
return $this->isErrNo($e, 1213) || $this->isErrNo($e, 1205) || $this->isErrNo($e, 1020);
}

return $db->isErrNo($e, 1213) || $db->isErrNo($e, 1205) || $db->isErrNo($e, 1020);
/**
* The mysqli adapter reads the number from the connection, which no longer has it once the failed statement was
* freed, but the tracker's DbException carries it as its code.
*/
private function isErrNo(Exception $e, int $errno): bool
{
return $this->getDb()->isErrNo($e, $errno) || ($e instanceof DbException && $e->getCode() === $errno);
}

/**
Expand Down
3 changes: 2 additions & 1 deletion tests/Framework/Mock/Tracker.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace Piwik\Plugins\QueuedTracking\tests\Framework\Mock;

use Piwik\Tracker\Db\DbException;
use Piwik\Tracker\Request;

class Tracker extends \Piwik\Tests\Framework\Mock\Tracker
Expand Down Expand Up @@ -49,7 +50,7 @@ public function trackRequest(Request $request)
) {
$this->numDeadlocks++;
$this->numDeadlocksPerRequest[$key] = $numOwnDeadlocks + 1;
throw new ForcedException('SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction');
throw new DbException('SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction', 1213);
}

return parent::trackRequest($request);
Expand Down
46 changes: 38 additions & 8 deletions tests/Integration/Queue/Processor/HandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
use Piwik\Plugins\QueuedTracking\tests\Framework\TestCase\IntegrationTestCase;
use Piwik\Tests\Framework\Fixture;
use Piwik\Tests\Framework\Mock\Tracker\RequestSet;
use Piwik\Tracker\Db as TrackerDb;
use Piwik\Tracker\Db\DbException;

class TestHandler extends Handler
{
Expand Down Expand Up @@ -102,7 +104,7 @@ public function test_onException_shouldMarkTheHandlerAsFaulty()
public function test_init_shouldResetAFaultyHandler()
{
$this->handler->onException(new RequestSet(), new \Exception());
$this->handler->onException(new RequestSet(), new \Exception('SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction'));
$this->handler->onException(new RequestSet(), new DbException('SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction', 1213));
$this->assertTrue($this->handler->hasErrors());
$this->assertTrue($this->handler->hasTransactionErrors());

Expand Down Expand Up @@ -317,7 +319,7 @@ public function test_process_ShouldRestoreTheEnvironmentOfARequest()
/**
* @dataProvider getRetryableTransactionErrors
*/
public function testOnExceptionShouldRetryTheRequestSetAndDiscardWorkDoneInTheTransaction(string $message, bool $serverEndsTransaction)
public function testOnExceptionShouldRetryTheRequestSetAndDiscardWorkDoneInTheTransaction(string $message, int $errno, bool $serverEndsTransaction)
{
$this->insertOption('QueuedTrackingTest_beforeError');

Expand All @@ -326,7 +328,7 @@ public function testOnExceptionShouldRetryTheRequestSetAndDiscardWorkDoneInTheTr
}

$requestSet = $this->buildRequestSet(3);
$this->handler->onException($requestSet, new \Exception($message));
$this->handler->onException($requestSet, new DbException($message, $errno));

// without a new transaction PDO would store this in autocommit mode and the rollBack below would throw
$this->insertOption('QueuedTrackingTest_afterError');
Expand All @@ -344,15 +346,43 @@ public function testOnExceptionShouldRetryTheRequestSetAndDiscardWorkDoneInTheTr
public function getRetryableTransactionErrors(): array
{
return [
'deadlock' => ['SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction', true],
'snapshot isolation' => ["SQLSTATE[HY000]: General error: 1020 Record has changed since last read in table 'log_visit'; try restarting transaction", true],
'lock wait timeout' => ['SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction', false],
'deadlock' => ['SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction', 1213, true],
'snapshot isolation' => ["SQLSTATE[HY000]: General error: 1020 Record has changed since last read in table 'log_visit'; try restarting transaction", 1020, true],
'lock wait timeout' => ['SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction', 1205, false],
];
}

private function insertOption(string $name): void
public function testOnExceptionShouldRetryARequestSetThatHitARealLockWaitTimeout()
{
Tracker::getDatabase()->query(
$db = Tracker::getDatabase();
$lockWaitTimeout = (int) $db->fetchOne('SELECT @@SESSION.innodb_lock_wait_timeout');
$db->query('SET SESSION innodb_lock_wait_timeout = 1');

// another connection holds the row's lock, so the handler's transaction times out waiting for it
$otherDb = TrackerDb::connectPiwikTrackerDb();
$otherTransactionId = $otherDb->beginTransaction();
$this->insertOption('QueuedTrackingTest_locked', $otherDb);

$requestSet = $this->buildRequestSet(3);

try {
$this->insertOption('QueuedTrackingTest_locked');
$this->fail('An expected exception was not triggered');
} catch (DbException $e) {
$this->handler->onException($requestSet, $e);
} finally {
$otherDb->rollBack($otherTransactionId);
$otherDb->disconnect();
$db->query('SET SESSION innodb_lock_wait_timeout = ' . $lockWaitTimeout);
}

$this->assertTrue($this->handler->hasTransactionErrors());
$this->assertEquals([$requestSet], $this->handler->getRequestSetsToRetry());
}

private function insertOption(string $name, ?TrackerDb $db = null): void
{
($db ?? Tracker::getDatabase())->query(
'INSERT INTO `' . Common::prefixTable('option') . '` (option_name, option_value, autoload) VALUES (?, ?, 0)',
[$name, '1']
);
Expand Down
14 changes: 14 additions & 0 deletions tests/Unit/Queue/Processor/HandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
use Piwik\Tests\Framework\Mock\Tracker;
use Piwik\Tests\Framework\Mock\Tracker\Db;
use Piwik\Tests\Framework\TestCase\UnitTestCase;
use Piwik\Tracker\Db\DbException;
use Piwik\Tracker\RequestSet;

class TestHandler extends Handler
{
Expand Down Expand Up @@ -95,4 +97,16 @@ public function test_rollback_ShouldRollbackTransaction()
$this->assertEquals($this->transactionId, $this->db->rollbackTransactionId);
$this->assertFalse($this->db->commitTransactionId);
}

public function testOnExceptionShouldRetryATransactionErrorWhoseNumberIsOnlyInTheCode()
{
$this->handler->init($this->tracker);

// as thrown by the mysqli adapter, whose message does not include the number
$requestSet = new RequestSet();
$this->handler->onException($requestSet, new DbException('Error query: Mysqli statement execute error : Deadlock found when trying to get lock; try restarting transaction', 1213));

$this->assertTrue($this->handler->hasTransactionErrors());
$this->assertSame([$requestSet], $this->handler->getRequestSetsToRetry());
}
}
Loading