Skip to content

Commit e1e6b4e

Browse files
authored
Merge pull request #7795 from ProcessMaker/FOUR-20532
FOUR-20532 clear cache by pattern
2 parents 272d3b7 + 2c61caa commit e1e6b4e

8 files changed

Lines changed: 251 additions & 2 deletions

File tree

‎.env.example‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,5 @@ OPEN_AI_SECRET="sk-O2D..."
4747
AI_MICROSERVICE_HOST="http://localhost:8010"
4848
PROCESS_REQUEST_ERRORS_RATE_LIMIT=1
4949
PROCESS_REQUEST_ERRORS_RATE_LIMIT_DURATION=86400
50+
CACHE_SETTING_DRIVER=cache_settings
51+
CACHE_SETTING_PREFIX=settings

‎ProcessMaker/Cache/Screens/ScreenCacheManager.php‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,33 @@ public function get(string $key, mixed $default = null): mixed
5555
return $default;
5656
}
5757

58+
/**
59+
* Get a screen from cache, or store the value from the callback if the key exists
60+
*
61+
* @param string $key Screen cache key
62+
* @param callable $callback Callback to generate screen content
63+
* @param null|int|\DateInterval $ttl Time to live
64+
* @return mixed
65+
*/
66+
public function getOrCache(string $key, callable $callback, null|int|\DateInterval $ttl = null): mixed
67+
{
68+
$value = $this->get($key);
69+
70+
if ($value !== null) {
71+
return $value;
72+
}
73+
74+
$value = $callback();
75+
76+
if ($value === null) {
77+
return $value;
78+
}
79+
80+
$this->set($key, $value, $ttl);
81+
82+
return $value;
83+
}
84+
5885
/**
5986
* Store a screen in memory cache
6087
*
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace ProcessMaker\Cache\Settings;
4+
5+
use Exception;
6+
7+
class SettingCacheException extends Exception
8+
{
9+
}

‎ProcessMaker/Cache/Settings/SettingCacheManager.php‎

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,35 @@
33
namespace ProcessMaker\Cache\Settings;
44

55
use Illuminate\Cache\CacheManager;
6+
use Illuminate\Support\Facades\Redis;
67
use ProcessMaker\Cache\CacheInterface;
78

89
class SettingCacheManager implements CacheInterface
910
{
11+
const DEFAULT_CACHE_DRIVER = 'cache_settings';
12+
1013
protected CacheManager $cacheManager;
1114

1215
public function __construct(CacheManager $cacheManager)
1316
{
17+
$driver = $this->determineCacheDriver();
18+
1419
$this->cacheManager = $cacheManager;
20+
$this->cacheManager->store($driver);
21+
}
22+
23+
/**
24+
* Determine the cache driver to use.
25+
*
26+
* @return string
27+
*/
28+
private function determineCacheDriver(): string
29+
{
30+
$defaultCache = config('cache.default');
31+
if (in_array($defaultCache, ['redis', 'cache_settings'])) {
32+
return self::DEFAULT_CACHE_DRIVER;
33+
}
34+
return $defaultCache;
1535
}
1636

1737
/**
@@ -105,7 +125,41 @@ public function delete(string $key): bool
105125
*/
106126
public function clear(): bool
107127
{
108-
return $this->cacheManager->flush();
128+
return $this->cacheManager->clear();
129+
}
130+
131+
/**
132+
* Remove items from the settings cache by a given pattern.
133+
*
134+
* @param string $pattern
135+
*
136+
* @throws \Exception
137+
* @return void
138+
*/
139+
public function clearBy(string $pattern): void
140+
{
141+
$defaultDriver = $this->cacheManager->getDefaultDriver();
142+
143+
if ($defaultDriver !== 'cache_settings') {
144+
throw new SettingCacheException('The cache driver must be Redis.');
145+
}
146+
147+
try {
148+
// get the connection name from the cache manager
149+
$connection = $this->cacheManager->connection()->getName();
150+
// Get all keys
151+
$keys = Redis::connection($connection)->keys($this->cacheManager->getPrefix() . '*');
152+
// Filter keys by pattern
153+
$matchedKeys = array_filter($keys, fn($key) => preg_match('/' . $pattern . '/', $key));
154+
155+
if (!empty($matchedKeys)) {
156+
Redis::connection($connection)->del($matchedKeys);
157+
}
158+
} catch (\Exception $e) {
159+
\Log::error('SettingCacheException' . $e->getMessage());
160+
161+
throw new SettingCacheException('Failed to delete keys.');
162+
}
109163
}
110164

111165
/**

‎ProcessMaker/Providers/ProcessMakerServiceProvider.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ public function register(): void
168168
});
169169

170170
$this->app->singleton('setting.cache', function ($app) {
171-
if ($app['config']->get('cache.default')) {
171+
if ($app['config']->get('cache.stores.cache_settings')) {
172172
return new SettingCacheManager($app->make('cache'));
173173
} else {
174174
throw new RuntimeException('Cache configuration is missing.');

‎config/cache.php‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@
9999
'driver' => 'octane',
100100
],
101101

102+
'cache_settings' => [
103+
'driver' => 'redis',
104+
'connection' => 'cache_settings',
105+
'lock_connection' => 'cache_settings',
106+
'prefix' => env('CACHE_SETTING_PREFIX', 'settings'),
107+
],
108+
102109
],
103110

104111
/*

‎config/database.php‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,14 @@
181181
'database' => env('REDIS_CACHE_DB', '1'),
182182
],
183183

184+
'cache_settings' => [
185+
'url' => env('REDIS_URL'),
186+
'host' => env('REDIS_HOST', '127.0.0.1'),
187+
'username' => env('REDIS_USERNAME'),
188+
'password' => env('REDIS_PASSWORD'),
189+
'port' => env('REDIS_PORT', '6379'),
190+
'database' => env('REDIS_CACHE_SETTING_DB', '2'),
191+
],
184192
],
185193

186194
];

‎tests/Feature/Cache/SettingCacheTest.php‎

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33
namespace Tests\Feature\Cache;
44

55
use Illuminate\Foundation\Testing\RefreshDatabase;
6+
use Illuminate\Support\Facades\Cache;
67
use Illuminate\Support\Facades\DB;
8+
use Illuminate\Support\Facades\Redis;
9+
use ProcessMaker\Cache\Settings\SettingCacheException;
710
use ProcessMaker\Models\Setting;
11+
use ProcessMaker\Models\User;
812
use Tests\Feature\Shared\RequestHelper;
913
use Tests\TestCase;
1014

@@ -13,6 +17,26 @@ class SettingCacheTest extends TestCase
1317
use RequestHelper;
1418
use RefreshDatabase;
1519

20+
protected function setUp(): void
21+
{
22+
parent::setUp();
23+
24+
$this->user = User::factory()->create([
25+
'is_administrator' => true,
26+
]);
27+
28+
config()->set('cache.default', 'cache_settings');
29+
}
30+
31+
protected function tearDown(): void
32+
{
33+
\SettingCache::clear();
34+
35+
config()->set('cache.default', 'array');
36+
37+
parent::tearDown();
38+
}
39+
1640
private function upgrade()
1741
{
1842
$this->artisan('migrate', [
@@ -115,4 +139,122 @@ public function testGetSettingByNotExistingKey()
115139

116140
$this->assertNull($setting);
117141
}
142+
143+
public function testClearByPattern()
144+
{
145+
\SettingCache::set('password-policies.users_can_change', 1);
146+
\SettingCache::set('password-policies.numbers', 2);
147+
\SettingCache::set('password-policies.uppercase', 3);
148+
Cache::put('session-control.ip_restriction', 0);
149+
150+
$this->assertEquals(1, \SettingCache::get('password-policies.users_can_change'));
151+
$this->assertEquals(2, \SettingCache::get('password-policies.numbers'));
152+
$this->assertEquals(3, \SettingCache::get('password-policies.uppercase'));
153+
154+
$pattern = 'password-policies';
155+
156+
\SettingCache::clearBy($pattern);
157+
158+
$this->assertNull(\SettingCache::get('password-policies.users_can_change'));
159+
$this->assertNull(\SettingCache::get('password-policies.numbers'));
160+
$this->assertNull(\SettingCache::get('password-policies.uppercase'));
161+
}
162+
163+
public function testClearByPatternRemainUnmatched()
164+
{
165+
\SettingCache::set('session-control.ip_restriction', 0);
166+
\SettingCache::set('password-policies.users_can_change', 1);
167+
\SettingCache::set('password-policies.numbers', 2);
168+
\SettingCache::set('password-policies.uppercase', 3);
169+
170+
$this->assertEquals(0, \SettingCache::get('session-control.ip_restriction'));
171+
$this->assertEquals(1, \SettingCache::get('password-policies.users_can_change'));
172+
$this->assertEquals(2, \SettingCache::get('password-policies.numbers'));
173+
$this->assertEquals(3, \SettingCache::get('password-policies.uppercase'));
174+
175+
$pattern = 'password-policies';
176+
177+
\SettingCache::clearBy($pattern);
178+
179+
$this->assertEquals(0, \SettingCache::get('session-control.ip_restriction'));
180+
$this->assertNull(\SettingCache::get('password-policies.users_can_change'));
181+
$this->assertNull(\SettingCache::get('password-policies.numbers'));
182+
$this->assertNull(\SettingCache::get('password-policies.uppercase'));
183+
}
184+
185+
public function testClearByPatternWithFailedDeletion()
186+
{
187+
$pattern = 'test_pattern';
188+
$keys = [
189+
'settings:test_pattern:1',
190+
'settings:test_pattern:2'
191+
];
192+
\SettingCache::set('test_pattern:1', 1);
193+
\SettingCache::set('test_pattern:2', 2);
194+
195+
Redis::shouldReceive('keys')
196+
->with('*settings:*')
197+
->andReturn($keys);
198+
199+
Redis::shouldReceive('del')
200+
->with($keys)
201+
->andThrow(new SettingCacheException('Failed to delete keys.'));
202+
203+
$this->expectException(SettingCacheException::class);
204+
$this->expectExceptionMessage('Failed to delete keys.');
205+
206+
\SettingCache::clearBy($pattern);
207+
}
208+
209+
public function testTryClearByPatternWithNonRedisDriver()
210+
{
211+
config()->set('cache.default', 'array');
212+
213+
$this->expectException(SettingCacheException::class);
214+
$this->expectExceptionMessage('The cache driver must be Redis.');
215+
216+
\SettingCache::clearBy('pattern');
217+
}
218+
219+
public function testClearAllSettings()
220+
{
221+
\SettingCache::set('password-policies.users_can_change', 1);
222+
\SettingCache::set('password-policies.numbers', 2);
223+
\SettingCache::set('password-policies.uppercase', 3);
224+
225+
$this->assertEquals(1, \SettingCache::get('password-policies.users_can_change'));
226+
$this->assertEquals(2, \SettingCache::get('password-policies.numbers'));
227+
$this->assertEquals(3, \SettingCache::get('password-policies.uppercase'));
228+
229+
\SettingCache::clear();
230+
231+
$this->assertNull(\SettingCache::get('password-policies.users_can_change'));
232+
$this->assertNull(\SettingCache::get('password-policies.numbers'));
233+
$this->assertNull(\SettingCache::get('password-policies.uppercase'));
234+
}
235+
236+
public function testClearOnlySettings()
237+
{
238+
\SettingCache::set('password-policies.users_can_change', 1);
239+
\SettingCache::set('password-policies.numbers', 2);
240+
241+
config()->set('cache.default', 'array');
242+
Cache::put('password-policies.uppercase', 3);
243+
244+
config()->set('cache.default', 'cache_settings');
245+
$this->assertEquals(1, \SettingCache::get('password-policies.users_can_change'));
246+
$this->assertEquals(2, \SettingCache::get('password-policies.numbers'));
247+
248+
config()->set('cache.default', 'array');
249+
$this->assertEquals(3, Cache::get('password-policies.uppercase'));
250+
251+
config()->set('cache.default', 'cache_settings');
252+
\SettingCache::clear();
253+
254+
$this->assertNull(\SettingCache::get('password-policies.users_can_change'));
255+
$this->assertNull(\SettingCache::get('password-policies.numbers'));
256+
257+
config()->set('cache.default', 'array');
258+
$this->assertEquals(3, Cache::get('password-policies.uppercase'));
259+
}
118260
}

0 commit comments

Comments
 (0)