Skip to content

Commit

Permalink
Add mutex on datastore I/O operations
Browse files Browse the repository at this point in the history
To make sure that there is no concurrent operation on the datastore file.

Fixes #1132
  • Loading branch information
ArthurHoaro committed Oct 13, 2020
1 parent 458b6b9 commit fd1ddad
Show file tree
Hide file tree
Showing 26 changed files with 218 additions and 63 deletions.
2 changes: 2 additions & 0 deletions application/api/ApiMiddleware.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php
namespace Shaarli\Api;

use malkusch\lock\mutex\FlockMutex;
use Shaarli\Api\Exceptions\ApiAuthorizationException;
use Shaarli\Api\Exceptions\ApiException;
use Shaarli\Bookmark\BookmarkFileService;
Expand Down Expand Up @@ -143,6 +144,7 @@ protected function setLinkDb($conf)
$linkDb = new BookmarkFileService(
$conf,
$this->container->get('history'),
new FlockMutex(fopen(SHAARLI_MUTEX_FILE, 'r'), 2),
true
);
$this->container['db'] = $linkDb;
Expand Down
9 changes: 7 additions & 2 deletions application/bookmark/BookmarkFileService.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@


use Exception;
use malkusch\lock\mutex\Mutex;
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
use Shaarli\Bookmark\Exception\EmptyDataStoreException;
Expand Down Expand Up @@ -47,15 +48,19 @@ class BookmarkFileService implements BookmarkServiceInterface
/** @var bool true for logged in users. Default value to retrieve private bookmarks. */
protected $isLoggedIn;

/** @var Mutex */
protected $mutex;

/**
* @inheritDoc
*/
public function __construct(ConfigManager $conf, History $history, $isLoggedIn)
public function __construct(ConfigManager $conf, History $history, Mutex $mutex, $isLoggedIn)
{
$this->conf = $conf;
$this->history = $history;
$this->mutex = $mutex;
$this->pageCacheManager = new PageCacheManager($this->conf->get('resource.page_cache'), $isLoggedIn);
$this->bookmarksIO = new BookmarkIO($this->conf);
$this->bookmarksIO = new BookmarkIO($this->conf, $this->mutex);
$this->isLoggedIn = $isLoggedIn;

if (!$this->isLoggedIn && $this->conf->get('privacy.hide_public_links', false)) {
Expand Down
35 changes: 27 additions & 8 deletions application/bookmark/BookmarkIO.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Shaarli\Bookmark;

use malkusch\lock\mutex\Mutex;
use malkusch\lock\mutex\NoMutex;
use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
use Shaarli\Bookmark\Exception\EmptyDataStoreException;
use Shaarli\Bookmark\Exception\NotWritableDataStoreException;
Expand All @@ -27,11 +29,14 @@ class BookmarkIO
*/
protected $conf;


/** @var Mutex */
protected $mutex;

/**
* string Datastore PHP prefix
*/
protected static $phpPrefix = '<?php /* ';

/**
* string Datastore PHP suffix
*/
Expand All @@ -42,10 +47,15 @@ class BookmarkIO
*
* @param ConfigManager $conf instance
*/
public function __construct($conf)
public function __construct(ConfigManager $conf, Mutex $mutex = null)
{
if ($mutex === null) {
// This should only happen with legacy classes
$mutex = new NoMutex();
}
$this->conf = $conf;
$this->datastore = $conf->get('resource.datastore');
$this->mutex = $mutex;
}

/**
Expand All @@ -67,11 +77,16 @@ public function read()
throw new NotWritableDataStoreException($this->datastore);
}

$content = null;
$this->mutex->synchronized(function () use (&$content) {
$content = file_get_contents($this->datastore);
});

// Note that gzinflate is faster than gzuncompress.
// See: http://www.php.net/manual/en/function.gzdeflate.php#96439
$links = unserialize(gzinflate(base64_decode(
substr(file_get_contents($this->datastore),
strlen(self::$phpPrefix), -strlen(self::$phpSuffix)))));
substr($content, strlen(self::$phpPrefix), -strlen(self::$phpSuffix))
)));

if (empty($links)) {
if (filesize($this->datastore) > 100) {
Expand Down Expand Up @@ -100,9 +115,13 @@ public function write($links)
throw new NotWritableDataStoreException(dirname($this->datastore));
}

file_put_contents(
$this->datastore,
self::$phpPrefix.base64_encode(gzdeflate(serialize($links))).self::$phpSuffix
);
$data = self::$phpPrefix.base64_encode(gzdeflate(serialize($links))).self::$phpSuffix;

$this->mutex->synchronized(function () use ($data) {
file_put_contents(
$this->datastore,
$data
);
});
}
}
11 changes: 0 additions & 11 deletions application/bookmark/BookmarkServiceInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@

use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Bookmark\Exception\NotWritableDataStoreException;
use Shaarli\Config\ConfigManager;
use Shaarli\History;

/**
* Class BookmarksService
Expand All @@ -15,15 +13,6 @@
*/
interface BookmarkServiceInterface
{
/**
* BookmarksService constructor.
*
* @param ConfigManager $conf instance
* @param History $history instance
* @param bool $isLoggedIn true if the current user is logged in
*/
public function __construct(ConfigManager $conf, History $history, $isLoggedIn);

/**
* Find a bookmark by hash
*
Expand Down
2 changes: 2 additions & 0 deletions application/container/ContainerBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Shaarli\Container;

use malkusch\lock\mutex\FlockMutex;
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager;
Expand Down Expand Up @@ -84,6 +85,7 @@ public function build(): ShaarliContainer
return new BookmarkFileService(
$container->conf,
$container->history,
new FlockMutex(fopen(SHAARLI_MUTEX_FILE, 'r'), 2),
$container->loginManager->isLoggedIn()
);
};
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"erusev/parsedown": "^1.6",
"erusev/parsedown-extra": "^0.8.1",
"gettext/gettext": "^4.4",
"malkusch/lock": "^2.1",
"pubsubhubbub/publisher": "dev-master",
"shaarli/netscape-bookmark-parser": "^2.1",
"slim/slim": "^3.0"
Expand Down
87 changes: 86 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions init.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
ini_set('session.use_trans_sid', false);

define('SHAARLI_VERSION', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE));
define('SHAARLI_MUTEX_FILE', __FILE__);

session_name('shaarli');
// Start session if needed (Some server auto-start sessions).
Expand Down
4 changes: 3 additions & 1 deletion tests/api/controllers/info/InfoTest.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php
namespace Shaarli\Api\Controllers;

use malkusch\lock\mutex\NoMutex;
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Config\ConfigManager;
use Shaarli\History;
Expand Down Expand Up @@ -49,6 +50,7 @@ class InfoTest extends TestCase
*/
protected function setUp(): void
{
$mutex = new NoMutex();
$this->conf = new ConfigManager('tests/utils/config/configJson');
$this->conf->set('resource.datastore', self::$testDatastore);
$this->refDB = new \ReferenceLinkDB();
Expand All @@ -58,7 +60,7 @@ protected function setUp(): void

$this->container = new Container();
$this->container['conf'] = $this->conf;
$this->container['db'] = new BookmarkFileService($this->conf, $history, true);
$this->container['db'] = new BookmarkFileService($this->conf, $history, $mutex, true);
$this->container['history'] = null;

$this->controller = new Info($this->container);
Expand Down
9 changes: 7 additions & 2 deletions tests/api/controllers/links/DeleteLinkTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace Shaarli\Api\Controllers;

use malkusch\lock\mutex\NoMutex;
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Config\ConfigManager;
use Shaarli\History;
Expand Down Expand Up @@ -53,19 +54,23 @@ class DeleteLinkTest extends \Shaarli\TestCase
*/
protected $controller;

/** @var NoMutex */
protected $mutex;

/**
* Before each test, instantiate a new Api with its config, plugins and bookmarks.
*/
protected function setUp(): void
{
$this->mutex = new NoMutex();
$this->conf = new ConfigManager('tests/utils/config/configJson');
$this->conf->set('resource.datastore', self::$testDatastore);
$this->refDB = new \ReferenceLinkDB();
$this->refDB->write(self::$testDatastore);
$refHistory = new \ReferenceHistory();
$refHistory->write(self::$testHistory);
$this->history = new History(self::$testHistory);
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);

$this->container = new Container();
$this->container['conf'] = $this->conf;
Expand Down Expand Up @@ -100,7 +105,7 @@ public function testDeleteLinkValid()
$this->assertEquals(204, $response->getStatusCode());
$this->assertEmpty((string) $response->getBody());

$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
$this->assertFalse($this->bookmarkService->exists($id));

$historyEntry = $this->history->getHistory()[0];
Expand Down
4 changes: 3 additions & 1 deletion tests/api/controllers/links/GetLinkIdTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Shaarli\Api\Controllers;

use malkusch\lock\mutex\NoMutex;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Config\ConfigManager;
Expand Down Expand Up @@ -57,6 +58,7 @@ class GetLinkIdTest extends \Shaarli\TestCase
*/
protected function setUp(): void
{
$mutex = new NoMutex();
$this->conf = new ConfigManager('tests/utils/config/configJson');
$this->conf->set('resource.datastore', self::$testDatastore);
$this->refDB = new \ReferenceLinkDB();
Expand All @@ -65,7 +67,7 @@ protected function setUp(): void

$this->container = new Container();
$this->container['conf'] = $this->conf;
$this->container['db'] = new BookmarkFileService($this->conf, $history, true);
$this->container['db'] = new BookmarkFileService($this->conf, $history, $mutex, true);
$this->container['history'] = null;

$this->controller = new Links($this->container);
Expand Down
4 changes: 3 additions & 1 deletion tests/api/controllers/links/GetLinksTest.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php
namespace Shaarli\Api\Controllers;

use malkusch\lock\mutex\NoMutex;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Bookmark\LinkDB;
Expand Down Expand Up @@ -57,6 +58,7 @@ class GetLinksTest extends \Shaarli\TestCase
*/
protected function setUp(): void
{
$mutex = new NoMutex();
$this->conf = new ConfigManager('tests/utils/config/configJson');
$this->conf->set('resource.datastore', self::$testDatastore);
$this->refDB = new \ReferenceLinkDB();
Expand All @@ -65,7 +67,7 @@ protected function setUp(): void

$this->container = new Container();
$this->container['conf'] = $this->conf;
$this->container['db'] = new BookmarkFileService($this->conf, $history, true);
$this->container['db'] = new BookmarkFileService($this->conf, $history, $mutex, true);
$this->container['history'] = null;

$this->controller = new Links($this->container);
Expand Down
Loading

0 comments on commit fd1ddad

Please sign in to comment.