summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--db_structure.xml73
-rw-r--r--lib/private/appframework/db/db.php9
-rw-r--r--lib/private/db/connection.php10
-rw-r--r--lib/private/lock/abstractlockingprovider.php102
-rw-r--r--lib/private/lock/dblockingprovider.php162
-rw-r--r--lib/private/lock/memcachelockingprovider.php40
-rw-r--r--lib/private/server.php12
-rw-r--r--lib/public/idbconnection.php8
-rw-r--r--tests/lib/lock/dblockingprovider.php43
-rw-r--r--tests/lib/lock/lockingprovider.php30
10 files changed, 446 insertions, 43 deletions
diff --git a/db_structure.xml b/db_structure.xml
index 870c0ab018d..c7b7ffec601 100644
--- a/db_structure.xml
+++ b/db_structure.xml
@@ -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>
diff --git a/lib/private/appframework/db/db.php b/lib/private/appframework/db/db.php
index cde85831687..8e3fa6e4197 100644
--- a/lib/private/appframework/db/db.php
+++ b/lib/private/appframework/db/db.php
@@ -154,6 +154,15 @@ class Db implements IDb {
}
/**
+ * 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
*/
public function commit() {
diff --git a/lib/private/db/connection.php b/lib/private/db/connection.php
index def3f2fd120..4d33cd968af 100644
--- a/lib/private/db/connection.php
+++ b/lib/private/db/connection.php
@@ -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;
+ }
}
diff --git a/lib/private/lock/abstractlockingprovider.php b/lib/private/lock/abstractlockingprovider.php
new file mode 100644
index 00000000000..eb86be68500
--- /dev/null
+++ b/lib/private/lock/abstractlockingprovider.php
@@ -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);
+ }
+ }
+}
diff --git a/lib/private/lock/dblockingprovider.php b/lib/private/lock/dblockingprovider.php
new file mode 100644
index 00000000000..f3e684d0b4d
--- /dev/null
+++ b/lib/private/lock/dblockingprovider.php
@@ -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();
+ }
+}
diff --git a/lib/private/lock/memcachelockingprovider.php b/lib/private/lock/memcachelockingprovider.php
index 5f2b5e5a4b8..871572f7e3e 100644
--- a/lib/private/lock/memcachelockingprovider.php
+++ b/lib/private/lock/memcachelockingprovider.php
@@ -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);
}
}
diff --git a/lib/private/server.php b/lib/private/server.php
index 5a3a6328fae..287b70eb806 100644
--- a/lib/private/server.php
+++ b/lib/private/server.php
@@ -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();
});
diff --git a/lib/public/idbconnection.php b/lib/public/idbconnection.php
index 0d04c43d73e..6a4373583fa 100644
--- a/lib/public/idbconnection.php
+++ b/lib/public/idbconnection.php
@@ -115,6 +115,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
*/
diff --git a/tests/lib/lock/dblockingprovider.php b/tests/lib/lock/dblockingprovider.php
new file mode 100644
index 00000000000..fd6550d9c47
--- /dev/null
+++ b/tests/lib/lock/dblockingprovider.php
@@ -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();
+ }
+}
diff --git a/tests/lib/lock/lockingprovider.php b/tests/lib/lock/lockingprovider.php
index efd6e1939f2..ca72c1bb7f3 100644
--- a/tests/lib/lock/lockingprovider.php
+++ b/tests/lib/lock/lockingprovider.php
@@ -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);