├── 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 |
4 |
5 |
Yii Mutex
6 |
7 |
8 |
9 | [](https://packagist.org/packages/yiisoft/mutex)
10 | [](https://packagist.org/packages/yiisoft/mutex)
11 | [](https://github.com/yiisoft/mutex/actions/workflows/build.yml)
12 | [](https://codecov.io/gh/yiisoft/mutex)
13 | [](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/mutex/master)
14 | [](https://github.com/yiisoft/mutex/actions/workflows/static.yml?query=branch%3Amaster)
15 | [](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 | [](https://opencollective.com/yiisoft)
133 |
134 | ## Follow updates
135 |
136 | [](https://www.yiiframework.com/)
137 | [](https://twitter.com/yiiframework)
138 | [](https://t.me/yii3en)
139 | [](https://www.facebook.com/groups/yiitalk)
140 | [](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 |
--------------------------------------------------------------------------------