From 8041a2b42883ab4f7b31f6be6369520f642ab8a9 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 2 Sep 2024 16:48:06 +0200 Subject: [PATCH] Bump --- src/FileMutex.php | 38 +++++++++++++++++++--- src/KeyedFileMutex.php | 8 ++++- src/KeyedLockingFileMutex.php | 61 +++++++++++++++++++++++++++++++++++ src/LockingFileMutex.php | 53 ++++++++++++++++++++++++++++++ 4 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 src/KeyedLockingFileMutex.php create mode 100644 src/LockingFileMutex.php diff --git a/src/FileMutex.php b/src/FileMutex.php index f4fc820..3d40717 100644 --- a/src/FileMutex.php +++ b/src/FileMutex.php @@ -8,6 +8,13 @@ use Amp\Sync\Mutex; use Amp\Sync\SyncException; 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 { private const LATENCY_TIMEOUT = 0.01; @@ -31,16 +38,39 @@ final class FileMutex implements Mutex 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)); + try { + $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; + } 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, + ); } } } diff --git a/src/KeyedFileMutex.php b/src/KeyedFileMutex.php index a6a340c..ce6b7a6 100644 --- a/src/KeyedFileMutex.php +++ b/src/KeyedFileMutex.php @@ -7,7 +7,13 @@ use Amp\Sync\KeyedMutex; use Amp\Sync\Lock; use Amp\Sync\SyncException; 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 { private const LATENCY_TIMEOUT = 0.01; diff --git a/src/KeyedLockingFileMutex.php b/src/KeyedLockingFileMutex.php new file mode 100644 index 0000000..e50e3e2 --- /dev/null +++ b/src/KeyedLockingFileMutex.php @@ -0,0 +1,61 @@ +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'; + } +} diff --git a/src/LockingFileMutex.php b/src/LockingFileMutex.php new file mode 100644 index 0000000..c86c1df --- /dev/null +++ b/src/LockingFileMutex.php @@ -0,0 +1,53 @@ +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); + } + } +}