Skip to content

Commit

Permalink
Merge pull request #7836 from ProcessMaker/FOUR-21263
Browse files Browse the repository at this point in the history
FOUR-21263 implement a general method to index cache keys
  • Loading branch information
caleeli authored Dec 19, 2024
2 parents fa63a6a + 626adf1 commit fafc69e
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 49 deletions.
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

0 comments on commit fafc69e

Please sign in to comment.