| <?php |
|
|
| |
| |
| |
| |
| |
| |
|
|
| namespace Piwik\Concurrency; |
|
|
| use Piwik\Common; |
| use Piwik\Date; |
|
|
| class Lock |
| { |
| public const MAX_KEY_LEN = 70; |
| public const DEFAULT_TTL = 60; |
|
|
| |
| |
| |
| private $backend; |
|
|
| private $namespace; |
|
|
| private $lockKey = null; |
| private $lockValue = null; |
| private $defaultTtl = null; |
| private $lastAcquireTime = null; |
|
|
| |
| |
| |
| |
| public function __construct(LockBackend $backend, $namespace, $defaultTtl = null) |
| { |
| if (mb_strlen($namespace) > self::MAX_KEY_LEN - 32) { |
| |
| 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; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function reexpireLock(): bool |
| { |
| return $this->reacquireLock(); |
| } |
|
|
| |
| |
| |
| 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()); |
| } |
|
|
| |
| |
| |
| |
| |
| public function getAllAcquiredLockKeys(): array |
| { |
| return $this->backend->getKeysMatchingPattern($this->namespace . '*'); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| public function execute($id, $callback) |
| { |
| $i = 0; |
| while (!$this->acquireLock($id)) { |
| $i++; |
| usleep(100 * 1000); |
| if ($i > 50) { |
| throw new \Exception('Could not get the lock for ID: ' . $id); |
| } |
| }; |
| try { |
| return $callback(); |
| } finally { |
| $this->unlock(); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| public function acquireLock($id, $ttlInSeconds = 60) |
| { |
| $this->lockKey = $this->namespace . $id; |
|
|
| if (mb_strlen($this->lockKey) > self::MAX_KEY_LEN) { |
| |
| |
| $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; |
| } |
|
|
| |
| |
| |
| public function isLocked(): bool |
| { |
| if (!$this->lockValue) { |
| return false; |
| } |
|
|
| return $this->lockValue === $this->backend->get($this->lockKey); |
| } |
|
|
| |
| |
| |
| public function unlock(): void |
| { |
| if ($this->lockValue) { |
| $this->backend->deleteIfKeyHasValue($this->lockKey, $this->lockValue); |
| $this->lockValue = null; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| 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; |
| } |
| } |
|
|