File size: 6,421 Bytes
800aa60 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | <?php
/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Concurrency;
use Piwik\Common;
use Piwik\Date;
class Lock
{
public const MAX_KEY_LEN = 70;
public const DEFAULT_TTL = 60;
/**
* @var LockBackend
*/
private $backend;
private $namespace;
private $lockKey = null;
private $lockValue = null;
private $defaultTtl = null;
private $lastAcquireTime = null;
/**
* @param string $namespace
* @param int|null $defaultTtl defaults to {@link self::DEFAULT_TTL}
*/
public function __construct(LockBackend $backend, $namespace, $defaultTtl = null)
{
if (mb_strlen($namespace) > self::MAX_KEY_LEN - 32) {
// A longer namespace could be cut off for too long ids, so we don't support it
throw new \InvalidArgumentException('Lock namespace must be shorter than ' . (self::MAX_KEY_LEN - 32) . ' chars');
}
$this->backend = $backend;
$this->namespace = $namespace;
$this->lockKey = $this->namespace;
$this->defaultTtl = $defaultTtl ?: self::DEFAULT_TTL;
}
/**
* For BC only
*
* @todo remove in Matomo 6.0
* @deprecated use {@link reacquireLock()} instead.
*/
public function reexpireLock(): bool
{
return $this->reacquireLock();
}
/**
* Reacquires the current lock. The TTL will be extended if 1/4 of the TTL already passed by.
*/
public function reacquireLock(): bool
{
$timeBetweenReexpires = $this->defaultTtl - ($this->defaultTtl / 4);
$now = Date::getNowTimestamp();
if (
!empty($this->lastAcquireTime) &&
$now <= $this->lastAcquireTime + $timeBetweenReexpires
) {
return false;
}
return $this->expireLock($this->defaultTtl);
}
public function getNumberOfAcquiredLocks(): int
{
return count($this->getAllAcquiredLockKeys());
}
/**
* Returns all acquired lock keys for the iniatially set namespace.
*
* @return string[]
*/
public function getAllAcquiredLockKeys(): array
{
return $this->backend->getKeysMatchingPattern($this->namespace . '*');
}
/**
* Executes and returns the result of the provided callback if a lock with given id can be acquired
* The method will automatically retry to acquire the lock for up to 5 seconds.
*
* @param string $id
* @param callable $callback
* @return mixed
*/
public function execute($id, $callback)
{
$i = 0;
while (!$this->acquireLock($id)) {
$i++;
usleep(100 * 1000); // 100ms
if ($i > 50) { // give up after 5seconds (50 * 100ms)
throw new \Exception('Could not get the lock for ID: ' . $id);
}
};
try {
return $callback();
} finally {
$this->unlock();
}
}
/**
* Acquires a lock with the given id using the provided TTL
*
* @param string $id
* @param int $ttlInSeconds
* @return bool
*/
public function acquireLock($id, $ttlInSeconds = 60)
{
$this->lockKey = $this->namespace . $id;
if (mb_strlen($this->lockKey) > self::MAX_KEY_LEN) {
// Lock key might be too long for DB column, so we hash it but leave the start of the original as well
// to make it more readable and to ensure the namespaceis still containe
$md5Len = 32;
$this->lockKey = mb_substr($this->lockKey, 0, self::MAX_KEY_LEN - $md5Len - 1) . md5($id);
}
$lockValue = substr(Common::generateUniqId(), 0, 12);
$locked = $this->backend->setIfNotExists($this->lockKey, $lockValue, $ttlInSeconds);
if ($locked) {
$this->lockValue = $lockValue;
$this->lastAcquireTime = Date::getNowTimestamp();
}
return !!$locked;
}
/**
* Return if the acquired lock is currently locked
*/
public function isLocked(): bool
{
if (!$this->lockValue) {
return false;
}
return $this->lockValue === $this->backend->get($this->lockKey);
}
/**
* Releases the acquired lock
*/
public function unlock(): void
{
if ($this->lockValue) {
$this->backend->deleteIfKeyHasValue($this->lockKey, $this->lockValue);
$this->lockValue = null;
}
}
/**
* For BC only
*
* @deprecated use {@link extendLock()} instead.
* @todo remove in Matomo 6.0
*/
public function expireLock($ttlInSeconds): bool
{
return $this->extendLock($ttlInSeconds);
}
public function extendLock($ttlInSeconds): bool
{
if ($ttlInSeconds > 0) {
if ($this->lockValue) {
$success = $this->backend->expireIfKeyHasValue($this->lockKey, $this->lockValue, $ttlInSeconds);
if (!$success) {
$value = $this->backend->get($this->lockKey);
$message = sprintf('Failed to expire key %s (%s / %s).', $this->lockKey, $this->lockValue, (string)$value);
if ($value === false) {
Common::printDebug($message . ' It seems like the key already expired as it no longer exists.');
} elseif (!empty($value) && $value == $this->lockValue) {
Common::printDebug($message . ' We still have the lock but for some reason it did not expire.');
} elseif (!empty($value)) {
Common::printDebug($message . ' This lock has been acquired by another process/server.');
} else {
Common::printDebug($message . ' Failed to expire key.');
}
return false;
}
$this->lastAcquireTime = Date::getNowTimestamp();
return true;
} else {
Common::printDebug('Lock is not acquired, cannot update expiration.');
}
} else {
Common::printDebug('Provided TTL ' . $ttlInSeconds . ' is in valid in Lock::expireLock().');
}
return false;
}
}
|