From 200371e75a0a0f700b6ec7887be1b84d80d3ec84 Mon Sep 17 00:00:00 2001 From: Tpt Date: Sat, 30 Dec 2023 12:07:49 +0100 Subject: [PATCH] Uses a UNIX semaphore to limit concurrent ebook-convert calls --- README.md | 2 +- config/services.yaml | 6 +++ src/Generator/ConvertGenerator.php | 23 +++++++++-- src/Util/Semaphore/Semaphore.php | 13 +++++++ src/Util/Semaphore/SemaphoreHandle.php | 11 ++++++ src/Util/Semaphore/UnixSemaphore.php | 45 ++++++++++++++++++++++ src/Util/Semaphore/UnixSemaphoreHandle.php | 27 +++++++++++++ tests/Util/Semaphore/UnixSemaphoreTest.php | 23 +++++++++++ 8 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 src/Util/Semaphore/Semaphore.php create mode 100644 src/Util/Semaphore/SemaphoreHandle.php create mode 100644 src/Util/Semaphore/UnixSemaphore.php create mode 100644 src/Util/Semaphore/UnixSemaphoreHandle.php create mode 100644 tests/Util/Semaphore/UnixSemaphoreTest.php diff --git a/README.md b/README.md index c9607369..d82ffc36 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ This section deals with a full installation of WS Export on the local developmen For example, in Debian-based Linux distributions: ```console - apt install php-sqlite3 php-zip php-curl + apt install php-sqlite3 php-zip php-curl php-sysvsem ``` Then create a `.env.local` file: diff --git a/config/services.yaml b/config/services.yaml index 7cd30ac6..ff5aec6f 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -70,5 +70,11 @@ services: $rateLimit: '%env(int:APP_RATE_LIMIT)%' $rateDuration: '%env(int:APP_RATE_DURATION)%' + App\Util\Semaphore\Semaphore: + class: 'App\Util\Semaphore\UnixSemaphore' + arguments: + $semaphoreKey: 123455435644 + $capacity: 4 + # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones diff --git a/src/Generator/ConvertGenerator.php b/src/Generator/ConvertGenerator.php index a19fb1e6..f8e1356b 100644 --- a/src/Generator/ConvertGenerator.php +++ b/src/Generator/ConvertGenerator.php @@ -5,6 +5,7 @@ use App\Book; use App\Exception\WsExportException; use App\FileCache; +use App\Util\Semaphore\Semaphore; use App\Util\Util; use InvalidArgumentException; use Symfony\Component\HttpFoundation\Response; @@ -77,9 +78,7 @@ public static function getSupportedTypes() { return array_keys( self::$CONFIG ); } - /** - * @var string - */ + /** @var string */ private $format; /** @var int Command timeout in seconds. */ @@ -91,10 +90,14 @@ public static function getSupportedTypes() { /** @var EpubGenerator */ private $epubGenerator; - public function __construct( int $timeout, FileCache $fileCache, EpubGenerator $epubGenerator ) { + /** @var ?Semaphore */ + private $semaphore; + + public function __construct( int $timeout, FileCache $fileCache, EpubGenerator $epubGenerator, ?Semaphore $semaphore = null ) { $this->timeout = $timeout; $this->fileCache = $fileCache; $this->epubGenerator = $epubGenerator; + $this->semaphore = $semaphore; } /** @@ -146,6 +149,14 @@ public function create( Book $book ) { } private function convert( $epubFileName, $outputFileName ) { + $lock = null; + if ( $this->semaphore !== null ) { + $lock = $this->semaphore->tryLock(); + if ( $lock === null ) { + // Overload + throw new WsExportException( 'book-conversion', [], Response::HTTP_INTERNAL_SERVER_ERROR ); + } + } try { $command = array_merge( [ 'ebook-convert', $epubFileName, $outputFileName ], @@ -156,6 +167,10 @@ private function convert( $epubFileName, $outputFileName ) { $process->mustRun(); } catch ( ProcessTimedOutException $e ) { throw new WsExportException( 'book-conversion', [], Response::HTTP_INTERNAL_SERVER_ERROR ); + } finally { + if ( $lock !== null ) { + $lock->release(); + } } } } diff --git a/src/Util/Semaphore/Semaphore.php b/src/Util/Semaphore/Semaphore.php new file mode 100644 index 00000000..c199a099 --- /dev/null +++ b/src/Util/Semaphore/Semaphore.php @@ -0,0 +1,13 @@ +semaphoreKey = $semaphoreKey; + $this->capacity = $capacity; + $this->semaphore = null; + } + + public function tryLock(): ?SemaphoreHandle { + if ( $this->semaphore === null ) { + $semaphore = sem_get( $this->semaphoreKey, $this->capacity ); + if ( $semaphore === false ) { + throw new Exception( "Failed to create semaphore key $this->semaphoreKey" ); + } + $this->semaphore = $semaphore; + } + + if ( !sem_acquire( $this->semaphore, true ) ) { + return null; // Semaphore already full + } + + // We return the handle + return new UnixSemaphoreHandle( $this->semaphore ); + } +} diff --git a/src/Util/Semaphore/UnixSemaphoreHandle.php b/src/Util/Semaphore/UnixSemaphoreHandle.php new file mode 100644 index 00000000..eb45da86 --- /dev/null +++ b/src/Util/Semaphore/UnixSemaphoreHandle.php @@ -0,0 +1,27 @@ +semaphore = $semaphore; + $this->isReleased = false; + } + + public function release(): void { + if ( $this->isReleased ) { + return; // Already released + } + $this->isReleased = true; + sem_release( $this->semaphore ); + } + + public function __destruct() { + $this->release(); + } +} diff --git a/tests/Util/Semaphore/UnixSemaphoreTest.php b/tests/Util/Semaphore/UnixSemaphoreTest.php new file mode 100644 index 00000000..73547f9e --- /dev/null +++ b/tests/Util/Semaphore/UnixSemaphoreTest.php @@ -0,0 +1,23 @@ +tryLock(); + $this->assertInstanceOf( SemaphoreHandle::class, $handle ); + $this->assertNull( $semaphore->tryLock() ); + $handle->release(); + $this->assertInstanceOf( SemaphoreHandle::class, $semaphore->tryLock() ); + } +}