1
0
mirror of https://github.com/danog/file.git synced 2024-12-02 09:17:57 +01:00
This commit is contained in:
Daniil Gentili 2024-09-02 16:48:06 +02:00
parent 600acf5474
commit 8041a2b428
4 changed files with 155 additions and 5 deletions

View File

@ -8,6 +8,13 @@ use Amp\Sync\Mutex;
use Amp\Sync\SyncException; use Amp\Sync\SyncException;
use function Amp\delay; use function Amp\delay;
/**
* Async mutex based on files.
*
* A crash of the program will NOT release the lock, manual user action will be required to remove the lockfile.
*
* For a mutex that will automatically release the lock in case of a crash, see LockingFileMutex.
*/
final class FileMutex implements Mutex final class FileMutex implements Mutex
{ {
private const LATENCY_TIMEOUT = 0.01; private const LATENCY_TIMEOUT = 0.01;
@ -31,16 +38,39 @@ final class FileMutex implements Mutex
if (!$this->filesystem->isDirectory($this->directory)) { if (!$this->filesystem->isDirectory($this->directory)) {
throw new SyncException(\sprintf('Directory of "%s" does not exist or is not a directory', $this->fileName)); throw new SyncException(\sprintf('Directory of "%s" does not exist or is not a directory', $this->fileName));
} }
$f = \fopen($this->fileName, 'c');
// Try to create the lock file. If the file already exists, someone else // Try to create the lock file. If the file already exists, someone else
// has the lock, so set an asynchronous timer and try again. // has the lock, so set an asynchronous timer and try again.
for ($attempt = 0; true; ++$attempt) { for ($attempt = 0; true; ++$attempt) {
if (\flock($f, LOCK_EX|LOCK_NB)) { try {
$lock = new Lock(fn () => \flock($f, LOCK_UN)); $file = $this->filesystem->openFile($this->fileName, 'x');
// Return a lock object that can be used to release the lock on the mutex.
$lock = new Lock($this->release(...));
$file->close();
return $lock; return $lock;
} catch (FilesystemException) {
delay(\min(self::DELAY_LIMIT, self::LATENCY_TIMEOUT * (2 ** $attempt)), cancellation: $cancellation);
} }
delay(\min(self::DELAY_LIMIT, self::LATENCY_TIMEOUT * (2 ** $attempt)), cancellation: $cancellation); }
}
/**
* Releases the lock on the mutex.
*
* @throws SyncException
*/
private function release(): void
{
try {
$this->filesystem->deleteFile($this->fileName);
} catch (\Throwable $exception) {
throw new SyncException(
'Failed to unlock the mutex file: ' . $this->fileName,
previous: $exception,
);
} }
} }
} }

View File

@ -7,7 +7,13 @@ use Amp\Sync\KeyedMutex;
use Amp\Sync\Lock; use Amp\Sync\Lock;
use Amp\Sync\SyncException; use Amp\Sync\SyncException;
use function Amp\delay; use function Amp\delay;
/**
* Asynckeyed mutex based on files.
*
* A crash of the program will NOT release the lock, manual user action will be required to remove the lockfile.
*
* For a mutex that will automatically release the lock in case of a crash, see KeyedLockingFileMutex.
*/
final class KeyedFileMutex implements KeyedMutex final class KeyedFileMutex implements KeyedMutex
{ {
private const LATENCY_TIMEOUT = 0.01; private const LATENCY_TIMEOUT = 0.01;

View File

@ -0,0 +1,61 @@
<?php declare(strict_types=1);
namespace Amp\File;
use Amp\Cancellation;
use Amp\Sync\KeyedMutex;
use Amp\Sync\Lock;
use Amp\Sync\SyncException;
use function Amp\delay;
/**
* Async keyed mutex based on flock.
*
* A crash of the program will automatically release the lock, but the lockfiles will never be removed from the filesystem (even in case of successful release).
*
* For a mutex that removes the lockfiles but does not release the lock in case of a crash (requiring manual user action to clean up), see KeyedFileMutex.
*/
final class KeyedLockingFileMutex implements KeyedMutex
{
private const LATENCY_TIMEOUT = 0.01;
private const DELAY_LIMIT = 1;
private readonly Filesystem $filesystem;
private readonly string $directory;
/**
* @param string $directory Directory in which to store key files.
*/
public function __construct(string $directory, ?Filesystem $filesystem = null)
{
$this->filesystem = $filesystem ?? filesystem();
$this->directory = \rtrim($directory, "/\\");
}
public function acquire(string $key, ?Cancellation $cancellation = null): Lock
{
if (!$this->filesystem->isDirectory($this->directory)) {
throw new SyncException(\sprintf('Directory "%s" does not exist or is not a directory', $this->directory));
}
$filename = $this->getFilename($key);
$f = \fopen($filename, 'c');
// Try to create the lock file. If the file already exists, someone else
// has the lock, so set an asynchronous timer and try again.
for ($attempt = 0; true; ++$attempt) {
if (\flock($f, LOCK_EX|LOCK_NB)) {
$lock = new Lock(fn () => \flock($f, LOCK_UN));
return $lock;
}
delay(\min(self::DELAY_LIMIT, self::LATENCY_TIMEOUT * (2 ** $attempt)), cancellation: $cancellation);
}
}
private function getFilename(string $key): string
{
return $this->directory . '/' . \hash('sha256', $key) . '.lock';
}
}

53
src/LockingFileMutex.php Normal file
View File

@ -0,0 +1,53 @@
<?php declare(strict_types=1);
namespace Amp\File;
use Amp\Cancellation;
use Amp\Sync\Lock;
use Amp\Sync\Mutex;
use Amp\Sync\SyncException;
use function Amp\delay;
/**
* Async mutex based on flock.
*
* A crash of the program will automatically release the lock, but the lockfiles will never be removed from the filesystem (even in case of successful release).
*
* For a mutex that removes the lockfiles but does not release the lock in case of a crash (requiring manual user action to clean up), see FileMutex.
*/
final class LockingFileMutex implements Mutex
{
private const LATENCY_TIMEOUT = 0.01;
private const DELAY_LIMIT = 1;
private readonly Filesystem $filesystem;
private readonly string $directory;
/**
* @param string $fileName Name of temporary file to use as a mutex.
*/
public function __construct(private readonly string $fileName, ?Filesystem $filesystem = null)
{
$this->filesystem = $filesystem ?? filesystem();
$this->directory = \dirname($this->fileName);
}
public function acquire(?Cancellation $cancellation = null): Lock
{
if (!$this->filesystem->isDirectory($this->directory)) {
throw new SyncException(\sprintf('Directory of "%s" does not exist or is not a directory', $this->fileName));
}
$f = \fopen($this->fileName, 'c');
// Try to create the lock file. If the file already exists, someone else
// has the lock, so set an asynchronous timer and try again.
for ($attempt = 0; true; ++$attempt) {
if (\flock($f, LOCK_EX|LOCK_NB)) {
$lock = new Lock(fn () => \flock($f, LOCK_UN));
return $lock;
}
delay(\min(self::DELAY_LIMIT, self::LATENCY_TIMEOUT * (2 ** $attempt)), cancellation: $cancellation);
}
}
}