Skip to content

Commit dac5208

Browse files
Merge pull request #1312 from nextcloud/fix/stable27/chunk-mariadb
[stable27] fix(db): also chunk MariaDB deletes
2 parents 2e9488d + 891af76 commit dac5208

File tree

6 files changed

+88
-26
lines changed

6 files changed

+88
-26
lines changed

lib/AppInfo/Application.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
use OCP\User\Events\UserDeletedEvent;
5757
use OCP\Util;
5858
use Psr\Container\ContainerInterface;
59+
use Psr\Log\LoggerInterface;
5960

6061
class Application extends App implements IBootstrap {
6162
public const APP_ID = 'activity';
@@ -107,7 +108,8 @@ public function register(IRegistrationContext $context): void {
107108
$context->registerService(Data::class, function (ContainerInterface $c) {
108109
return new Data(
109110
$c->get(IManager::class),
110-
$c->get('ActivityConnectionAdapter')
111+
$c->get('ActivityConnectionAdapter'),
112+
$c->get(LoggerInterface::class),
111113
);
112114
});
113115

lib/Data.php

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<?php
2+
3+
declare(strict_types=1);
24
/**
35
* @copyright Copyright (c) 2016, ownCloud, Inc.
46
*
@@ -32,6 +34,7 @@
3234
use OCP\Activity\IManager;
3335
use OCP\DB\QueryBuilder\IQueryBuilder;
3436
use OCP\IDBConnection;
37+
use Psr\Log\LoggerInterface;
3538

3639
/**
3740
* @brief Class for managing the data in the activities
@@ -48,14 +51,16 @@ class Data {
4851

4952
/** @var ?IQueryBuilder */
5053
protected $insertMail;
54+
private LoggerInterface $logger;
5155

5256
/**
5357
* @param IManager $activityManager
5458
* @param IDBConnection $connection
5559
*/
56-
public function __construct(IManager $activityManager, IDBConnection $connection) {
60+
public function __construct(IManager $activityManager, IDBConnection $connection, LoggerInterface $logger) {
5761
$this->activityManager = $activityManager;
5862
$this->connection = $connection;
63+
$this->logger = $logger;
5964
}
6065

6166
/**
@@ -368,9 +373,16 @@ public function expire($expireDays = 365) {
368373
* 'field' => 'value' => `field` = 'value'
369374
* 'field' => array('value', 'operator') => `field` operator 'value'
370375
*/
371-
public function deleteActivities($conditions) {
372-
$delete = $this->connection->getQueryBuilder();
373-
$delete->delete('activity');
376+
public function deleteActivities($conditions): void {
377+
$platform = $this->connection->getDatabasePlatform();
378+
if ($platform instanceof MySQLPlatform) {
379+
$this->logger->debug('Choosing chunked activity delete for MySQL/MariaDB', ['app' => 'activity']);
380+
$this->deleteActivitiesForMySQL($conditions);
381+
return;
382+
}
383+
$this->logger->debug('Choosing regular activity delete', ['app' => 'activity']);
384+
$deleteQuery = $this->connection->getQueryBuilder();
385+
$deleteQuery->delete('activity');
374386

375387
foreach ($conditions as $column => $comparison) {
376388
if (is_array($comparison)) {
@@ -381,22 +393,14 @@ public function deleteActivities($conditions) {
381393
$value = $comparison;
382394
}
383395

384-
$delete->andWhere($delete->expr()->comparison($column, $operation, $delete->createNamedParameter($value)));
396+
$deleteQuery->andWhere($deleteQuery->expr()->comparison($column, $operation, $deleteQuery->createNamedParameter($value)));
385397
}
386398

387-
// Add galera safe delete chunking if using mysql
388-
// Stops us hitting wsrep_max_ws_rows when large row counts are deleted
389-
if ($this->connection->getDatabasePlatform() instanceof MySQLPlatform) {
390-
// Then use chunked delete
391-
$max = 100000;
392-
$delete->setMaxResults($max);
393-
do {
394-
$deleted = $delete->executeStatement();
395-
} while ($deleted === $max);
396-
} else {
397-
// Dont use chunked delete - let the DB handle the large row count natively
398-
$delete->executeStatement();
399-
}
399+
400+
401+
402+
// Dont use chunked delete - let the DB handle the large row count natively
403+
$deleteQuery->executeStatement();
400404
}
401405

402406
public function getById(int $activityId): ?IEvent {
@@ -467,4 +471,48 @@ public function getActivitySince(string $user, int $since, bool $byOthers) {
467471

468472
return $query->execute()->fetch();
469473
}
474+
475+
/**
476+
* Add galera safe delete chunking if using mysql
477+
* Stops us hitting wsrep_max_ws_rows when large row counts are deleted
478+
*
479+
* @param array $conditions
480+
* @return void
481+
*/
482+
private function deleteActivitiesForMySQL(array $conditions): void {
483+
$query = $this->connection->getQueryBuilder();
484+
$query->select('activity_id')
485+
->from('activity');
486+
487+
foreach ($conditions as $column => $comparison) {
488+
if (is_array($comparison)) {
489+
$operation = $comparison[1] ?? '=';
490+
$value = $comparison[0];
491+
} else {
492+
$operation = '=';
493+
$value = $comparison;
494+
}
495+
$query->where($query->expr()->comparison($column, $operation, $query->createNamedParameter($value)));
496+
}
497+
498+
$query->setMaxResults(10000);
499+
$result = $query->executeQuery();
500+
$count = $result->rowCount();
501+
if ($count === 0) {
502+
return;
503+
}
504+
$ids = array_map(static function (array $id) {
505+
return (int)$id[0];
506+
}, $result->fetchAll(\PDO::FETCH_NUM));
507+
$result->closeCursor();
508+
509+
$deleteQuery = $this->connection->getQueryBuilder();
510+
$deleteQuery->delete('activity');
511+
$deleteQuery->where($deleteQuery->expr()->in('activity_id', $deleteQuery->createParameter('ids'), IQueryBuilder::PARAM_INT_ARRAY));
512+
$deleteQuery->setParameter('ids', $ids, IQueryBuilder::PARAM_INT_ARRAY);
513+
$queryResult = $deleteQuery->executeStatement();
514+
if ($queryResult === 10000) {
515+
$this->deleteActivitiesForMySQL($conditions);
516+
}
517+
}
470518
}

psalm.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<UndefinedClass>
1818
<errorLevel type="suppress">
1919
<referencedClass name="Doctrine\DBAL\Platforms\MySQLPlatform" />
20+
<referencedClass name="Doctrine\DBAL\Platforms\AbstractMySQLPlatform" />
2021
<referencedClass name="Doctrine\DBAL\Types\Types" />
2122
<referencedClass name="OC" />
2223
<referencedClass name="OC\Core\Command\Base" />

tests/Controller/APIv1ControllerTest.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
use OCP\IRequest;
4242
use OCP\IUserSession;
4343
use OCP\RichObjectStrings\IValidator;
44+
use Psr\Log\LoggerInterface;
4445

4546
/**
4647
* Class APIv1Test
@@ -106,7 +107,8 @@ protected function tearDown(): void {
106107
protected function cleanUp(): void {
107108
$data = new Data(
108109
$this->createMock(IManager::class),
109-
\OC::$server->getDatabaseConnection()
110+
\OC::$server->getDatabaseConnection(),
111+
$this->createMock(LoggerInterface::class)
110112
);
111113

112114
$this->deleteUser($data, 'activity-api-user1');
@@ -220,7 +222,10 @@ public function testGet(string $user, int $start, int $count, array $expected):
220222
->method('getUID')
221223
->willReturn($user);
222224

223-
$data = new Data($activityManager, \OC::$server->getDatabaseConnection());
225+
$data = new Data($activityManager,
226+
\OC::$server->getDatabaseConnection(),
227+
$this->createMock(LoggerInterface::class)
228+
);
224229

225230
$controller = new APIv1Controller(
226231
'activity',

tests/DataDeleteActivitiesTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@
2727
use OCA\Activity\BackgroundJob\ExpireActivities;
2828
use OCA\Activity\Data;
2929
use OCP\Activity\IExtension;
30+
use OCP\Activity\IManager;
3031
use OCP\AppFramework\Utility\ITimeFactory;
32+
use OCP\BackgroundJob\IJobList;
3133
use OCP\DB\IPreparedStatement;
3234
use OCP\IConfig;
33-
use OCP\IUserSession;
34-
use OCP\Activity\IManager;
35-
use OCP\BackgroundJob\IJobList;
35+
use Psr\Log\LoggerInterface;
3636

3737
/**
3838
* Class DataDeleteActivitiesTest
@@ -74,7 +74,7 @@ protected function setUp(): void {
7474
$this->data = new Data(
7575
$this->createMock(IManager::class),
7676
\OC::$server->getDatabaseConnection(),
77-
$this->createMock(IUserSession::class)
77+
$this->createMock(LoggerInterface::class)
7878
);
7979
}
8080

tests/DataTest.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
use OCP\IUserSession;
3232
use OCP\Util;
3333
use PHPUnit\Framework\MockObject\MockObject;
34+
use Psr\Log\NullLogger;
3435

3536
/**
3637
* Class DataTest
@@ -54,17 +55,22 @@ class DataTest extends TestCase {
5455
/** @var IManager */
5556
protected $realActivityManager;
5657

58+
/** @var NullLogger */
59+
protected $logger;
60+
5761
protected function setUp(): void {
5862
parent::setUp();
5963

6064
$this->activityLanguage = Util::getL10N('activity', 'en');
6165
$activityManager = $this->createMock(IManager::class);
6266
$this->dbConnection = \OC::$server->get(IDBConnection::class);
6367
$this->realActivityManager = \OC::$server->get(IManager::class);
68+
$this->logger = \OC::$server->get(NullLogger::class);
6469

6570
$this->data = new Data(
6671
$activityManager,
67-
$this->dbConnection
72+
$this->dbConnection,
73+
$this->logger
6874
);
6975
}
7076

0 commit comments

Comments
 (0)