Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions deptrac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ deptrac:
collectors:
- type: classNameRegex
value: '/^CodeIgniter\\Language\\.*$/'
- name: Lock
collectors:
- type: classNameRegex
value: '/^CodeIgniter\\Lock\\.*$/'
- name: Log
collectors:
- type: classNameRegex
Expand Down Expand Up @@ -170,6 +174,7 @@ deptrac:
- URI
Cache:
- I18n
- Lock
Controller:
- HTTP
- Validation
Expand Down Expand Up @@ -207,6 +212,8 @@ deptrac:
Images:
- Files
- I18n
Lock:
- Cache
Model:
- Database
- DataCaster
Expand Down
163 changes: 162 additions & 1 deletion system/Cache/Handlers/FileHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use CodeIgniter\Cache\Exceptions\CacheException;
use CodeIgniter\I18n\Time;
use CodeIgniter\Lock\LockStoreInterface;
use Config\Cache;
use Throwable;

Expand All @@ -23,7 +24,7 @@
*
* @see \CodeIgniter\Cache\Handlers\FileHandlerTest
*/
class FileHandler extends BaseHandler
class FileHandler extends BaseHandler implements LockStoreInterface
{
/**
* Maximum key length.
Expand Down Expand Up @@ -155,6 +156,78 @@ public function decrement(string $key, int $offset = 1): bool|int
return $this->increment($key, -$offset);
}

public function acquireLock(string $key, string $owner, int $ttl): bool
{
return $this->withLockFile($key, static function ($handle) use ($owner, $ttl): bool {
$data = self::readLockData($handle);
$now = Time::now()->getTimestamp();

if ($data !== null && $data['expires'] > $now) {
return false;
}

return self::writeLockData($handle, $owner, $now + $ttl);
});
}

public function releaseLock(string $key, string $owner): bool
{
return $this->withLockFile($key, static function ($handle) use ($owner): bool {
$data = self::readLockData($handle);

if ($data === null || $data['owner'] !== $owner) {
return false;
}

return self::clearLockFile($handle);
});
}

public function forceReleaseLock(string $key): bool
{
return ! is_file($this->path . static::validateKey($key, $this->prefix))
|| $this->withLockFile($key, static fn ($handle): bool => self::clearLockFile($handle), false);
}

public function refreshLock(string $key, string $owner, int $ttl): bool
{
return $this->withLockFile($key, static function ($handle) use ($owner, $ttl): bool {
$data = self::readLockData($handle);
$now = Time::now()->getTimestamp();

if ($data === null || $data['owner'] !== $owner || $data['expires'] <= $now) {
return false;
}

return self::writeLockData($handle, $owner, $now + $ttl);
});
}

public function getLockOwner(string $key): ?string
{
$owner = null;

$this->withLockFile($key, static function ($handle) use (&$owner): bool {
$data = self::readLockData($handle);

if ($data === null) {
return true;
}

if ($data['expires'] <= Time::now()->getTimestamp()) {
self::clearLockFile($handle);

return true;
}

$owner = $data['owner'];

return true;
}, false);

return $owner;
}

public function clean(): bool
{
return delete_files($this->path, false, true);
Expand Down Expand Up @@ -229,4 +302,92 @@ protected function getItem(string $filename): array|false

return $data;
}

/**
* @param callable(resource): bool $callback
*/
private function withLockFile(string $key, callable $callback, bool $create = true): bool
{
$key = static::validateKey($key, $this->prefix);
$handle = @fopen($this->path . $key, $create ? 'c+b' : 'r+b');

if ($handle === false) {
return false;
}

try {
if (! flock($handle, LOCK_EX)) {
return false;
}

return $callback($handle);
} finally {
flock($handle, LOCK_UN);
fclose($handle);

if (is_file($this->path . $key)) {
try {
chmod($this->path . $key, $this->mode);
} catch (Throwable $e) {
log_message('debug', 'Failed to set mode on cache lock file: ' . $e);
}
}
}
}

/**
* @param resource $handle
*
* @return array{owner: string, expires: int}|null
*/
private static function readLockData($handle): ?array
{
rewind($handle);

$content = stream_get_contents($handle);

if ($content === false || $content === '') {
return null;
}

try {
$data = unserialize($content);
} catch (Throwable) {
return null;
}

if (! is_array($data) || ! isset($data['owner'], $data['expires']) || ! is_string($data['owner']) || ! is_int($data['expires'])) {
return null;
}

return $data;
}

/**
* @param resource $handle
*/
private static function writeLockData($handle, string $owner, int $expires): bool
{
rewind($handle);

if (! ftruncate($handle, 0)) {
return false;
}

if (fwrite($handle, serialize(['owner' => $owner, 'expires' => $expires])) === false) {
return false;
}

return fflush($handle);
}

/**
* @param resource $handle
*/
private static function clearLockFile($handle): bool
{
rewind($handle);

return ftruncate($handle, 0) && fflush($handle);
}
}
57 changes: 56 additions & 1 deletion system/Cache/Handlers/PredisHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use CodeIgniter\Exceptions\CriticalError;
use CodeIgniter\I18n\Time;
use CodeIgniter\Lock\LockStoreInterface;
use Config\Cache;
use Exception;
use Predis\Client;
Expand All @@ -26,7 +27,7 @@
*
* @see \CodeIgniter\Cache\Handlers\PredisHandlerTest
*/
class PredisHandler extends BaseHandler
class PredisHandler extends BaseHandler implements LockStoreInterface
{
/**
* Default config
Expand Down Expand Up @@ -167,6 +168,60 @@ public function decrement(string $key, int $offset = 1): int
return $this->redis->hincrby($key, 'data', -$offset);
}

public function acquireLock(string $key, string $owner, int $ttl): bool
{
$key = static::validateKey($key);
$result = $this->redis->set($key, $owner, 'EX', $ttl, 'NX');

return $result instanceof Status && $result->getPayload() === 'OK';
}

public function releaseLock(string $key, string $owner): bool
{
$key = static::validateKey($key);

$script = <<<'LUA'
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
end

return 0
LUA;

return $this->redis->eval($script, 1, $key, $owner) === 1;
}

public function forceReleaseLock(string $key): bool
{
$key = static::validateKey($key);
$deleted = $this->redis->del($key);

return is_int($deleted) && $deleted >= 0;
}

public function refreshLock(string $key, string $owner, int $ttl): bool
{
$key = static::validateKey($key);

$script = <<<'LUA'
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("expire", KEYS[1], ARGV[2])
end

return 0
LUA;

return $this->redis->eval($script, 1, $key, $owner, (string) $ttl) === 1;
}

public function getLockOwner(string $key): ?string
{
$key = static::validateKey($key);
$owner = $this->redis->get($key);

return is_string($owner) ? $owner : null;
}

public function clean(): bool
{
return $this->redis->flushdb()->getPayload() === 'OK';
Expand Down
56 changes: 55 additions & 1 deletion system/Cache/Handlers/RedisHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use CodeIgniter\Exceptions\CriticalError;
use CodeIgniter\I18n\Time;
use CodeIgniter\Lock\LockStoreInterface;
use Config\Cache;
use Redis;
use RedisException;
Expand All @@ -24,7 +25,7 @@
*
* @see \CodeIgniter\Cache\Handlers\RedisHandlerTest
*/
class RedisHandler extends BaseHandler
class RedisHandler extends BaseHandler implements LockStoreInterface
{
/**
* Default config
Expand Down Expand Up @@ -185,6 +186,59 @@ public function decrement(string $key, int $offset = 1): int
return $this->increment($key, -$offset);
}

public function acquireLock(string $key, string $owner, int $ttl): bool
{
$key = static::validateKey($key, $this->prefix);

return (bool) $this->redis->set($key, $owner, ['nx', 'ex' => $ttl]);
}

public function releaseLock(string $key, string $owner): bool
{
$key = static::validateKey($key, $this->prefix);

$script = <<<'LUA'
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
end

return 0
LUA;

return (int) $this->redis->eval($script, [$key, $owner], 1) === 1;
}

public function forceReleaseLock(string $key): bool
{
$key = static::validateKey($key, $this->prefix);
$deleted = $this->redis->del($key);

return is_int($deleted) && $deleted >= 0;
}

public function refreshLock(string $key, string $owner, int $ttl): bool
{
$key = static::validateKey($key, $this->prefix);

$script = <<<'LUA'
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("expire", KEYS[1], ARGV[2])
end

return 0
LUA;

return (int) $this->redis->eval($script, [$key, $owner, $ttl], 1) === 1;
}

public function getLockOwner(string $key): ?string
{
$key = static::validateKey($key, $this->prefix);
$owner = $this->redis->get($key);

return is_string($owner) ? $owner : null;
}

public function clean(): bool
{
return $this->redis->flushDB();
Expand Down
2 changes: 2 additions & 0 deletions system/Config/BaseService.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
use CodeIgniter\HTTP\URI;
use CodeIgniter\Images\Handlers\BaseHandler;
use CodeIgniter\Language\Language;
use CodeIgniter\Lock\LockManager;
use CodeIgniter\Log\Logger;
use CodeIgniter\Pager\Pager;
use CodeIgniter\Router\RouteCollection;
Expand Down Expand Up @@ -119,6 +120,7 @@
* @method static IncomingRequest incomingrequest(?App $config = null, bool $getShared = true)
* @method static Iterator iterator($getShared = true)
* @method static Language language($locale = null, $getShared = true)
* @method static LockManager locks(?CacheInterface $cache = null, bool $getShared = true)
* @method static Logger logger($getShared = true)
* @method static MigrationRunner migrations(Migrations $config = null, ConnectionInterface $db = null, $getShared = true)
* @method static Negotiate negotiator(RequestInterface $request = null, $getShared = true)
Expand Down
Loading
Loading