Skip to content

Commit f8b955c

Browse files
committed
MAGETWO-98151: Add support ZooKeeper and flock locks
1 parent 251ce3c commit f8b955c

File tree

7 files changed

+342
-2
lines changed

7 files changed

+342
-2
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Framework\Lock\Backend;
9+
10+
/**
11+
* \Magento\Framework\Lock\Backend\Database test case
12+
*/
13+
class FileTest extends \PHPUnit\Framework\TestCase
14+
{
15+
/**
16+
* @var \Magento\Framework\Lock\Backend\File
17+
*/
18+
private $model;
19+
20+
/**
21+
* @var \Magento\Framework\ObjectManagerInterface
22+
*/
23+
private $objectManager;
24+
25+
protected function setUp()
26+
{
27+
$this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
28+
$this->model = $this->objectManager->create(\Magento\Framework\Lock\Backend\File::class, ['path' => '/tmp']);
29+
}
30+
31+
public function testLockAndUnlock()
32+
{
33+
$name = 'test_lock';
34+
35+
$this->assertFalse($this->model->isLocked($name));
36+
37+
$this->assertTrue($this->model->lock($name));
38+
$this->assertTrue($this->model->isLocked($name));
39+
$this->assertFalse($this->model->lock($name, 2));
40+
41+
$this->assertTrue($this->model->unlock($name));
42+
$this->assertFalse($this->model->isLocked($name));
43+
}
44+
45+
public function testUnlockWithoutExistingLock()
46+
{
47+
$name = 'test_lock';
48+
49+
$this->assertFalse($this->model->isLocked($name));
50+
$this->assertFalse($this->model->unlock($name));
51+
}
52+
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Framework\Lock\Backend;
9+
10+
use Magento\Framework\Lock\LockManagerInterface;
11+
use Magento\Framework\Filesystem\Driver\File as FileDriver;
12+
use Magento\Framework\Exception\RuntimeException;
13+
use Magento\Framework\Exception\FileSystemException;
14+
use Magento\Framework\Phrase;
15+
16+
/**
17+
* LockManager using the file system for locks
18+
*/
19+
class File implements LockManagerInterface
20+
{
21+
/**
22+
* The file driver instance
23+
*
24+
* @var FileDriver
25+
*/
26+
private $fileDriver;
27+
28+
/**
29+
* The path to the locks storage folder
30+
*
31+
* @var string
32+
*/
33+
private $path;
34+
35+
/**
36+
* How many microseconds to wait before re-try to acquire a lock
37+
*
38+
* @var int
39+
*/
40+
private $sleepCycle = 100000;
41+
42+
/**
43+
* The mapping list of the path lock with the file resource
44+
*
45+
* @var array
46+
*/
47+
private $locks = [];
48+
49+
/**
50+
* @param FileDriver $fileDriver The file driver
51+
* @param string $path The path to the locks storage folder
52+
* @throws RuntimeException Throws RuntimeException if $path is empty
53+
* or cannot create the directory for locks
54+
*/
55+
public function __construct(FileDriver $fileDriver, string $path)
56+
{
57+
if (!$path) {
58+
throw new RuntimeException(new Phrase('The path needs to be a non-empty string.'));
59+
}
60+
61+
$this->fileDriver = $fileDriver;
62+
$this->path = preg_replace('#\/*$#', '', $path) ?: '/';
63+
64+
try {
65+
if (!$this->fileDriver->isExists($this->path)) {
66+
$this->fileDriver->createDirectory($this->path);
67+
}
68+
} catch (FileSystemException $exception) {
69+
throw new RuntimeException(
70+
new Phrase('Cannot create the directory for locks: %1', [$this->path]),
71+
$exception
72+
);
73+
}
74+
}
75+
76+
/**
77+
* Acquires a lock by name
78+
*
79+
* @param string $name The lock name
80+
* @param int $timeout Timeout in seconds. A negative timeout value means infinite timeout
81+
* @return bool Returns true if the lock is acquired, otherwise returns false
82+
* @throws RuntimeException Throws RuntimeException if cannot acquires the lock because FS problems
83+
*/
84+
public function lock(string $name, int $timeout = -1): bool
85+
{
86+
try {
87+
$lockFile = $this->getLockPath($name);
88+
$fileResource = $this->fileDriver->fileOpen($lockFile, 'w+');
89+
$skipDeadline = $timeout < 0;
90+
$deadline = microtime(true) + $timeout;
91+
92+
while (!$this->tryToLock($fileResource)) {
93+
if (!$skipDeadline && $deadline <= microtime(true)) {
94+
$this->fileDriver->fileClose($fileResource);
95+
return false;
96+
}
97+
usleep($this->sleepCycle);
98+
}
99+
} catch (FileSystemException $exception) {
100+
throw new RuntimeException(new Phrase('Cannot acquire a lock.'), $exception);
101+
}
102+
103+
$this->locks[$lockFile] = $fileResource;
104+
return true;
105+
}
106+
107+
/**
108+
* Checks if a lock exists by name
109+
*
110+
* @param string $name The lock name
111+
* @return bool Returns true if the lock exists, otherwise returns false
112+
* @throws RuntimeException Throws RuntimeException if cannot check that the lock exists
113+
*/
114+
public function isLocked(string $name): bool
115+
{
116+
$lockFile = $this->getLockPath($name);
117+
$result = false;
118+
119+
try {
120+
if ($this->fileDriver->isExists($lockFile)) {
121+
$fileResource = $this->fileDriver->fileOpen($lockFile, 'w+');
122+
if ($this->tryToLock($fileResource)) {
123+
$result = false;
124+
} else {
125+
$result = true;
126+
}
127+
$this->fileDriver->fileClose($fileResource);
128+
}
129+
} catch (FileSystemException $exception) {
130+
throw new RuntimeException(new Phrase('Cannot verify that the lock exists.'), $exception);
131+
}
132+
133+
return $result;
134+
}
135+
136+
/**
137+
* Remove the lock by name
138+
*
139+
* @param string $name The lock name
140+
* @return bool If the lock is removed returns true, otherwise returns false
141+
*/
142+
public function unlock(string $name): bool
143+
{
144+
$lockFile = $this->getLockPath($name);
145+
146+
if (isset($this->locks[$lockFile]) && $this->tryToUnlock($this->locks[$lockFile])) {
147+
unset($this->locks[$lockFile]);
148+
return true;
149+
}
150+
151+
return false;
152+
}
153+
154+
/**
155+
* Returns the full path to the lock file by name
156+
*
157+
* @param string $name The lock name
158+
* @return string The path to the lock file
159+
*/
160+
private function getLockPath(string $name): string
161+
{
162+
return $this->path . '/' . $name;
163+
}
164+
165+
/**
166+
* Tries to lock a file resource
167+
*
168+
* @param resource $resource The file resource
169+
* @return bool If the lock is acquired returns true, otherwise returns false
170+
*/
171+
private function tryToLock($resource): bool
172+
{
173+
try {
174+
return $this->fileDriver->fileLock($resource, LOCK_EX | LOCK_NB);
175+
} catch (FileSystemException $exception) {
176+
return false;
177+
}
178+
}
179+
180+
/**
181+
* Tries to unlock a file resource
182+
*
183+
* @param resource $resource The file resource
184+
* @return bool If the lock is removed returns true, otherwise returns false
185+
*/
186+
private function tryToUnlock($resource): bool
187+
{
188+
try {
189+
return $this->fileDriver->fileLock($resource, LOCK_UN | LOCK_NB);
190+
} catch (FileSystemException $exception) {
191+
return false;
192+
}
193+
}
194+
}

lib/internal/Magento/Framework/Lock/Backend/Zookeeper.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class Zookeeper implements LockManagerInterface
5454
/**
5555
* How many microseconds to wait before recheck connections or nodes
5656
*
57-
* @var float
57+
* @var int
5858
*/
5959
private $sleepCycle = 100000;
6060

lib/internal/Magento/Framework/Lock/LockBackendFactory.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Magento\Framework\Lock\Backend\Database as DatabaseLock;
1515
use Magento\Framework\Lock\Backend\Zookeeper as ZookeeperLock;
1616
use Magento\Framework\Lock\Backend\Cache as CacheLock;
17+
use Magento\Framework\Lock\Backend\File as FileLock;
1718

1819
/**
1920
* The factory to create object that implements LockManagerInterface
@@ -55,6 +56,13 @@ class LockBackendFactory
5556
*/
5657
const LOCK_CACHE = 'cache';
5758

59+
/**
60+
* File lock provider name
61+
*
62+
* @const string
63+
*/
64+
const LOCK_FILE = 'file';
65+
5866
/**
5967
* The list of lock providers with mapping on classes
6068
*
@@ -64,6 +72,7 @@ class LockBackendFactory
6472
self::LOCK_DB => DatabaseLock::class,
6573
self::LOCK_ZOOKEEPER => ZookeeperLock::class,
6674
self::LOCK_CACHE => CacheLock::class,
75+
self::LOCK_FILE => FileLock::class,
6776
];
6877

6978
/**

lib/internal/Magento/Framework/Lock/Test/Unit/LockBackendFactoryTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Magento\Framework\Lock\Backend\Database as DatabaseLock;
1111
use Magento\Framework\Lock\Backend\Zookeeper as ZookeeperLock;
1212
use Magento\Framework\Lock\Backend\Cache as CacheLock;
13+
use Magento\Framework\Lock\Backend\File as FileLock;
1314
use Magento\Framework\Lock\LockBackendFactory;
1415
use Magento\Framework\ObjectManagerInterface;
1516
use Magento\Framework\Lock\LockManagerInterface;
@@ -95,6 +96,11 @@ public function createDataProvider(): array
9596
'lockProviderClass' => CacheLock::class,
9697
'config' => [],
9798
],
99+
'file' => [
100+
'lockProvider' => LockBackendFactory::LOCK_FILE,
101+
'lockProviderClass' => FileLock::class,
102+
'config' => ['path' => '/my/path'],
103+
],
98104
];
99105

100106
if (extension_loaded('zookeeper')) {

0 commit comments

Comments
 (0)