Completed
Push — stable8.2 ( e00fe4...971a4b )
by Thomas
12:01
created

DBLockingProvider::isLocallyLocked()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Individual IT Services <[email protected]>
4
 * @author Robin Appelman <[email protected]>
5
 *
6
 * @copyright Copyright (c) 2015, ownCloud, Inc.
7
 * @license AGPL-3.0
8
 *
9
 * This code is free software: you can redistribute it and/or modify
10
 * it under the terms of the GNU Affero General Public License, version 3,
11
 * as published by the Free Software Foundation.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
 * GNU Affero General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU Affero General Public License, version 3,
19
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
20
 *
21
 */
22
23
namespace OC\Lock;
24
25
use OCP\AppFramework\Utility\ITimeFactory;
26
use OCP\IDBConnection;
27
use OCP\ILogger;
28
use OCP\Lock\ILockingProvider;
29
use OCP\Lock\LockedException;
30
31
/**
32
 * Locking provider that stores the locks in the database
33
 */
34
class DBLockingProvider extends AbstractLockingProvider {
35
	/**
36
	 * @var \OCP\IDBConnection
37
	 */
38
	private $connection;
39
40
	/**
41
	 * @var \OCP\ILogger
42
	 */
43
	private $logger;
44
45
	/**
46
	 * @var \OCP\AppFramework\Utility\ITimeFactory
47
	 */
48
	private $timeFactory;
49
50
	private $sharedLocks = [];
51
52
	/**
53
	 * Check if we have an open shared lock for a path
54
	 *
55
	 * @param string $path
56
	 * @return bool
57
	 */
58 21
	protected function isLocallyLocked($path) {
59 21
		return isset($this->sharedLocks[$path]) && $this->sharedLocks[$path];
60
	}
61
62
	/**
63
	 * Mark a locally acquired lock
64
	 *
65
	 * @param string $path
66
	 * @param int $type self::LOCK_SHARED or self::LOCK_EXCLUSIVE
67
	 */
68 21
	protected function markAcquire($path, $type) {
69 21
		parent::markAcquire($path, $type);
70 21
		if ($type === self::LOCK_SHARED) {
71 13
			$this->sharedLocks[$path] = true;
72 13
		}
73 21
	}
74
75
	/**
76
	 * Change the type of an existing tracked lock
77
	 *
78
	 * @param string $path
79
	 * @param int $targetType self::LOCK_SHARED or self::LOCK_EXCLUSIVE
80
	 */
81 4
	protected function markChange($path, $targetType) {
82 4
		parent::markChange($path, $targetType);
83 4
		if ($targetType === self::LOCK_SHARED) {
84 2
			$this->sharedLocks[$path] = true;
85 4
		} else if ($targetType === self::LOCK_EXCLUSIVE) {
86 2
			$this->sharedLocks[$path] = false;
87 2
		}
88 4
	}
89
90
	/**
91
	 * @param \OCP\IDBConnection $connection
92
	 * @param \OCP\ILogger $logger
93
	 * @param \OCP\AppFramework\Utility\ITimeFactory $timeFactory
94
	 */
95 23
	public function __construct(IDBConnection $connection, ILogger $logger, ITimeFactory $timeFactory) {
96 23
		$this->connection = $connection;
97 23
		$this->logger = $logger;
98 23
		$this->timeFactory = $timeFactory;
99 23
	}
100
101
	/**
102
	 * Insert a file locking row if it does not exists.
103
	 *
104
	 * @param string $path
105
	 * @param int $lock
106
	 * @return int number of inserted rows
107
	 */
108
109 21
	protected function initLockField($path, $lock = 0) {
110 21
		$expire = $this->getExpireTime();
111 21
		return $this->connection->insertIfNotExist('*PREFIX*file_locks', ['key' => $path, 'lock' => $lock, 'ttl' => $expire], ['key']);
112
	}
113
114
	/**
115
	 * @return int
116
	 */
117 23
	protected function getExpireTime() {
118 23
		return $this->timeFactory->getTime() + self::TTL;
119
	}
120
121
	/**
122
	 * @param string $path
123
	 * @param int $type self::LOCK_SHARED or self::LOCK_EXCLUSIVE
124
	 * @return bool
125
	 */
126 17
	public function isLocked($path, $type) {
127 17
		if ($this->hasAcquiredLock($path, $type)) {
128 13
			return true;
129
		}
130 11
		$query = $this->connection->prepare('SELECT `lock` from `*PREFIX*file_locks` WHERE `key` = ?');
131 11
		$query->execute([$path]);
132 11
		$lockValue = (int)$query->fetchColumn();
133 11
		if ($type === self::LOCK_SHARED) {
134 7
			if ($this->isLocallyLocked($path)) {
135
				// 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
136 5
				return $lockValue > 1;
137
			} else {
138 3
				return $lockValue > 0;
139
			}
140 8
		} else if ($type === self::LOCK_EXCLUSIVE) {
141 8
			return $lockValue === -1;
142
		} else {
143
			return false;
144
		}
145
	}
146
147
	/**
148
	 * @param string $path
149
	 * @param int $type self::LOCK_SHARED or self::LOCK_EXCLUSIVE
150
	 * @throws \OCP\Lock\LockedException
151
	 */
152 21
	public function acquireLock($path, $type) {
153 21
		if (strlen($path) > 64) { // max length in file_locks
154 21
			throw new \InvalidArgumentException("Lock key length too long");
155 15
		}
156 15
		$expire = $this->getExpireTime();
157 15
		if ($type === self::LOCK_SHARED) {
158 2
			if (!$this->isLocallyLocked($path)) {
159 2
				$result = $this->initLockField($path, 1);
160 2
				if ($result <= 0) {
161 2
					$result = $this->connection->executeUpdate(
162 2
						'UPDATE `*PREFIX*file_locks` SET `lock` = `lock` + 1, `ttl` = ? WHERE `key` = ? AND `lock` >= 0',
163 15
						[$expire, $path]
164 7
					);
165
				}
166 15
			} else {
167 14
				$result = 1;
168 14
			}
169 1
		} else {
170 1
			$existing = 0;
171 14
			if ($this->hasAcquiredLock($path, ILockingProvider::LOCK_SHARED) === false && $this->isLocallyLocked($path)) {
172 14
				$existing = 1;
173 5
			}
174 5
			$result = $this->initLockField($path, -1);
175 5
			if ($result <= 0) {
176 5
				$result = $this->connection->executeUpdate(
177 5
					'UPDATE `*PREFIX*file_locks` SET `lock` = -1, `ttl` = ? WHERE `key` = ? AND `lock` = ?',
178
					[$expire, $path, $existing]
179 21
				);
180 5
			}
181
		}
182 21
		if ($result !== 1) {
183 21
			throw new LockedException($path);
184
		}
185
		$this->markAcquire($path, $type);
186
	}
187
188
	/**
189 7
	 * @param string $path
190 7
	 * @param int $type self::LOCK_SHARED or self::LOCK_EXCLUSIVE
191
	 */
192
	public function releaseLock($path, $type) {
193 7
		$this->markRelease($path, $type);
194 4
195 4
		// we keep shared locks till the end of the request so we can re-use them
196 4
		if ($type === self::LOCK_EXCLUSIVE) {
197 4
			$this->connection->executeUpdate(
198 4
				'UPDATE `*PREFIX*file_locks` SET `lock` = 0 WHERE `key` = ? AND `lock` = -1',
199 7
				[$path]
200
			);
201
		}
202
	}
203
204
	/**
205
	 * Change the type of an existing lock
206
	 *
207
	 * @param string $path
208 9
	 * @param int $targetType self::LOCK_SHARED or self::LOCK_EXCLUSIVE
209 9
	 * @throws \OCP\Lock\LockedException
210 9
	 */
211 4
	public function changeLock($path, $targetType) {
212 4
		$expire = $this->getExpireTime();
213 4
		if ($targetType === self::LOCK_SHARED) {
214 4
			$result = $this->connection->executeUpdate(
215 4
				'UPDATE `*PREFIX*file_locks` SET `lock` = 1, `ttl` = ? WHERE `key` = ? AND `lock` = -1',
216
				[$expire, $path]
217 5
			);
218 1
		} else {
219
			// since we only keep one shared lock in the db we need to check if we have more then one shared lock locally manually
220 4 View Code Duplication
			if (isset($this->acquiredLocks['shared'][$path]) && $this->acquiredLocks['shared'][$path] > 1) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
221 4
				throw new LockedException($path);
222 4
			}
223 4
			$result = $this->connection->executeUpdate(
224
				'UPDATE `*PREFIX*file_locks` SET `lock` = -1, `ttl` = ? WHERE `key` = ? AND `lock` = 1',
225 8
				[$expire, $path]
226 4
			);
227
		}
228 4
		if ($result !== 1) {
229 4
			throw new LockedException($path);
230
		}
231
		$this->markChange($path, $targetType);
232
	}
233
234 1
	/**
235 1
	 * cleanup empty locks
236 1
	 */
237 1
	public function cleanExpiredLocks() {
238 1
		$expire = $this->timeFactory->getTime();
239 1
		try {
240 1
			$this->connection->executeUpdate(
241
				'DELETE FROM `*PREFIX*file_locks` WHERE `ttl` < ?',
242
				[$expire]
243
			);
244
		} catch (\Exception $e) {
245 4
			// If the table is missing, the clean up was successful
246 4
			if ($this->connection->tableExists('file_locks')) {
247
				throw $e;
248
			}
249 4
		}
250 4
	}
251 4
252 4
	/**
253 4
	 * release all lock acquired by this instance which were marked using the mark* methods
254 4
	 */
255 4
	public function releaseAll() {
256 4
		parent::releaseAll();
257 4
258
		// since we keep shared locks we need to manually clean those
259
		foreach ($this->sharedLocks as $path => $lock) {
260
			if ($lock) {
261
				$this->connection->executeUpdate(
262
					'UPDATE `*PREFIX*file_locks` SET `lock` = `lock` - 1 WHERE `key` = ? AND `lock` > 0',
263
					[$path]
264
				);
265
			}
266
		}
267
	}
268
}
269