diff --git a/src/FileMutex.php b/src/FileMutex.php index b827f94..6c2a280 100644 --- a/src/FileMutex.php +++ b/src/FileMutex.php @@ -8,6 +8,13 @@ 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; diff --git a/src/KeyedFileMutex.php b/src/KeyedFileMutex.php index a6a340c..91c7fbd 100644 --- a/src/KeyedFileMutex.php +++ b/src/KeyedFileMutex.php @@ -8,6 +8,13 @@ use Amp\Sync\SyncException; use function Amp\delay; +/** + * Async keyed 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..33a692f --- /dev/null +++ b/src/KeyedLockingFileMutex.php @@ -0,0 +1,62 @@ +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..0384c7a --- /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); + } + } +} diff --git a/test/KeyedLockingFileMutexTest.php b/test/KeyedLockingFileMutexTest.php new file mode 100644 index 0000000..3f8bf56 --- /dev/null +++ b/test/KeyedLockingFileMutexTest.php @@ -0,0 +1,27 @@ +