PageRenderTime 45ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/private/Lock/DBLockingProvider.php

https://gitlab.com/wuhang2003/core
PHP | 280 lines | 155 code | 25 blank | 100 comment | 28 complexity | 6df8b90a87dc062a516fad2da00dc19e MD5 | raw file
  1. <?php
  2. /**
  3. * @author Björn Schießle <schiessle@owncloud.com>
  4. * @author Individual IT Services <info@individual-it.net>
  5. * @author Joas Schilling <nickvergessen@owncloud.com>
  6. * @author Robin Appelman <icewind@owncloud.com>
  7. *
  8. * @copyright Copyright (c) 2016, ownCloud, Inc.
  9. * @license AGPL-3.0
  10. *
  11. * This code is free software: you can redistribute it and/or modify
  12. * it under the terms of the GNU Affero General Public License, version 3,
  13. * as published by the Free Software Foundation.
  14. *
  15. * This program is distributed in the hope that it will be useful,
  16. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. * GNU Affero General Public License for more details.
  19. *
  20. * You should have received a copy of the GNU Affero General Public License, version 3,
  21. * along with this program. If not, see <http://www.gnu.org/licenses/>
  22. *
  23. */
  24. namespace OC\Lock;
  25. use OC\DB\QueryBuilder\Literal;
  26. use OCP\AppFramework\Utility\ITimeFactory;
  27. use OCP\DB\QueryBuilder\IQueryBuilder;
  28. use OCP\IDBConnection;
  29. use OCP\ILogger;
  30. use OCP\Lock\ILockingProvider;
  31. use OCP\Lock\LockedException;
  32. /**
  33. * Locking provider that stores the locks in the database
  34. */
  35. class DBLockingProvider extends AbstractLockingProvider {
  36. /**
  37. * @var \OCP\IDBConnection
  38. */
  39. private $connection;
  40. /**
  41. * @var \OCP\ILogger
  42. */
  43. private $logger;
  44. /**
  45. * @var \OCP\AppFramework\Utility\ITimeFactory
  46. */
  47. private $timeFactory;
  48. private $sharedLocks = [];
  49. /**
  50. * Check if we have an open shared lock for a path
  51. *
  52. * @param string $path
  53. * @return bool
  54. */
  55. protected function isLocallyLocked($path) {
  56. return isset($this->sharedLocks[$path]) && $this->sharedLocks[$path];
  57. }
  58. /**
  59. * Mark a locally acquired lock
  60. *
  61. * @param string $path
  62. * @param int $type self::LOCK_SHARED or self::LOCK_EXCLUSIVE
  63. */
  64. protected function markAcquire($path, $type) {
  65. parent::markAcquire($path, $type);
  66. if ($type === self::LOCK_SHARED) {
  67. $this->sharedLocks[$path] = true;
  68. }
  69. }
  70. /**
  71. * Change the type of an existing tracked lock
  72. *
  73. * @param string $path
  74. * @param int $targetType self::LOCK_SHARED or self::LOCK_EXCLUSIVE
  75. */
  76. protected function markChange($path, $targetType) {
  77. parent::markChange($path, $targetType);
  78. if ($targetType === self::LOCK_SHARED) {
  79. $this->sharedLocks[$path] = true;
  80. } else if ($targetType === self::LOCK_EXCLUSIVE) {
  81. $this->sharedLocks[$path] = false;
  82. }
  83. }
  84. /**
  85. * @param \OCP\IDBConnection $connection
  86. * @param \OCP\ILogger $logger
  87. * @param \OCP\AppFramework\Utility\ITimeFactory $timeFactory
  88. * @param int $ttl
  89. */
  90. public function __construct(IDBConnection $connection, ILogger $logger, ITimeFactory $timeFactory, $ttl = 3600) {
  91. $this->connection = $connection;
  92. $this->logger = $logger;
  93. $this->timeFactory = $timeFactory;
  94. $this->ttl = $ttl;
  95. }
  96. /**
  97. * Insert a file locking row if it does not exists.
  98. *
  99. * @param string $path
  100. * @param int $lock
  101. * @return int number of inserted rows
  102. */
  103. protected function initLockField($path, $lock = 0) {
  104. $expire = $this->getExpireTime();
  105. return $this->connection->insertIfNotExist('*PREFIX*file_locks', ['key' => $path, 'lock' => $lock, 'ttl' => $expire], ['key']);
  106. }
  107. /**
  108. * @return int
  109. */
  110. protected function getExpireTime() {
  111. return $this->timeFactory->getTime() + $this->ttl;
  112. }
  113. /**
  114. * @param string $path
  115. * @param int $type self::LOCK_SHARED or self::LOCK_EXCLUSIVE
  116. * @return bool
  117. */
  118. public function isLocked($path, $type) {
  119. if ($this->hasAcquiredLock($path, $type)) {
  120. return true;
  121. }
  122. $query = $this->connection->prepare('SELECT `lock` from `*PREFIX*file_locks` WHERE `key` = ?');
  123. $query->execute([$path]);
  124. $lockValue = (int)$query->fetchColumn();
  125. if ($type === self::LOCK_SHARED) {
  126. if ($this->isLocallyLocked($path)) {
  127. // 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
  128. return $lockValue > 1;
  129. } else {
  130. return $lockValue > 0;
  131. }
  132. } else if ($type === self::LOCK_EXCLUSIVE) {
  133. return $lockValue === -1;
  134. } else {
  135. return false;
  136. }
  137. }
  138. /**
  139. * @param string $path
  140. * @param int $type self::LOCK_SHARED or self::LOCK_EXCLUSIVE
  141. * @throws \OCP\Lock\LockedException
  142. */
  143. public function acquireLock($path, $type) {
  144. $expire = $this->getExpireTime();
  145. if ($type === self::LOCK_SHARED) {
  146. if (!$this->isLocallyLocked($path)) {
  147. $result = $this->initLockField($path, 1);
  148. if ($result <= 0) {
  149. $result = $this->connection->executeUpdate(
  150. 'UPDATE `*PREFIX*file_locks` SET `lock` = `lock` + 1, `ttl` = ? WHERE `key` = ? AND `lock` >= 0',
  151. [$expire, $path]
  152. );
  153. }
  154. } else {
  155. $result = 1;
  156. }
  157. } else {
  158. $existing = 0;
  159. if ($this->hasAcquiredLock($path, ILockingProvider::LOCK_SHARED) === false && $this->isLocallyLocked($path)) {
  160. $existing = 1;
  161. }
  162. $result = $this->initLockField($path, -1);
  163. if ($result <= 0) {
  164. $result = $this->connection->executeUpdate(
  165. 'UPDATE `*PREFIX*file_locks` SET `lock` = -1, `ttl` = ? WHERE `key` = ? AND `lock` = ?',
  166. [$expire, $path, $existing]
  167. );
  168. }
  169. }
  170. if ($result !== 1) {
  171. throw new LockedException($path);
  172. }
  173. $this->markAcquire($path, $type);
  174. }
  175. /**
  176. * @param string $path
  177. * @param int $type self::LOCK_SHARED or self::LOCK_EXCLUSIVE
  178. */
  179. public function releaseLock($path, $type) {
  180. $this->markRelease($path, $type);
  181. // we keep shared locks till the end of the request so we can re-use them
  182. if ($type === self::LOCK_EXCLUSIVE) {
  183. $this->connection->executeUpdate(
  184. 'UPDATE `*PREFIX*file_locks` SET `lock` = 0 WHERE `key` = ? AND `lock` = -1',
  185. [$path]
  186. );
  187. }
  188. }
  189. /**
  190. * Change the type of an existing lock
  191. *
  192. * @param string $path
  193. * @param int $targetType self::LOCK_SHARED or self::LOCK_EXCLUSIVE
  194. * @throws \OCP\Lock\LockedException
  195. */
  196. public function changeLock($path, $targetType) {
  197. $expire = $this->getExpireTime();
  198. if ($targetType === self::LOCK_SHARED) {
  199. $result = $this->connection->executeUpdate(
  200. 'UPDATE `*PREFIX*file_locks` SET `lock` = 1, `ttl` = ? WHERE `key` = ? AND `lock` = -1',
  201. [$expire, $path]
  202. );
  203. } else {
  204. // since we only keep one shared lock in the db we need to check if we have more then one shared lock locally manually
  205. if (isset($this->acquiredLocks['shared'][$path]) && $this->acquiredLocks['shared'][$path] > 1) {
  206. throw new LockedException($path);
  207. }
  208. $result = $this->connection->executeUpdate(
  209. 'UPDATE `*PREFIX*file_locks` SET `lock` = -1, `ttl` = ? WHERE `key` = ? AND `lock` = 1',
  210. [$expire, $path]
  211. );
  212. }
  213. if ($result !== 1) {
  214. throw new LockedException($path);
  215. }
  216. $this->markChange($path, $targetType);
  217. }
  218. /**
  219. * cleanup empty locks
  220. */
  221. public function cleanExpiredLocks() {
  222. $expire = $this->timeFactory->getTime();
  223. try {
  224. $this->connection->executeUpdate(
  225. 'DELETE FROM `*PREFIX*file_locks` WHERE `ttl` < ?',
  226. [$expire]
  227. );
  228. } catch (\Exception $e) {
  229. // If the table is missing, the clean up was successful
  230. if ($this->connection->tableExists('file_locks')) {
  231. throw $e;
  232. }
  233. }
  234. }
  235. /**
  236. * release all lock acquired by this instance which were marked using the mark* methods
  237. */
  238. public function releaseAll() {
  239. parent::releaseAll();
  240. // since we keep shared locks we need to manually clean those
  241. $lockedPaths = array_keys($this->sharedLocks);
  242. $lockedPaths = array_filter($lockedPaths, function ($path) {
  243. return $this->sharedLocks[$path];
  244. });
  245. $chunkedPaths = array_chunk($lockedPaths, 100);
  246. foreach ($chunkedPaths as $chunk) {
  247. $builder = $this->connection->getQueryBuilder();
  248. $query = $builder->update('file_locks')
  249. ->set('lock', $builder->createFunction('`lock` -1'))
  250. ->where($builder->expr()->in('key', $builder->createNamedParameter($chunk, IQueryBuilder::PARAM_STR_ARRAY)))
  251. ->andWhere($builder->expr()->gt('lock', new Literal(0)));
  252. $query->execute();
  253. }
  254. }
  255. }