Merge pull request #54435 from nextcloud/perf/caldav/preload-calendar-publish-status

perf(caldav): preload publish statuses for a whole calendar home at once
This commit is contained in:
Richard Steinmetz 2025-08-28 16:40:32 +02:00 committed by GitHub
commit 9001ae2a4e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 86 additions and 4 deletions

View file

@ -78,6 +78,7 @@ $calDavBackend = new CalDavBackend(
$config,
Server::get(\OCA\DAV\CalDAV\Sharing\Backend::class),
Server::get(FederatedCalendarMapper::class),
Server::get(\OCP\ICacheFactory::class),
true
);

View file

@ -43,6 +43,8 @@ use OCP\Calendar\Exceptions\CalendarException;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IUserManager;
@ -202,6 +204,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
private string $dbObjectInvitationsTable = 'calendar_invitations';
private array $cachedObjects = [];
private readonly ICache $publishStatusCache;
public function __construct(
private IDBConnection $db,
private Principal $principalBackend,
@ -212,8 +216,10 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
private IConfig $config,
private Sharing\Backend $calendarSharingBackend,
private FederatedCalendarMapper $federatedCalendarMapper,
ICacheFactory $cacheFactory,
private bool $legacyEndpoint = false,
) {
$this->publishStatusCache = $cacheFactory->createInMemory();
}
/**
@ -923,6 +929,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return void
*/
public function deleteCalendar($calendarId, bool $forceDeletePermanently = false) {
$this->publishStatusCache->remove((string)$calendarId);
$this->atomic(function () use ($calendarId, $forceDeletePermanently): void {
// The calendar is deleted right away if this is either enforced by the caller
// or the special contacts birthday calendar or when the preference of an empty
@ -3221,7 +3229,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return string|null
*/
public function setPublishStatus($value, $calendar) {
return $this->atomic(function () use ($value, $calendar) {
$publishStatus = $this->atomic(function () use ($value, $calendar) {
$calendarId = $calendar->getResourceId();
$calendarData = $this->getCalendarById($calendarId);
@ -3249,13 +3257,21 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent($calendarId, $calendarData));
return null;
}, $this->db);
$this->publishStatusCache->set((string)$calendar->getResourceId(), $publishStatus ?? false);
return $publishStatus;
}
/**
* @param Calendar $calendar
* @return mixed
* @return string|false
*/
public function getPublishStatus($calendar) {
$cached = $this->publishStatusCache->get((string)$calendar->getResourceId());
if ($cached !== null) {
return $cached;
}
$query = $this->db->getQueryBuilder();
$result = $query->select('publicuri')
->from('dav_shares')
@ -3263,9 +3279,46 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
->executeQuery();
$row = $result->fetch();
$publishStatus = $result->fetchOne();
$result->closeCursor();
$this->publishStatusCache->set((string)$calendar->getResourceId(), $publishStatus);
return $publishStatus;
}
/**
* @param int[] $resourceIds
*/
public function preloadPublishStatuses(array $resourceIds): void {
$query = $this->db->getQueryBuilder();
$result = $query->select('resourceid', 'publicuri')
->from('dav_shares')
->where($query->expr()->in(
'resourceid',
$query->createNamedParameter($resourceIds, IQueryBuilder::PARAM_INT_ARRAY),
IQueryBuilder::PARAM_INT_ARRAY,
))
->andWhere($query->expr()->eq(
'access',
$query->createNamedParameter(self::ACCESS_PUBLIC, IQueryBuilder::PARAM_INT),
IQueryBuilder::PARAM_INT,
))
->executeQuery();
$hasPublishStatuses = [];
while ($row = $result->fetch()) {
$this->publishStatusCache->set((string)$row['resourceid'], $row['publicuri']);
$hasPublishStatuses[(int)$row['resourceid']] = true;
}
// Also remember resources with no publish status
foreach ($resourceIds as $resourceId) {
if (!isset($hasPublishStatuses[$resourceId])) {
$this->publishStatusCache->set((string)$resourceId, false);
}
}
$result->closeCursor();
return $row ? reset($row) : false;
}
/**

View file

@ -6,7 +6,9 @@
*/
namespace OCA\DAV\CalDAV\Publishing;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\Calendar;
use OCA\DAV\CalDAV\CalendarHome;
use OCA\DAV\CalDAV\Publishing\Xml\Publisher;
use OCP\AppFramework\Http;
use OCP\IConfig;
@ -91,6 +93,20 @@ class PublishPlugin extends ServerPlugin {
}
public function propFind(PropFind $propFind, INode $node) {
if ($node instanceof CalendarHome && $propFind->getDepth() === 1) {
$backend = $node->getCalDAVBackend();
if ($backend instanceof CalDavBackend) {
$calendars = array_filter(
$node->getChildren(),
static fn ($child) => $child instanceof Calendar,
);
$resourceIds = array_map(
static fn (Calendar $calendar) => $calendar->getResourceId(),
$calendars,
);
$backend->preloadPublishStatuses($resourceIds);
}
}
if ($node instanceof Calendar) {
$propFind->handle('{' . self::NS_CALENDARSERVER . '}publish-url', function () use ($node) {
if ($node->getPublishStatus()) {

View file

@ -16,6 +16,7 @@ use OCA\DAV\Connector\Sabre\Principal;
use OCP\Accounts\IAccountManager;
use OCP\App\IAppManager;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IGroupManager;
@ -82,6 +83,7 @@ class CreateCalendar extends Command {
$config,
Server::get(Backend::class),
Server::get(FederatedCalendarMapper::class),
Server::get(ICacheFactory::class),
);
$caldav->createCalendar("principals/users/$user", $name, []);
return self::SUCCESS;

View file

@ -36,6 +36,7 @@ use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Comments\ICommentsManager;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\IRootFolder;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IGroupManager;
@ -108,6 +109,7 @@ class RootCollection extends SimpleCollection {
$config,
$calendarSharingBackend,
Server::get(FederatedCalendarMapper::class),
Server::get(ICacheFactory::class),
false,
);
$userCalendarRoot = new CalendarRoot($userPrincipalBackend, $caldavBackend, 'principals/users', $logger, $l10n, $config, $federatedCalendarFactory);

View file

@ -59,6 +59,8 @@ abstract class AbstractCalDavBackend extends TestCase {
protected RemoteUserPrincipalBackend&MockObject $remoteUserPrincipalBackend;
protected FederationSharingService&MockObject $federationSharingService;
protected FederatedCalendarMapper&MockObject $federatedCalendarMapper;
protected ICacheFactory $cacheFactory;
public const UNIT_TEST_USER = 'principals/users/caldav-unit-test';
public const UNIT_TEST_USER1 = 'principals/users/caldav-unit-test1';
public const UNIT_TEST_GROUP = 'principals/groups/caldav-unit-test-group';
@ -110,6 +112,7 @@ abstract class AbstractCalDavBackend extends TestCase {
new Service(new SharingMapper($this->db)),
$this->federationSharingService,
$this->logger);
$this->cacheFactory = $this->createMock(ICacheFactory::class);
$this->backend = new CalDavBackend(
$this->db,
$this->principal,
@ -120,6 +123,7 @@ abstract class AbstractCalDavBackend extends TestCase {
$this->config,
$this->sharingBackend,
$this->federatedCalendarMapper,
$this->cacheFactory,
false,
);

View file

@ -14,6 +14,7 @@ use OCA\DAV\CalDAV\PublicCalendar;
use OCA\DAV\CalDAV\PublicCalendarRoot;
use OCA\DAV\Connector\Sabre\Principal;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IGroupManager;
@ -43,6 +44,7 @@ class PublicCalendarRootTest extends TestCase {
protected IConfig&MockObject $config;
private ISecureRandom $random;
private LoggerInterface&MockObject $logger;
protected ICacheFactory&MockObject $cacheFactory;
protected FederatedCalendarMapper&MockObject $federatedCalendarMapper;
@ -56,6 +58,7 @@ class PublicCalendarRootTest extends TestCase {
$this->random = Server::get(ISecureRandom::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->federatedCalendarMapper = $this->createMock(FederatedCalendarMapper::class);
$this->cacheFactory = $this->createMock(ICacheFactory::class);
$dispatcher = $this->createMock(IEventDispatcher::class);
$config = $this->createMock(IConfig::class);
$sharingBackend = $this->createMock(\OCA\DAV\CalDAV\Sharing\Backend::class);
@ -78,6 +81,7 @@ class PublicCalendarRootTest extends TestCase {
$config,
$sharingBackend,
$this->federatedCalendarMapper,
$this->cacheFactory,
false,
);
$this->l10n = $this->createMock(IL10N::class);