Merge pull request #17662 from owncloud/locking-db

Database backend for locking
This commit is contained in:
Thomas Müller 2015-08-26 03:56:37 +02:00
commit 534b2e407a
10 changed files with 446 additions and 43 deletions

View File

@ -1192,5 +1192,78 @@
</table>
<table>
<!--
Table for storing transactional file locking
-->
<name>*dbprefix*file_locks</name>
<declaration>
<field>
<name>id</name>
<type>integer</type>
<default>0</default>
<notnull>true</notnull>
<unsigned>true</unsigned>
<length>4</length>
<autoincrement>1</autoincrement>
</field>
<field>
<name>lock</name>
<type>integer</type>
<default>0</default>
<notnull>true</notnull>
<length>4</length>
</field>
<field>
<name>key</name>
<type>text</type>
<notnull>true</notnull>
<length>64</length>
</field>
<field>
<name>ttl</name>
<type>integer</type>
<default>-1</default>
<notnull>true</notnull>
<length>4</length>
</field>
<index>
<primary>true</primary>
<unique>true</unique>
<name>lock_id_index</name>
<field>
<name>id</name>
<sorting>ascending</sorting>
</field>
</index>
<index>
<unique>true</unique>
<name>lock_key_index</name>
<field>
<name>key</name>
<sorting>ascending</sorting>
</field>
</index>
<index>
<name>lock_ttl_index</name>
<field>
<name>ttl</name>
<sorting>ascending</sorting>
</field>
</index>
</declaration>
</table>
</database>

View File

@ -153,6 +153,15 @@ class Db implements IDb {
$this->connection->beginTransaction();
}
/**
* Check if a transaction is active
*
* @return bool
*/
public function inTransaction() {
return $this->connection->inTransaction();
}
/**
* Commit the database changes done during a transaction that is in progress
*/

View File

@ -291,4 +291,14 @@ class Connection extends \Doctrine\DBAL\Connection implements IDBConnection {
protected function replaceTablePrefix($statement) {
return str_replace( '*PREFIX*', $this->tablePrefix, $statement );
}
/**
* Check if a transaction is active
*
* @return bool
* @since 8.2.0
*/
public function inTransaction() {
return $this->getTransactionNestingLevel() > 0;
}
}

View File

@ -0,0 +1,102 @@
<?php
/**
* @author Robin Appelman <icewind@owncloud.com>
*
* @copyright Copyright (c) 2015, ownCloud, Inc.
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OC\Lock;
use OCP\Lock\ILockingProvider;
/**
* Base locking provider that keeps track of locks acquired during the current request
* to release any left over locks at the end of the request
*/
abstract class AbstractLockingProvider implements ILockingProvider {
protected $acquiredLocks = [
'shared' => [],
'exclusive' => []
];
/**
* Mark a locally acquired lock
*
* @param string $path
* @param int $type self::LOCK_SHARED or self::LOCK_EXCLUSIVE
*/
protected function markAcquire($path, $type) {
if ($type === self::LOCK_SHARED) {
if (!isset($this->acquiredLocks['shared'][$path])) {
$this->acquiredLocks['shared'][$path] = 0;
}
$this->acquiredLocks['shared'][$path]++;
} else {
$this->acquiredLocks['exclusive'][$path] = true;
}
}
/**
* Mark a release of a locally acquired lock
*
* @param string $path
* @param int $type self::LOCK_SHARED or self::LOCK_EXCLUSIVE
*/
protected function markRelease($path, $type) {
if ($type === self::LOCK_SHARED) {
if (isset($this->acquiredLocks['shared'][$path]) and $this->acquiredLocks['shared'][$path] > 0) {
$this->acquiredLocks['shared'][$path]--;
}
} else if ($type === self::LOCK_EXCLUSIVE) {
unset($this->acquiredLocks['exclusive'][$path]);
}
}
/**
* Change the type of an existing tracked lock
*
* @param string $path
* @param int $targetType self::LOCK_SHARED or self::LOCK_EXCLUSIVE
*/
protected function markChange($path, $targetType) {
if ($targetType === self::LOCK_SHARED) {
unset($this->acquiredLocks['exclusive'][$path]);
if (!isset($this->acquiredLocks['shared'][$path])) {
$this->acquiredLocks['shared'][$path] = 0;
}
$this->acquiredLocks['shared'][$path]++;
} else if ($targetType === self::LOCK_EXCLUSIVE) {
$this->acquiredLocks['exclusive'][$path] = true;
$this->acquiredLocks['shared'][$path]--;
}
}
/**
* release all lock acquired by this instance which were marked using the mark* methods
*/
public function releaseAll() {
foreach ($this->acquiredLocks['shared'] as $path => $count) {
for ($i = 0; $i < $count; $i++) {
$this->releaseLock($path, self::LOCK_SHARED);
}
}
foreach ($this->acquiredLocks['exclusive'] as $path => $hasLock) {
$this->releaseLock($path, self::LOCK_EXCLUSIVE);
}
}
}

View File

@ -0,0 +1,162 @@
<?php
/**
* @author Robin Appelman <icewind@owncloud.com>
*
* @copyright Copyright (c) 2015, ownCloud, Inc.
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OC\Lock;
use OCP\IDBConnection;
use OCP\ILogger;
use OCP\Lock\LockedException;
/**
* Locking provider that stores the locks in the database
*/
class DBLockingProvider extends AbstractLockingProvider {
/**
* @var \OCP\IDBConnection
*/
private $connection;
/**
* @var \OCP\ILogger
*/
private $logger;
/**
* @param \OCP\IDBConnection $connection
* @param \OCP\ILogger $logger
*/
public function __construct(IDBConnection $connection, ILogger $logger) {
$this->connection = $connection;
$this->logger = $logger;
}
protected function initLockField($path) {
$this->connection->insertIfNotExist('*PREFIX*file_locks', ['key' => $path, 'lock' => 0, 'ttl' => 0], ['key']);
}
/**
* @param string $path
* @param int $type self::LOCK_SHARED or self::LOCK_EXCLUSIVE
* @return bool
*/
public function isLocked($path, $type) {
$query = $this->connection->prepare('SELECT `lock` from `*PREFIX*file_locks` WHERE `key` = ?');
$query->execute([$path]);
$lockValue = (int)$query->fetchColumn();
if ($type === self::LOCK_SHARED) {
return $lockValue > 0;
} else if ($type === self::LOCK_EXCLUSIVE) {
return $lockValue === -1;
} else {
return false;
}
}
/**
* @param string $path
* @param int $type self::LOCK_SHARED or self::LOCK_EXCLUSIVE
* @throws \OCP\Lock\LockedException
*/
public function acquireLock($path, $type) {
if ($this->connection->inTransaction()){
$this->logger->warning("Trying to acquire a lock for '$path' while inside a transition");
}
$this->connection->beginTransaction();
$this->initLockField($path);
if ($type === self::LOCK_SHARED) {
$result = $this->connection->executeUpdate(
'UPDATE `*PREFIX*file_locks` SET `lock` = `lock` + 1 WHERE `key` = ? AND `lock` >= 0',
[$path]
);
} else {
$result = $this->connection->executeUpdate(
'UPDATE `*PREFIX*file_locks` SET `lock` = -1 WHERE `key` = ? AND `lock` = 0',
[$path]
);
}
$this->connection->commit();
if ($result !== 1) {
throw new LockedException($path);
}
$this->markAcquire($path, $type);
}
/**
* @param string $path
* @param int $type self::LOCK_SHARED or self::LOCK_EXCLUSIVE
*/
public function releaseLock($path, $type) {
$this->initLockField($path);
if ($type === self::LOCK_SHARED) {
$this->connection->executeUpdate(
'UPDATE `*PREFIX*file_locks` SET `lock` = `lock` - 1 WHERE `key` = ? AND `lock` > 0',
[$path]
);
} else {
$this->connection->executeUpdate(
'UPDATE `*PREFIX*file_locks` SET `lock` = 0 WHERE `key` = ? AND `lock` = -1',
[$path]
);
}
$this->markRelease($path, $type);
}
/**
* Change the type of an existing lock
*
* @param string $path
* @param int $targetType self::LOCK_SHARED or self::LOCK_EXCLUSIVE
* @throws \OCP\Lock\LockedException
*/
public function changeLock($path, $targetType) {
$this->initLockField($path);
if ($targetType === self::LOCK_SHARED) {
$result = $this->connection->executeUpdate(
'UPDATE `*PREFIX*file_locks` SET `lock` = 1 WHERE `key` = ? AND `lock` = -1',
[$path]
);
} else {
$result = $this->connection->executeUpdate(
'UPDATE `*PREFIX*file_locks` SET `lock` = -1 WHERE `key` = ? AND `lock` = 1',
[$path]
);
}
if ($result !== 1) {
throw new LockedException($path);
}
$this->markChange($path, $targetType);
}
/**
* cleanup empty locks
*/
public function cleanEmptyLocks() {
$this->connection->executeUpdate(
'DELETE FROM `*PREFIX*file_locks` WHERE `lock` = 0'
);
}
public function __destruct() {
$this->cleanEmptyLocks();
}
}

View File

@ -23,21 +23,15 @@
namespace OC\Lock;
use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException;
use OCP\IMemcache;
class MemcacheLockingProvider implements ILockingProvider {
class MemcacheLockingProvider extends AbstractLockingProvider {
/**
* @var \OCP\IMemcache
*/
private $memcache;
private $acquiredLocks = [
'shared' => [],
'exclusive' => []
];
/**
* @param \OCP\IMemcache $memcache
*/
@ -71,17 +65,13 @@ class MemcacheLockingProvider implements ILockingProvider {
if (!$this->memcache->inc($path)) {
throw new LockedException($path);
}
if (!isset($this->acquiredLocks['shared'][$path])) {
$this->acquiredLocks['shared'][$path] = 0;
}
$this->acquiredLocks['shared'][$path]++;
} else {
$this->memcache->add($path, 0);
if (!$this->memcache->cas($path, 0, 'exclusive')) {
throw new LockedException($path);
}
$this->acquiredLocks['exclusive'][$path] = true;
}
$this->markAcquire($path, $type);
}
/**
@ -92,13 +82,12 @@ class MemcacheLockingProvider implements ILockingProvider {
if ($type === self::LOCK_SHARED) {
if (isset($this->acquiredLocks['shared'][$path]) and $this->acquiredLocks['shared'][$path] > 0) {
$this->memcache->dec($path);
$this->acquiredLocks['shared'][$path]--;
$this->memcache->cad($path, 0);
}
} else if ($type === self::LOCK_EXCLUSIVE) {
$this->memcache->cad($path, 'exclusive');
unset($this->acquiredLocks['exclusive'][$path]);
}
$this->markRelease($path, $type);
}
/**
@ -113,33 +102,12 @@ class MemcacheLockingProvider implements ILockingProvider {
if (!$this->memcache->cas($path, 'exclusive', 1)) {
throw new LockedException($path);
}
unset($this->acquiredLocks['exclusive'][$path]);
if (!isset($this->acquiredLocks['shared'][$path])) {
$this->acquiredLocks['shared'][$path] = 0;
}
$this->acquiredLocks['shared'][$path]++;
} else if ($targetType === self::LOCK_EXCLUSIVE) {
// we can only change a shared lock to an exclusive if there's only a single owner of the shared lock
if (!$this->memcache->cas($path, 1, 'exclusive')) {
throw new LockedException($path);
}
$this->acquiredLocks['exclusive'][$path] = true;
$this->acquiredLocks['shared'][$path]--;
}
}
/**
* release all lock acquired by this instance
*/
public function releaseAll() {
foreach ($this->acquiredLocks['shared'] as $path => $count) {
for ($i = 0; $i < $count; $i++) {
$this->releaseLock($path, self::LOCK_SHARED);
}
}
foreach ($this->acquiredLocks['exclusive'] as $path => $hasLock) {
$this->releaseLock($path, self::LOCK_EXCLUSIVE);
}
$this->markChange($path, $targetType);
}
}

View File

@ -49,6 +49,7 @@ use OC\Diagnostics\QueryLogger;
use OC\Files\Node\Root;
use OC\Files\View;
use OC\Http\Client\ClientService;
use OC\Lock\DBLockingProvider;
use OC\Lock\MemcacheLockingProvider;
use OC\Lock\NoopLockingProvider;
use OC\Mail\Mailer;
@ -441,13 +442,10 @@ class Server extends SimpleContainer implements IServerContainer {
/** @var \OC\Memcache\Factory $memcacheFactory */
$memcacheFactory = $c->getMemCacheFactory();
$memcache = $memcacheFactory->createLocking('lock');
if (!($memcache instanceof \OC\Memcache\NullCache)) {
return new MemcacheLockingProvider($memcache);
}
throw new HintException(
'File locking is enabled but the locking cache class was not found',
'Please check the "memcache.locking" setting and make sure the matching PHP module is installed and enabled'
);
// if (!($memcache instanceof \OC\Memcache\NullCache)) {
// return new MemcacheLockingProvider($memcache);
// }
return new DBLockingProvider($c->getDatabaseConnection(), $c->getLogger());
}
return new NoopLockingProvider();
});

View File

@ -114,6 +114,14 @@ interface IDBConnection {
*/
public function beginTransaction();
/**
* Check if a transaction is active
*
* @return bool
* @since 8.2.0
*/
public function inTransaction();
/**
* Commit the database changes done during a transaction that is in progress
* @since 6.0.0

View File

@ -0,0 +1,43 @@
<?php
/**
* @author Robin Appelman <icewind@owncloud.com>
*
* @copyright Copyright (c) 2015, ownCloud, Inc.
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace Test\Lock;
class DBLockingProvider extends LockingProvider {
/**
* @var \OCP\IDBConnection
*/
private $connection;
/**
* @return \OCP\Lock\ILockingProvider
*/
protected function getInstance() {
$this->connection = \OC::$server->getDatabaseConnection();
return new \OC\Lock\DBLockingProvider($this->connection, \OC::$server->getLogger());
}
public function tearDown() {
$this->connection->executeQuery('DELETE FROM `*PREFIX*file_locks`');
parent::tearDown();
}
}

View File

@ -120,6 +120,36 @@ abstract class LockingProvider extends TestCase {
$this->assertFalse($this->instance->isLocked('asd', ILockingProvider::LOCK_EXCLUSIVE));
}
public function testReleaseAllAfterChange() {
$this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED);
$this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED);
$this->instance->acquireLock('bar', ILockingProvider::LOCK_SHARED);
$this->instance->acquireLock('asd', ILockingProvider::LOCK_EXCLUSIVE);
$this->instance->changeLock('bar', ILockingProvider::LOCK_EXCLUSIVE);
$this->instance->releaseAll();
$this->assertFalse($this->instance->isLocked('foo', ILockingProvider::LOCK_SHARED));
$this->assertFalse($this->instance->isLocked('bar', ILockingProvider::LOCK_SHARED));
$this->assertFalse($this->instance->isLocked('bar', ILockingProvider::LOCK_EXCLUSIVE));
$this->assertFalse($this->instance->isLocked('asd', ILockingProvider::LOCK_EXCLUSIVE));
}
public function testReleaseAllAfterUnlock() {
$this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED);
$this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED);
$this->instance->acquireLock('bar', ILockingProvider::LOCK_SHARED);
$this->instance->acquireLock('asd', ILockingProvider::LOCK_EXCLUSIVE);
$this->instance->releaseLock('bar', ILockingProvider::LOCK_SHARED);
$this->instance->releaseAll();
$this->assertFalse($this->instance->isLocked('foo', ILockingProvider::LOCK_SHARED));
$this->assertFalse($this->instance->isLocked('asd', ILockingProvider::LOCK_EXCLUSIVE));
}
public function testReleaseAfterReleaseAll() {
$this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED);
$this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED);