Your IP : 127.0.0.1
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\Framework\Lock\Backend;
use Magento\Framework\Lock\LockManagerInterface;
use Magento\Framework\Exception\RuntimeException;
use Magento\Framework\Phrase;
/**
* LockManager using the Zookeeper for locks
*/
class Zookeeper implements LockManagerInterface
{
/**
* Zookeeper provider
*
* @var \Zookeeper
*/
private $zookeeper;
/**
* The base path to locks in Zookeeper
*
* @var string
*/
private $path;
/**
* The name of sequence nodes
*
* @var string
*/
private $lockName = 'lock-';
/**
* The host to connect to Zookeeper
*
* @var string
*/
private $host;
/**
* How many seconds to wait before timing out on connections
*
* @var int
*/
private $connectionTimeout = 2;
/**
* How many microseconds to wait before recheck connections or nodes
*
* @var int
*/
private $sleepCycle = 100000;
/**
* The default permissions for Zookeeper nodes
*
* @var array
*/
private $acl = [['perms'=>\Zookeeper::PERM_ALL, 'scheme' => 'world', 'id' => 'anyone']];
/**
* The mapping list of the lock name with the full lock path
*
* @var array
*/
private $locks = [];
/**
* The default path to storage locks
*/
const DEFAULT_PATH = '/magento/locks';
/**
* @param string $host The host to connect to Zookeeper
* @param string $path The base path to locks in Zookeeper
* @throws RuntimeException
*/
public function __construct(string $host, string $path = self::DEFAULT_PATH)
{
if (!$path) {
throw new RuntimeException(
new Phrase('The path needs to be a non-empty string.')
);
}
if (!$host) {
throw new RuntimeException(
new Phrase('The host needs to be a non-empty string.')
);
}
$this->host = $host;
$this->path = rtrim($path, '/') . '/';
}
/**
* @inheritdoc
*
* You can see the lock algorithm by the link
* @link https://zookeeper.apache.org/doc/r3.1.2/recipes.html#sc_recipes_Locks
*
* @throws RuntimeException
*/
public function lock(string $name, int $timeout = -1): bool
{
$skipDeadline = $timeout < 0;
$lockPath = $this->getFullPathToLock($name);
$deadline = microtime(true) + $timeout;
if (!$this->checkAndCreateParentNode($lockPath)) {
throw new RuntimeException(new Phrase('Failed creating the path %1', [$lockPath]));
}
$lockKey = $this->getProvider()
->create($lockPath, '1', $this->acl, \Zookeeper::EPHEMERAL | \Zookeeper::SEQUENCE);
if (!$lockKey) {
throw new RuntimeException(new Phrase('Failed creating lock %1', [$lockPath]));
}
while ($this->isAnyLock($lockKey, $this->getIndex($lockKey))) {
if (!$skipDeadline && $deadline <= microtime(true)) {
$this->getProvider()->delete($lockKey);
return false;
}
usleep($this->sleepCycle);
}
$this->locks[$name] = $lockKey;
return true;
}
/**
* @inheritdoc
*
* @throws RuntimeException
*/
public function unlock(string $name): bool
{
if (!isset($this->locks[$name])) {
return false;
}
return $this->getProvider()->delete($this->locks[$name]);
}
/**
* @inheritdoc
*
* @throws RuntimeException
*/
public function isLocked(string $name): bool
{
return $this->isAnyLock($this->getFullPathToLock($name));
}
/**
* Gets full path to lock by its name
*
* @param string $name
* @return string
*/
private function getFullPathToLock(string $name): string
{
return $this->path . $name . '/' . $this->lockName;
}
/**
* Initiolizes and returns Zookeeper provider
*
* @return \Zookeeper
* @throws RuntimeException
*/
private function getProvider(): \Zookeeper
{
if (!$this->zookeeper) {
$this->zookeeper = new \Zookeeper($this->host);
}
$deadline = microtime(true) + $this->connectionTimeout;
while ($this->zookeeper->getState() != \Zookeeper::CONNECTED_STATE) {
if ($deadline <= microtime(true)) {
throw new RuntimeException(new Phrase('Zookeeper connection timed out!'));
}
usleep($this->sleepCycle);
}
return $this->zookeeper;
}
/**
* Checks and creates base path recursively
*
* @param string $path
* @return bool
* @throws RuntimeException
*/
private function checkAndCreateParentNode(string $path): bool
{
$path = dirname($path);
if ($this->getProvider()->exists($path)) {
return true;
}
if (!$this->checkAndCreateParentNode($path)) {
return false;
}
if ($this->getProvider()->create($path, '1', $this->acl)) {
return true;
}
return $this->getProvider()->exists($path);
}
/**
* Gets int increment of lock key
*
* @param string $key
* @return int|null
*/
private function getIndex(string $key)
{
if (!preg_match('/' . $this->lockName . '([0-9]+)$/', $key, $matches)) {
return null;
}
return intval($matches[1]);
}
/**
* Checks if there is any sequence node under parent of $fullKey.
*
* At first checks that the $fullKey node is present, if not - returns false.
* If $indexKey is non-null and there is a smaller index than $indexKey then returns true,
* otherwise returns false.
*
* @param string $fullKey The full path without any sequence info
* @param int|null $indexKey The index to compare
* @return bool
* @throws RuntimeException
*/
private function isAnyLock(string $fullKey, int $indexKey = null): bool
{
$parent = dirname($fullKey);
if (!$this->getProvider()->exists($parent)) {
return false;
}
$children = $this->getProvider()->getChildren($parent);
if (null === $indexKey && !empty($children)) {
return true;
}
foreach ($children as $childKey) {
$childIndex = $this->getIndex($childKey);
if (null === $childIndex) {
continue;
}
if ($childIndex < $indexKey) {
return true;
}
}
return false;
}
}