Completed
Push — master ( e4992c...6d0a35 )
by
unknown
10:42
created

DBLockingProvider::initLockField()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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