From f3021b14bb32fa3a8514aa7c50f70da0f4e3f914 Mon Sep 17 00:00:00 2001 From: Miguel Angel Date: Thu, 5 Dec 2024 00:33:19 -0400 Subject: [PATCH 1/3] feat: clear by pattern, clear all settings --- .../Cache/Screens/ScreenCacheManager.php | 27 ++++ .../Cache/Settings/SettingCacheException.php | 9 ++ .../Cache/Settings/SettingCacheManager.php | 36 ++++- .../Providers/ProcessMakerServiceProvider.php | 2 +- config/cache.php | 7 + config/database.php | 8 ++ tests/Feature/Cache/SettingCacheTest.php | 123 ++++++++++++++++++ 7 files changed, 209 insertions(+), 3 deletions(-) create mode 100644 ProcessMaker/Cache/Settings/SettingCacheException.php diff --git a/ProcessMaker/Cache/Screens/ScreenCacheManager.php b/ProcessMaker/Cache/Screens/ScreenCacheManager.php index d5ab8929da..993d221151 100644 --- a/ProcessMaker/Cache/Screens/ScreenCacheManager.php +++ b/ProcessMaker/Cache/Screens/ScreenCacheManager.php @@ -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 * diff --git a/ProcessMaker/Cache/Settings/SettingCacheException.php b/ProcessMaker/Cache/Settings/SettingCacheException.php new file mode 100644 index 0000000000..bdb0ab7cc2 --- /dev/null +++ b/ProcessMaker/Cache/Settings/SettingCacheException.php @@ -0,0 +1,9 @@ +cacheManager = $cacheManager; + $driver = env('CACHE_SETTING_DRIVER') ?? env('CACHE_DRIVER', 'redis'); + + $this->cacheManager = $cacheManager->store($driver); } /** @@ -108,6 +112,34 @@ public function clear(): bool return $this->cacheManager->flush(); } + /** + * Remove items from the settings cache by a given pattern. + * + * @param string $pattern + * + * @throws \Exception + * @return void + */ + public function clearBy(string $pattern): void + { + try { + // get the connection name from the cache manager + $connection = $this->cacheManager->connection()->getName(); + // Get all keys + $keys = Redis::connection($connection)->keys('*'); + // 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.'); + } + } + /** * Check if a value exists in the settings cache. * diff --git a/ProcessMaker/Providers/ProcessMakerServiceProvider.php b/ProcessMaker/Providers/ProcessMakerServiceProvider.php index 4c8af989f3..6234021af2 100644 --- a/ProcessMaker/Providers/ProcessMakerServiceProvider.php +++ b/ProcessMaker/Providers/ProcessMakerServiceProvider.php @@ -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.'); diff --git a/config/cache.php b/config/cache.php index 2c177975d7..4a1e0c2fda 100644 --- a/config/cache.php +++ b/config/cache.php @@ -99,6 +99,13 @@ 'driver' => 'octane', ], + 'cache_settings' => [ + 'driver' => 'redis', + 'connection' => 'cache_settings', + 'lock_connection' => 'cache_settings', + 'prefix' => env('CACHE_SETTING_PREFIX', 'settings'), + ], + ], /* diff --git a/config/database.php b/config/database.php index 2a3939c3b8..f50dc8bdab 100644 --- a/config/database.php +++ b/config/database.php @@ -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'), + ], ], ]; diff --git a/tests/Feature/Cache/SettingCacheTest.php b/tests/Feature/Cache/SettingCacheTest.php index de68e68829..f3a5bd6177 100644 --- a/tests/Feature/Cache/SettingCacheTest.php +++ b/tests/Feature/Cache/SettingCacheTest.php @@ -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; @@ -13,6 +17,25 @@ class SettingCacheTest extends TestCase use RequestHelper; use RefreshDatabase; + protected function setUp(): void + { + parent::setUp(); + + $this->user = User::factory()->create([ + 'is_administrator' => true, + ]); + + putenv('CACHE_SETTING_DRIVER=cache_settings'); + } + + protected function tearDown(): void + { + \SettingCache::clear(); + + putenv('CACHE_SETTING_DRIVER'); + parent::tearDown(); + } + private function upgrade() { $this->artisan('migrate', [ @@ -115,4 +138,104 @@ 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 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); + Cache::put('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, Cache::get('password-policies.uppercase')); + + \SettingCache::clear(); + + $this->assertNull(\SettingCache::get('password-policies.users_can_change')); + $this->assertNull(\SettingCache::get('password-policies.numbers')); + $this->assertEquals(3, Cache::get('password-policies.uppercase')); + } } From 3370ea6d7ceb13fd6a32351b3b41060911cf6255 Mon Sep 17 00:00:00 2001 From: Miguel Angel Date: Thu, 5 Dec 2024 08:40:00 -0400 Subject: [PATCH 2/3] feat: add cache settings to .env.example --- .env.example | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.env.example b/.env.example index 1b2468d570..3b710f8c94 100644 --- a/.env.example +++ b/.env.example @@ -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 From 2c61caa664a48005ec57929dd6766d21c2517ba4 Mon Sep 17 00:00:00 2001 From: Miguel Angel Date: Thu, 5 Dec 2024 11:58:45 -0400 Subject: [PATCH 3/3] fix(cr): enhance SettingCacheManager to determine cache driver and improve clearBy method --- .../Cache/Settings/SettingCacheManager.php | 34 +++++++++++++++---- tests/Feature/Cache/SettingCacheTest.php | 23 +++++++++++-- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/ProcessMaker/Cache/Settings/SettingCacheManager.php b/ProcessMaker/Cache/Settings/SettingCacheManager.php index 5c53a4d30b..eec30615a6 100644 --- a/ProcessMaker/Cache/Settings/SettingCacheManager.php +++ b/ProcessMaker/Cache/Settings/SettingCacheManager.php @@ -3,19 +3,35 @@ namespace ProcessMaker\Cache\Settings; use Illuminate\Cache\CacheManager; -use Illuminate\Contracts\Cache\Repository; use Illuminate\Support\Facades\Redis; use ProcessMaker\Cache\CacheInterface; class SettingCacheManager implements CacheInterface { - protected Repository $cacheManager; + const DEFAULT_CACHE_DRIVER = 'cache_settings'; + + protected CacheManager $cacheManager; public function __construct(CacheManager $cacheManager) { - $driver = env('CACHE_SETTING_DRIVER') ?? env('CACHE_DRIVER', 'redis'); + $driver = $this->determineCacheDriver(); - $this->cacheManager = $cacheManager->store($driver); + $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; } /** @@ -109,7 +125,7 @@ public function delete(string $key): bool */ public function clear(): bool { - return $this->cacheManager->flush(); + return $this->cacheManager->clear(); } /** @@ -122,11 +138,17 @@ public function clear(): bool */ 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('*'); + $keys = Redis::connection($connection)->keys($this->cacheManager->getPrefix() . '*'); // Filter keys by pattern $matchedKeys = array_filter($keys, fn($key) => preg_match('/' . $pattern . '/', $key)); diff --git a/tests/Feature/Cache/SettingCacheTest.php b/tests/Feature/Cache/SettingCacheTest.php index f3a5bd6177..cc05304f7b 100644 --- a/tests/Feature/Cache/SettingCacheTest.php +++ b/tests/Feature/Cache/SettingCacheTest.php @@ -25,14 +25,15 @@ protected function setUp(): void 'is_administrator' => true, ]); - putenv('CACHE_SETTING_DRIVER=cache_settings'); + config()->set('cache.default', 'cache_settings'); } protected function tearDown(): void { \SettingCache::clear(); - putenv('CACHE_SETTING_DRIVER'); + config()->set('cache.default', 'array'); + parent::tearDown(); } @@ -205,6 +206,16 @@ public function testClearByPatternWithFailedDeletion() \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); @@ -226,16 +237,24 @@ 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')); } }