DBLockingProvider.php 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  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 IDBConnection $connection;
  41. private ITimeFactory $timeFactory;
  42. private array $sharedLocks = [];
  43. private bool $cacheSharedLocks;
  44. public function __construct(
  45. IDBConnection $connection,
  46. ITimeFactory $timeFactory,
  47. int $ttl = 3600,
  48. bool $cacheSharedLocks = true
  49. ) {
  50. $this->connection = $connection;
  51. $this->timeFactory = $timeFactory;
  52. $this->ttl = $ttl;
  53. $this->cacheSharedLocks = $cacheSharedLocks;
  54. }
  55. /**
  56. * Check if we have an open shared lock for a path
  57. */
  58. protected function isLocallyLocked(string $path): bool {
  59. return isset($this->sharedLocks[$path]) && $this->sharedLocks[$path];
  60. }
  61. /** @inheritDoc */
  62. protected function markAcquire(string $path, int $targetType): void {
  63. parent::markAcquire($path, $targetType);
  64. if ($this->cacheSharedLocks) {
  65. if ($targetType === self::LOCK_SHARED) {
  66. $this->sharedLocks[$path] = true;
  67. }
  68. }
  69. }
  70. /**
  71. * Change the type of an existing tracked lock
  72. */
  73. protected function markChange(string $path, int $targetType): void {
  74. parent::markChange($path, $targetType);
  75. if ($this->cacheSharedLocks) {
  76. if ($targetType === self::LOCK_SHARED) {
  77. $this->sharedLocks[$path] = true;
  78. } elseif ($targetType === self::LOCK_EXCLUSIVE) {
  79. $this->sharedLocks[$path] = false;
  80. }
  81. }
  82. }
  83. /**
  84. * Insert a file locking row if it does not exists.
  85. */
  86. protected function initLockField(string $path, int $lock = 0): int {
  87. $expire = $this->getExpireTime();
  88. return $this->connection->insertIgnoreConflict('file_locks', [
  89. 'key' => $path,
  90. 'lock' => $lock,
  91. 'ttl' => $expire
  92. ]);
  93. }
  94. protected function getExpireTime(): int {
  95. return $this->timeFactory->getTime() + $this->ttl;
  96. }
  97. /** @inheritDoc */
  98. public function isLocked(string $path, int $type): bool {
  99. if ($this->hasAcquiredLock($path, $type)) {
  100. return true;
  101. }
  102. $query = $this->connection->getQueryBuilder();
  103. $query->select('lock')
  104. ->from('file_locks')
  105. ->where($query->expr()->eq('key', $query->createNamedParameter($path)));
  106. $result = $query->executeQuery();
  107. $lockValue = (int)$result->fetchOne();
  108. if ($type === self::LOCK_SHARED) {
  109. if ($this->isLocallyLocked($path)) {
  110. // 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
  111. return $lockValue > 1;
  112. } else {
  113. return $lockValue > 0;
  114. }
  115. } elseif ($type === self::LOCK_EXCLUSIVE) {
  116. return $lockValue === -1;
  117. } else {
  118. return false;
  119. }
  120. }
  121. /** @inheritDoc */
  122. public function acquireLock(string $path, int $type, ?string $readablePath = null): void {
  123. $expire = $this->getExpireTime();
  124. if ($type === self::LOCK_SHARED) {
  125. if (!$this->isLocallyLocked($path)) {
  126. $result = $this->initLockField($path, 1);
  127. if ($result <= 0) {
  128. $query = $this->connection->getQueryBuilder();
  129. $query->update('file_locks')
  130. ->set('lock', $query->func()->add('lock', $query->createNamedParameter(1, IQueryBuilder::PARAM_INT)))
  131. ->set('ttl', $query->createNamedParameter($expire))
  132. ->where($query->expr()->eq('key', $query->createNamedParameter($path)))
  133. ->andWhere($query->expr()->gte('lock', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
  134. $result = $query->executeStatement();
  135. }
  136. } else {
  137. $result = 1;
  138. }
  139. } else {
  140. $existing = 0;
  141. if ($this->hasAcquiredLock($path, ILockingProvider::LOCK_SHARED) === false && $this->isLocallyLocked($path)) {
  142. $existing = 1;
  143. }
  144. $result = $this->initLockField($path, -1);
  145. if ($result <= 0) {
  146. $query = $this->connection->getQueryBuilder();
  147. $query->update('file_locks')
  148. ->set('lock', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT))
  149. ->set('ttl', $query->createNamedParameter($expire, IQueryBuilder::PARAM_INT))
  150. ->where($query->expr()->eq('key', $query->createNamedParameter($path)))
  151. ->andWhere($query->expr()->eq('lock', $query->createNamedParameter($existing)));
  152. $result = $query->executeStatement();
  153. }
  154. }
  155. if ($result !== 1) {
  156. throw new LockedException($path, null, null, $readablePath);
  157. }
  158. $this->markAcquire($path, $type);
  159. }
  160. /** @inheritDoc */
  161. public function releaseLock(string $path, int $type): void {
  162. $this->markRelease($path, $type);
  163. // we keep shared locks till the end of the request so we can re-use them
  164. if ($type === self::LOCK_EXCLUSIVE) {
  165. $qb = $this->connection->getQueryBuilder();
  166. $qb->update('file_locks')
  167. ->set('lock', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))
  168. ->where($qb->expr()->eq('key', $qb->createNamedParameter($path)))
  169. ->andWhere($qb->expr()->eq('lock', $qb->createNamedParameter(-1, IQueryBuilder::PARAM_INT)));
  170. $qb->executeStatement();
  171. } elseif (!$this->cacheSharedLocks) {
  172. $qb = $this->connection->getQueryBuilder();
  173. $qb->update('file_locks')
  174. ->set('lock', $qb->func()->subtract('lock', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT)))
  175. ->where($qb->expr()->eq('key', $qb->createNamedParameter($path)))
  176. ->andWhere($qb->expr()->gt('lock', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
  177. $qb->executeStatement();
  178. }
  179. }
  180. /** @inheritDoc */
  181. public function changeLock(string $path, int $targetType): void {
  182. $expire = $this->getExpireTime();
  183. if ($targetType === self::LOCK_SHARED) {
  184. $qb = $this->connection->getQueryBuilder();
  185. $result = $qb->update('file_locks')
  186. ->set('lock', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))
  187. ->set('ttl', $qb->createNamedParameter($expire, IQueryBuilder::PARAM_INT))
  188. ->where($qb->expr()->andX(
  189. $qb->expr()->eq('key', $qb->createNamedParameter($path)),
  190. $qb->expr()->eq('lock', $qb->createNamedParameter(-1, IQueryBuilder::PARAM_INT))
  191. ))->executeStatement();
  192. } else {
  193. // since we only keep one shared lock in the db we need to check if we have more then one shared lock locally manually
  194. if (isset($this->acquiredLocks['shared'][$path]) && $this->acquiredLocks['shared'][$path] > 1) {
  195. throw new LockedException($path);
  196. }
  197. $qb = $this->connection->getQueryBuilder();
  198. $result = $qb->update('file_locks')
  199. ->set('lock', $qb->createNamedParameter(-1, IQueryBuilder::PARAM_INT))
  200. ->set('ttl', $qb->createNamedParameter($expire, IQueryBuilder::PARAM_INT))
  201. ->where($qb->expr()->andX(
  202. $qb->expr()->eq('key', $qb->createNamedParameter($path)),
  203. $qb->expr()->eq('lock', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))
  204. ))->executeStatement();
  205. }
  206. if ($result !== 1) {
  207. throw new LockedException($path);
  208. }
  209. $this->markChange($path, $targetType);
  210. }
  211. /** @inheritDoc */
  212. public function cleanExpiredLocks(): void {
  213. $expire = $this->timeFactory->getTime();
  214. try {
  215. $qb = $this->connection->getQueryBuilder();
  216. $qb->delete('file_locks')
  217. ->where($qb->expr()->lt('ttl', $qb->createNamedParameter($expire, IQueryBuilder::PARAM_INT)))
  218. ->executeStatement();
  219. } catch (\Exception $e) {
  220. // If the table is missing, the clean up was successful
  221. if ($this->connection->tableExists('file_locks')) {
  222. throw $e;
  223. }
  224. }
  225. }
  226. /** @inheritDoc */
  227. public function releaseAll(): void {
  228. parent::releaseAll();
  229. if (!$this->cacheSharedLocks) {
  230. return;
  231. }
  232. // since we keep shared locks we need to manually clean those
  233. $lockedPaths = array_keys($this->sharedLocks);
  234. $lockedPaths = array_filter($lockedPaths, function ($path) {
  235. return $this->sharedLocks[$path];
  236. });
  237. $chunkedPaths = array_chunk($lockedPaths, 100);
  238. $qb = $this->connection->getQueryBuilder();
  239. $qb->update('file_locks')
  240. ->set('lock', $qb->func()->subtract('lock', $qb->expr()->literal(1)))
  241. ->where($qb->expr()->in('key', $qb->createParameter('chunk')))
  242. ->andWhere($qb->expr()->gt('lock', new Literal(0)));
  243. foreach ($chunkedPaths as $chunk) {
  244. $qb->setParameter('chunk', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
  245. $qb->executeStatement();
  246. }
  247. }
  248. }