Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
{
}
36 changes: 34 additions & 2 deletions ProcessMaker/Cache/Settings/SettingCacheManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@
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 CacheManager $cacheManager;
protected Repository $cacheManager;

public function __construct(CacheManager $cacheManager)
{
$this->cacheManager = $cacheManager;
$driver = env('CACHE_SETTING_DRIVER') ?? env('CACHE_DRIVER', 'redis');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use config instead of env.
In a production environment the .env file is not loaded, only the config from cache.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was fixed


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

/**
Expand Down Expand Up @@ -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('*');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the cache drive is not Redis?
Consider to close the Settings driver CACHE_SETTING_DRIVER to be always cache_settings or implement a general logic for other types of drivers.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A validation was added

// 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.
*
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'),
],
],

];
123 changes: 123 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,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');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider to use config instead of env

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was fixed

parent::tearDown();
}

private function upgrade()
{
$this->artisan('migrate', [
Expand Down Expand Up @@ -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'));
}
}