You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

315 lines
8.8 KiB

3 years ago
  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 Lukas Reschke <lukas@statuscode.ch>
  9. * @author Morris Jobke <hey@morrisjobke.de>
  10. * @author Ole Ostergaard <ole.c.ostergaard@gmail.com>
  11. * @author Robin Appelman <robin@icewind.nl>
  12. * @author Roeland Jago Douma <roeland@famdouma.nl>
  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\ILogger;
  35. use OCP\Lock\ILockingProvider;
  36. use OCP\Lock\LockedException;
  37. /**
  38. * Locking provider that stores the locks in the database
  39. */
  40. class DBLockingProvider extends AbstractLockingProvider {
  41. /**
  42. * @var \OCP\IDBConnection
  43. */
  44. private $connection;
  45. /**
  46. * @var \OCP\ILogger
  47. */
  48. private $logger;
  49. /**
  50. * @var \OCP\AppFramework\Utility\ITimeFactory
  51. */
  52. private $timeFactory;
  53. private $sharedLocks = [];
  54. /**
  55. * @var bool
  56. */
  57. private $cacheSharedLocks;
  58. /**
  59. * Check if we have an open shared lock for a path
  60. *
  61. * @param string $path
  62. * @return bool
  63. */
  64. protected function isLocallyLocked(string $path): bool {
  65. return isset($this->sharedLocks[$path]) && $this->sharedLocks[$path];
  66. }
  67. /**
  68. * Mark a locally acquired lock
  69. *
  70. * @param string $path
  71. * @param int $type self::LOCK_SHARED or self::LOCK_EXCLUSIVE
  72. */
  73. protected function markAcquire(string $path, int $type) {
  74. parent::markAcquire($path, $type);
  75. if ($this->cacheSharedLocks) {
  76. if ($type === self::LOCK_SHARED) {
  77. $this->sharedLocks[$path] = true;
  78. }
  79. }
  80. }
  81. /**
  82. * Change the type of an existing tracked lock
  83. *
  84. * @param string $path
  85. * @param int $targetType self::LOCK_SHARED or self::LOCK_EXCLUSIVE
  86. */
  87. protected function markChange(string $path, int $targetType) {
  88. parent::markChange($path, $targetType);
  89. if ($this->cacheSharedLocks) {
  90. if ($targetType === self::LOCK_SHARED) {
  91. $this->sharedLocks[$path] = true;
  92. } elseif ($targetType === self::LOCK_EXCLUSIVE) {
  93. $this->sharedLocks[$path] = false;
  94. }
  95. }
  96. }
  97. /**
  98. * @param \OCP\IDBConnection $connection
  99. * @param \OCP\ILogger $logger
  100. * @param \OCP\AppFramework\Utility\ITimeFactory $timeFactory
  101. * @param int $ttl
  102. * @param bool $cacheSharedLocks
  103. */
  104. public function __construct(
  105. IDBConnection $connection,
  106. ILogger $logger,
  107. ITimeFactory $timeFactory,
  108. int $ttl = 3600,
  109. $cacheSharedLocks = true
  110. ) {
  111. $this->connection = $connection;
  112. $this->logger = $logger;
  113. $this->timeFactory = $timeFactory;
  114. $this->ttl = $ttl;
  115. $this->cacheSharedLocks = $cacheSharedLocks;
  116. }
  117. /**
  118. * Insert a file locking row if it does not exists.
  119. *
  120. * @param string $path
  121. * @param int $lock
  122. * @return int number of inserted rows
  123. */
  124. protected function initLockField(string $path, int $lock = 0): int {
  125. $expire = $this->getExpireTime();
  126. return $this->connection->insertIgnoreConflict('file_locks', [
  127. 'key' => $path,
  128. 'lock' => $lock,
  129. 'ttl' => $expire
  130. ]);
  131. }
  132. /**
  133. * @return int
  134. */
  135. protected function getExpireTime(): int {
  136. return $this->timeFactory->getTime() + $this->ttl;
  137. }
  138. /**
  139. * @param string $path
  140. * @param int $type self::LOCK_SHARED or self::LOCK_EXCLUSIVE
  141. * @return bool
  142. */
  143. public function isLocked(string $path, int $type): bool {
  144. if ($this->hasAcquiredLock($path, $type)) {
  145. return true;
  146. }
  147. $query = $this->connection->prepare('SELECT `lock` from `*PREFIX*file_locks` WHERE `key` = ?');
  148. $query->execute([$path]);
  149. $lockValue = (int)$query->fetchColumn();
  150. if ($type === self::LOCK_SHARED) {
  151. if ($this->isLocallyLocked($path)) {
  152. // 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
  153. return $lockValue > 1;
  154. } else {
  155. return $lockValue > 0;
  156. }
  157. } elseif ($type === self::LOCK_EXCLUSIVE) {
  158. return $lockValue === -1;
  159. } else {
  160. return false;
  161. }
  162. }
  163. /**
  164. * @param string $path
  165. * @param int $type self::LOCK_SHARED or self::LOCK_EXCLUSIVE
  166. * @throws \OCP\Lock\LockedException
  167. */
  168. public function acquireLock(string $path, int $type, string $readablePath = null) {
  169. $expire = $this->getExpireTime();
  170. if ($type === self::LOCK_SHARED) {
  171. if (!$this->isLocallyLocked($path)) {
  172. $result = $this->initLockField($path, 1);
  173. if ($result <= 0) {
  174. $result = $this->connection->executeUpdate(
  175. 'UPDATE `*PREFIX*file_locks` SET `lock` = `lock` + 1, `ttl` = ? WHERE `key` = ? AND `lock` >= 0',
  176. [$expire, $path]
  177. );
  178. }
  179. } else {
  180. $result = 1;
  181. }
  182. } else {
  183. $existing = 0;
  184. if ($this->hasAcquiredLock($path, ILockingProvider::LOCK_SHARED) === false && $this->isLocallyLocked($path)) {
  185. $existing = 1;
  186. }
  187. $result = $this->initLockField($path, -1);
  188. if ($result <= 0) {
  189. $result = $this->connection->executeUpdate(
  190. 'UPDATE `*PREFIX*file_locks` SET `lock` = -1, `ttl` = ? WHERE `key` = ? AND `lock` = ?',
  191. [$expire, $path, $existing]
  192. );
  193. }
  194. }
  195. if ($result !== 1) {
  196. throw new LockedException($path, null, null, $readablePath);
  197. }
  198. $this->markAcquire($path, $type);
  199. }
  200. /**
  201. * @param string $path
  202. * @param int $type self::LOCK_SHARED or self::LOCK_EXCLUSIVE
  203. */
  204. public function releaseLock(string $path, int $type) {
  205. $this->markRelease($path, $type);
  206. // we keep shared locks till the end of the request so we can re-use them
  207. if ($type === self::LOCK_EXCLUSIVE) {
  208. $this->connection->executeUpdate(
  209. 'UPDATE `*PREFIX*file_locks` SET `lock` = 0 WHERE `key` = ? AND `lock` = -1',
  210. [$path]
  211. );
  212. } elseif (!$this->cacheSharedLocks) {
  213. $query = $this->connection->getQueryBuilder();
  214. $query->update('file_locks')
  215. ->set('lock', $query->func()->subtract('lock', $query->createNamedParameter(1)))
  216. ->where($query->expr()->eq('key', $query->createNamedParameter($path)))
  217. ->andWhere($query->expr()->gt('lock', $query->createNamedParameter(0)));
  218. $query->execute();
  219. }
  220. }
  221. /**
  222. * Change the type of an existing lock
  223. *
  224. * @param string $path
  225. * @param int $targetType self::LOCK_SHARED or self::LOCK_EXCLUSIVE
  226. * @throws \OCP\Lock\LockedException
  227. */
  228. public function changeLock(string $path, int $targetType) {
  229. $expire = $this->getExpireTime();
  230. if ($targetType === self::LOCK_SHARED) {
  231. $result = $this->connection->executeUpdate(
  232. 'UPDATE `*PREFIX*file_locks` SET `lock` = 1, `ttl` = ? WHERE `key` = ? AND `lock` = -1',
  233. [$expire, $path]
  234. );
  235. } else {
  236. // since we only keep one shared lock in the db we need to check if we have more then one shared lock locally manually
  237. if (isset($this->acquiredLocks['shared'][$path]) && $this->acquiredLocks['shared'][$path] > 1) {
  238. throw new LockedException($path);
  239. }
  240. $result = $this->connection->executeUpdate(
  241. 'UPDATE `*PREFIX*file_locks` SET `lock` = -1, `ttl` = ? WHERE `key` = ? AND `lock` = 1',
  242. [$expire, $path]
  243. );
  244. }
  245. if ($result !== 1) {
  246. throw new LockedException($path);
  247. }
  248. $this->markChange($path, $targetType);
  249. }
  250. /**
  251. * cleanup empty locks
  252. */
  253. public function cleanExpiredLocks() {
  254. $expire = $this->timeFactory->getTime();
  255. try {
  256. $this->connection->executeUpdate(
  257. 'DELETE FROM `*PREFIX*file_locks` WHERE `ttl` < ?',
  258. [$expire]
  259. );
  260. } catch (\Exception $e) {
  261. // If the table is missing, the clean up was successful
  262. if ($this->connection->tableExists('file_locks')) {
  263. throw $e;
  264. }
  265. }
  266. }
  267. /**
  268. * release all lock acquired by this instance which were marked using the mark* methods
  269. */
  270. public function releaseAll() {
  271. parent::releaseAll();
  272. if (!$this->cacheSharedLocks) {
  273. return;
  274. }
  275. // since we keep shared locks we need to manually clean those
  276. $lockedPaths = array_keys($this->sharedLocks);
  277. $lockedPaths = array_filter($lockedPaths, function ($path) {
  278. return $this->sharedLocks[$path];
  279. });
  280. $chunkedPaths = array_chunk($lockedPaths, 100);
  281. foreach ($chunkedPaths as $chunk) {
  282. $builder = $this->connection->getQueryBuilder();
  283. $query = $builder->update('file_locks')
  284. ->set('lock', $builder->func()->subtract('lock', $builder->expr()->literal(1)))
  285. ->where($builder->expr()->in('key', $builder->createNamedParameter($chunk, IQueryBuilder::PARAM_STR_ARRAY)))
  286. ->andWhere($builder->expr()->gt('lock', new Literal(0)));
  287. $query->execute();
  288. }
  289. }
  290. }