Skip to content
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
61 changes: 61 additions & 0 deletions ProcessMaker/Cache/AbstractCacheFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace ProcessMaker\Cache;

use Illuminate\Cache\CacheManager;
use ProcessMaker\Cache\Monitoring\CacheMetricsDecorator;
use ProcessMaker\Cache\Monitoring\CacheMetricsInterface;
use ProcessMaker\Cache\Monitoring\RedisMetricsManager;

abstract class AbstractCacheFactory implements CacheFactoryInterface
{
protected static ?CacheInterface $testInstance = null;

/**
* Set a test instance for the factory
*
* @param CacheInterface|null $instance
*/
public static function setTestInstance(?CacheInterface $instance): void
{
static::$testInstance = $instance;
}

/**
* Create a new cache instance with metrics monitoring
*
* @param CacheManager $cacheManager
* @param CacheMetricsInterface $metrics
* @return CacheInterface
*/
public static function create(CacheManager $cacheManager, CacheMetricsInterface $metrics): CacheInterface
{
if (static::$testInstance !== null) {
return static::$testInstance;
}

// Create base cache instance
$cache = static::createInstance($cacheManager);

// Wrap with metrics decorator
return new CacheMetricsDecorator($cache, $metrics);
}

/**
* Get the current cache instance
*
* @return CacheInterface
*/
protected static function getInstance(): CacheInterface
{
return static::create(app('cache'), app()->make(RedisMetricsManager::class));
}

/**
* Create the specific cache instance
*
* @param CacheManager $cacheManager
* @return CacheInterface
*/
abstract protected static function createInstance(CacheManager $cacheManager): CacheInterface;
}
18 changes: 18 additions & 0 deletions ProcessMaker/Cache/CacheFactoryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace ProcessMaker\Cache;

use Illuminate\Cache\CacheManager;
use ProcessMaker\Cache\Monitoring\CacheMetricsInterface;

interface CacheFactoryInterface
{
/**
* Create a new cache instance with metrics monitoring
*
* @param CacheManager $cacheManager
* @param CacheMetricsInterface $metrics
* @return CacheInterface
*/
public static function create(CacheManager $cacheManager, CacheMetricsInterface $metrics): CacheInterface;
}
8 changes: 8 additions & 0 deletions ProcessMaker/Cache/CacheInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,12 @@ public function has(string $key): bool;
* @return bool True if the item is missing from the cache, false otherwise.
*/
public function missing(string $key): bool;

/**
* Creates a cache key based on provided parameters
*
* @param array $params Key parameters
* @return string Generated cache key
*/
public function createKey(array $params): string;
}
1 change: 1 addition & 0 deletions ProcessMaker/Cache/CacheManagerBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public function getKeysByPattern(string $pattern, string $connection = null, str
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) {
Expand Down
57 changes: 36 additions & 21 deletions ProcessMaker/Cache/Monitoring/CacheMetricsDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@

use ProcessMaker\Cache\CacheInterface;
use ProcessMaker\Cache\Monitoring\CacheMetricsInterface;
use ProcessMaker\Cache\Screens\ScreenCacheInterface;

/**
* Decorator class that adds metrics tracking about cache operations
* including hits, misses, write sizes, and timing information.
*/
class CacheMetricsDecorator implements CacheInterface, ScreenCacheInterface
class CacheMetricsDecorator implements CacheInterface
{
protected CacheInterface|ScreenCacheInterface $cache;
protected CacheInterface $cache;

protected CacheMetricsInterface $metrics;

Expand All @@ -22,27 +21,22 @@ class CacheMetricsDecorator implements CacheInterface, ScreenCacheInterface
* @param CacheInterface|ScreenCacheInterface $cache The cache implementation to decorate
* @param CacheMetricsInterface $metrics The metrics implementation to use
*/
public function __construct(CacheInterface|ScreenCacheInterface $cache, CacheMetricsInterface $metrics)
public function __construct(CacheInterface $cache, CacheMetricsInterface $metrics)
{
$this->cache = $cache;
$this->metrics = $metrics;
}

/**
* Create a cache key for screen data
* Create a cache key based on provided parameters
*
* @param int $processId Process ID
* @param int $processVersionId Process version ID
* @param string $language Language code
* @param int $screenId Screen ID
* @param int $screenVersionId Screen version ID
* @param array $params Key parameters
* @return string Generated cache key
* @throws \RuntimeException If underlying cache doesn't support createKey
*/
public function createKey(int $processId, int $processVersionId, string $language, int $screenId, int $screenVersionId): string
public function createKey(array $params): string
{
if ($this->cache instanceof ScreenCacheInterface) {
return $this->cache->createKey($processId, $processVersionId, $language, $screenId, $screenVersionId);
if ($this->cache instanceof CacheInterface) {
return $this->cache->createKey($params);
}

throw new \RuntimeException('Underlying cache implementation does not support createKey method');
Expand All @@ -60,14 +54,21 @@ public function createKey(int $processId, int $processVersionId, string $languag
public function get(string $key, mixed $default = null): mixed
{
$startTime = microtime(true);

// First check if the key exists
$exists = $this->cache->has($key);

// Get the value
$value = $this->cache->get($key, $default);

$endTime = microtime(true);
$duration = $endTime - $startTime;

if ($value === $default) {
$this->metrics->recordMiss($key, $duration);
} else {
// Record metrics based on key existence, not value comparison
if ($exists) {
$this->metrics->recordHit($key, $duration);
} else {
$this->metrics->recordMiss($key, $duration);
}

return $value;
Expand Down Expand Up @@ -146,13 +147,13 @@ public function missing(string $key): bool
* @return bool
* @throws \RuntimeException If underlying cache doesn't support invalidate
*/
public function invalidate(int $screenId, string $language): bool
public function invalidate($params): void
{
if ($this->cache instanceof ScreenCacheInterface) {
return $this->cache->invalidate($screenId, $language);
if (!$this->cache instanceof CacheInterface) {
throw new \RuntimeException('Underlying cache implementation does not support invalidate method');
}

throw new \RuntimeException('Underlying cache implementation does not support invalidate method');
$this->cache->invalidate($params);
}

/**
Expand Down Expand Up @@ -204,4 +205,18 @@ public function getOrCache(string $key, callable $callback): mixed

return $value;
}

/**
* Clear compiled assets from cache and record metrics
*
* This method clears compiled assets from the cache and records the operation
* as a write with size 0 since we are removing content rather than adding it.
* The execution time is measured but not currently used.
*/
public function clearCompiledAssets(): void
{
$startTime = microtime(true);
$this->cache->clearCompiledAssets();
$timeTaken = microtime(true) - $startTime;
}
}
7 changes: 6 additions & 1 deletion ProcessMaker/Cache/Monitoring/RedisMetricsManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -236,10 +236,15 @@ public function getSummary(): array
$totalHitTime += $this->getHitAvgTime($key);
$totalMissTime += $this->getMissAvgTime($key);

// Total represents the total number of cache access attempts (hits + misses)
// We need this sum to calculate hit_ratio and miss_ratio percentages
// Example: If hits=8 and misses=2, total=10, so hit_ratio=8/10=0.8 (80%) and miss_ratio=2/10=0.2 (20%)
$total = $hits + $misses;
$metrics[$key] = [
'hits' => $hits,
'misses' => $misses,
'hit_ratio' => $hits + $misses > 0 ? $hits / ($hits + $misses) : 0,
'hit_ratio' => $total > 0 ? $hits / $total : 0,
'miss_ratio' => $total > 0 ? $misses / $total : 0,
'avg_hit_time' => $this->getHitAvgTime($key),
'avg_miss_time' => $this->getMissAvgTime($key),
'memory_usage' => $memory,
Expand Down
67 changes: 57 additions & 10 deletions ProcessMaker/Cache/Screens/LegacyScreenCacheAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

namespace ProcessMaker\Cache\Screens;

use DateInterval;
use Illuminate\Support\Facades\Storage;
use ProcessMaker\Cache\CacheInterface;
use ProcessMaker\Managers\ScreenCompiledManager;

class LegacyScreenCacheAdapter implements ScreenCacheInterface
class LegacyScreenCacheAdapter implements CacheInterface
{
protected ScreenCompiledManager $compiledManager;

Expand All @@ -17,14 +19,20 @@ public function __construct(ScreenCompiledManager $compiledManager)
/**
* Create a cache key for a screen
*/
public function createKey(int $processId, int $processVersionId, string $language, int $screenId, int $screenVersionId): string
public function createKey(array $params): string
{
// Validate required parameters
if (!isset($params['process_id'], $params['process_version_id'], $params['language'],
$params['screen_id'], $params['screen_version_id'])) {
throw new \InvalidArgumentException('Missing required parameters for screen cache key');
}

return $this->compiledManager->createKey(
(string) $processId,
(string) $processVersionId,
$language,
(string) $screenId,
(string) $screenVersionId
(string) $params['process_id'],
(string) $params['process_version_id'],
$params['language'],
(string) $params['screen_id'],
(string) $params['screen_version_id']
);
}

Expand All @@ -38,11 +46,40 @@ public function get(string $key, mixed $default = null): mixed
return $content ?? $default;
}

/**
* Get a value from the cache, or store the value from the callback if it doesn't exist
*
* @param string $key The key to look up
* @param callable $callback The callback that will return the value to store
* @return mixed The value from cache or the callback
* @throws \InvalidArgumentException
*/
public function getOrCache(string $key, callable $callback): mixed
{
$value = $this->get($key);

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

$value = $callback();
$this->set($key, $value);

return $value;
}

/**
* Store a screen in cache
*
* @param string $key The key of the item to store
* @param mixed $value The value of the item to store
* @param DateInterval|int|null $ttl Optional TTL value
* @return bool True on success and false on failure
*/
public function set(string $key, mixed $value): bool
public function set(string $key, mixed $value, DateInterval|int|null $ttl = null): bool
{
// Note: The legacy compiled manager doesn't support TTL,
// so we ignore the TTL parameter for backward compatibility
$this->compiledManager->storeCompiledContent($key, $value);

return true;
Expand Down Expand Up @@ -86,9 +123,19 @@ public function missing(string $key): bool
* @param int $screenId Screen ID
* @return bool
*/
public function invalidate(int $screenId, string $language): bool
public function invalidate($params): void
{
// Get all files from storage that match the pattern for this screen ID
return $this->compiledManager->deleteScreenCompiledContent($screenId, $language);
$screenId = $params['screen_id'];
$language = $params['language'];
$this->compiledManager->deleteScreenCompiledContent($screenId, $language);
}

/**
* Clear all compiled screen assets
*/
public function clearCompiledAssets(): void
{
$this->compiledManager->clearCompiledAssets();
}
}
Loading