Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement collaborative locking #2248

Merged
merged 8 commits into from
Apr 21, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Implement collaborative locking
Signed-off-by: Julius Härtl <jus@bitgrid.net>

TMP: Switch to OCP branch for tests

Signed-off-by: Julius Härtl <jus@bitgrid.net>

Let locked files open in read only

Signed-off-by: Julius Härtl <jus@bitgrid.net>

Adapt LockScope to LockContext rename

Signed-off-by: Julius Härtl <jus@bitgrid.net>
  • Loading branch information
juliusknorr committed Apr 21, 2022
commit 8c00c8eb25b622ac2f32b9c6e2928ca8fcae7feb
6 changes: 6 additions & 0 deletions lib/Service/ApiService.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ public function create($fileId = null, $filePath = null, $token = null, $guestNa
$this->logger->logException($e, ['level' => ILogger::INFO]);
$content = null;
}

$isLocked = $this->documentService->lock($fileId);
if (!$isLocked) {
$readOnly = true;
}

return new DataResponse([
'document' => $document,
'session' => $session,
Expand Down
106 changes: 66 additions & 40 deletions lib/Service/DocumentService.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,18 @@
namespace OCA\Text\Service;

use \InvalidArgumentException;
use OCA\Text\AppInfo\Application;
use OCA\Text\Db\Session;
use OCA\Text\Db\SessionMapper;
use OCP\DirectEditing\IManager;
use OCP\Files\Lock\ILock;
use OCP\Files\Lock\ILockManager;
use OCP\Files\Lock\LockContext;
use OCP\Files\Lock\NoLockProviderException;
use OCP\Files\Lock\OwnerLockedException;
use OCP\IRequest;
use OCP\Lock\ILockingProvider;
use OCP\PreConditionNotMetException;
use function json_encode;
use OC\Files\Node\File;
use OCA\Text\Db\Document;
Expand Down Expand Up @@ -67,44 +74,18 @@ class DocumentService {
*/
public const AUTOSAVE_MINIMUM_DELAY = 10;

/**
* @var string|null
*/
private $userId;
/**
* @var DocumentMapper
*/
private $documentMapper;
/**
* @var SessionMapper
*/
private $sessionMapper;
/**
* @var ILogger
*/
private $logger;
/**
* @var ShareManager
*/
private $shareManager;
/**
* @var StepMapper
*/
private $stepMapper;
/**
* @var IRootFolder
*/
private $rootFolder;
/**
* @var ICache
*/
private $cache;
/**
* @var IAppData
*/
private $appData;

public function __construct(DocumentMapper $documentMapper, StepMapper $stepMapper, SessionMapper $sessionMapper, IAppData $appData, $userId, IRootFolder $rootFolder, ICacheFactory $cacheFactory, ILogger $logger, ShareManager $shareManager, IRequest $request, IManager $directManager, ILockingProvider $lockingProvider) {
private ?string $userId;
private DocumentMapper $documentMapper;
private SessionMapper $sessionMapper;
private ILogger $logger;
private ShareManager $shareManager;
private StepMapper $stepMapper;
private IRootFolder $rootFolder;
private ICache $cache;
private IAppData $appData;
private ILockManager $lockManager;

public function __construct(DocumentMapper $documentMapper, StepMapper $stepMapper, SessionMapper $sessionMapper, IAppData $appData, $userId, IRootFolder $rootFolder, ICacheFactory $cacheFactory, ILogger $logger, ShareManager $shareManager, IRequest $request, IManager $directManager, ILockingProvider $lockingProvider, ILockManager $lockManager) {
$this->documentMapper = $documentMapper;
$this->stepMapper = $stepMapper;
$this->sessionMapper = $sessionMapper;
Expand All @@ -115,7 +96,7 @@ public function __construct(DocumentMapper $documentMapper, StepMapper $stepMapp
$this->logger = $logger;
$this->shareManager = $shareManager;
$this->lockingProvider = $lockingProvider;

$this->lockManager = $lockManager;
$token = $request->getParam('token');
if ($this->userId === null && $token !== null) {
try {
Expand Down Expand Up @@ -328,7 +309,13 @@ public function autosave($file, $documentId, $version, $autoaveDocument, $force
}
$this->cache->set('document-save-lock-' . $documentId, true, 10);
try {
$file->putContent($autoaveDocument);
$this->lockManager->runInScope(new LockContext(
$file,
ILock::TYPE_APP,
Application::APP_NAME
), function () use ($file, $autoaveDocument) {
$file->putContent($autoaveDocument);
});
} catch (LockedException $e) {
// Ignore lock since it might occur when multiple people save at the same time
return $document;
Expand All @@ -348,6 +335,8 @@ public function autosave($file, $documentId, $version, $autoaveDocument, $force
*/
public function resetDocument($documentId, $force = false): void {
try {
$this->unlock($documentId);

$document = $this->documentMapper->find($documentId);

if ($force || !$this->hasUnsavedChanges($document)) {
Expand Down Expand Up @@ -490,4 +479,41 @@ private function ensureDocumentsFolder(): bool {

return true;
}

public function lock(int $fileId): bool {
juliusknorr marked this conversation as resolved.
Show resolved Hide resolved
if (!$this->lockManager->isLockProviderAvailable()) {
return true;
}

$file = $this->getFileById($fileId);
try {
$this->lockManager->lock(new LockContext(
$file,
ILock::TYPE_APP,
Application::APP_NAME
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of topic but this could become Application::APP_ID at some point.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, something for a separate PR.

));
} catch (NoLockProviderException $e) {
} catch (OwnerLockedException $e) {
return false;
} catch (PreConditionNotMetException $e) {
}
return true;
}

public function unlock(int $fileId): void {
if (!$this->lockManager->isLockProviderAvailable()) {
return;
}

$file = $this->getFileById($fileId);
try {
$this->lockManager->unlock(new LockContext(
$file,
ILock::TYPE_APP,
Application::APP_NAME
));
} catch (NoLockProviderException $e) {
} catch (PreConditionNotMetException $e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No warning logs here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd let the actual lock provider handle that, from the text app side there is no special interest if we unlock a file that has already been unlocked.

}
}
}