Skip to content

FOUR-21263 implement a general method to index cache keys #7836

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Dec 19, 2024
Merged
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
51 changes: 51 additions & 0 deletions ProcessMaker/Cache/CacheManagerBase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace ProcessMaker\Cache;

use Exception;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;

abstract class CacheManagerBase
{
/**
* The available cache connections.
*
* @var array
*/
protected const AVAILABLE_CONNECTIONS = ['redis', 'cache_settings'];

/**
* Retrieve an array of cache keys that match a specific pattern.
*
* @param string $pattern The pattern to match.
* @param string|null $connection The cache connection to use.
*
* @return array An array of cache keys that match the pattern.
*/
public function getKeysByPattern(string $pattern, string $connection = null, string $prefix = null): array
{
if (!$connection) {
$connection = config('cache.default');
}

if (!$prefix) {
$prefix = config('cache.prefix');
}

if (!in_array($connection, self::AVAILABLE_CONNECTIONS)) {
throw new CacheManagerException('`getKeysByPattern` method only supports Redis connections.');
}

try {
// Get all keys
$keys = Redis::connection($connection)->keys($prefix . '*');
// Filter keys by pattern
return array_filter($keys, fn ($key) => preg_match('/' . $pattern . '/', $key));
} catch (Exception $e) {
Log::info('CacheManagerBase: ' . $e->getMessage());
}

return [];
}
}
9 changes: 9 additions & 0 deletions ProcessMaker/Cache/CacheManagerException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace ProcessMaker\Cache;

use Exception;

class CacheManagerException extends Exception
{
}
41 changes: 22 additions & 19 deletions ProcessMaker/Cache/Settings/SettingCacheManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,43 @@
namespace ProcessMaker\Cache\Settings;

use Illuminate\Cache\CacheManager;
use Illuminate\Cache\Repository;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use ProcessMaker\Cache\CacheInterface;
use ProcessMaker\Cache\CacheManagerBase;

class SettingCacheManager implements CacheInterface
class SettingCacheManager extends CacheManagerBase implements CacheInterface
{
const DEFAULT_CACHE_DRIVER = 'cache_settings';

protected CacheManager $cacheManager;
protected CacheManager $manager;

protected Repository $cacheManager;

public function __construct(CacheManager $cacheManager)
{
$driver = $this->determineCacheDriver();
$this->manager = $cacheManager;

$this->cacheManager = $cacheManager;
$this->cacheManager->store($driver);
$this->setCacheDriver();
}

/**
* Determine the cache driver to use.
* Determine and set the cache driver to use.
*
* @param CacheManager $cacheManager
*
* @return string
* @return void
*/
private function determineCacheDriver(): string
private function setCacheDriver(): void
{
$defaultCache = config('cache.default');
if (in_array($defaultCache, ['redis', 'cache_settings'])) {
return self::DEFAULT_CACHE_DRIVER;
}
$isAvailableConnection = in_array($defaultCache, self::AVAILABLE_CONNECTIONS);

return $defaultCache;
// Set the cache driver to use
$cacheDriver = $isAvailableConnection ? self::DEFAULT_CACHE_DRIVER : $defaultCache;
// Store the cache driver
$this->cacheManager = $this->manager->store($cacheDriver);
}

/**
Expand Down Expand Up @@ -140,22 +146,19 @@ public function clear(): bool
*/
public function clearBy(string $pattern): void
{
$defaultDriver = $this->cacheManager->getDefaultDriver();
$defaultDriver = $this->manager->getDefaultDriver();

if ($defaultDriver !== 'cache_settings') {
throw new SettingCacheException('The cache driver must be Redis.');
}

try {
// get the connection name from the cache manager
$connection = $this->cacheManager->connection()->getName();
// Get all keys
$keys = Redis::connection($connection)->keys($this->cacheManager->getPrefix() . '*');
$prefix = $this->manager->getPrefix();
// Filter keys by pattern
$matchedKeys = array_filter($keys, fn ($key) => preg_match('/' . $pattern . '/', $key));
$matchedKeys = $this->getKeysByPattern($pattern, $defaultDriver, $prefix);

if (!empty($matchedKeys)) {
Redis::connection($connection)->del($matchedKeys);
Redis::connection($defaultDriver)->del($matchedKeys);
}
} catch (\Exception $e) {
Log::error('SettingCacheException' . $e->getMessage());
Expand Down
32 changes: 32 additions & 0 deletions ProcessMaker/Console/Commands/CacheSettingClear.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace ProcessMaker\Console\Commands;

use Illuminate\Console\Command;

class CacheSettingClear extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cache:settings-clear';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Remove all of items from the settings cache';

/**
* Execute the console command.
*/
public function handle()
{
\SettingCache::clear();

$this->info('Settings cache cleared.');
}
}
108 changes: 108 additions & 0 deletions tests/Feature/Cache/CacheManagerBaseTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

namespace Tests\Feature\Cache;

use Exception;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use ProcessMaker\Cache\CacheManagerBase;
use ProcessMaker\Cache\CacheManagerException;
use Tests\TestCase;

class CacheManagerBaseTest extends TestCase
{
protected $cacheManagerBase;

protected function setUp(): void
{
parent::setUp();

config()->set('cache.default', 'redis');
}

protected function tearDown(): void
{
config()->set('cache.default', 'array');

parent::tearDown();
}

public function testGetKeysByPatternWithValidConnectionAndMatchingKeys()
{
$this->cacheManagerBase = $this->getMockForAbstractClass(CacheManagerBase::class);

$pattern = 'test-pattern';
$prefix = config('cache.prefix');
$keys = [$prefix . ':test-pattern:1', $prefix . ':test-pattern:2'];

Redis::shouldReceive('connection')
->with('redis')
->andReturnSelf();

Redis::shouldReceive('keys')
->with($prefix . '*')
->andReturn($keys);

$result = $this->cacheManagerBase->getKeysByPattern($pattern);

$this->assertCount(2, $result);
$this->assertEquals($keys, $result);
}

public function testGetKeysByPatternWithValidConnectionAndNoMatchingKeys()
{
$this->cacheManagerBase = $this->getMockForAbstractClass(CacheManagerBase::class);

$pattern = 'non-matching-pattern';
$prefix = config('cache.prefix');
$keys = [$prefix . ':test-pattern:1', $prefix . ':test-pattern:2'];

Redis::shouldReceive('connection')
->with('redis')
->andReturnSelf();

Redis::shouldReceive('keys')
->with($prefix . '*')
->andReturn($keys);

$result = $this->cacheManagerBase->getKeysByPattern($pattern);

$this->assertCount(0, $result);
}

public function testGetKeysByPatternWithInvalidConnection()
{
config()->set('cache.default', 'array');

$this->cacheManagerBase = $this->getMockForAbstractClass(CacheManagerBase::class);

$this->expectException(CacheManagerException::class);
$this->expectExceptionMessage('`getKeysByPattern` method only supports Redis connections.');

$this->cacheManagerBase->getKeysByPattern('pattern');
}

public function testGetKeysByPatternWithExceptionDuringKeyRetrieval()
{
$this->cacheManagerBase = $this->getMockForAbstractClass(CacheManagerBase::class);

$pattern = 'test-pattern';
$prefix = config('cache.prefix');

Redis::shouldReceive('connection')
->with('redis')
->andReturnSelf();

Redis::shouldReceive('keys')
->with($prefix . '*')
->andThrow(new Exception('Redis error'));

Log::shouldReceive('info')
->with('CacheManagerBase: ' . 'Redis error')
->once();

$result = $this->cacheManagerBase->getKeysByPattern($pattern);

$this->assertCount(0, $result);
}
}
60 changes: 30 additions & 30 deletions tests/Feature/Cache/SettingCacheTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,44 +142,44 @@ public function testGetSettingByNotExistingKey()

public function testClearByPattern()
{
\SettingCache::set('password-policies.users_can_change', 1);
\SettingCache::set('password-policies.numbers', 2);
\SettingCache::set('password-policies.uppercase', 3);
Cache::store('cache_settings')->put('password-policies.users_can_change', 1);
Cache::store('cache_settings')->put('password-policies.numbers', 2);
Cache::store('cache_settings')->put('password-policies.uppercase', 3);
Cache::put('session-control.ip_restriction', 0);

$this->assertEquals(1, \SettingCache::get('password-policies.users_can_change'));
$this->assertEquals(2, \SettingCache::get('password-policies.numbers'));
$this->assertEquals(3, \SettingCache::get('password-policies.uppercase'));
$this->assertEquals(1, Cache::store('cache_settings')->get('password-policies.users_can_change'));
$this->assertEquals(2, Cache::store('cache_settings')->get('password-policies.numbers'));
$this->assertEquals(3, Cache::store('cache_settings')->get('password-policies.uppercase'));

$pattern = 'password-policies';

\SettingCache::clearBy($pattern);

$this->assertNull(\SettingCache::get('password-policies.users_can_change'));
$this->assertNull(\SettingCache::get('password-policies.numbers'));
$this->assertNull(\SettingCache::get('password-policies.uppercase'));
$this->assertNull(Cache::store('cache_settings')->get('password-policies.users_can_change'));
$this->assertNull(Cache::store('cache_settings')->get('password-policies.numbers'));
$this->assertNull(Cache::store('cache_settings')->get('password-policies.uppercase'));
}

public function testClearByPatternRemainUnmatched()
{
\SettingCache::set('session-control.ip_restriction', 0);
\SettingCache::set('password-policies.users_can_change', 1);
\SettingCache::set('password-policies.numbers', 2);
\SettingCache::set('password-policies.uppercase', 3);
Cache::store('cache_settings')->put('session-control.ip_restriction', 0);
Cache::store('cache_settings')->put('password-policies.users_can_change', 1);
Cache::store('cache_settings')->put('password-policies.numbers', 2);
Cache::store('cache_settings')->put('password-policies.uppercase', 3);

$this->assertEquals(0, \SettingCache::get('session-control.ip_restriction'));
$this->assertEquals(1, \SettingCache::get('password-policies.users_can_change'));
$this->assertEquals(2, \SettingCache::get('password-policies.numbers'));
$this->assertEquals(3, \SettingCache::get('password-policies.uppercase'));
$this->assertEquals(0, Cache::store('cache_settings')->get('session-control.ip_restriction'));
$this->assertEquals(1, Cache::store('cache_settings')->get('password-policies.users_can_change'));
$this->assertEquals(2, Cache::store('cache_settings')->get('password-policies.numbers'));
$this->assertEquals(3, Cache::store('cache_settings')->get('password-policies.uppercase'));

$pattern = 'password-policies';

\SettingCache::clearBy($pattern);

$this->assertEquals(0, \SettingCache::get('session-control.ip_restriction'));
$this->assertNull(\SettingCache::get('password-policies.users_can_change'));
$this->assertNull(\SettingCache::get('password-policies.numbers'));
$this->assertNull(\SettingCache::get('password-policies.uppercase'));
$this->assertEquals(0, Cache::store('cache_settings')->get('session-control.ip_restriction'));
$this->assertNull(Cache::store('cache_settings')->get('password-policies.users_can_change'));
$this->assertNull(Cache::store('cache_settings')->get('password-policies.numbers'));
$this->assertNull(Cache::store('cache_settings')->get('password-policies.uppercase'));
}

public function testClearByPatternWithFailedDeletion()
Expand All @@ -192,8 +192,13 @@ public function testClearByPatternWithFailedDeletion()
\SettingCache::set('test_pattern:1', 1);
\SettingCache::set('test_pattern:2', 2);

// Set up the expectation for the connection method
Redis::shouldReceive('connection')
->with('cache_settings')
->andReturnSelf();

Redis::shouldReceive('keys')
->with('*settings:*')
->with('settings:*')
->andReturn($keys);

Redis::shouldReceive('del')
Expand Down Expand Up @@ -238,24 +243,19 @@ public function testClearOnlySettings()
\SettingCache::set('password-policies.users_can_change', 1);
\SettingCache::set('password-policies.numbers', 2);

config()->set('cache.default', 'array');
Cache::put('password-policies.uppercase', 3);
Cache::store('file')->put('password-policies.uppercase', 3);

config()->set('cache.default', 'cache_settings');
$this->assertEquals(1, \SettingCache::get('password-policies.users_can_change'));
$this->assertEquals(2, \SettingCache::get('password-policies.numbers'));

config()->set('cache.default', 'array');
$this->assertEquals(3, Cache::get('password-policies.uppercase'));
$this->assertEquals(3, Cache::store('file')->get('password-policies.uppercase'));

config()->set('cache.default', 'cache_settings');
\SettingCache::clear();

$this->assertNull(\SettingCache::get('password-policies.users_can_change'));
$this->assertNull(\SettingCache::get('password-policies.numbers'));

config()->set('cache.default', 'array');
$this->assertEquals(3, Cache::get('password-policies.uppercase'));
$this->assertEquals(3, Cache::store('file')->get('password-policies.uppercase'));
}

public function testInvalidateOnSaved()
Expand Down