Skip to content

Commit

Permalink
Add batch methods in user backends
Browse files Browse the repository at this point in the history
This allows for faster group search with significantly less DB traffic

Signed-off-by: Carl Schwan <carl@carlschwan.eu>
  • Loading branch information
CarlSchwan committed Jul 14, 2022
1 parent d3f66e2 commit ed477f9
Show file tree
Hide file tree
Showing 10 changed files with 283 additions and 42 deletions.
14 changes: 7 additions & 7 deletions apps/user_ldap/lib/Group_LDAP.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,13 @@
use OC;
use OC\Cache\CappedMemoryCache;
use OC\ServerNotAvailableException;
use OCP\Group\Backend\ABackend;
use OCP\Group\Backend\IGetDisplayNameBackend;
use OCP\Group\Backend\IDeleteGroupBackend;
use OCP\GroupInterface;
use Psr\Log\LoggerInterface;

class Group_LDAP extends BackendUtility implements GroupInterface, IGroupLDAP, IGetDisplayNameBackend, IDeleteGroupBackend {
class Group_LDAP extends ABackend implements GroupInterface, IGroupLDAP, IGetDisplayNameBackend, IDeleteGroupBackend {
protected $enabled = false;

/** @var CappedMemoryCache<string[]> $cachedGroupMembers array of users with gid as key */
Expand All @@ -61,18 +62,17 @@ class Group_LDAP extends BackendUtility implements GroupInterface, IGroupLDAP, I
protected CappedMemoryCache $cachedGroupsByMember;
/** @var CappedMemoryCache<string[]> $cachedNestedGroups array of groups with gid (DN) as key */
protected CappedMemoryCache $cachedNestedGroups;
/** @var GroupPluginManager */
protected $groupPluginManager;
/** @var LoggerInterface */
protected $logger;
protected GroupPluginManager $groupPluginManager;
protected LoggerInterface $logger;
protected Access $access;

/**
* @var string $ldapGroupMemberAssocAttr contains the LDAP setting (in lower case) with the same name
*/
protected $ldapGroupMemberAssocAttr;

public function __construct(Access $access, GroupPluginManager $groupPluginManager) {
parent::__construct($access);
$this->access = $access;
$filter = $this->access->connection->ldapGroupFilter;
$gAssoc = $this->access->connection->ldapGroupMemberAssocAttr;
if (!empty($filter) && !empty($gAssoc)) {
Expand Down Expand Up @@ -1203,7 +1203,7 @@ protected function filterValidGroups(array $listOfGroups): array {
* Returns the supported actions as int to be
* compared with GroupInterface::CREATE_GROUP etc.
*/
public function implementsActions($actions) {
public function implementsActions($actions): bool {
return (bool)((GroupInterface::COUNT_USERS |
GroupInterface::DELETE_GROUP |
$this->groupPluginManager->getImplementedActions()) & $actions);
Expand Down
31 changes: 31 additions & 0 deletions apps/user_ldap/lib/Group_Proxy.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@

use OCP\Group\Backend\IDeleteGroupBackend;
use OCP\Group\Backend\IGetDisplayNameBackend;
use OCP\Group\Backend\IGroupDetailsBackend;
use OCP\Group\Backend\INamedBackend;
use OCP\GroupInterface;

class Group_Proxy extends Proxy implements \OCP\GroupInterface, IGroupLDAP, IGetDisplayNameBackend, INamedBackend, IDeleteGroupBackend {
private $backends = [];
Expand Down Expand Up @@ -229,6 +231,21 @@ public function getGroupDetails($gid) {
$gid, 'getGroupDetails', [$gid]);
}

/**
* {@inheritdoc}
*/
public function getGroupsDetails(array $gids): array {
if (!($this instanceof IGroupDetailsBackend || $this->implementsActions(GroupInterface::GROUP_DETAILS))) {
throw new \Exception("Should not have been called");
}

$groupData = [];
foreach ($gids as $gid) {
$groupData[$gid] = $this->handleRequest($gid, 'getGroupDetails', [$gid]);
}
return $groupData;
}

/**
* get a list of all groups
*
Expand Down Expand Up @@ -259,6 +276,20 @@ public function groupExists($gid) {
return $this->handleRequest($gid, 'groupExists', [$gid]);
}

/**
* {@inheritdoc}
*/
public function groupsExists(array $gids): array {
$existingGroups = [];
foreach ($gids as $gid) {
$exits = $this->handleRequest($gid, 'groupExists', [$gid]);
if ($exits) {
$existingGroups[] = $gid;
}
}
return $existingGroups;
}

/**
* Check if backend implements actions
*
Expand Down
10 changes: 0 additions & 10 deletions build/psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3317,16 +3317,6 @@
<code>is_null($this-&gt;getContent())</code>
</TypeDoesNotContainNull>
</file>
<file src="lib/private/Group/Database.php">
<InvalidArrayOffset occurrences="1">
<code>$this-&gt;groupCache[$gid]['displayname']</code>
</InvalidArrayOffset>
<InvalidPropertyAssignmentValue occurrences="3">
<code>$this-&gt;groupCache</code>
<code>$this-&gt;groupCache</code>
<code>$this-&gt;groupCache</code>
</InvalidPropertyAssignmentValue>
</file>
<file src="lib/private/Group/Group.php">
<InvalidArgument occurrences="7">
<code>IGroup::class . '::postAddUser'</code>
Expand Down
85 changes: 80 additions & 5 deletions lib/private/Group/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,9 @@ class Database extends ABackend implements
ISetDisplayNameBackend,
INamedBackend {

/** @var string[] */
/** @var array<string, array{gid: string, displayname: string}> */
private $groupCache = [];

/** @var IDBConnection */
private $dbConn;
private ?IDBConnection $dbConn;

/**
* \OC\Group\Database constructor.
Expand Down Expand Up @@ -267,7 +265,7 @@ public function getGroups($search = '', $limit = null, $offset = null) {
$this->fixDI();

$query = $this->dbConn->getQueryBuilder();
$query->select('gid')
$query->select('gid', 'displayname')
->from('groups')
->orderBy('gid', 'ASC');

Expand All @@ -286,6 +284,10 @@ public function getGroups($search = '', $limit = null, $offset = null) {

$groups = [];
while ($row = $result->fetch()) {
$this->groupCache[$row['gid']] = [
'displayname' => $row['displayname'],
'gid' => $row['gid'],
];
$groups[] = $row['gid'];
}
$result->closeCursor();
Expand Down Expand Up @@ -324,6 +326,42 @@ public function groupExists($gid) {
return false;
}

/**
* {@inheritdoc}
*/
public function groupsExists(array $gids): array {
$notFounsGids = [];
$existingGroups = [];

// In case the data is already locally accessible, not need to do SQL query
// or do a SQL query but with a smaller in clause
foreach ($gids as $gid) {
if (isset($this->groupCache[$gid])) {
$existingGroups[] = $gid;
} else {
$notFounsGids[] = $gid;
}
}

foreach (array_chunk($gids, 1000) as $chunk) {
$qb = $this->dbConn->getQueryBuilder();
$result = $qb->select('gid', 'displayname')
->from('groups')
->where($qb->expr()->in('gid', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_STR_ARRAY)))
->executeQuery();
while ($row = $result->fetch()) {
$this->groupCache[$row['gid']] = [
'displayname' => $row['displayname'],
'gid' => $row['gid'],
];
$existingGroups[] = $gid;
}
$result->closeCursor();
}

return $existingGroups;
}

/**
* get a list of all users in a group
* @param string $gid
Expand Down Expand Up @@ -474,6 +512,43 @@ public function getGroupDetails(string $gid): array {
return [];
}

/**
* {@inheritdoc}
*/
public function getGroupsDetails(array $gids): array {
$notFounsGids = [];
$details = [];

// In case the data is already locally accessible, not need to do SQL query
// or do a SQL query but with a smaller in clause
foreach ($gids as $gid) {
if (isset($this->groupCache[$gid])) {
$details[$gid] = ['displayName' => $this->groupCache[$gid]['displayname']];
} else {
$notFounsGids[] = $gid;
}
}

foreach (array_chunk($notFounsGids, 1000) as $chunk) {
$query = $this->dbConn->getQueryBuilder();
$query->select('gid', 'displayname')
->from('groups')
->where($query->expr()->in('gid', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_STR_ARRAY)));

$result = $query->executeQuery();
while ($row = $result->fetch()) {
$details[$row['gid']] = ['displayName' => $row['displayname']];
$this->groupCache[$row['gid']] = [
'displayname' => $row['displayname'],
'gid' => $row['gid'],
];
}
$result->closeCursor();
}

return $details;
}

public function setDisplayName(string $gid, string $displayName): bool {
if (!$this->groupExists($gid)) {
return false;
Expand Down
63 changes: 54 additions & 9 deletions lib/private/Group/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
* @author Vincent Petry <vincent@nextcloud.com>
* @author Vinicius Cubas Brand <vinicius@eita.org.br>
* @author voxsim "Simon Vocella"
* @author Carl Schwan <carl@carlschwan.eu>
*
* @license AGPL-3.0
*
Expand All @@ -41,6 +42,7 @@

use OC\Hooks\PublicEmitter;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Group\Backend\IGroupDetailsBackend;
use OCP\GroupInterface;
use OCP\IGroup;
use OCP\IGroupManager;
Expand Down Expand Up @@ -73,10 +75,10 @@ class Manager extends PublicEmitter implements IGroupManager {
private $dispatcher;
private LoggerInterface $logger;

/** @var \OC\Group\Group[] */
/** @var array<string, IGroup> */
private $cachedGroups = [];

/** @var (string[])[] */
/** @var array<string, list<string>> */
private $cachedUserGroups = [];

/** @var \OC\SubAdmin */
Expand Down Expand Up @@ -193,10 +195,57 @@ protected function getGroupObject($gid, $displayName = null) {
if (count($backends) === 0) {
return null;
}
/** @var GroupInterface[] $backends */
$this->cachedGroups[$gid] = new Group($gid, $backends, $this->dispatcher, $this->userManager, $this, $displayName);
return $this->cachedGroups[$gid];
}

/**
* @brief Batch method to create group objects
*
* @param list<string> $gids List of groupIds for which we want to create a IGroup object
* @param array<string, string> $displayNames Array containing already know display name for a groupId
* @return array<string, IGroup>
*/
protected function getGroupsObject(array $gids, array $displayNames = []): array {
$backends = [];
$groups = [];
foreach ($gids as $gid) {
$backends[$gid] = [];
if (!isset($displayNames[$gid])) {
$displayNames[$gid] = null;
}
}
foreach ($this->backends as $backend) {
if ($backend instanceof IGroupDetailsBackend || $backend->implementsActions(GroupInterface::GROUP_DETAILS)) {
/** @var IGroupDetailsBackend $backend */
$groupDatas = $backend->getGroupsDetails($gids);
foreach ($groupDatas as $gid => $groupData) {
if (!empty($groupData)) {
// take the display name from the first backend that has a non-null one
if (isset($groupData['displayName'])) {
$displayNames[$gid] = $groupData['displayName'];
}
$backends[$gid][] = $backend;
}
}
} else {
$existingGroups = $backend->groupsExists($gids);
foreach ($existingGroups as $group) {
$backends[$group][] = $backend;
}
}
}
foreach ($gids as $gid) {
if (count($backends[$gid]) === 0) {
continue;
}
$this->cachedGroups[$gid] = new Group($gid, $backends[$gid], $this->dispatcher, $this->userManager, $this, $displayNames[$gid]);
$groups[$gid] = $this->cachedGroups[$gid];
}
return $groups;
}

/**
* @param string $gid
* @return bool
Expand Down Expand Up @@ -239,13 +288,9 @@ public function search($search, $limit = null, $offset = null) {
$groups = [];
foreach ($this->backends as $backend) {
$groupIds = $backend->getGroups($search, $limit, $offset);
foreach ($groupIds as $groupId) {
$aGroup = $this->get($groupId);
if ($aGroup instanceof IGroup) {
$groups[$groupId] = $aGroup;
} else {
$this->logger->debug('Group "' . $groupId . '" was returned by search but not found through direct access', ['app' => 'core']);
}
$newGroups = $this->getGroupsObject($groupIds);
foreach ($newGroups as $groupId => $group) {
$groups[$groupId] = $group;
}
if (!is_null($limit) and $limit <= 0) {
return array_values($groups);
Expand Down
30 changes: 30 additions & 0 deletions lib/public/Group/Backend/ABackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* @copyright Copyright (c) 2018 Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Carl Schwan <carl@carlschwan.eu>
*
* @license GNU AGPL version 3 or any later version
*
Expand Down Expand Up @@ -65,4 +66,33 @@ public function implementsActions($actions): bool {

return (bool)($actions & $implements);
}

/**
* @since 25.0.0
*/
public function groupsExists(array $gids): array {
$existingGroups = [];
foreach ($gids as $gid) {
$exists = $this->groupExists($gid);
if ($exists) {
$existringGroups[] = $gid;
}
}
return $existingGroups;
}

/**
* @since 25.0.0
*/
public function getGroupsDetails(array $gids): array {
if (!($this instanceof IGroupDetailsBackend || $this->implementsActions(GroupInterface::GROUP_DETAILS))) {
throw new \Exception("Should not have been called");
}
/** @var IGroupDetailsBackend $this */
$groupData = [];
foreach ($gids as $gid) {
$groupData[$gid] = $this->getGroupDetails($gid);
}
return $groupData;
}
}
Loading

0 comments on commit ed477f9

Please sign in to comment.