Completed
Push — stable8.2 ( bf9be4...0c03e7 )
by
unknown
10:30
created

DBLockingProvider::__destruct()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12
Metric Value
dl 0
loc 10
ccs 0
cts 6
cp 0
rs 9.4285
cc 3
eloc 6
nc 3
nop 0
crap 12
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
		$expire = $this->getExpireTime();
154 21
		if ($type === self::LOCK_SHARED) {
155 15
			if (!$this->isLocallyLocked($path)) {
156 15
				$result = $this->initLockField($path, 1);
157 15
				if ($result <= 0) {
158 2
					$result = $this->connection->executeUpdate(
159 2
						'UPDATE `*PREFIX*file_locks` SET `lock` = `lock` + 1, `ttl` = ? WHERE `key` = ? AND `lock` >= 0',
160 2
						[$expire, $path]
161 2
					);
162 2
				}
163 15
			} else {
164 7
				$result = 1;
165
			}
166 15
		} else {
167 14
			$existing = 0;
168 14
			if ($this->hasAcquiredLock($path, ILockingProvider::LOCK_SHARED) === false && $this->isLocallyLocked($path)) {
169 1
				$existing = 1;
170 1
			}
171 14
			$result = $this->initLockField($path, -1);
172 14
			if ($result <= 0) {
173 5
				$result = $this->connection->executeUpdate(
174 5
					'UPDATE `*PREFIX*file_locks` SET `lock` = -1, `ttl` = ? WHERE `key` = ? AND `lock` = ?',
175 5
					[$expire, $path, $existing]
176 5
				);
177 5
			}
178
		}
179 21
		if ($result !== 1) {
180 5
			throw new LockedException($path);
181
		}
182 21
		$this->markAcquire($path, $type);
183 21
	}
184
185
	/**
186
	 * @param string $path
187
	 * @param int $type self::LOCK_SHARED or self::LOCK_EXCLUSIVE
188
	 */
189 7
	public function releaseLock($path, $type) {
190 7
		$this->markRelease($path, $type);
191
192
		// we keep shared locks till the end of the request so we can re-use them
193 7
		if ($type === self::LOCK_EXCLUSIVE) {
194 4
			$this->connection->executeUpdate(
195 4
				'UPDATE `*PREFIX*file_locks` SET `lock` = 0 WHERE `key` = ? AND `lock` = -1',
196 4
				[$path]
197 4
			);
198 4
		}
199 7
	}
200
201
	/**
202
	 * Change the type of an existing lock
203
	 *
204
	 * @param string $path
205
	 * @param int $targetType self::LOCK_SHARED or self::LOCK_EXCLUSIVE
206
	 * @throws \OCP\Lock\LockedException
207
	 */
208 9
	public function changeLock($path, $targetType) {
209 9
		$expire = $this->getExpireTime();
210 9
		if ($targetType === self::LOCK_SHARED) {
211 4
			$result = $this->connection->executeUpdate(
212 4
				'UPDATE `*PREFIX*file_locks` SET `lock` = 1, `ttl` = ? WHERE `key` = ? AND `lock` = -1',
213 4
				[$expire, $path]
214 4
			);
215 4
		} else {
216
			// since we only keep one shared lock in the db we need to check if we have more then one shared lock locally manually
217 5 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...
218 1
				throw new LockedException($path);
219
			}
220 4
			$result = $this->connection->executeUpdate(
221 4
				'UPDATE `*PREFIX*file_locks` SET `lock` = -1, `ttl` = ? WHERE `key` = ? AND `lock` = 1',
222 4
				[$expire, $path]
223 4
			);
224
		}
225 8
		if ($result !== 1) {
226 4
			throw new LockedException($path);
227
		}
228 4
		$this->markChange($path, $targetType);
229 4
	}
230
231
	/**
232
	 * cleanup empty locks
233
	 */
234 1
	public function cleanExpiredLocks() {
235 1
		$expire = $this->timeFactory->getTime();
236 1
		try {
237 1
			$this->connection->executeUpdate(
238 1
				'DELETE FROM `*PREFIX*file_locks` WHERE `ttl` < ?',
239 1
				[$expire]
240 1
			);
241
		} catch (\Exception $e) {
242
			// If the table is missing, the clean up was successful
243
			if ($this->connection->tableExists('file_locks')) {
244
				throw $e;
245 4
			}
246 4
		}
247
	}
248
249 4
	/**
250 4
	 * release all lock acquired by this instance which were marked using the mark* methods
251 4
	 */
252 4
	public function releaseAll() {
253 4
		parent::releaseAll();
254 4
255 4
		// since we keep shared locks we need to manually clean those
256 4
		foreach ($this->sharedLocks as $path => $lock) {
257 4
			if ($lock) {
258
				$this->connection->executeUpdate(
259
					'UPDATE `*PREFIX*file_locks` SET `lock` = `lock` - 1 WHERE `key` = ? AND `lock` > 0',
260
					[$path]
261
				);
262
			}
263
		}
264
	}
265
}
266