├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── rector.php └── src ├── Exception ├── MutexLockedException.php └── MutexReleaseException.php ├── Mutex.php ├── MutexFactory.php ├── MutexFactoryInterface.php ├── MutexInterface.php ├── RetryAcquireTrait.php ├── SimpleMutex.php └── Synchronizer.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Yii Mutex Change Log 2 | 3 | ## 1.1.2 under development 4 | 5 | - Chg #74: Change PHP constraint in `composer.json` to `7.4.* || 8.0 - 8.4` (@vjik) 6 | 7 | ## 1.1.1 January 12, 2022 8 | 9 | - Enh #48: Throw an `InvalidArgumentException` if the retry delay is less than one millisecond (@devanych) 10 | 11 | ## 1.1.0 October 19, 2021 12 | 13 | - New #45: Add `MutexLockedException` and `MutexReleaseException` and throw them instead of `RuntimeException` 14 | (@devanych) 15 | 16 | ## 1.0.1 August 19, 2021 17 | 18 | - no changes in this release. 19 | 20 | ## 1.0.0 August 18, 2021 21 | 22 | - Initial release. 23 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2008 by Yii Software () 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Yii Software nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Yii 4 | 5 |

Yii Mutex

6 |
7 |

8 | 9 | [![Latest Stable Version](https://poser.pugx.org/yiisoft/mutex/v)](https://packagist.org/packages/yiisoft/mutex) 10 | [![Total Downloads](https://poser.pugx.org/yiisoft/mutex/downloads)](https://packagist.org/packages/yiisoft/mutex) 11 | [![Build status](https://github.com/yiisoft/mutex/actions/workflows/build.yml/badge.svg)](https://github.com/yiisoft/mutex/actions/workflows/build.yml) 12 | [![Code Coverage](https://codecov.io/gh/yiisoft/mutex/branch/master/graph/badge.svg)](https://codecov.io/gh/yiisoft/mutex) 13 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyiisoft%2Fmutex%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/mutex/master) 14 | [![Static analysis](https://github.com/yiisoft/mutex/actions/workflows/static.yml/badge.svg?branch=master)](https://github.com/yiisoft/mutex/actions/workflows/static.yml?query=branch%3Amaster) 15 | [![type-coverage](https://shepherd.dev/github/yiisoft/mutex/coverage.svg)](https://shepherd.dev/github/yiisoft/mutex) 16 | 17 | This package provides mutex implementation and allows mutual execution of concurrent processes in order to prevent 18 | "race conditions". 19 | 20 | This is achieved by using a "lock" mechanism. Each possibly concurrent processes cooperates by acquiring 21 | a lock before accessing the corresponding data. 22 | 23 | ## Requirements 24 | 25 | - PHP 7.4 or higher. 26 | 27 | ## Installation 28 | 29 | The package could be installed with [Composer](https://getcomposer.org): 30 | 31 | ```shell 32 | composer require yiisoft/mutex 33 | ``` 34 | 35 | ## General usage 36 | 37 | There are multiple ways you can use the package. You can execute a callback in a synchronized mode i.e. only a 38 | single instance of the callback is executed at the same time: 39 | 40 | ```php 41 | /** @var \Yiisoft\Mutex\Synchronizer $synchronizer */ 42 | $newCount = $synchronizer->execute('critical', function () { 43 | return $counter->increase(); 44 | }, 10); 45 | ``` 46 | 47 | Another way is to manually open and close mutex: 48 | 49 | ```php 50 | /** @var \Yiisoft\Mutex\SimpleMutex $simpleMutex */ 51 | if (!$simpleMutex->acquire('critical', 10)) { 52 | throw new \Yiisoft\Mutex\Exception\MutexLockedException('Unable to acquire the "critical" mutex.'); 53 | } 54 | $newCount = $counter->increase(); 55 | $simpleMutex->release('critical'); 56 | ``` 57 | 58 | It could be done on lower level: 59 | 60 | ```php 61 | /** @var \Yiisoft\Mutex\MutexFactoryInterface $mutexFactory */ 62 | $mutex = $mutexFactory->createAndAcquire('critical', 10); 63 | $newCount = $counter->increase(); 64 | $mutex->release(); 65 | ``` 66 | 67 | And if you want even more control, you can acquire mutex manually: 68 | 69 | ```php 70 | /** @var \Yiisoft\Mutex\MutexFactoryInterface $mutexFactory */ 71 | $mutex = $mutexFactory->create('critical'); 72 | if (!$mutex->acquire(10)) { 73 | throw new \Yiisoft\Mutex\Exception\MutexLockedException('Unable to acquire the "critical" mutex.'); 74 | } 75 | $newCount = $counter->increase(); 76 | $mutex->release(); 77 | ``` 78 | 79 | ## Mutex drivers 80 | 81 | There are some mutex drivers available as separate packages: 82 | 83 | - [File](https://github.com/yiisoft/mutex-file) 84 | - [PDO MySQL](https://github.com/yiisoft/mutex-pdo-mysql) 85 | - [PDO Oracle](https://github.com/yiisoft/mutex-pdo-oracle) 86 | - [PDO Postgres](https://github.com/yiisoft/mutex-pdo-pgsql) 87 | - [Redis](https://github.com/yiisoft/mutex-redis) 88 | 89 | If you want to provide your own driver, you need to implement `MutexFactoryInterface` and `MutexInterface`. 90 | There is ready to extend `Mutex`, `MutexFactory` and a `RetryAcquireTrait` that contains `retryAcquire()` 91 | method implementing the "wait for a lock for a certain time" functionality. 92 | 93 | When implementing your own drivers, you need to take care of automatic unlocking. For example using a destructor: 94 | 95 | ```php 96 | public function __destruct() 97 | { 98 | $this->release(); 99 | } 100 | ``` 101 | 102 | and shutdown function: 103 | 104 | ```php 105 | public function __construct() 106 | { 107 | register_shutdown_function(function () { 108 | $this->release(); 109 | }); 110 | } 111 | ``` 112 | 113 | Note that you should not call the `exit()` or `die()` functions in the destructor or shutdown function. Since calling 114 | these functions in the destructor and shutdown function will prevent all subsequent completion functions from executing. 115 | 116 | ## Documentation 117 | 118 | - [Internals](docs/internals.md) 119 | 120 | If you need help or have a question, the [Yii Forum](https://forum.yiiframework.com/c/yii-3-0/63) is a good place for 121 | that. You may also check out other [Yii Community Resources](https://www.yiiframework.com/community). 122 | 123 | ## License 124 | 125 | The Yii Mutex is free software. It is released under the terms of the BSD License. 126 | Please see [`LICENSE`](./LICENSE.md) for more information. 127 | 128 | Maintained by [Yii Software](https://www.yiiframework.com/). 129 | 130 | ## Support the project 131 | 132 | [![Open Collective](https://img.shields.io/badge/Open%20Collective-sponsor-7eadf1?logo=open%20collective&logoColor=7eadf1&labelColor=555555)](https://opencollective.com/yiisoft) 133 | 134 | ## Follow updates 135 | 136 | [![Official website](https://img.shields.io/badge/Powered_by-Yii_Framework-green.svg?style=flat)](https://www.yiiframework.com/) 137 | [![Twitter](https://img.shields.io/badge/twitter-follow-1DA1F2?logo=twitter&logoColor=1DA1F2&labelColor=555555?style=flat)](https://twitter.com/yiiframework) 138 | [![Telegram](https://img.shields.io/badge/telegram-join-1DA1F2?style=flat&logo=telegram)](https://t.me/yii3en) 139 | [![Facebook](https://img.shields.io/badge/facebook-join-1DA1F2?style=flat&logo=facebook&logoColor=ffffff)](https://www.facebook.com/groups/yiitalk) 140 | [![Slack](https://img.shields.io/badge/slack-join-1DA1F2?style=flat&logo=slack)](https://yiiframework.com/go/slack) 141 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yiisoft/mutex", 3 | "type": "library", 4 | "description": "Yii Mutex Library", 5 | "keywords": [ 6 | "yii", 7 | "mutex" 8 | ], 9 | "homepage": "https://www.yiiframework.com/", 10 | "license": "BSD-3-Clause", 11 | "support": { 12 | "issues": "https://github.com/yiisoft/mutex/issues?state=open", 13 | "source": "https://github.com/yiisoft/mutex", 14 | "forum": "https://www.yiiframework.com/forum/", 15 | "wiki": "https://www.yiiframework.com/wiki/", 16 | "irc": "ircs://irc.libera.chat:6697/yii", 17 | "chat": "https://t.me/yii3en" 18 | }, 19 | "funding": [ 20 | { 21 | "type": "opencollective", 22 | "url": "https://opencollective.com/yiisoft" 23 | }, 24 | { 25 | "type": "github", 26 | "url": "https://github.com/sponsors/yiisoft" 27 | } 28 | ], 29 | "require": { 30 | "php": "7.4.* || 8.0 - 8.4" 31 | }, 32 | "require-dev": { 33 | "maglnet/composer-require-checker": "^3.8 || ^4.2", 34 | "phpunit/phpunit": "^9.6.22", 35 | "rector/rector": "^2.0.10", 36 | "roave/infection-static-analysis-plugin": "^1.18", 37 | "spatie/phpunit-watcher": "^1.23.6", 38 | "vimeo/psalm": "^4.30 || ^5.26.1 || ^6.8.8" 39 | }, 40 | "suggest": { 41 | "yiisoft/mutex-file": "Use the File driver for mutex", 42 | "yiisoft/mutex-pdo-mysql": "Use the MySQL PDO driver for mutex", 43 | "yiisoft/mutex-pdo-oracle": "Use the Oracle PDO driver for mutex", 44 | "yiisoft/mutex-pdo-pgsql": "Use the Postgres PDO driver for mutex", 45 | "yiisoft/mutex-redis": "Use the Redis driver for mutex" 46 | }, 47 | "autoload": { 48 | "psr-4": { 49 | "Yiisoft\\Mutex\\": "src" 50 | } 51 | }, 52 | "autoload-dev": { 53 | "psr-4": { 54 | "Yiisoft\\Mutex\\Tests\\": "tests" 55 | } 56 | }, 57 | "config": { 58 | "sort-packages": true, 59 | "bump-after-update": "dev", 60 | "allow-plugins": { 61 | "infection/extension-installer": true, 62 | "composer/package-versions-deprecated": true 63 | } 64 | }, 65 | "scripts": { 66 | "test": "phpunit --testdox --no-interaction", 67 | "test-watch": "phpunit-watcher watch" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPaths([ 11 | __DIR__ . '/src', 12 | __DIR__ . '/tests', 13 | ]) 14 | ->withPhpSets(php74: true) 15 | ->withRules([ 16 | InlineConstructorDefaultToPropertyRector::class, 17 | ]) 18 | ->withSkip([ 19 | ClosureToArrowFunctionRector::class, 20 | __DIR__ . '/tests/SynchronizerTest.php', 21 | ]); 22 | -------------------------------------------------------------------------------- /src/Exception/MutexLockedException.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | private static array $currentProcessLocks = []; 27 | 28 | public function __construct(string $driverName, string $mutexName) 29 | { 30 | $this->lockName = md5($driverName . $mutexName); 31 | $this->mutexName = $mutexName; 32 | } 33 | 34 | final public function __destruct() 35 | { 36 | $this->release(); 37 | } 38 | 39 | final public function acquire(int $timeout = 0): bool 40 | { 41 | return $this->retryAcquire($timeout, function () use ($timeout): bool { 42 | if (!$this->isCurrentProcessLocked() && $this->acquireLock($timeout)) { 43 | return self::$currentProcessLocks[$this->lockName] = true; 44 | } 45 | 46 | return false; 47 | }); 48 | } 49 | 50 | final public function release(): void 51 | { 52 | if (!$this->isCurrentProcessLocked()) { 53 | return; 54 | } 55 | 56 | if (!$this->releaseLock()) { 57 | throw new MutexReleaseException("Unable to release the \"$this->mutexName\" mutex."); 58 | } 59 | 60 | unset(self::$currentProcessLocks[$this->lockName]); 61 | } 62 | 63 | /** 64 | * Acquires lock. 65 | * 66 | * This method should be extended by a concrete Mutex implementations. 67 | * 68 | * @param int $timeout Time (in seconds) to wait for lock to be released. Defaults to zero meaning that method 69 | * will return false immediately in case lock was already acquired. 70 | * 71 | * @return bool The acquiring result. 72 | */ 73 | abstract protected function acquireLock(int $timeout = 0): bool; 74 | 75 | /** 76 | * Releases lock. 77 | * 78 | * This method should be extended by a concrete Mutex implementations. 79 | * 80 | * @return bool The release result. 81 | */ 82 | abstract protected function releaseLock(): bool; 83 | 84 | /** 85 | * Checks whether a lock has been set in the current process. 86 | * 87 | * @return bool Whether a lock has been set in the current process. 88 | */ 89 | private function isCurrentProcessLocked(): bool 90 | { 91 | return isset(self::$currentProcessLocks[$this->lockName]); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/MutexFactory.php: -------------------------------------------------------------------------------- 1 | create($name); 19 | 20 | if (!$mutex->acquire($timeout)) { 21 | throw new MutexLockedException("Unable to acquire the \"$name\" mutex."); 22 | } 23 | 24 | return $mutex; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/MutexFactoryInterface.php: -------------------------------------------------------------------------------- 1 | acquire(1000)) { 19 | * throw new \Yiisoft\Mutex\Exception\MutexLockedException('Unable to acquire the "critical_logic" mutex.'); 20 | * } 21 | * 22 | * // ... 23 | * // business logic execution 24 | * // ... 25 | * 26 | * $mutex->release(); 27 | * ``` 28 | */ 29 | interface MutexInterface 30 | { 31 | /** 32 | * Acquires a lock. 33 | * 34 | * @param int $timeout Time (in seconds) to wait for lock to be released. Defaults to zero meaning that method 35 | * will return false immediately in case lock was already acquired. 36 | * 37 | * @return bool Whether a lock is acquired. 38 | */ 39 | public function acquire(int $timeout = 0): bool; 40 | 41 | /** 42 | * Releases a lock. 43 | */ 44 | public function release(): void; 45 | } 46 | -------------------------------------------------------------------------------- /src/RetryAcquireTrait.php: -------------------------------------------------------------------------------- 1 | retryDelay = $retryDelay; 40 | return $new; 41 | } 42 | 43 | private function retryAcquire(int $timeout, callable $callback): bool 44 | { 45 | $start = microtime(true); 46 | 47 | do { 48 | if ($callback()) { 49 | return true; 50 | } 51 | usleep($this->retryDelay * 1000); 52 | } while (microtime(true) - $start < $timeout); 53 | 54 | return false; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/SimpleMutex.php: -------------------------------------------------------------------------------- 1 | acquire('critical_logic', 1000)) { 14 | * throw new \Yiisoft\Mutex\Exception\MutexLockedException('Unable to acquire the "critical_logic" mutex.'); 15 | * } 16 | * 17 | * // ... 18 | * // business logic execution 19 | * // ... 20 | * 21 | * $mutex->release(); 22 | * ``` 23 | */ 24 | final class SimpleMutex 25 | { 26 | private MutexFactoryInterface $mutexFactory; 27 | 28 | /** 29 | * @var MutexInterface[] 30 | */ 31 | private array $acquired = []; 32 | 33 | public function __construct(MutexFactoryInterface $mutexFactory) 34 | { 35 | $this->mutexFactory = $mutexFactory; 36 | } 37 | 38 | /** 39 | * Acquires a lock with a given name. 40 | * 41 | * @param string $name Name of the mutex to acquire. 42 | * @param int $timeout Time (in seconds) to wait for lock to be released. Defaults to zero meaning that method 43 | * will return false immediately in case lock was already acquired. 44 | * 45 | * @return bool Whether a lock is acquired. 46 | */ 47 | public function acquire(string $name, int $timeout = 0): bool 48 | { 49 | $mutex = $this->mutexFactory->create($name); 50 | 51 | if ($mutex->acquire($timeout)) { 52 | $this->acquired[$name] = $mutex; 53 | return true; 54 | } 55 | 56 | return false; 57 | } 58 | 59 | /** 60 | * Releases a lock with a given name. 61 | */ 62 | public function release(string $name): void 63 | { 64 | if (!isset($this->acquired[$name])) { 65 | return; 66 | } 67 | 68 | $this->acquired[$name]->release(); 69 | unset($this->acquired[$name]); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Synchronizer.php: -------------------------------------------------------------------------------- 1 | execute('critical_logic', function () { 22 | * return $counter->increase(); 23 | * }, 10); 24 | * ``` 25 | */ 26 | final class Synchronizer 27 | { 28 | private MutexFactoryInterface $mutexFactory; 29 | 30 | public function __construct(MutexFactoryInterface $mutexFactory) 31 | { 32 | $this->mutexFactory = $mutexFactory; 33 | } 34 | 35 | /** 36 | * Executes a PHP callable with a lock and returns the result. 37 | * 38 | * @param string $name Name of the mutex to acquire. 39 | * @param callable $callback PHP callable to execution. 40 | * @param int $timeout Time (in seconds) to wait for lock to be released. Defaults to zero meaning that 41 | * method {@see MutexInterface::acquire()} will return false immediately in case lock was already acquired. 42 | * 43 | * @throws MutexLockedException If unable to acquire lock. 44 | * @throws Throwable If an error occurred during the execution of the PHP callable. 45 | * 46 | * @return mixed The result of the PHP callable execution. 47 | */ 48 | public function execute(string $name, callable $callback, int $timeout = 0) 49 | { 50 | $mutex = $this->mutexFactory->createAndAcquire($name, $timeout); 51 | 52 | set_error_handler(static function (int $severity, string $message, string $file, int $line): bool { 53 | if (!(error_reporting() & $severity)) { 54 | // This error code is not included in error_reporting. 55 | return true; 56 | } 57 | 58 | throw new ErrorException($message, $severity, $severity, $file, $line); 59 | }); 60 | 61 | try { 62 | /** @var mixed $result */ 63 | return $callback(); 64 | } finally { 65 | restore_error_handler(); 66 | $mutex->release(); 67 | } 68 | } 69 | } 70 | --------------------------------------------------------------------------------