perf(caldav): Cache calendars in CustomPropertiesBackend

We already do that for files, we are now also doing for calendars.
With relatively small amount of calendars, I managed to reduce the
number of DB requests by 35% and from 23 DB requests touching the
oc_properties table to only 3.

Signed-off-by: Carl Schwan <carl.schwan@nextclound.com>
pull/54386/head
Carl Schwan 5 months ago
parent 977541cedf
commit 46f0c6ebb5
  1. 6
      apps/dav/lib/CalDAV/Calendar.php
  2. 2
      apps/dav/lib/CalDAV/CalendarProvider.php
  3. 2
      apps/dav/lib/Connector/Sabre/ServerFactory.php
  4. 77
      apps/dav/lib/DAV/CustomPropertiesBackend.php
  5. 1
      apps/dav/lib/Db/Property.php
  6. 25
      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

@ -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

@ -72,7 +72,7 @@ class CalendarProvider implements ICalendarProvider {
$calendars[$user][] = 'calendars/' . $user . '/' . $uri['uri'];
}
$properties = $this->propertyMapper->findPropertiesByPaths($calendars);
$properties = $this->propertyMapper->findPropertiesByPathsAndUsers($calendars);
$list = [];
foreach ($properties as $property) {

@ -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),
)
)

@ -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,58 @@ class CustomPropertiesBackend implements BackendInterface {
$this->userCache = array_merge($this->userCache, $propsByPath);
}
private function cacheCalendars(CalendarHome $node): 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
$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);
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
*

@ -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 {

@ -44,17 +44,34 @@ class PropertyMapper extends QBMapper {
* @return Property[]
* @throws \OCP\DB\Exception
*/
public function findPropertiesByPaths(array $calendars): array {
public function findPropertiesByPathsAndUsers(array $calendars): array {
$selectQb = $this->db->getQueryBuilder();
$selectQb->select('*')
->from(self::TABLE_NAME);
foreach ($calendars as $user => $paths) {
$selectQb->andWhere(
$selectQb->expr()->eq('userid', $selectQb->createNamedParameter($user)),
$selectQb->expr()->in('propertypath', $selectQb->createNamedParameter($paths, IQueryBuilder::PARAM_STR_ARRAY)),
$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
* @return Property[]
* @throws \OCP\DB\Exception
*/
public function findPropertiesByPaths(array $calendars): array {
$selectQb = $this->db->getQueryBuilder();
$selectQb->select('*')
->from(self::TABLE_NAME)
->where($selectQb->expr()->in('propertypath', $selectQb->createNamedParameter($calendars, IQueryBuilder::PARAM_STR_ARRAY)));
return $this->findEntities($selectQb);
}
}

@ -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),
)
)

@ -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,
);
}

@ -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