Increase performance by adding cache for ACLs

This commit is contained in:
Maximilian Ruta 2020-04-19 23:22:58 +02:00
parent 045577176e
commit b8eba88598
5 changed files with 299 additions and 31 deletions

View file

@ -35,9 +35,9 @@ class ACLManager {
private $rootStorageId = null;
private $rootFolderProvider;
public function __construct(RuleManager $ruleManager, IUser $user, callable $rootFolderProvider) {
public function __construct(RuleManager $ruleManager, ACLRuleCache $ruleCache, IUser $user, callable $rootFolderProvider) {
$this->ruleManager = $ruleManager;
$this->ruleCache = new CappedMemoryCache();
$this->ruleCache = $ruleCache;
$this->user = $user;
$this->rootFolderProvider = $rootFolderProvider;
}
@ -53,40 +53,24 @@ class ACLManager {
return $this->rootStorageId;
}
private function pathsAreCached(array $paths): bool {
foreach ($paths as $path) {
if (!$this->ruleCache->hasKey($path)) {
return false;
}
}
return true;
}
/**
* @param int $folderId
* @param array $paths
* @return (Rule[])[]
*/
private function getRules(array $paths): array {
if ($this->pathsAreCached($paths)) {
$rules = array_combine($paths, array_map(function (string $path) {
return $this->ruleCache->get($path);
}, $paths));
} else {
$rules = $this->ruleManager->getRulesForFilesByPath($this->user, $this->getRootStorageId(), $paths);
foreach ($rules as $path => $rulesForPath) {
$this->ruleCache->set($path, $rulesForPath);
}
$rules = $this->ruleCache->getByPaths($this->user, $this->getRootStorageId(), $paths);
if (count($paths) > 2) {
// also cache the direct sibling since it's likely that we'll be needing those later
$directParent = $paths[1];
$siblingRules = $this->ruleManager->getRulesForFilesByParent($this->user, $this->getRootStorageId(), $directParent);
foreach ($siblingRules as $path => $rulesForPath) {
$this->ruleCache->set($path, $rulesForPath);
}
}
$uncachedPaths = array_values(array_diff($paths, array_keys($rules)));
if (count($uncachedPaths) > 0) {
$this->ruleCache->cachePath(
$this->getRootStorageId(),
$this->ruleManager->getAllRulesForFilesByPath($this->getRootStorageId(), $uncachedPaths)
);
$rules = $this->ruleCache->getByPaths($this->user, $this->getRootStorageId(), $paths);
}
ksort($rules);
return $rules;

View file

@ -27,13 +27,15 @@ use OCP\IUser;
class ACLManagerFactory {
private $ruleManager;
private $rootFolderProvider;
private $ruleCache;
public function __construct(RuleManager $ruleManager, callable $rootFolderProvider) {
public function __construct(RuleManager $ruleManager, ACLRuleCache $ruleCache, callable $rootFolderProvider) {
$this->ruleManager = $ruleManager;
$this->ruleCache = $ruleCache;
$this->rootFolderProvider = $rootFolderProvider;
}
public function getACLManager(IUser $user): ACLManager {
return new ACLManager($this->ruleManager, $user, $this->rootFolderProvider);
return new ACLManager($this->ruleManager, $this->ruleCache, $user, $this->rootFolderProvider);
}
}

248
lib/ACL/ACLRuleCache.php Normal file
View file

@ -0,0 +1,248 @@
<?php
namespace OCA\GroupFolders\ACL;
use OC\Memcache\APCu;
use OCA\GroupFolders\ACL\UserMapping\IUserMapping;
use OCA\GroupFolders\ACL\UserMapping\IUserMappingManager;
use OCP\IUser;
class ACLRuleCache
{
const DERIVATION = 0.1;
const DEFAULT_TTL = 3600 * 24;
const INDEX_TTL = (3600 * 24) + 3600;
/**
* @var APCu
*/
private $ruleCache;
/**
* @var IUserMappingManager
*/
protected $userMappingManager;
public function __construct(IUserMappingManager $userMappingManager) {
$this->userMappingManager = $userMappingManager;
$this->ruleCache = new APCu('acl');
}
protected function set($key, $value, $ttl = null) : void
{
if ($ttl === null) {
$ttl = self::DEFAULT_TTL;
}
$ttl = rand($ttl, round($ttl * self::DERIVATION));
$this->ruleCache->set($key, $value, $ttl);
}
protected function has($key) : bool
{
return $this->ruleCache->hasKey($key);
}
protected function get($key)
{
return $this->ruleCache->get($key);
}
protected function remove($key) : void
{
$this->ruleCache->remove($key);
}
protected function keyPath(int $storageId, string $path) : string
{
return 'path_' . $storageId . '_' . $path;
}
protected function keyFileId(int $fileId) : string
{
return 'file_' . $fileId;
}
protected function keyPathMapping(int $storageId, string $path, IUserMapping $mapping) : string
{
return $this->keyPath($storageId, $path) . '_' . $mapping->getType() . '_' . $mapping->getId();
}
public function clearByPathKey(string $pathKey) : void
{
$keys = $this->get($pathKey);
if (!isset($keys['rule_path_keys'])) {
$keys['rule_path_keys'] = [];
}
if (!isset($keys['rule_id_keys'])) {
$keys['rule_id_keys'] = [];
}
foreach (array_merge($keys['rule_path_keys'], $keys['rule_id_keys']) as $key) {
$this->remove($key);
}
}
public function clearByPath(int $storageId, string $path) : void
{
$pathKey = $this->keyPath($storageId, $path);
if (!$this->has($pathKey)) {
return;
}
$this->clearByPathKey($pathKey);
}
public function clearByFileId(int $fileId) : void
{
$fileIdKey = $this->keyFileId($fileId);
if (!$this->has($fileIdKey)) {
return;
}
$this->clearByPathKey($this->get($fileIdKey));
}
/**
* @return Rule[]|null
*/
public function getByPath(IUser $user, int $storageId, string $path) : ?array
{
return $this->getByPathKey($user, $this->keyPath($storageId, $path));
}
/**
* @param string[] $paths
* @return (Rule[]|null)[]
*/
public function getByPaths(IUser $user, int $storageId, array $paths) : array
{
$rulePaths = [];
foreach ($paths as $path) {
$cached = $this->getByPath($user, $storageId, $path);
if ($cached !== null) {
$rulePaths[$path] = $cached;
}
}
return $rulePaths;
}
/**
* @return Rule[]|null
*/
public function getByFileId(IUser $user, int $fileId) : ?array
{
$fileIdKey = $this->keyFileId($fileId);
if (!$this->has($fileIdKey)) {
return null;
}
$cached = $this->getByPathKey($user, $this->get($fileIdKey));
if ($cached === null) {
return null;
}
$rules = [];
foreach ($cached as $rule) {
if ($rule->getFileId() == $fileIdKey) {
$rules[] = $rule;
}
}
return $rules;
}
/**
* @param (Rule[])[] $paths
*/
public function cachePath(int $storageId, array $paths) : void
{
foreach ($paths as $path => $rules) {
$this->cacheRules($storageId, $path, $rules);
}
}
/**
* @param Rule[] $rules
*/
public function cacheRules(int $storageId, string $path, array $rules) : void
{
$rulePathKeys = [];
$fileIdKeys = [];
foreach ($rules as $rule) {
$rulePathKeys[] = $rulePathKey = $this->keyPathMapping($storageId, $path, $rule->getUserMapping());
$fileIdKeys[] = $ruleFileIdKey = $this->keyFileId($rule->getFileId());
$this->set($rulePathKey, $rule);
}
$pathKey = $this->keyPath($storageId, $path);
$this->set($pathKey, [
'rule_path_keys' => $rulePathKeys,
'rule_id_keys' => $fileIdKeys
], self::INDEX_TTL);
foreach ($fileIdKeys as $fileIdKey) {
$this->set($fileIdKey, $pathKey, self::INDEX_TTL);
}
}
/**
* @param IUserMapping[] $userMappings
* @param Rule $rule
* @return bool
*/
protected function checkMappingInRule(array $userMappings, Rule $rule) : bool
{
foreach ($userMappings as $userMapping) {
if ($userMapping->getId() == $rule->getUserMapping()->getId() &&
$userMapping->getType() == $rule->getUserMapping()->getType()) {
return true;
}
}
return false;
}
/**
* @param IUser $user
* @param string $pathKey
* @return Rule[]|null
*/
protected function getByPathKey(IUser $user, string $pathKey) : ?array
{
if (!$this->has($pathKey)) {
return null;
}
$keys = $this->get($pathKey);
if (!isset($keys['rule_path_keys'])) {
return null;
}
$userMappings = $this->userMappingManager->getMappingsForUser($user);
$rules = [];
foreach ($keys['rule_path_keys'] as $key) {
if (!$this->has($key)) {
return null;
}
$rule = $this->get($key);
if ($this->checkMappingInRule($userMappings, $rule)) {
$rules[] = $rule;
}
}
return $rules;
}
}

View file

@ -29,10 +29,12 @@ use OCP\IUser;
class RuleManager {
private $connection;
private $ruleCache;
private $userMappingManager;
public function __construct(IDBConnection $connection, IUserMappingManager $userMappingManager) {
public function __construct(IDBConnection $connection, ACLRuleCache $ruleCache, IUserMappingManager $userMappingManager) {
$this->connection = $connection;
$this->ruleCache = $ruleCache;
$this->userMappingManager = $userMappingManager;
}
@ -261,6 +263,28 @@ class RuleManager {
return $this->rulesByPath($rows);
}
/**
* @param int $storageId
* @param string[] $filePaths
* @return (Rule[])[] [$path => Rule[]]
*/
public function getAllRulesForFilesByPath(int $storageId, array $filePaths): array {
$query = $this->connection->getQueryBuilder();
$query->select(['f.fileid', 'mapping_type', 'mapping_id', 'mask', 'a.permissions', 'path'])
->from('group_folders_acl', 'a')
->innerJoin('a', 'filecache', 'f', $query->expr()->eq('f.fileid', 'a.fileid'))
->where($query->expr()->in('path_hash', $query->createNamedParameter(array_map('md5', $filePaths), IQueryBuilder::PARAM_STR_ARRAY)))
->andWhere($query->expr()->eq('storage', $query->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
$rows = $query->execute()->fetchAll();
$result = [];
foreach ($filePaths as $path) {
$result[$path] = [];
}
return $this->rulesByPath($rows, $result);
}
private function hasRule(IUserMapping $mapping, int $fileId): bool {
$query = $this->connection->getQueryBuilder();
$query->select('fileid')
@ -293,6 +317,7 @@ class RuleManager {
]);
$query->execute();
}
$this->ruleCache->clearByFileId($rule->getFileId());
}
public function deleteRule(Rule $rule) {
@ -302,5 +327,6 @@ class RuleManager {
->andWhere($query->expr()->eq('mapping_type', $query->createNamedParameter($rule->getUserMapping()->getType())))
->andWhere($query->expr()->eq('mapping_id', $query->createNamedParameter($rule->getUserMapping()->getId())));
$query->execute();
$this->ruleCache->clearByFileId($rule->getFileId());
}
}

View file

@ -23,6 +23,7 @@ namespace OCA\GroupFolders\AppInfo;
use OC\Group\Manager;
use OCA\GroupFolders\ACL\ACLManagerFactory;
use OCA\GroupFolders\ACL\ACLRuleCache;
use OCA\GroupFolders\ACL\RuleManager;
use OCA\GroupFolders\ACL\UserMapping\IUserMappingManager;
use OCA\GroupFolders\ACL\UserMapping\UserMappingManager;
@ -114,12 +115,19 @@ class Application extends App {
}
});
$container->registerService(ACLRuleCache::class, function(IAppContainer $c) {
return new ACLRuleCache(
$c->query(IUserMappingManager::class)
);
});
$container->registerService(ACLManagerFactory::class, function(IAppContainer $c) {
$rootFolderProvider = function () use ($c) {
return $c->getServer()->getRootFolder();
};
return new ACLManagerFactory(
$c->query(RuleManager::class),
$c->query(ACLRuleCache::class),
$rootFolderProvider
);
});