DBLockingProvider.php 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  6. * @author Individual IT Services <info@individual-it.net>
  7. * @author Joas Schilling <coding@schilljs.com>
  8. * @author Morris Jobke <hey@morrisjobke.de>
  9. * @author Ole Ostergaard <ole.c.ostergaard@gmail.com>
  10. * @author Robin Appelman <robin@icewind.nl>
  11. * @author Roeland Jago Douma <roeland@famdouma.nl>
  12. * @author Carl Schwan <carl@carlschwan.eu>
  13. *
  14. * @license AGPL-3.0
  15. *
  16. * This code is free software: you can redistribute it and/or modify
  17. * it under the terms of the GNU Affero General Public License, version 3,
  18. * as published by the Free Software Foundation.
  19. *
  20. * This program is distributed in the hope that it will be useful,
  21. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  22. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  23. * GNU Affero General Public License for more details.
  24. *
  25. * You should have received a copy of the GNU Affero General Public License, version 3,
  26. * along with this program. If not, see <http://www.gnu.org/licenses/>
  27. *
  28. */
  29. namespace OC\Lock;
  30. use OC\DB\QueryBuilder\Literal;
  31. use OCP\AppFramework\Utility\ITimeFactory;
  32. use OCP\DB\QueryBuilder\IQueryBuilder;
  33. use OCP\IDBConnection;
  34. use OCP\Lock\ILockingProvider;
  35. use OCP\Lock\LockedException;
  36. /**
  37. * Locking provider that stores the locks in the database
  38. */
  39. class DBLockingProvider extends AbstractLockingProvider {
  40. private array $sharedLocks = [];
  41. public function __construct(
  42. private IDBConnection $connection,
  43. private ITimeFactory $timeFactory,
  44. int $ttl = 3600,
  45. private bool $cacheSharedLocks = true
  46. ) {
  47. parent::__construct($ttl);
  48. }
  49. /**
  50. * Check if we have an open shared lock for a path
  51. */
  52. protected function isLocallyLocked(string $path): bool {
  53. return isset($this->sharedLocks[$path]) && $this->sharedLocks[$path];
  54. }
  55. /** @inheritDoc */
  56. protected function markAcquire(string $path, int $targetType): void {
  57. parent::markAcquire($path, $targetType);
  58. if ($this->cacheSharedLocks) {
  59. if ($targetType === self::LOCK_SHARED) {
  60. $this->sharedLocks[$path] = true;
  61. }
  62. }
  63. }
  64. /**
  65. * Change the type of an existing tracked lock
  66. */
  67. protected function markChange(string $path, int $targetType): void {
  68. parent::markChange($path, $targetType);
  69. if ($this->cacheSharedLocks) {
  70. if ($targetType === self::LOCK_SHARED) {
  71. $this->sharedLocks[$path] = true;
  72. } elseif ($targetType === self::LOCK_EXCLUSIVE) {
  73. $this->sharedLocks[$path] = false;
  74. }
  75. }
  76. }
  77. /**
  78. * Insert a file locking row if it does not exists.
  79. */
  80. protected function initLockField(string $path, int $lock = 0): int {
  81. $expire = $this->getExpireTime();
  82. return $this->connection->insertIgnoreConflict('file_locks', [
  83. 'key' => $path,
  84. 'lock' => $lock,
  85. 'ttl' => $expire
  86. ]);
  87. }
  88. protected function getExpireTime(): int {
  89. return $this->timeFactory->getTime() + $this->ttl;
  90. }
  91. /** @inheritDoc */
  92. public function isLocked(string $path, int $type): bool {
  93. if ($this->hasAcquiredLock($path, $type)) {
  94. return true;
  95. }
  96. $query = $this->connection->getQueryBuilder();
  97. $query->select('lock')
  98. ->from('file_locks')
  99. ->where($query->expr()->eq('key', $query->createNamedParameter($path)));
  100. $result = $query->executeQuery();
  101. $lockValue = (int)$result->fetchOne();
  102. if ($type === self::LOCK_SHARED) {
  103. if ($this->isLocallyLocked($path)) {
  104. // 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
  105. return $lockValue > 1;
  106. } else {
  107. return $lockValue > 0;
  108. }
  109. } elseif ($type === self::LOCK_EXCLUSIVE) {
  110. return $lockValue === -1;
  111. } else {
  112. return false;
  113. }
  114. }
  115. /** @inheritDoc */
  116. public function acquireLock(string $path, int $type, ?string $readablePath = null): void {
  117. $expire = $this->getExpireTime();
  118. if ($type === self::LOCK_SHARED) {
  119. if (!$this->isLocallyLocked($path)) {
  120. $result = $this->initLockField($path, 1);
  121. if ($result <= 0) {
  122. $query = $this->connection->getQueryBuilder();
  123. $query->update('file_locks')
  124. ->set('lock', $query->func()->add('lock', $query->createNamedParameter(1, IQueryBuilder::PARAM_INT)))
  125. ->set('ttl', $query->createNamedParameter($expire))
  126. ->where($query->expr()->eq('key', $query->createNamedParameter($path)))
  127. ->andWhere($query->expr()->gte('lock', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
  128. $result = $query->executeStatement();
  129. }
  130. } else {
  131. $result = 1;
  132. }
  133. } else {
  134. $existing = 0;
  135. if ($this->hasAcquiredLock($path, ILockingProvider::LOCK_SHARED) === false && $this->isLocallyLocked($path)) {
  136. $existing = 1;
  137. }
  138. $result = $this->initLockField($path, -1);
  139. if ($result <= 0) {
  140. $query = $this->connection->getQueryBuilder();
  141. $query->update('file_locks')
  142. ->set('lock', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT))
  143. ->set('ttl', $query->createNamedParameter($expire, IQueryBuilder::PARAM_INT))
  144. ->where($query->expr()->eq('key', $query->createNamedParameter($path)))
  145. ->andWhere($query->expr()->eq('lock', $query->createNamedParameter($existing)));
  146. $result = $query->executeStatement();
  147. }
  148. }
  149. if ($result !== 1) {
  150. throw new LockedException($path, null, null, $readablePath);
  151. }
  152. $this->markAcquire($path, $type);
  153. }
  154. /** @inheritDoc */
  155. public function releaseLock(string $path, int $type): void {
  156. $this->markRelease($path, $type);
  157. // we keep shared locks till the end of the request so we can re-use them
  158. if ($type === self::LOCK_EXCLUSIVE) {
  159. $qb = $this->connection->getQueryBuilder();
  160. $qb->update('file_locks')
  161. ->set('lock', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))
  162. ->where($qb->expr()->eq('key', $qb->createNamedParameter($path)))
  163. ->andWhere($qb->expr()->eq('lock', $qb->createNamedParameter(-1, IQueryBuilder::PARAM_INT)));
  164. $qb->executeStatement();
  165. } elseif (!$this->cacheSharedLocks) {
  166. $qb = $this->connection->getQueryBuilder();
  167. $qb->update('file_locks')
  168. ->set('lock', $qb->func()->subtract('lock', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT)))
  169. ->where($qb->expr()->eq('key', $qb->createNamedParameter($path)))
  170. ->andWhere($qb->expr()->gt('lock', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
  171. $qb->executeStatement();
  172. }
  173. }
  174. /** @inheritDoc */
  175. public function changeLock(string $path, int $targetType): void {
  176. $expire = $this->getExpireTime();
  177. if ($targetType === self::LOCK_SHARED) {
  178. $qb = $this->connection->getQueryBuilder();
  179. $result = $qb->update('file_locks')
  180. ->set('lock', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))
  181. ->set('ttl', $qb->createNamedParameter($expire, IQueryBuilder::PARAM_INT))
  182. ->where($qb->expr()->andX(
  183. $qb->expr()->eq('key', $qb->createNamedParameter($path)),
  184. $qb->expr()->eq('lock', $qb->createNamedParameter(-1, IQueryBuilder::PARAM_INT))
  185. ))->executeStatement();
  186. } else {
  187. // since we only keep one shared lock in the db we need to check if we have more then one shared lock locally manually
  188. if (isset($this->acquiredLocks['shared'][$path]) && $this->acquiredLocks['shared'][$path] > 1) {
  189. throw new LockedException($path);
  190. }
  191. $qb = $this->connection->getQueryBuilder();
  192. $result = $qb->update('file_locks')
  193. ->set('lock', $qb->createNamedParameter(-1, IQueryBuilder::PARAM_INT))
  194. ->set('ttl', $qb->createNamedParameter($expire, IQueryBuilder::PARAM_INT))
  195. ->where($qb->expr()->andX(
  196. $qb->expr()->eq('key', $qb->createNamedParameter($path)),
  197. $qb->expr()->eq('lock', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))
  198. ))->executeStatement();
  199. }
  200. if ($result !== 1) {
  201. throw new LockedException($path);
  202. }
  203. $this->markChange($path, $targetType);
  204. }
  205. /** @inheritDoc */
  206. public function cleanExpiredLocks(): void {
  207. $expire = $this->timeFactory->getTime();
  208. try {
  209. $qb = $this->connection->getQueryBuilder();
  210. $qb->delete('file_locks')
  211. ->where($qb->expr()->lt('ttl', $qb->createNamedParameter($expire, IQueryBuilder::PARAM_INT)))
  212. ->executeStatement();
  213. } catch (\Exception $e) {
  214. // If the table is missing, the clean up was successful
  215. if ($this->connection->tableExists('file_locks')) {
  216. throw $e;
  217. }
  218. }
  219. }
  220. /** @inheritDoc */
  221. public function releaseAll(): void {
  222. parent::releaseAll();
  223. if (!$this->cacheSharedLocks) {
  224. return;
  225. }
  226. // since we keep shared locks we need to manually clean those
  227. $lockedPaths = array_keys($this->sharedLocks);
  228. $lockedPaths = array_filter($lockedPaths, function ($path) {
  229. return $this->sharedLocks[$path];
  230. });
  231. $chunkedPaths = array_chunk($lockedPaths, 100);
  232. $qb = $this->connection->getQueryBuilder();
  233. $qb->update('file_locks')
  234. ->set('lock', $qb->func()->subtract('lock', $qb->expr()->literal(1)))
  235. ->where($qb->expr()->in('key', $qb->createParameter('chunk')))
  236. ->andWhere($qb->expr()->gt('lock', new Literal(0)));
  237. foreach ($chunkedPaths as $chunk) {
  238. $qb->setParameter('chunk', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
  239. $qb->executeStatement();
  240. }
  241. }
  242. }