Completed
Push — master ( 42b25f...6b66f2 )
by Morris
09:53
created

DBLockingProvider   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 243
Duplicated Lines 1.23 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 35
lcom 1
cbo 7
dl 3
loc 243
rs 9
c 3
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A isLocallyLocked() 0 3 2
A markAcquire() 0 6 2
A markChange() 0 8 3
A __construct() 0 6 1
A initLockField() 0 4 1
A getExpireTime() 0 3 1
B isLocked() 0 20 5
C acquireLock() 0 32 8
A releaseLock() 0 11 2
B changeLock() 3 22 5
A cleanExpiredLocks() 0 14 3
A releaseAll() 0 22 2

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

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