Skip to content

FOUR-20532 clear cache by pattern #7795

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 3 commits into from
Dec 6, 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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,5 @@ OPEN_AI_SECRET="sk-O2D..."
AI_MICROSERVICE_HOST="http://localhost:8010"
PROCESS_REQUEST_ERRORS_RATE_LIMIT=1
PROCESS_REQUEST_ERRORS_RATE_LIMIT_DURATION=86400
CACHE_SETTING_DRIVER=cache_settings
CACHE_SETTING_PREFIX=settings
27 changes: 27 additions & 0 deletions ProcessMaker/Cache/Screens/ScreenCacheManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,33 @@ public function get(string $key, mixed $default = null): mixed
return $default;
}

/**
* Get a screen from cache, or store the value from the callback if the key exists
*
* @param string $key Screen cache key
* @param callable $callback Callback to generate screen content
* @param null|int|\DateInterval $ttl Time to live
* @return mixed
*/
public function getOrCache(string $key, callable $callback, null|int|\DateInterval $ttl = null): mixed
{
$value = $this->get($key);

if ($value !== null) {
return $value;
}

$value = $callback();

if ($value === null) {
return $value;
}

$this->set($key, $value, $ttl);

return $value;
}

/**
* Store a screen in memory cache
*
Expand Down
9 changes: 9 additions & 0 deletions ProcessMaker/Cache/Settings/SettingCacheException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace ProcessMaker\Cache\Settings;

use Exception;

class SettingCacheException extends Exception
{
}
56 changes: 55 additions & 1 deletion ProcessMaker/Cache/Settings/SettingCacheManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,35 @@
namespace ProcessMaker\Cache\Settings;

use Illuminate\Cache\CacheManager;
use Illuminate\Support\Facades\Redis;
use ProcessMaker\Cache\CacheInterface;

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

protected CacheManager $cacheManager;

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

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

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

/**
Expand Down Expand Up @@ -105,7 +125,41 @@ public function delete(string $key): bool
*/
public function clear(): bool
{
return $this->cacheManager->flush();
return $this->cacheManager->clear();
}

/**
* Remove items from the settings cache by a given pattern.
*
* @param string $pattern
*
* @throws \Exception
* @return void
*/
public function clearBy(string $pattern): void
{
$defaultDriver = $this->cacheManager->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() . '*');
// Filter keys by pattern
$matchedKeys = array_filter($keys, fn($key) => preg_match('/' . $pattern . '/', $key));

if (!empty($matchedKeys)) {
Redis::connection($connection)->del($matchedKeys);
}
} catch (\Exception $e) {
\Log::error('SettingCacheException' . $e->getMessage());

throw new SettingCacheException('Failed to delete keys.');
}
}

/**
Expand Down
2 changes: 1 addition & 1 deletion ProcessMaker/Providers/ProcessMakerServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ public function register(): void
});

$this->app->singleton('setting.cache', function ($app) {
if ($app['config']->get('cache.default')) {
if ($app['config']->get('cache.stores.cache_settings')) {
return new SettingCacheManager($app->make('cache'));
} else {
throw new RuntimeException('Cache configuration is missing.');
Expand Down
7 changes: 7 additions & 0 deletions config/cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@
'driver' => 'octane',
],

'cache_settings' => [
'driver' => 'redis',
'connection' => 'cache_settings',
'lock_connection' => 'cache_settings',
'prefix' => env('CACHE_SETTING_PREFIX', 'settings'),
],

],

/*
Expand Down
8 changes: 8 additions & 0 deletions config/database.php
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,14 @@
'database' => env('REDIS_CACHE_DB', '1'),
],

'cache_settings' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_SETTING_DB', '2'),
],
],

];
142 changes: 142 additions & 0 deletions tests/Feature/Cache/SettingCacheTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
namespace Tests\Feature\Cache;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
use ProcessMaker\Cache\Settings\SettingCacheException;
use ProcessMaker\Models\Setting;
use ProcessMaker\Models\User;
use Tests\Feature\Shared\RequestHelper;
use Tests\TestCase;

Expand All @@ -13,6 +17,26 @@ class SettingCacheTest extends TestCase
use RequestHelper;
use RefreshDatabase;

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

$this->user = User::factory()->create([
'is_administrator' => true,
]);

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

protected function tearDown(): void
{
\SettingCache::clear();

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

parent::tearDown();
}

private function upgrade()
{
$this->artisan('migrate', [
Expand Down Expand Up @@ -115,4 +139,122 @@ public function testGetSettingByNotExistingKey()

$this->assertNull($setting);
}

public function testClearByPattern()
{
\SettingCache::set('password-policies.users_can_change', 1);
\SettingCache::set('password-policies.numbers', 2);
\SettingCache::set('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'));

$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'));
}

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);

$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'));

$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'));
}

public function testClearByPatternWithFailedDeletion()
{
$pattern = 'test_pattern';
$keys = [
'settings:test_pattern:1',
'settings:test_pattern:2'
];
\SettingCache::set('test_pattern:1', 1);
\SettingCache::set('test_pattern:2', 2);

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

Redis::shouldReceive('del')
->with($keys)
->andThrow(new SettingCacheException('Failed to delete keys.'));

$this->expectException(SettingCacheException::class);
$this->expectExceptionMessage('Failed to delete keys.');

\SettingCache::clearBy($pattern);
}

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

$this->expectException(SettingCacheException::class);
$this->expectExceptionMessage('The cache driver must be Redis.');

\SettingCache::clearBy('pattern');
}

public function testClearAllSettings()
{
\SettingCache::set('password-policies.users_can_change', 1);
\SettingCache::set('password-policies.numbers', 2);
\SettingCache::set('password-policies.uppercase', 3);

$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'));

\SettingCache::clear();

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

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);

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'));

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'));
}
}
Loading