|
|
|
|
@ -9,6 +9,7 @@ |
|
|
|
|
* @author Ole Ostergaard <ole.c.ostergaard@gmail.com> |
|
|
|
|
* @author Robin Appelman <robin@icewind.nl> |
|
|
|
|
* @author Roeland Jago Douma <roeland@famdouma.nl> |
|
|
|
|
* @author Carl Schwan <carl@carlschwan.eu> |
|
|
|
|
* |
|
|
|
|
* @license AGPL-3.0 |
|
|
|
|
* |
|
|
|
|
@ -33,51 +34,40 @@ use OCP\DB\QueryBuilder\IQueryBuilder; |
|
|
|
|
use OCP\IDBConnection; |
|
|
|
|
use OCP\Lock\ILockingProvider; |
|
|
|
|
use OCP\Lock\LockedException; |
|
|
|
|
use Psr\Log\LoggerInterface; |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Locking provider that stores the locks in the database |
|
|
|
|
*/ |
|
|
|
|
class DBLockingProvider extends AbstractLockingProvider { |
|
|
|
|
/** |
|
|
|
|
* @var \OCP\IDBConnection |
|
|
|
|
*/ |
|
|
|
|
private $connection; |
|
|
|
|
|
|
|
|
|
private LoggerInterface $logger; |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* @var \OCP\AppFramework\Utility\ITimeFactory |
|
|
|
|
*/ |
|
|
|
|
private $timeFactory; |
|
|
|
|
|
|
|
|
|
private $sharedLocks = []; |
|
|
|
|
private IDBConnection $connection; |
|
|
|
|
private ITimeFactory $timeFactory; |
|
|
|
|
private array $sharedLocks = []; |
|
|
|
|
private bool $cacheSharedLocks; |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* @var bool |
|
|
|
|
*/ |
|
|
|
|
private $cacheSharedLocks; |
|
|
|
|
public function __construct( |
|
|
|
|
IDBConnection $connection, |
|
|
|
|
ITimeFactory $timeFactory, |
|
|
|
|
int $ttl = 3600, |
|
|
|
|
bool $cacheSharedLocks = true |
|
|
|
|
) { |
|
|
|
|
$this->connection = $connection; |
|
|
|
|
$this->timeFactory = $timeFactory; |
|
|
|
|
$this->ttl = $ttl; |
|
|
|
|
$this->cacheSharedLocks = $cacheSharedLocks; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Check if we have an open shared lock for a path |
|
|
|
|
* |
|
|
|
|
* @param string $path |
|
|
|
|
* @return bool |
|
|
|
|
*/ |
|
|
|
|
protected function isLocallyLocked(string $path): bool { |
|
|
|
|
return isset($this->sharedLocks[$path]) && $this->sharedLocks[$path]; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Mark a locally acquired lock |
|
|
|
|
* |
|
|
|
|
* @param string $path |
|
|
|
|
* @param int $type self::LOCK_SHARED or self::LOCK_EXCLUSIVE |
|
|
|
|
*/ |
|
|
|
|
protected function markAcquire(string $path, int $type) { |
|
|
|
|
parent::markAcquire($path, $type); |
|
|
|
|
/** @inheritDoc */ |
|
|
|
|
protected function markAcquire(string $path, int $targetType): void { |
|
|
|
|
parent::markAcquire($path, $targetType); |
|
|
|
|
if ($this->cacheSharedLocks) { |
|
|
|
|
if ($type === self::LOCK_SHARED) { |
|
|
|
|
if ($targetType === self::LOCK_SHARED) { |
|
|
|
|
$this->sharedLocks[$path] = true; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
@ -85,11 +75,8 @@ class DBLockingProvider extends AbstractLockingProvider { |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Change the type of an existing tracked lock |
|
|
|
|
* |
|
|
|
|
* @param string $path |
|
|
|
|
* @param int $targetType self::LOCK_SHARED or self::LOCK_EXCLUSIVE |
|
|
|
|
*/ |
|
|
|
|
protected function markChange(string $path, int $targetType) { |
|
|
|
|
protected function markChange(string $path, int $targetType): void { |
|
|
|
|
parent::markChange($path, $targetType); |
|
|
|
|
if ($this->cacheSharedLocks) { |
|
|
|
|
if ($targetType === self::LOCK_SHARED) { |
|
|
|
|
@ -100,29 +87,8 @@ class DBLockingProvider extends AbstractLockingProvider { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* @param bool $cacheSharedLocks |
|
|
|
|
*/ |
|
|
|
|
public function __construct( |
|
|
|
|
IDBConnection $connection, |
|
|
|
|
LoggerInterface $logger, |
|
|
|
|
ITimeFactory $timeFactory, |
|
|
|
|
int $ttl = 3600, |
|
|
|
|
$cacheSharedLocks = true |
|
|
|
|
) { |
|
|
|
|
$this->connection = $connection; |
|
|
|
|
$this->logger = $logger; |
|
|
|
|
$this->timeFactory = $timeFactory; |
|
|
|
|
$this->ttl = $ttl; |
|
|
|
|
$this->cacheSharedLocks = $cacheSharedLocks; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Insert a file locking row if it does not exists. |
|
|
|
|
* |
|
|
|
|
* @param string $path |
|
|
|
|
* @param int $lock |
|
|
|
|
* @return int number of inserted rows |
|
|
|
|
*/ |
|
|
|
|
protected function initLockField(string $path, int $lock = 0): int { |
|
|
|
|
$expire = $this->getExpireTime(); |
|
|
|
|
@ -133,25 +99,21 @@ class DBLockingProvider extends AbstractLockingProvider { |
|
|
|
|
]); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* @return int |
|
|
|
|
*/ |
|
|
|
|
protected function getExpireTime(): int { |
|
|
|
|
return $this->timeFactory->getTime() + $this->ttl; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* @param string $path |
|
|
|
|
* @param int $type self::LOCK_SHARED or self::LOCK_EXCLUSIVE |
|
|
|
|
* @return bool |
|
|
|
|
*/ |
|
|
|
|
/** @inheritDoc */ |
|
|
|
|
public function isLocked(string $path, int $type): bool { |
|
|
|
|
if ($this->hasAcquiredLock($path, $type)) { |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
$query = $this->connection->prepare('SELECT `lock` from `*PREFIX*file_locks` WHERE `key` = ?'); |
|
|
|
|
$query->execute([$path]); |
|
|
|
|
$lockValue = (int)$query->fetchOne(); |
|
|
|
|
$query = $this->connection->getQueryBuilder(); |
|
|
|
|
$query->select('lock') |
|
|
|
|
->from('file_locks') |
|
|
|
|
->where($query->expr()->eq('key', $query->createNamedParameter($path))); |
|
|
|
|
$result = $query->executeQuery(); |
|
|
|
|
$lockValue = (int)$result->fetchOne(); |
|
|
|
|
if ($type === self::LOCK_SHARED) { |
|
|
|
|
if ($this->isLocallyLocked($path)) { |
|
|
|
|
// if we have a shared lock we kept open locally but it's released we always have at least 1 shared lock in the db |
|
|
|
|
@ -166,21 +128,20 @@ class DBLockingProvider extends AbstractLockingProvider { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* @param string $path |
|
|
|
|
* @param int $type self::LOCK_SHARED or self::LOCK_EXCLUSIVE |
|
|
|
|
* @throws \OCP\Lock\LockedException |
|
|
|
|
*/ |
|
|
|
|
public function acquireLock(string $path, int $type, string $readablePath = null) { |
|
|
|
|
/** @inheritDoc */ |
|
|
|
|
public function acquireLock(string $path, int $type, ?string $readablePath = null): void { |
|
|
|
|
$expire = $this->getExpireTime(); |
|
|
|
|
if ($type === self::LOCK_SHARED) { |
|
|
|
|
if (!$this->isLocallyLocked($path)) { |
|
|
|
|
$result = $this->initLockField($path, 1); |
|
|
|
|
if ($result <= 0) { |
|
|
|
|
$result = $this->connection->executeUpdate( |
|
|
|
|
'UPDATE `*PREFIX*file_locks` SET `lock` = `lock` + 1, `ttl` = ? WHERE `key` = ? AND `lock` >= 0', |
|
|
|
|
[$expire, $path] |
|
|
|
|
); |
|
|
|
|
$query = $this->connection->getQueryBuilder(); |
|
|
|
|
$query->update('file_locks') |
|
|
|
|
->set('lock', $query->func()->add('lock', $query->createNamedParameter(1, IQueryBuilder::PARAM_INT))) |
|
|
|
|
->set('ttl', $query->createNamedParameter($expire)) |
|
|
|
|
->where($query->expr()->eq('key', $query->createNamedParameter($path))) |
|
|
|
|
->andWhere($query->expr()->gte('lock', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT))); |
|
|
|
|
$result = $query->executeStatement(); |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
$result = 1; |
|
|
|
|
@ -192,10 +153,13 @@ class DBLockingProvider extends AbstractLockingProvider { |
|
|
|
|
} |
|
|
|
|
$result = $this->initLockField($path, -1); |
|
|
|
|
if ($result <= 0) { |
|
|
|
|
$result = $this->connection->executeUpdate( |
|
|
|
|
'UPDATE `*PREFIX*file_locks` SET `lock` = -1, `ttl` = ? WHERE `key` = ? AND `lock` = ?', |
|
|
|
|
[$expire, $path, $existing] |
|
|
|
|
); |
|
|
|
|
$query = $this->connection->getQueryBuilder(); |
|
|
|
|
$query->update('file_locks') |
|
|
|
|
->set('lock', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT)) |
|
|
|
|
->set('ttl', $query->createNamedParameter($expire, IQueryBuilder::PARAM_INT)) |
|
|
|
|
->where($query->expr()->eq('key', $query->createNamedParameter($path))) |
|
|
|
|
->andWhere($query->expr()->eq('lock', $query->createNamedParameter($existing))); |
|
|
|
|
$result = $query->executeStatement(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
if ($result !== 1) { |
|
|
|
|
@ -204,52 +168,53 @@ class DBLockingProvider extends AbstractLockingProvider { |
|
|
|
|
$this->markAcquire($path, $type); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* @param string $path |
|
|
|
|
* @param int $type self::LOCK_SHARED or self::LOCK_EXCLUSIVE |
|
|
|
|
*/ |
|
|
|
|
public function releaseLock(string $path, int $type) { |
|
|
|
|
/** @inheritDoc */ |
|
|
|
|
public function releaseLock(string $path, int $type): void { |
|
|
|
|
$this->markRelease($path, $type); |
|
|
|
|
|
|
|
|
|
// we keep shared locks till the end of the request so we can re-use them |
|
|
|
|
if ($type === self::LOCK_EXCLUSIVE) { |
|
|
|
|
$this->connection->executeUpdate( |
|
|
|
|
'UPDATE `*PREFIX*file_locks` SET `lock` = 0 WHERE `key` = ? AND `lock` = -1', |
|
|
|
|
[$path] |
|
|
|
|
); |
|
|
|
|
$qb = $this->connection->getQueryBuilder(); |
|
|
|
|
$qb->update('file_locks') |
|
|
|
|
->set('lock', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)) |
|
|
|
|
->where($qb->expr()->eq('key', $qb->createNamedParameter($path))) |
|
|
|
|
->andWhere($qb->expr()->eq('lock', $qb->createNamedParameter(-1, IQueryBuilder::PARAM_INT))); |
|
|
|
|
$qb->executeStatement(); |
|
|
|
|
} elseif (!$this->cacheSharedLocks) { |
|
|
|
|
$query = $this->connection->getQueryBuilder(); |
|
|
|
|
$query->update('file_locks') |
|
|
|
|
->set('lock', $query->func()->subtract('lock', $query->createNamedParameter(1))) |
|
|
|
|
->where($query->expr()->eq('key', $query->createNamedParameter($path))) |
|
|
|
|
->andWhere($query->expr()->gt('lock', $query->createNamedParameter(0))); |
|
|
|
|
$query->execute(); |
|
|
|
|
$qb = $this->connection->getQueryBuilder(); |
|
|
|
|
$qb->update('file_locks') |
|
|
|
|
->set('lock', $qb->func()->subtract('lock', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))) |
|
|
|
|
->where($qb->expr()->eq('key', $qb->createNamedParameter($path))) |
|
|
|
|
->andWhere($qb->expr()->gt('lock', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))); |
|
|
|
|
$qb->executeStatement(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Change the type of an existing lock |
|
|
|
|
* |
|
|
|
|
* @param string $path |
|
|
|
|
* @param int $targetType self::LOCK_SHARED or self::LOCK_EXCLUSIVE |
|
|
|
|
* @throws \OCP\Lock\LockedException |
|
|
|
|
*/ |
|
|
|
|
public function changeLock(string $path, int $targetType) { |
|
|
|
|
/** @inheritDoc */ |
|
|
|
|
public function changeLock(string $path, int $targetType): void { |
|
|
|
|
$expire = $this->getExpireTime(); |
|
|
|
|
if ($targetType === self::LOCK_SHARED) { |
|
|
|
|
$result = $this->connection->executeUpdate( |
|
|
|
|
'UPDATE `*PREFIX*file_locks` SET `lock` = 1, `ttl` = ? WHERE `key` = ? AND `lock` = -1', |
|
|
|
|
[$expire, $path] |
|
|
|
|
); |
|
|
|
|
$qb = $this->connection->getQueryBuilder(); |
|
|
|
|
$result = $qb->update('file_locks') |
|
|
|
|
->set('lock', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT)) |
|
|
|
|
->set('ttl', $qb->createNamedParameter($expire, IQueryBuilder::PARAM_INT)) |
|
|
|
|
->where($qb->expr()->andX( |
|
|
|
|
$qb->expr()->eq('key', $qb->createNamedParameter($path)), |
|
|
|
|
$qb->expr()->eq('lock', $qb->createNamedParameter(-1, IQueryBuilder::PARAM_INT)) |
|
|
|
|
))->executeStatement(); |
|
|
|
|
} else { |
|
|
|
|
// since we only keep one shared lock in the db we need to check if we have more then one shared lock locally manually |
|
|
|
|
if (isset($this->acquiredLocks['shared'][$path]) && $this->acquiredLocks['shared'][$path] > 1) { |
|
|
|
|
throw new LockedException($path); |
|
|
|
|
} |
|
|
|
|
$result = $this->connection->executeUpdate( |
|
|
|
|
'UPDATE `*PREFIX*file_locks` SET `lock` = -1, `ttl` = ? WHERE `key` = ? AND `lock` = 1', |
|
|
|
|
[$expire, $path] |
|
|
|
|
); |
|
|
|
|
$qb = $this->connection->getQueryBuilder(); |
|
|
|
|
$result = $qb->update('file_locks') |
|
|
|
|
->set('lock', $qb->createNamedParameter(-1, IQueryBuilder::PARAM_INT)) |
|
|
|
|
->set('ttl', $qb->createNamedParameter($expire, IQueryBuilder::PARAM_INT)) |
|
|
|
|
->where($qb->expr()->andX( |
|
|
|
|
$qb->expr()->eq('key', $qb->createNamedParameter($path)), |
|
|
|
|
$qb->expr()->eq('lock', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT)) |
|
|
|
|
))->executeStatement(); |
|
|
|
|
} |
|
|
|
|
if ($result !== 1) { |
|
|
|
|
throw new LockedException($path); |
|
|
|
|
@ -257,16 +222,14 @@ class DBLockingProvider extends AbstractLockingProvider { |
|
|
|
|
$this->markChange($path, $targetType); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* cleanup empty locks |
|
|
|
|
*/ |
|
|
|
|
public function cleanExpiredLocks() { |
|
|
|
|
/** @inheritDoc */ |
|
|
|
|
public function cleanExpiredLocks(): void { |
|
|
|
|
$expire = $this->timeFactory->getTime(); |
|
|
|
|
try { |
|
|
|
|
$this->connection->executeUpdate( |
|
|
|
|
'DELETE FROM `*PREFIX*file_locks` WHERE `ttl` < ?', |
|
|
|
|
[$expire] |
|
|
|
|
); |
|
|
|
|
$qb = $this->connection->getQueryBuilder(); |
|
|
|
|
$qb->delete('file_locks') |
|
|
|
|
->where($qb->expr()->lt('ttl', $qb->createNamedParameter($expire, IQueryBuilder::PARAM_INT))) |
|
|
|
|
->executeStatement(); |
|
|
|
|
} catch (\Exception $e) { |
|
|
|
|
// If the table is missing, the clean up was successful |
|
|
|
|
if ($this->connection->tableExists('file_locks')) { |
|
|
|
|
@ -275,10 +238,8 @@ class DBLockingProvider extends AbstractLockingProvider { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* release all lock acquired by this instance which were marked using the mark* methods |
|
|
|
|
*/ |
|
|
|
|
public function releaseAll() { |
|
|
|
|
/** @inheritDoc */ |
|
|
|
|
public function releaseAll(): void { |
|
|
|
|
parent::releaseAll(); |
|
|
|
|
|
|
|
|
|
if (!$this->cacheSharedLocks) { |
|
|
|
|
@ -292,15 +253,15 @@ class DBLockingProvider extends AbstractLockingProvider { |
|
|
|
|
|
|
|
|
|
$chunkedPaths = array_chunk($lockedPaths, 100); |
|
|
|
|
|
|
|
|
|
foreach ($chunkedPaths as $chunk) { |
|
|
|
|
$builder = $this->connection->getQueryBuilder(); |
|
|
|
|
|
|
|
|
|
$query = $builder->update('file_locks') |
|
|
|
|
->set('lock', $builder->func()->subtract('lock', $builder->expr()->literal(1))) |
|
|
|
|
->where($builder->expr()->in('key', $builder->createNamedParameter($chunk, IQueryBuilder::PARAM_STR_ARRAY))) |
|
|
|
|
->andWhere($builder->expr()->gt('lock', new Literal(0))); |
|
|
|
|
$qb = $this->connection->getQueryBuilder(); |
|
|
|
|
$qb->update('file_locks') |
|
|
|
|
->set('lock', $qb->func()->subtract('lock', $qb->expr()->literal(1))) |
|
|
|
|
->where($qb->expr()->in('key', $qb->createParameter('chunk'))) |
|
|
|
|
->andWhere($qb->expr()->gt('lock', new Literal(0))); |
|
|
|
|
|
|
|
|
|
$query->execute(); |
|
|
|
|
foreach ($chunkedPaths as $chunk) { |
|
|
|
|
$qb->setParameter('chunk', $chunk, IQueryBuilder::PARAM_STR_ARRAY); |
|
|
|
|
$qb->executeStatement(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|