From 86dca58fbb9c9b4ed48132b36b5901a347d37c15 Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Fri, 6 Dec 2024 17:22:56 -0600 Subject: [PATCH 1/3] flock-based File Mutex --- src/FileMutex.php | 48 +++++++++++++++++++++++++++++++----------- src/KeyedFileMutex.php | 45 ++++----------------------------------- 2 files changed, 40 insertions(+), 53 deletions(-) diff --git a/src/FileMutex.php b/src/FileMutex.php index b827f94..c51ebba 100644 --- a/src/FileMutex.php +++ b/src/FileMutex.php @@ -13,6 +13,8 @@ final class FileMutex implements Mutex private const LATENCY_TIMEOUT = 0.01; private const DELAY_LIMIT = 1; + private static ?\Closure $errorHandler = null; + private readonly Filesystem $filesystem; private readonly string $directory; @@ -26,39 +28,61 @@ public function __construct(private readonly string $fileName, ?Filesystem $file $this->directory = \dirname($this->fileName); } + /** + * @throws SyncException + */ 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)); } - // Try to create the lock file. If the file already exists, someone else - // has the lock, so set an asynchronous timer and try again. + // Try to create and lock the file. If flock fails, someone else already has the lock, + // so set an asynchronous timer and try again. for ($attempt = 0; true; ++$attempt) { - 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(...)); + \set_error_handler(self::$errorHandler ??= static fn () => true); - $file->close(); + try { + $handle = \fopen($this->fileName, 'c'); + if (!$handle) { + throw new SyncException(\sprintf( + 'Unable to open or create file at %s: %s', + $this->fileName, + \error_get_last()['message'] ?? 'Unknown error', + )); + } - return $lock; - } catch (FilesystemException) { - delay(\min(self::DELAY_LIMIT, self::LATENCY_TIMEOUT * (2 ** $attempt)), cancellation: $cancellation); + if (\flock($handle, \LOCK_EX | \LOCK_NB)) { + return new Lock(fn () => $this->release($handle)); + } + } finally { + \restore_error_handler(); } + + $multiplier = 2 ** \min(31, $attempt); + delay(\min(self::DELAY_LIMIT, self::LATENCY_TIMEOUT * $multiplier), cancellation: $cancellation); } } /** * Releases the lock on the mutex. * + * @param resource $handle + * * @throws SyncException */ - private function release(): void + private function release($handle): void { try { $this->filesystem->deleteFile($this->fileName); + + \set_error_handler(self::$errorHandler ??= static fn () => true); + + try { + \fclose($handle); + } finally { + \restore_error_handler(); + } } catch (\Throwable $exception) { throw new SyncException( 'Failed to unlock the mutex file: ' . $this->fileName, diff --git a/src/KeyedFileMutex.php b/src/KeyedFileMutex.php index a6a340c..6cfaeec 100644 --- a/src/KeyedFileMutex.php +++ b/src/KeyedFileMutex.php @@ -6,13 +6,9 @@ use Amp\Sync\KeyedMutex; use Amp\Sync\Lock; use Amp\Sync\SyncException; -use function Amp\delay; final class KeyedFileMutex implements KeyedMutex { - private const LATENCY_TIMEOUT = 0.01; - private const DELAY_LIMIT = 1; - private readonly Filesystem $filesystem; private readonly string $directory; @@ -26,47 +22,14 @@ public function __construct(string $directory, ?Filesystem $filesystem = null) $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); - - // 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) { - try { - $file = $this->filesystem->openFile($filename, 'x'); - - // Return a lock object that can be used to release the lock on the mutex. - $lock = new Lock(fn () => $this->release($filename)); - - $file->close(); - - return $lock; - } catch (FilesystemException) { - delay(\min(self::DELAY_LIMIT, self::LATENCY_TIMEOUT * (2 ** $attempt)), cancellation: $cancellation); - } - } - } - /** - * Releases the lock on the mutex. - * * @throws SyncException */ - private function release(string $filename): void + public function acquire(string $key, ?Cancellation $cancellation = null): Lock { - try { - $this->filesystem->deleteFile($filename); - } catch (\Throwable $exception) { - throw new SyncException( - 'Failed to unlock the mutex file: ' . $filename, - previous: $exception, - ); - } + $mutex = new FileMutex($this->getFilename($key), $this->filesystem); + + return $mutex->acquire($cancellation); } private function getFilename(string $key): string From 4085e345d6fa0ed09721f5fe57cd2f725e44b0bd Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Fri, 6 Dec 2024 17:48:35 -0600 Subject: [PATCH 2/3] Remove open check for Windows --- src/FileMutex.php | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/FileMutex.php b/src/FileMutex.php index c51ebba..c4cb6ee 100644 --- a/src/FileMutex.php +++ b/src/FileMutex.php @@ -44,15 +44,7 @@ public function acquire(?Cancellation $cancellation = null): Lock try { $handle = \fopen($this->fileName, 'c'); - if (!$handle) { - throw new SyncException(\sprintf( - 'Unable to open or create file at %s: %s', - $this->fileName, - \error_get_last()['message'] ?? 'Unknown error', - )); - } - - if (\flock($handle, \LOCK_EX | \LOCK_NB)) { + if ($handle && \flock($handle, \LOCK_EX | \LOCK_NB)) { return new Lock(fn () => $this->release($handle)); } } finally { From 9c01cefb6ccf75eaa475ecbfd996c3e821ec223a Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Sat, 7 Dec 2024 11:47:07 -0600 Subject: [PATCH 3/3] Check would-block --- src/FileMutex.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/FileMutex.php b/src/FileMutex.php index c4cb6ee..5bcc997 100644 --- a/src/FileMutex.php +++ b/src/FileMutex.php @@ -44,8 +44,18 @@ public function acquire(?Cancellation $cancellation = null): Lock try { $handle = \fopen($this->fileName, 'c'); - if ($handle && \flock($handle, \LOCK_EX | \LOCK_NB)) { - return new Lock(fn () => $this->release($handle)); + if ($handle) { + if (\flock($handle, \LOCK_EX | \LOCK_NB, $wouldBlock)) { + return new Lock(fn () => $this->release($handle)); + } + + if (!$wouldBlock) { + throw new FilesystemException(\sprintf( + 'flock call on "%s" failed: %s', + $this->fileName, + \error_get_last()['message'] ?? 'Unknown error', + )); + } } } finally { \restore_error_handler();