Skip to content

Commit 91c96ff

Browse files
rob006samdark
authored andcommitted
Fixes #16839
- Increase frequency of lock tries for `yii\mutex\FileMutex::acquireLock()` when $timeout is provided (rob006) - Add support for `$timeout` in `yii\mutex\PgsqlMutex::acquire()`
1 parent 46c50e4 commit 91c96ff

File tree

7 files changed

+179
-26
lines changed

7 files changed

+179
-26
lines changed

framework/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ Yii Framework 2 Change Log
7777
- Bug #16424: `yii\db\Transaction::begin()` throws now `NotSupportedException` for nested transaction and DBMS not supporting savepoints (bizley)
7878
- Bug #15204: `yii\helpers\BaseInflector::slug()` is not removing substrings matching provided replacement from given string anymore (bizley)
7979
- Bug #16101: Fixed Error Handler to clear registered meta tags, link tags, css/js scripts and files in error view (bizley)
80-
- Bug #16836: Fix `yii\mutex\MysqlMutex` to handle locks with names longer than 64 character (rob006)
80+
- Bug #16836: Fix `yii\mutex\MysqlMutex` to handle locks with names longer than 64 characters (rob006)
81+
- Enh #16839: Increase frequency of lock tries for `yii\mutex\FileMutex::acquireLock()` when $timeout is provided (rob006)
82+
- Enh #16839: Add support for `$timeout` in `yii\mutex\PgsqlMutex::acquire()` (rob006)
8183

8284

8385
2.0.15.1 March 21, 2018

framework/mutex/FileMutex.php

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
*/
4242
class FileMutex extends Mutex
4343
{
44+
use RetryAcquireTrait;
45+
4446
/**
4547
* @var string the directory to store mutex files. You may use [path alias](guide:concept-aliases) here.
4648
* Defaults to the "mutex" subdirectory under the application runtime path.
@@ -99,11 +101,8 @@ public function init()
99101
protected function acquireLock($name, $timeout = 0)
100102
{
101103
$filePath = $this->getLockFilePath($name);
102-
$waitTime = 0;
103-
104-
while (true) {
104+
return $this->retryAcquire($timeout, function () use ($filePath, $name) {
105105
$file = fopen($filePath, 'w+');
106-
107106
if ($file === false) {
108107
return false;
109108
}
@@ -114,13 +113,7 @@ protected function acquireLock($name, $timeout = 0)
114113

115114
if (!flock($file, LOCK_EX | LOCK_NB)) {
116115
fclose($file);
117-
118-
if (++$waitTime > $timeout) {
119-
return false;
120-
}
121-
122-
sleep(1);
123-
continue;
116+
return false;
124117
}
125118

126119
// Under unix we delete the lock file before releasing the related handle. Thus it's possible that we've acquired a lock on
@@ -137,15 +130,12 @@ protected function acquireLock($name, $timeout = 0)
137130
clearstatcache(true, $filePath);
138131
flock($file, LOCK_UN);
139132
fclose($file);
140-
continue;
133+
return false;
141134
}
142135

143136
$this->_files[$name] = $file;
144137
return true;
145-
}
146-
147-
// Should not be reached normally.
148-
return false;
138+
});
149139
}
150140

151141
/**

framework/mutex/PgsqlMutex.php

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
*/
3737
class PgsqlMutex extends DbMutex
3838
{
39+
use RetryAcquireTrait;
40+
3941
/**
4042
* Initializes PgSQL specific mutex component implementation.
4143
* @throws InvalidConfigException if [[db]] is not PgSQL connection.
@@ -67,16 +69,16 @@ private function getKeysFromName($name)
6769
*/
6870
protected function acquireLock($name, $timeout = 0)
6971
{
70-
if ($timeout !== 0) {
71-
throw new InvalidArgumentException('PgsqlMutex does not support timeout.');
72-
}
7372
list($key1, $key2) = $this->getKeysFromName($name);
74-
return $this->db->useMaster(function ($db) use ($key1, $key2) {
75-
/** @var \yii\db\Connection $db */
76-
return (bool) $db->createCommand(
77-
'SELECT pg_try_advisory_lock(:key1, :key2)',
78-
[':key1' => $key1, ':key2' => $key2]
79-
)->queryScalar();
73+
74+
return $this->retryAcquire($timeout, function () use ($key1, $key2) {
75+
return $this->db->useMaster(function ($db) use ($key1, $key2) {
76+
/** @var \yii\db\Connection $db */
77+
return (bool) $db->createCommand(
78+
'SELECT pg_try_advisory_lock(:key1, :key2)',
79+
[':key1' => $key1, ':key2' => $key2]
80+
)->queryScalar();
81+
});
8082
});
8183
}
8284

framework/mutex/RetryAcquireTrait.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
/**
3+
* @link http://www.yiiframework.com/
4+
* @copyright Copyright (c) 2008 Yii Software LLC
5+
* @license http://www.yiiframework.com/license/
6+
*/
7+
8+
namespace yii\mutex;
9+
10+
use Closure;
11+
12+
/**
13+
* Trait RetryAcquireTrait.
14+
*
15+
* @author Robert Korulczyk <[email protected]>
16+
* @internal
17+
*/
18+
trait RetryAcquireTrait
19+
{
20+
/**
21+
* @var int Number of milliseconds between each try in [[acquire()]] until specified timeout times out.
22+
* By default it is 50 milliseconds - it means that [[acquire()]] may try acquire lock up to 20 times per second.
23+
* @since 2.0.16
24+
*/
25+
public $retryDelay = 50;
26+
27+
28+
private function retryAcquire($timeout, Closure $callback)
29+
{
30+
$start = microtime(true);
31+
do {
32+
if ($callback()) {
33+
return true;
34+
}
35+
usleep($this->retryDelay * 1000);
36+
} while (microtime(true) - $start < $timeout);
37+
38+
return false;
39+
}
40+
}

tests/framework/mutex/MutexTestTrait.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,21 @@ public function testThatMutexLockIsWorking($mutexName)
5252
$this->assertTrue($mutexTwo->acquire($mutexName));
5353
}
5454

55+
public function testTimeout()
56+
{
57+
$mutexName = __FUNCTION__;
58+
$mutexOne = $this->createMutex();
59+
$mutexTwo = $this->createMutex();
60+
61+
$this->assertTrue($mutexOne->acquire($mutexName));
62+
$microtime = microtime(true);
63+
$this->assertFalse($mutexTwo->acquire($mutexName, 1));
64+
$diff = microtime(true) - $microtime;
65+
$this->assertTrue($diff >= 1 && $diff < 2);
66+
$this->assertTrue($mutexOne->release($mutexName));
67+
$this->assertFalse($mutexTwo->release($mutexName));
68+
}
69+
5570
public static function mutexDataProvider()
5671
{
5772
$utf = <<<'UTF'
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
/**
3+
* @link http://www.yiiframework.com/
4+
* @copyright Copyright (c) 2008 Yii Software LLC
5+
* @license http://www.yiiframework.com/license/
6+
*/
7+
8+
namespace yiiunit\framework\mutex;
9+
10+
use Yii;
11+
use yii\base\InvalidConfigException;
12+
use yiiunit\framework\mutex\mocks\DumbMutex;
13+
use yiiunit\TestCase;
14+
15+
/**
16+
* Class RetryAcquireTraitTest.
17+
*
18+
* @group mutex
19+
*
20+
* @author Robert Korulczyk <[email protected]>
21+
*/
22+
class RetryAcquireTraitTest extends TestCase
23+
{
24+
/**
25+
* @throws InvalidConfigException
26+
*/
27+
public function testRetryAcquire()
28+
{
29+
$mutexName = __FUNCTION__;
30+
$mutexOne = $this->createMutex();
31+
$mutexTwo = $this->createMutex();
32+
33+
$this->assertTrue($mutexOne->acquire($mutexName));
34+
$this->assertFalse($mutexTwo->acquire($mutexName, 1));
35+
36+
$this->assertSame(20, $mutexTwo->attemptsCounter);
37+
}
38+
39+
/**
40+
* @return DumbMutex
41+
* @throws InvalidConfigException
42+
*/
43+
private function createMutex()
44+
{
45+
return Yii::createObject([
46+
'class' => DumbMutex::className(),
47+
]);
48+
}
49+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
/**
3+
* @link http://www.yiiframework.com/
4+
* @copyright Copyright (c) 2008 Yii Software LLC
5+
* @license http://www.yiiframework.com/license/
6+
*/
7+
8+
namespace yiiunit\framework\mutex\mocks;
9+
10+
use yii\mutex\Mutex;
11+
use yii\mutex\RetryAcquireTrait;
12+
13+
/**
14+
* Class DumbMutex.
15+
*
16+
* @author Robert Korulczyk <[email protected]>
17+
*/
18+
class DumbMutex extends Mutex
19+
{
20+
use RetryAcquireTrait;
21+
22+
public $attemptsCounter = 0;
23+
public static $locked = false;
24+
25+
/**
26+
* {@inheritdoc}
27+
*/
28+
protected function acquireLock($name, $timeout = 0)
29+
{
30+
return $this->retryAcquire($timeout, function () {
31+
$this->attemptsCounter++;
32+
if (!static::$locked) {
33+
static::$locked = true;
34+
35+
return true;
36+
}
37+
38+
return false;
39+
});
40+
}
41+
42+
/**
43+
* {@inheritdoc}
44+
*/
45+
protected function releaseLock($name)
46+
{
47+
if (static::$locked) {
48+
static::$locked = false;
49+
50+
return true;
51+
}
52+
53+
return false;
54+
}
55+
}

0 commit comments

Comments
 (0)