Browse Source

Merge pull request #54386 from nextcloud/fix-n+1-caldav

fix(performance): Fix n+1 issue when fetching calendar properties
pull/54434/head
Robin Appelman 5 months ago
committed by GitHub
parent
commit
89fa14fd77
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      apps/dav/lib/CalDAV/Calendar.php
  2. 35
      apps/dav/lib/CalDAV/CalendarProvider.php
  3. 2
      apps/dav/lib/Connector/Sabre/ServerFactory.php
  4. 81
      apps/dav/lib/DAV/CustomPropertiesBackend.php
  5. 1
      apps/dav/lib/Db/Property.php
  6. 37
      apps/dav/lib/Db/PropertyMapper.php
  7. 2
      apps/dav/lib/Server.php
  8. 2
      apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php
  9. 5
      apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php

6
apps/dav/lib/CalDAV/Calendar.php

@ -36,7 +36,7 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
public function __construct(
BackendInterface $caldavBackend,
$calendarInfo,
array $calendarInfo,
IL10N $l10n,
private IConfig $config,
private LoggerInterface $logger,
@ -60,6 +60,10 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
$this->l10n = $l10n;
}
public function getUri(): string {
return $this->calendarInfo['uri'];
}
/**
* {@inheritdoc}
* @throws Forbidden

35
apps/dav/lib/CalDAV/CalendarProvider.php

@ -36,9 +36,14 @@ class CalendarProvider implements ICalendarProvider {
});
}
$additionalProperties = $this->getAdditionalPropertiesForCalendars($calendarInfos);
$iCalendars = [];
foreach ($calendarInfos as $calendarInfo) {
$calendarInfo = array_merge($calendarInfo, $this->getAdditionalProperties($calendarInfo['principaluri'], $calendarInfo['uri']));
$user = str_replace('principals/users/', '', $calendarInfo['principaluri']);
$path = 'calendars/' . $user . '/' . $calendarInfo['uri'];
$calendarInfo = array_merge($calendarInfo, $additionalProperties[$path] ?? []);
$calendar = new Calendar($this->calDavBackend, $calendarInfo, $this->l10n, $this->config, $this->logger);
$iCalendars[] = new CalendarImpl(
$calendar,
@ -49,16 +54,34 @@ class CalendarProvider implements ICalendarProvider {
return $iCalendars;
}
public function getAdditionalProperties(string $principalUri, string $calendarUri): array {
$user = str_replace('principals/users/', '', $principalUri);
$path = 'calendars/' . $user . '/' . $calendarUri;
/**
* @param array{
* principaluri: string,
* uri: string,
* }[] $uris
* @return array<string, array<string, string|bool>>
*/
private function getAdditionalPropertiesForCalendars(array $uris): array {
$calendars = [];
foreach ($uris as $uri) {
/** @var string $user */
$user = str_replace('principals/users/', '', $uri['principaluri']);
if (!array_key_exists($user, $calendars)) {
$calendars[$user] = [];
}
$calendars[$user][] = 'calendars/' . $user . '/' . $uri['uri'];
}
$properties = $this->propertyMapper->findPropertiesByPath($user, $path);
$properties = $this->propertyMapper->findPropertiesByPathsAndUsers($calendars);
$list = [];
foreach ($properties as $property) {
if ($property instanceof Property) {
$list[$property->getPropertyname()] = match ($property->getPropertyname()) {
if (!isset($list[$property->getPropertypath()])) {
$list[$property->getPropertypath()] = [];
}
$list[$property->getPropertypath()][$property->getPropertyname()] = match ($property->getPropertyname()) {
'{http://owncloud.org/ns}calendar-enabled' => (bool)$property->getPropertyvalue(),
default => $property->getPropertyvalue()
};

2
apps/dav/lib/Connector/Sabre/ServerFactory.php

@ -14,6 +14,7 @@ use OCA\DAV\CalDAV\DefaultCalendarValidator;
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
use OCA\DAV\DAV\CustomPropertiesBackend;
use OCA\DAV\DAV\ViewOnlyPlugin;
use OCA\DAV\Db\PropertyMapper;
use OCA\DAV\Files\BrowserErrorPagePlugin;
use OCA\DAV\Files\Sharing\RootCollection;
use OCA\DAV\Upload\CleanupService;
@ -226,6 +227,7 @@ class ServerFactory {
$tree,
$this->databaseConnection,
$this->userSession->getUser(),
\OCP\Server::get(PropertyMapper::class),
\OCP\Server::get(DefaultCalendarValidator::class),
)
)

81
apps/dav/lib/DAV/CustomPropertiesBackend.php

@ -9,13 +9,20 @@
namespace OCA\DAV\DAV;
use Exception;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\Calendar;
use OCA\DAV\CalDAV\CalendarHome;
use OCA\DAV\CalDAV\CalendarObject;
use OCA\DAV\CalDAV\DefaultCalendarValidator;
use OCA\DAV\CalDAV\Integration\ExternalCalendar;
use OCA\DAV\CalDAV\Outbox;
use OCA\DAV\CalDAV\Trashbin\TrashbinHome;
use OCA\DAV\Connector\Sabre\Directory;
use OCA\DAV\Db\PropertyMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IUser;
use Sabre\CalDAV\Schedule\Inbox;
use Sabre\DAV\Exception as DavException;
use Sabre\DAV\PropertyStorage\Backend\BackendInterface;
use Sabre\DAV\PropFind;
@ -98,10 +105,9 @@ class CustomPropertiesBackend implements BackendInterface {
/**
* Properties cache
*
* @var array
*/
private $userCache = [];
private array $userCache = [];
private array $publishedCache = [];
private XmlService $xmlService;
/**
@ -114,6 +120,7 @@ class CustomPropertiesBackend implements BackendInterface {
private Tree $tree,
private IDBConnection $connection,
private IUser $user,
private PropertyMapper $propertyMapper,
private DefaultCalendarValidator $defaultCalendarValidator,
) {
$this->xmlService = new XmlService();
@ -197,6 +204,13 @@ class CustomPropertiesBackend implements BackendInterface {
$this->cacheDirectory($path, $node);
}
if ($node instanceof CalendarHome && $propFind->getDepth() !== 0) {
$backend = $node->getCalDAVBackend();
if ($backend instanceof CalDavBackend) {
$this->cacheCalendars($node, $requestedProps);
}
}
if ($node instanceof CalendarObject) {
// No custom properties supported on individual events
return;
@ -316,6 +330,10 @@ class CustomPropertiesBackend implements BackendInterface {
return [];
}
if (isset($this->publishedCache[$path])) {
return $this->publishedCache[$path];
}
$qb = $this->connection->getQueryBuilder();
$qb->select('*')
->from(self::TABLE_NAME)
@ -326,6 +344,7 @@ class CustomPropertiesBackend implements BackendInterface {
$props[$row['propertyname']] = $this->decodeValueFromDatabase($row['propertyvalue'], $row['valuetype']);
}
$result->closeCursor();
$this->publishedCache[$path] = $props;
return $props;
}
@ -364,6 +383,62 @@ class CustomPropertiesBackend implements BackendInterface {
$this->userCache = array_merge($this->userCache, $propsByPath);
}
private function cacheCalendars(CalendarHome $node, array $requestedProperties): void {
$calendars = $node->getChildren();
$users = [];
foreach ($calendars as $calendar) {
if ($calendar instanceof Calendar) {
$user = str_replace('principals/users/', '', $calendar->getPrincipalURI());
if (!isset($users[$user])) {
$users[$user] = ['calendars/' . $user];
}
$users[$user][] = 'calendars/' . $user . '/' . $calendar->getUri();
} elseif ($calendar instanceof Inbox || $calendar instanceof Outbox || $calendar instanceof TrashbinHome || $calendar instanceof ExternalCalendar) {
if ($calendar->getOwner()) {
$user = str_replace('principals/users/', '', $calendar->getOwner());
if (!isset($users[$user])) {
$users[$user] = ['calendars/' . $user];
}
$users[$user][] = 'calendars/' . $user . '/' . $calendar->getName();
}
}
}
// user properties
$properties = $this->propertyMapper->findPropertiesByPathsAndUsers($users);
$propsByPath = [];
foreach ($users as $paths) {
foreach ($paths as $path) {
$propsByPath[$path] = [];
}
}
foreach ($properties as $property) {
$propsByPath[$property->getPropertypath()][$property->getPropertyname()] = $this->decodeValueFromDatabase($property->getPropertyvalue(), $property->getValuetype());
}
$this->userCache = array_merge($this->userCache, $propsByPath);
// published properties
$allowedProps = array_intersect(self::PUBLISHED_READ_ONLY_PROPERTIES, $requestedProperties);
if (empty($allowedProps)) {
return;
}
$paths = [];
foreach ($users as $nestedPaths) {
$paths = array_merge($paths, $nestedPaths);
}
$paths = array_unique($paths);
$propsByPath = array_fill_keys(array_values($paths), []);
$properties = $this->propertyMapper->findPropertiesByPaths($paths, $allowedProps);
foreach ($properties as $property) {
$propsByPath[$property->getPropertypath()][$property->getPropertyname()] = $this->decodeValueFromDatabase($property->getPropertyvalue(), $property->getValuetype());
}
$this->publishedCache = array_merge($this->publishedCache, $propsByPath);
}
/**
* Returns a list of properties for the given path and current user
*

1
apps/dav/lib/Db/Property.php

@ -16,6 +16,7 @@ use OCP\AppFramework\Db\Entity;
* @method string getPropertypath()
* @method string getPropertyname()
* @method string getPropertyvalue()
* @method int getValuetype()
*/
class Property extends Entity {

37
apps/dav/lib/Db/PropertyMapper.php

@ -10,6 +10,7 @@ declare(strict_types=1);
namespace OCA\DAV\Db;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
/**
@ -39,17 +40,43 @@ class PropertyMapper extends QBMapper {
}
/**
* @param array<string, string[]> $calendars
* @return Property[]
* @throws \OCP\DB\Exception
*/
public function findPropertiesByPath(string $userId, string $path): array {
public function findPropertiesByPathsAndUsers(array $calendars): array {
$selectQb = $this->db->getQueryBuilder();
$selectQb->select('*')
->from(self::TABLE_NAME)
->where(
$selectQb->expr()->eq('userid', $selectQb->createNamedParameter($userId)),
$selectQb->expr()->eq('propertypath', $selectQb->createNamedParameter($path)),
->from(self::TABLE_NAME);
foreach ($calendars as $user => $paths) {
$selectQb->orWhere(
$selectQb->expr()->andX(
$selectQb->expr()->eq('userid', $selectQb->createNamedParameter($user)),
$selectQb->expr()->in('propertypath', $selectQb->createNamedParameter($paths, IQueryBuilder::PARAM_STR_ARRAY)),
)
);
}
return $this->findEntities($selectQb);
}
/**
* @param string[] $calendars
* @param string[] $allowedProperties
* @return Property[]
* @throws \OCP\DB\Exception
*/
public function findPropertiesByPaths(array $calendars, array $allowedProperties = []): array {
$selectQb = $this->db->getQueryBuilder();
$selectQb->select('*')
->from(self::TABLE_NAME)
->where($selectQb->expr()->in('propertypath', $selectQb->createNamedParameter($calendars, IQueryBuilder::PARAM_STR_ARRAY)));
if ($allowedProperties) {
$selectQb->andWhere($selectQb->expr()->in('propertyname', $selectQb->createNamedParameter($allowedProperties, IQueryBuilder::PARAM_STR_ARRAY)));
}
return $this->findEntities($selectQb);
}
}

2
apps/dav/lib/Server.php

@ -54,6 +54,7 @@ use OCA\DAV\Connector\Sabre\ZipFolderPlugin;
use OCA\DAV\DAV\CustomPropertiesBackend;
use OCA\DAV\DAV\PublicAuth;
use OCA\DAV\DAV\ViewOnlyPlugin;
use OCA\DAV\Db\PropertyMapper;
use OCA\DAV\Events\SabrePluginAddEvent;
use OCA\DAV\Events\SabrePluginAuthInitEvent;
use OCA\DAV\Files\BrowserErrorPagePlugin;
@ -306,6 +307,7 @@ class Server {
$this->server->tree,
\OCP\Server::get(IDBConnection::class),
\OCP\Server::get(IUserSession::class)->getUser(),
\OCP\Server::get(PropertyMapper::class),
\OCP\Server::get(DefaultCalendarValidator::class),
)
)

2
apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php

@ -12,6 +12,7 @@ use OCA\DAV\CalDAV\DefaultCalendarValidator;
use OCA\DAV\Connector\Sabre\Directory;
use OCA\DAV\Connector\Sabre\File;
use OCA\DAV\DAV\CustomPropertiesBackend;
use OCA\DAV\Db\PropertyMapper;
use OCP\IDBConnection;
use OCP\IUser;
use OCP\Server;
@ -52,6 +53,7 @@ class CustomPropertiesBackendTest extends \Test\TestCase {
$this->tree,
Server::get(IDBConnection::class),
$this->user,
Server::get(PropertyMapper::class),
$this->defaultCalendarValidator,
);
}

5
apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php

@ -10,6 +10,7 @@ namespace OCA\DAV\Tests\unit\DAV;
use OCA\DAV\CalDAV\Calendar;
use OCA\DAV\CalDAV\DefaultCalendarValidator;
use OCA\DAV\DAV\CustomPropertiesBackend;
use OCA\DAV\Db\PropertyMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IUser;
@ -36,6 +37,7 @@ class CustomPropertiesBackendTest extends TestCase {
private IUser&MockObject $user;
private DefaultCalendarValidator&MockObject $defaultCalendarValidator;
private CustomPropertiesBackend $backend;
private PropertyMapper $propertyMapper;
protected function setUp(): void {
parent::setUp();
@ -49,6 +51,7 @@ class CustomPropertiesBackendTest extends TestCase {
->with()
->willReturn('dummy_user_42');
$this->dbConnection = \OCP\Server::get(IDBConnection::class);
$this->propertyMapper = \OCP\Server::get(PropertyMapper::class);
$this->defaultCalendarValidator = $this->createMock(DefaultCalendarValidator::class);
$this->backend = new CustomPropertiesBackend(
@ -56,6 +59,7 @@ class CustomPropertiesBackendTest extends TestCase {
$this->tree,
$this->dbConnection,
$this->user,
$this->propertyMapper,
$this->defaultCalendarValidator,
);
}
@ -129,6 +133,7 @@ class CustomPropertiesBackendTest extends TestCase {
$this->tree,
$db,
$this->user,
$this->propertyMapper,
$this->defaultCalendarValidator,
);

Loading…
Cancel
Save