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
6 changes: 6 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 @@ -207,6 +211,8 @@ deptrac:
Images:
- Files
- I18n
Lock:
- Cache
Comment thread
memleakd marked this conversation as resolved.
Model:
- Database
- DataCaster
Expand Down
12 changes: 11 additions & 1 deletion system/Cache/Handlers/FileHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
namespace CodeIgniter\Cache\Handlers;

use CodeIgniter\Cache\Exceptions\CacheException;
use CodeIgniter\Cache\LockStoreInterface;
use CodeIgniter\Cache\LockStoreProvider;
use CodeIgniter\Cache\LockStores\FileLockStore;
use CodeIgniter\I18n\Time;
use Config\Cache;
use Throwable;
Expand All @@ -23,7 +26,7 @@
*
* @see \CodeIgniter\Cache\Handlers\FileHandlerTest
*/
class FileHandler extends BaseHandler
class FileHandler extends BaseHandler implements LockStoreProvider
{
/**
* Maximum key length.
Expand All @@ -47,6 +50,8 @@ class FileHandler extends BaseHandler
*/
protected $mode;

private ?LockStoreInterface $lockStore = null;

/**
* Note: Use `CacheFactory::getHandler()` to instantiate.
*
Expand Down Expand Up @@ -185,6 +190,11 @@ public function isSupported(): bool
return is_writable($this->path);
}

public function lockStore(): LockStoreInterface
{
return $this->lockStore ??= new FileLockStore($this->path, $this->mode, $this->prefix);
}

/**
* Does the heavy lifting of actually retrieving the file and
* verifying its age.
Expand Down
15 changes: 13 additions & 2 deletions system/Cache/Handlers/PredisHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

namespace CodeIgniter\Cache\Handlers;

use CodeIgniter\Cache\LockStoreInterface;
use CodeIgniter\Cache\LockStoreProvider;
use CodeIgniter\Cache\LockStores\PredisLockStore;
use CodeIgniter\Exceptions\CriticalError;
use CodeIgniter\I18n\Time;
use Config\Cache;
Expand All @@ -26,7 +29,7 @@
*
* @see \CodeIgniter\Cache\Handlers\PredisHandlerTest
*/
class PredisHandler extends BaseHandler
class PredisHandler extends BaseHandler implements LockStoreProvider
{
/**
* Default config
Expand Down Expand Up @@ -58,6 +61,8 @@ class PredisHandler extends BaseHandler
*/
protected $redis;

private ?LockStoreInterface $lockStore = null;

/**
* Note: Use `CacheFactory::getHandler()` to instantiate.
*/
Expand All @@ -71,7 +76,8 @@ public function __construct(Cache $config)
public function initialize(): void
{
try {
$this->redis = new Client($this->config, ['prefix' => $this->prefix]);
$this->redis = new Client($this->config, ['prefix' => $this->prefix]);
$this->lockStore = null;
$this->redis->time();
} catch (Exception $e) {
throw new CriticalError('Cache: Predis connection refused (' . $e->getMessage() . ').', $e->getCode(), $e);
Expand Down Expand Up @@ -202,6 +208,11 @@ public function isSupported(): bool
return class_exists(Client::class);
}

public function lockStore(): LockStoreInterface
{
return $this->lockStore ??= new PredisLockStore($this->redis);
}

public function ping(): bool
{
try {
Expand Down
17 changes: 15 additions & 2 deletions system/Cache/Handlers/RedisHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

namespace CodeIgniter\Cache\Handlers;

use CodeIgniter\Cache\LockStoreInterface;
use CodeIgniter\Cache\LockStoreProvider;
use CodeIgniter\Cache\LockStores\RedisLockStore;
use CodeIgniter\Exceptions\CriticalError;
use CodeIgniter\I18n\Time;
use Config\Cache;
Expand All @@ -24,7 +27,7 @@
*
* @see \CodeIgniter\Cache\Handlers\RedisHandlerTest
*/
class RedisHandler extends BaseHandler
class RedisHandler extends BaseHandler implements LockStoreProvider
{
/**
* Default config
Expand Down Expand Up @@ -54,6 +57,8 @@ class RedisHandler extends BaseHandler
*/
protected $redis;

private ?LockStoreInterface $lockStore = null;

/**
* Note: Use `CacheFactory::getHandler()` to instantiate.
*/
Expand All @@ -68,7 +73,8 @@ public function initialize(): void
{
$config = $this->config;

$this->redis = new Redis();
$this->redis = new Redis();
$this->lockStore = null;

try {
$funcConnection = isset($config['persistent']) && $config['persistent'] ? 'pconnect' : 'connect';
Expand Down Expand Up @@ -219,6 +225,13 @@ public function isSupported(): bool
return extension_loaded('redis');
}

public function lockStore(): LockStoreInterface
{
assert($this->redis instanceof Redis);

return $this->lockStore ??= new RedisLockStore($this->redis, $this->prefix);
}

public function ping(): bool
{
if (! isset($this->redis)) {
Expand Down
27 changes: 27 additions & 0 deletions system/Cache/LockStoreInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\Cache;

interface LockStoreInterface
{
public function acquireLock(string $key, string $owner, int $ttl): bool;

public function releaseLock(string $key, string $owner): bool;

public function forceReleaseLock(string $key): bool;

public function refreshLock(string $key, string $owner, int $ttl): bool;

public function getLockOwner(string $key): ?string;
}
19 changes: 19 additions & 0 deletions system/Cache/LockStoreProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\Cache;

interface LockStoreProvider
{
public function lockStore(): LockStoreInterface;
}
189 changes: 189 additions & 0 deletions system/Cache/LockStores/FileLockStore.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<?php

declare(strict_types=1);

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\Cache\LockStores;

use CodeIgniter\Cache\Handlers\FileHandler;
use CodeIgniter\Cache\LockStoreInterface;
use CodeIgniter\I18n\Time;
use Throwable;

class FileLockStore implements LockStoreInterface
{
public function __construct(
private readonly string $path,
private readonly int $mode,
private readonly string $prefix = '',
) {
}

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 . FileHandler::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;
}

/**
* @param callable(resource): bool $callback
*/
private function withLockFile(string $key, callable $callback, bool $create = true): bool
{
$key = FileHandler::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);
}
}
Loading
Loading