From 10df63b8cfeb2e25cfc4b262c53ca4a1002cffab Mon Sep 17 00:00:00 2001 From: Rodrigo Quelca Date: Wed, 4 Dec 2024 08:58:57 -0400 Subject: [PATCH 1/8] FOUR-20915: Cache Performance Monitoring --- .../Monitoring/CacheMetricsDecorator.php | 92 ++++++ .../Monitoring/CacheMetricsInterface.php | 28 ++ .../Cache/Monitoring/RedisMetricsManager.php | 263 +++++++++++++++++ .../Commands/CacheMetricsClearCommand.php | 28 ++ .../Console/Commands/CacheMetricsCommand.php | 213 ++++++++++++++ .../Commands/CacheMetricsPopulateCommand.php | 111 ++++++++ .../Commands/CacheMetricsSummaryCommand.php | 169 +++++++++++ ProcessMaker/Console/Kernel.php | 8 + .../Providers/CacheServiceProvider.php | 94 ++++++ config/app.php | 1 + config/savedsearch.php | 19 ++ config/screens.php | 15 + .../Monitoring/CacheMetricsDecoratorTest.php | 225 +++++++++++++++ .../Monitoring/RedisMetricsManagerTest.php | 267 ++++++++++++++++++ .../Commands/CacheMetricsClearCommandTest.php | 48 ++++ 15 files changed, 1581 insertions(+) create mode 100644 ProcessMaker/Cache/Monitoring/CacheMetricsDecorator.php create mode 100644 ProcessMaker/Cache/Monitoring/CacheMetricsInterface.php create mode 100644 ProcessMaker/Cache/Monitoring/RedisMetricsManager.php create mode 100644 ProcessMaker/Console/Commands/CacheMetricsClearCommand.php create mode 100644 ProcessMaker/Console/Commands/CacheMetricsCommand.php create mode 100644 ProcessMaker/Console/Commands/CacheMetricsPopulateCommand.php create mode 100644 ProcessMaker/Console/Commands/CacheMetricsSummaryCommand.php create mode 100644 ProcessMaker/Providers/CacheServiceProvider.php create mode 100644 config/savedsearch.php create mode 100644 tests/unit/ProcessMaker/Cache/Monitoring/CacheMetricsDecoratorTest.php create mode 100644 tests/unit/ProcessMaker/Cache/Monitoring/RedisMetricsManagerTest.php create mode 100644 tests/unit/ProcessMaker/Console/Commands/CacheMetricsClearCommandTest.php diff --git a/ProcessMaker/Cache/Monitoring/CacheMetricsDecorator.php b/ProcessMaker/Cache/Monitoring/CacheMetricsDecorator.php new file mode 100644 index 0000000000..81fbd43eb0 --- /dev/null +++ b/ProcessMaker/Cache/Monitoring/CacheMetricsDecorator.php @@ -0,0 +1,92 @@ +cache = $cache; + $this->metrics = $metrics; + } + + public function get(string $key, mixed $default = null): mixed + { + $startTime = microtime(true); + $value = $this->cache->get($key, $default); + $endTime = microtime(true); + $duration = $endTime - $startTime; + + if ($value === $default) { + $this->metrics->recordMiss($key, $duration); + } else { + $this->metrics->recordHit($key, $duration); + } + + return $value; + } + + public function set(string $key, mixed $value, null|int|\DateInterval $ttl = null): bool + { + $result = $this->cache->set($key, $value, $ttl); + + if ($result) { + // Calculate approximate size in bytes + $size = $this->calculateSize($value); + $this->metrics->recordWrite($key, $size); + } + + return $result; + } + + public function delete(string $key): bool + { + return $this->cache->delete($key); + } + + public function clear(): bool + { + return $this->cache->clear(); + } + + public function has(string $key): bool + { + return $this->cache->has($key); + } + + public function missing(string $key): bool + { + return $this->cache->missing($key); + } + + protected function calculateSize(mixed $value): int + { + if (is_string($value)) { + return strlen($value); + } + + if (is_array($value) || is_object($value)) { + return strlen(serialize($value)); + } + + if (is_int($value)) { + return PHP_INT_SIZE; + } + + if (is_float($value)) { + return 8; // typical double size + } + + if (is_bool($value)) { + return 1; + } + + return 0; // for null or other types + } +} diff --git a/ProcessMaker/Cache/Monitoring/CacheMetricsInterface.php b/ProcessMaker/Cache/Monitoring/CacheMetricsInterface.php new file mode 100644 index 0000000000..dc8ffc4d1e --- /dev/null +++ b/ProcessMaker/Cache/Monitoring/CacheMetricsInterface.php @@ -0,0 +1,28 @@ +hincrby($baseKey, self::HITS_KEY, 1); + $pipe->rpush($baseKey . ':' . self::HIT_TIMES_KEY, $microtime); + $pipe->ltrim($baseKey . ':' . self::HIT_TIMES_KEY, -100, -1); + }); + } + + /** + * Record a cache miss + * + * @param string $key Cache key + * @param float $microtime Time taken in microseconds + */ + public function recordMiss(string $key, $microtime): void + { + $baseKey = self::METRICS_PREFIX . $key; + Redis::pipeline(function ($pipe) use ($baseKey, $microtime) { + $pipe->hincrby($baseKey, self::MISSES_KEY, 1); + $pipe->rpush($baseKey . ':' . self::MISS_TIMES_KEY, $microtime); + $pipe->ltrim($baseKey . ':' . self::MISS_TIMES_KEY, -100, -1); + }); + } + + /** + * Record a cache write operation + * + * @param string $key Cache key + * @param int $size Size in bytes + */ + public function recordWrite(string $key, int $size): void + { + $baseKey = self::METRICS_PREFIX . $key; + Redis::pipeline(function ($pipe) use ($baseKey, $size) { + $pipe->hset($baseKey, self::MEMORY_KEY, $size); + $pipe->hset($baseKey, self::LAST_WRITE_KEY, microtime(true)); + }); + } + + /** + * Get hit rate for a specific key + * + * @param string $key Cache key + * @return float Hit rate percentage (0-1) + */ + public function getHitRate(string $key): float + { + $baseKey = self::METRICS_PREFIX . $key; + $hits = (int) Redis::hget($baseKey, self::HITS_KEY) ?: 0; + $misses = (int) Redis::hget($baseKey, self::MISSES_KEY) ?: 0; + $total = $hits + $misses; + + return $total > 0 ? $hits / $total : 0; + } + + /** + * Get miss rate for a specific key + * + * @param string $key Cache key + * @return float Miss rate percentage (0-1) + */ + public function getMissRate(string $key): float + { + $baseKey = self::METRICS_PREFIX . $key; + $hits = (int) Redis::hget($baseKey, self::HITS_KEY) ?: 0; + $misses = (int) Redis::hget($baseKey, self::MISSES_KEY) ?: 0; + $total = $hits + $misses; + + return $total > 0 ? $misses / $total : 0; + } + + /** + * Get average hit time for a specific key + * + * @param string $key Cache key + * @return float Average hit time in seconds + */ + public function getHitAvgTime(string $key): float + { + $times = Redis::lrange(self::METRICS_PREFIX . $key . ':' . self::HIT_TIMES_KEY, 0, -1); + if (empty($times)) { + return 0; + } + + return array_sum(array_map('floatval', $times)) / count($times); + } + + /** + * Get average miss time for a specific key + * + * @param string $key Cache key + * @return float Average miss time in seconds + */ + public function getMissAvgTime(string $key): float + { + $times = Redis::lrange(self::METRICS_PREFIX . $key . ':' . self::MISS_TIMES_KEY, 0, -1); + if (empty($times)) { + return 0; + } + + return array_sum(array_map('floatval', $times)) / count($times); + } + + /** + * Get top accessed keys + * + * @param int $count Number of keys to return + * @return array Top keys with their metrics + */ + public function getTopKeys(int $count = 5): array + { + $keys = Redis::keys(self::METRICS_PREFIX . '*'); + $metrics = []; + + foreach ($keys as $redisKey) { + if (str_contains($redisKey, ':' . self::HIT_TIMES_KEY) || + str_contains($redisKey, ':' . self::MISS_TIMES_KEY)) { + continue; + } + + $key = str_replace(self::METRICS_PREFIX, '', $redisKey); + $hits = (int) Redis::hget($redisKey, self::HITS_KEY) ?: 0; + $misses = (int) Redis::hget($redisKey, self::MISSES_KEY) ?: 0; + $total = $hits + $misses; + + if ($total > 0) { + $metrics[$key] = [ + 'key' => $key, + 'hits' => $hits, + 'misses' => $misses, + 'total_accesses' => $total, + 'hit_ratio' => $hits / $total, + 'miss_ratio' => $misses / $total, + 'avg_hit_time' => $this->getHitAvgTime($key), + 'avg_miss_time' => $this->getMissAvgTime($key), + 'memory_usage' => $this->getMemoryUsage($key)['current_size'], + 'last_write' => Redis::hget($redisKey, self::LAST_WRITE_KEY), + ]; + } + } + + uasort($metrics, fn ($a, $b) => $b['total_accesses'] <=> $a['total_accesses']); + + return array_slice($metrics, 0, $count, true); + } + + /** + * Get memory usage for a specific key + * + * @param string $key Cache key + * @return array Memory usage statistics + */ + public function getMemoryUsage(string $key): array + { + $baseKey = self::METRICS_PREFIX . $key; + $currentSize = (int) Redis::hget($baseKey, self::MEMORY_KEY) ?: 0; + $lastWrite = Redis::hget($baseKey, self::LAST_WRITE_KEY); + + return [ + 'current_size' => $currentSize, + 'last_write' => $lastWrite ? (float) $lastWrite : null, + ]; + } + + /** + * Reset all metrics + */ + public function resetMetrics(): void + { + $keys = Redis::keys(self::METRICS_PREFIX . '*'); + if (!empty($keys)) { + Redis::del(...$keys); + } + } + + /** + * Get summary of all metrics + * + * @return array Summary statistics + */ + public function getSummary(): array + { + $keys = Redis::keys(self::METRICS_PREFIX . '*'); + $metrics = []; + $totalHits = 0; + $totalMisses = 0; + $totalMemory = 0; + $totalHitTime = 0; + $totalMissTime = 0; + $keyCount = 0; + + foreach ($keys as $redisKey) { + if (str_contains($redisKey, ':' . self::HIT_TIMES_KEY) || + str_contains($redisKey, ':' . self::MISS_TIMES_KEY)) { + continue; + } + + $key = str_replace(self::METRICS_PREFIX, '', $redisKey); + $hits = (int) Redis::hget($redisKey, self::HITS_KEY) ?: 0; + $misses = (int) Redis::hget($redisKey, self::MISSES_KEY) ?: 0; + $memory = (int) Redis::hget($redisKey, self::MEMORY_KEY) ?: 0; + + $totalHits += $hits; + $totalMisses += $misses; + $totalMemory += $memory; + $totalHitTime += $this->getHitAvgTime($key); + $totalMissTime += $this->getMissAvgTime($key); + + $metrics[$key] = [ + 'hits' => $hits, + 'misses' => $misses, + 'hit_ratio' => $hits + $misses > 0 ? $hits / ($hits + $misses) : 0, + 'avg_hit_time' => $this->getHitAvgTime($key), + 'avg_miss_time' => $this->getMissAvgTime($key), + 'memory_usage' => $memory, + ]; + + $keyCount++; + } + + $total = $totalHits + $totalMisses; + + return [ + 'keys' => $metrics, + 'overall_hit_ratio' => $total > 0 ? $totalHits / $total : 0, + 'overall_miss_ratio' => $total > 0 ? $totalMisses / $total : 0, + 'avg_hit_time' => $keyCount > 0 ? $totalHitTime / $keyCount : 0, + 'avg_miss_time' => $keyCount > 0 ? $totalMissTime / $keyCount : 0, + 'total_memory_usage' => $totalMemory, + 'total_keys' => $keyCount, + ]; + } +} diff --git a/ProcessMaker/Console/Commands/CacheMetricsClearCommand.php b/ProcessMaker/Console/Commands/CacheMetricsClearCommand.php new file mode 100644 index 0000000000..db446bebdb --- /dev/null +++ b/ProcessMaker/Console/Commands/CacheMetricsClearCommand.php @@ -0,0 +1,28 @@ +metrics = $metrics; + } + + public function handle() + { + $this->info('Clearing all cache metrics data...'); + $this->metrics->resetMetrics(); + $this->info('Cache metrics data cleared successfully!'); + } +} diff --git a/ProcessMaker/Console/Commands/CacheMetricsCommand.php b/ProcessMaker/Console/Commands/CacheMetricsCommand.php new file mode 100644 index 0000000000..87394681b8 --- /dev/null +++ b/ProcessMaker/Console/Commands/CacheMetricsCommand.php @@ -0,0 +1,213 @@ +metrics = $metrics; + } + + public function handle() + { + $key = $this->option('key'); + $type = $this->option('type'); + $format = $this->option('format'); + + if ($key) { + $this->displayKeyMetrics($key, $format); + } else { + $this->displaySummary($type, $format); + } + + return 0; + } + + protected function displayKeyMetrics(string $key, string $format): void + { + $hitRate = $this->metrics->getHitRate($key); + $missRate = $this->metrics->getMissRate($key); + $avgHitTime = $this->metrics->getHitAvgTime($key); + $avgMissTime = $this->metrics->getMissAvgTime($key); + $memory = $this->metrics->getMemoryUsage($key); + + $data = [ + 'key' => $key, + 'hit_rate' => $hitRate, + 'miss_rate' => $missRate, + 'avg_hit_time' => $avgHitTime, + 'avg_miss_time' => $avgMissTime, + 'memory_usage' => $memory, + ]; + + if ($format === 'json') { + $this->line(json_encode($data, JSON_PRETTY_PRINT)); + + return; + } + + $this->info("Cache Metrics for key: {$key}"); + $this->newLine(); + + $table = new Table($this->output); + $table->setHeaders(['Metric', 'Value']); + $table->setRows([ + ['Hit Rate', $this->formatPercentage($hitRate)], + ['Miss Rate', $this->formatPercentage($missRate)], + ['Avg Hit Time', sprintf('%.4f sec', $avgHitTime)], + ['Avg Miss Time', sprintf('%.4f sec', $avgMissTime)], + ['Memory Usage', $this->formatBytes($memory['current_size'])], + ['Last Write', $memory['last_write'] ? date('Y-m-d H:i:s', $memory['last_write']) : 'Never'], + ]); + $table->render(); + } + + protected function displaySummary(?string $type, string $format): void + { + $summary = $this->metrics->getSummary(); + + if ($type) { + $summary = $this->filterByType($summary, $type); + } + + if ($format === 'json') { + $this->line(json_encode($summary, JSON_PRETTY_PRINT)); + + return; + } + + $this->info('Cache Performance Summary'); + $this->newLine(); + + // Overall Statistics + $table = new Table($this->output); + $table->setHeaders(['Metric', 'Value']); + $table->setRows([ + ['Total Keys', number_format($summary['total_keys'])], + ['Overall Hit Ratio', $this->formatPercentage($summary['overall_hit_ratio'])], + ['Overall Miss Ratio', $this->formatPercentage($summary['overall_miss_ratio'])], + ['Average Hit Time', sprintf('%.4f sec', $summary['avg_hit_time'])], + ['Average Miss Time', sprintf('%.4f sec', $summary['avg_miss_time'])], + ['Total Memory Usage', $this->formatBytes($summary['total_memory_usage'])], + ]); + $table->render(); + + // Display key details if there are any + if (!empty($summary['keys'])) { + $this->newLine(); + $this->info('Key Details'); + $this->newLine(); + + $table = new Table($this->output); + $table->setHeaders([ + 'Key', + 'Hits', + 'Hit Ratio', + 'Avg Time', + 'Memory', + 'Status', + ]); + + foreach ($summary['keys'] as $key => $metrics) { + $table->addRow([ + $key, + number_format($metrics['hits']), + $this->formatPercentage($metrics['hit_ratio']), + sprintf('%.4f sec', $metrics['avg_hit_time']), + $this->formatBytes($metrics['memory_usage']), + $this->getPerformanceStatus($metrics['hit_ratio']), + ]); + } + + $table->render(); + } + } + + protected function filterByType(array $summary, string $type): array + { + $filtered = [ + 'keys' => [], + 'total_keys' => 0, + 'overall_hit_ratio' => 0, + 'overall_miss_ratio' => 0, + 'avg_hit_time' => 0, + 'avg_miss_time' => 0, + 'total_memory_usage' => 0, + ]; + + foreach ($summary['keys'] as $key => $metrics) { + if ($this->getKeyType($key) === $type) { + $filtered['keys'][$key] = $metrics; + $filtered['total_keys']++; + $filtered['total_memory_usage'] += $metrics['memory_usage']; + $filtered['avg_hit_time'] += $metrics['avg_hit_time']; + $filtered['avg_miss_time'] += $metrics['avg_miss_time']; + } + } + + if ($filtered['total_keys'] > 0) { + $filtered['avg_hit_time'] /= $filtered['total_keys']; + $filtered['avg_miss_time'] /= $filtered['total_keys']; + } + + return $filtered; + } + + protected function getKeyType(string $key): string + { + if (str_starts_with($key, 'screen_')) { + return 'legacy'; + } + if (str_starts_with($key, 'pid_')) { + return 'new'; + } + + return 'unknown'; + } + + protected function formatPercentage(float $value): string + { + return sprintf('%.2f%%', $value * 100); + } + + protected function formatBytes(int $bytes): string + { + $units = ['B', 'KB', 'MB', 'GB']; + $bytes = max($bytes, 0); + $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); + $pow = min($pow, count($units) - 1); + $bytes /= pow(1024, $pow); + + return sprintf('%.2f %s', $bytes, $units[$pow]); + } + + protected function getPerformanceStatus(float $hitRatio): string + { + if ($hitRatio >= 0.95) { + return '✅ Excellent'; + } + if ($hitRatio >= 0.80) { + return '✓ Good'; + } + if ($hitRatio >= 0.60) { + return '⚠️ Fair'; + } + + return '❌ Poor'; + } +} diff --git a/ProcessMaker/Console/Commands/CacheMetricsPopulateCommand.php b/ProcessMaker/Console/Commands/CacheMetricsPopulateCommand.php new file mode 100644 index 0000000000..95cf3a1f43 --- /dev/null +++ b/ProcessMaker/Console/Commands/CacheMetricsPopulateCommand.php @@ -0,0 +1,111 @@ +metrics = $metrics; + } + + public function handle() + { + $numKeys = (int) $this->option('keys'); + $type = $this->option('type'); + + $this->info("Populating {$numKeys} fake cache metrics..."); + + // Reset existing metrics + $this->metrics->resetMetrics(); + + // Generate keys based on type + $keys = $this->generateKeys($numKeys, $type); + + // Populate metrics for each key + foreach ($keys as $key) { + $this->populateKeyMetrics($key); + $this->output->write('.'); + } + + $this->newLine(); + $this->info('Done! You can now run cache:metrics to see the data.'); + } + + protected function generateKeys(int $count, string $type): array + { + $keys = []; + $prefixes = $this->getPrefixes($type); + + for ($i = 0; $i < $count; $i++) { + $prefix = $prefixes[array_rand($prefixes)]; + $id = rand(1, 1000); + + if ($prefix === 'screen_') { + $keys[] = "screen_{$id}_version_" . rand(1, 5); + } else { + $keys[] = "pid_{$id}_{$id}_en_sid_{$id}_" . rand(1, 5); + } + } + + return $keys; + } + + protected function getPrefixes(string $type): array + { + return match ($type) { + 'legacy' => ['screen_'], + 'new' => ['pid_'], + default => ['screen_', 'pid_'], + }; + } + + protected function populateKeyMetrics(string $key): void + { + // Generate random number of hits (0-1000) + $hits = rand(0, 1000); + + // Generate random number of misses (0-200) + $misses = rand(0, 200); + + // Record hits + for ($i = 0; $i < $hits; $i++) { + $time = $this->generateHitTime(); + $this->metrics->recordHit($key, $time); + } + + // Record misses + for ($i = 0; $i < $misses; $i++) { + $time = $this->generateMissTime(); + $this->metrics->recordMiss($key, $time); + } + + // Record memory usage (10KB - 1MB) + $size = rand(10 * 1024, 1024 * 1024); + $this->metrics->recordWrite($key, $size); + } + + protected function generateHitTime(): float + { + // Generate hit time between 0.001 and 0.1 seconds + return rand(1000, 100000) / 1000000; + } + + protected function generateMissTime(): float + { + // Generate miss time between 0.1 and 1 second + return rand(100000, 1000000) / 1000000; + } +} diff --git a/ProcessMaker/Console/Commands/CacheMetricsSummaryCommand.php b/ProcessMaker/Console/Commands/CacheMetricsSummaryCommand.php new file mode 100644 index 0000000000..11d65c1cdf --- /dev/null +++ b/ProcessMaker/Console/Commands/CacheMetricsSummaryCommand.php @@ -0,0 +1,169 @@ +metrics = $metrics; + } + + public function handle() + { + $days = (int) $this->option('days'); + $type = $this->option('type'); + $format = $this->option('format'); + + $summary = $this->metrics->getSummary(); + $topKeys = $this->metrics->getTopKeys(10); + + if ($format === 'json') { + $this->outputJson($summary, $topKeys); + + return; + } + + $this->outputTable($summary, $topKeys); + } + + protected function outputJson(array $summary, array $topKeys): void + { + $data = [ + 'summary' => $summary, + 'top_keys' => $topKeys, + 'generated_at' => now()->toIso8601String(), + ]; + + $this->line(json_encode($data, JSON_PRETTY_PRINT)); + } + + protected function outputTable(array $summary, array $topKeys): void + { + // Overall Statistics + $this->info('Cache Performance Summary'); + $this->newLine(); + + $table = new Table($this->output); + $table->setHeaders(['Metric', 'Value']); + $table->setRows([ + ['Total Keys', number_format($summary['total_keys'])], + ['Overall Hit Ratio', $this->formatPercentage($summary['overall_hit_ratio'])], + ['Overall Miss Ratio', $this->formatPercentage($summary['overall_miss_ratio'])], + ['Average Hit Time', sprintf('%.4f sec', $summary['avg_hit_time'])], + ['Average Miss Time', sprintf('%.4f sec', $summary['avg_miss_time'])], + ['Total Memory Usage', $this->formatBytes($summary['total_memory_usage'])], + ]); + $table->render(); + + // Performance Insights + $this->newLine(); + $this->info('Performance Insights'); + $this->newLine(); + + $insights = $this->generateInsights($summary); + foreach ($insights as $insight) { + $this->line(" • {$insight}"); + } + + // Top Keys + $this->newLine(); + $this->info('Top 10 Most Accessed Keys'); + $this->newLine(); + + $table = new Table($this->output); + $table->setHeaders([ + 'Key', + 'Hits', + 'Hit Ratio', + 'Avg Time', + 'Memory', + 'Status', + ]); + + foreach ($topKeys as $key => $metrics) { + $table->addRow([ + $key, + number_format($metrics['hits']), + $this->formatPercentage($metrics['hit_ratio']), + sprintf('%.4f sec', $metrics['avg_hit_time']), + $this->formatBytes($metrics['memory_usage']), + $this->getPerformanceStatus($metrics['hit_ratio']), + ]); + } + + $table->render(); + } + + protected function generateInsights(array $summary): array + { + $insights = []; + + // Hit ratio insights + if ($summary['overall_hit_ratio'] < 0.5) { + $insights[] = 'Low hit ratio indicates potential cache configuration issues'; + } elseif ($summary['overall_hit_ratio'] > 0.9) { + $insights[] = 'Excellent hit ratio shows effective cache utilization'; + } + + // Response time insights + if ($summary['avg_miss_time'] > $summary['avg_hit_time'] * 10) { + $insights[] = 'High miss penalty suggests optimization opportunities'; + } + + // Memory usage insights + $avgMemoryPerKey = $summary['total_memory_usage'] / ($summary['total_keys'] ?: 1); + if ($avgMemoryPerKey > 1024 * 1024) { // 1MB per key + $insights[] = 'High average memory usage per key'; + } + + return $insights; + } + + protected function formatPercentage(float $value): string + { + return sprintf('%.2f%%', $value * 100); + } + + protected function formatBytes(int $bytes): string + { + $units = ['B', 'KB', 'MB', 'GB']; + $bytes = max($bytes, 0); + $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); + $pow = min($pow, count($units) - 1); + $bytes /= pow(1024, $pow); + + return sprintf('%.2f %s', $bytes, $units[$pow]); + } + + protected function getPerformanceStatus(float $hitRatio): string + { + if ($hitRatio >= 0.95) { + return '✅ Excellent'; + } + if ($hitRatio >= 0.80) { + return '✓ Good'; + } + if ($hitRatio >= 0.60) { + return '⚠️ Fair'; + } + + return '❌ Poor'; + } +} diff --git a/ProcessMaker/Console/Kernel.php b/ProcessMaker/Console/Kernel.php index b4bb48cf3e..824bf35488 100644 --- a/ProcessMaker/Console/Kernel.php +++ b/ProcessMaker/Console/Kernel.php @@ -41,6 +41,14 @@ protected function schedule(Schedule $schedule) $schedule->command('processmaker:sync-screen-templates --queue') ->daily(); + + $schedule->command('cache:metrics --format=json > storage/logs/cache-metrics.json') + ->hourly() + ->onOneServer(); + + $schedule->command('cache:metrics-summary') + ->dailyAt('23:55') + ->onOneServer(); } /** diff --git a/ProcessMaker/Providers/CacheServiceProvider.php b/ProcessMaker/Providers/CacheServiceProvider.php new file mode 100644 index 0000000000..e1f6282f68 --- /dev/null +++ b/ProcessMaker/Providers/CacheServiceProvider.php @@ -0,0 +1,94 @@ +app->singleton(RedisMetricsManager::class); + $this->app->bind(CacheMetricsInterface::class, RedisMetricsManager::class); + + // Register screen cache with metrics + $this->app->singleton(ScreenCacheManager::class, function ($app) { + $cache = new ScreenCacheManager( + $app['cache'], + $app->make(ScreenCompiledManager::class) + ); + + return new CacheMetricsDecorator( + $cache, + $app->make(RedisMetricsManager::class) + ); + }); + + // Register settings cache with metrics + $this->app->singleton(SettingCacheManager::class, function ($app) { + $cache = new SettingCacheManager($app['cache']); + + return new CacheMetricsDecorator( + $cache, + $app->make(RedisMetricsManager::class) + ); + }); + + // Register legacy screen cache with metrics + $this->app->bind(LegacyScreenCacheAdapter::class, function ($app) { + $cache = new LegacyScreenCacheAdapter( + $app->make(ScreenCompiledManager::class) + ); + + return new CacheMetricsDecorator( + $cache, + $app->make(RedisMetricsManager::class) + ); + }); + + // Update the screen cache factory to use the metrics-enabled instances + $this->app->extend(ScreenCacheFactory::class, function ($factory, $app) { + return new class($app) extends ScreenCacheFactory { + protected $app; + + public function __construct($app) + { + $this->app = $app; + } + + public static function create(): ScreenCacheInterface + { + $manager = config('screens.cache.manager', 'legacy'); + + if ($manager === 'new') { + return app(ScreenCacheManager::class); + } + + return app(LegacyScreenCacheAdapter::class); + } + }; + }); + } + + public function boot(): void + { + // Register the metrics commands + if ($this->app->runningInConsole()) { + $this->commands([ + \ProcessMaker\Console\Commands\CacheMetricsCommand::class, + \ProcessMaker\Console\Commands\CacheMetricsSummaryCommand::class, + \ProcessMaker\Console\Commands\CacheMetricsPopulateCommand::class, + \ProcessMaker\Console\Commands\CacheMetricsClearCommand::class, + ]); + } + } +} diff --git a/config/app.php b/config/app.php index 07a788a4b1..5b7019909c 100644 --- a/config/app.php +++ b/config/app.php @@ -188,6 +188,7 @@ ProcessMaker\Providers\OauthMailServiceProvider::class, ProcessMaker\Providers\OpenAiServiceProvider::class, ProcessMaker\Providers\LicenseServiceProvider::class, + ProcessMaker\Providers\CacheServiceProvider::class, ])->toArray(), 'aliases' => Facade::defaultAliases()->merge([ diff --git a/config/savedsearch.php b/config/savedsearch.php new file mode 100644 index 0000000000..bd3fdedb7a --- /dev/null +++ b/config/savedsearch.php @@ -0,0 +1,19 @@ + env('SAVED_SEARCH_COUNT', true), + 'add_defaults' => env('SAVED_SEARCH_ADD_DEFAULTS', true), + +]; diff --git a/config/screens.php b/config/screens.php index 4635f58c3c..10b838f944 100644 --- a/config/screens.php +++ b/config/screens.php @@ -19,5 +19,20 @@ // Default TTL for cached screens (24 hours) 'ttl' => env('SCREEN_CACHE_TTL', 86400), + + // Cache metrics configuration + 'metrics' => [ + // Enable or disable cache metrics + 'enabled' => env('CACHE_METRICS_ENABLED', true), + + // Maximum number of timing samples to keep per key + 'max_samples' => env('CACHE_METRICS_MAX_SAMPLES', 100), + + // Redis prefix for metrics data + 'redis_prefix' => env('CACHE_METRICS_PREFIX', 'cache:metrics:'), + + // How long to keep metrics data (in seconds) + 'ttl' => env('CACHE_METRICS_TTL', 86400), // 24 hours + ], ], ]; diff --git a/tests/unit/ProcessMaker/Cache/Monitoring/CacheMetricsDecoratorTest.php b/tests/unit/ProcessMaker/Cache/Monitoring/CacheMetricsDecoratorTest.php new file mode 100644 index 0000000000..3865cd3df0 --- /dev/null +++ b/tests/unit/ProcessMaker/Cache/Monitoring/CacheMetricsDecoratorTest.php @@ -0,0 +1,225 @@ +cache = Mockery::mock(CacheInterface::class); + $this->metrics = Mockery::mock(CacheMetricsInterface::class); + + // Create decorator with mocks + $this->decorator = new CacheMetricsDecorator($this->cache, $this->metrics); + } + + public function testGetWithHit() + { + // Setup expectations for cache hit + $this->cache->shouldReceive('get') + ->once() + ->with($this->testKey, null) + ->andReturn($this->testValue); + + $this->metrics->shouldReceive('recordHit') + ->once() + ->withArgs(function ($key, $time) { + return $key === $this->testKey && is_float($time); + }); + + // Execute and verify + $result = $this->decorator->get($this->testKey); + $this->assertEquals($this->testValue, $result); + } + + public function testGetWithMiss() + { + $default = 'default_value'; + + // Setup expectations for cache miss + $this->cache->shouldReceive('get') + ->once() + ->with($this->testKey, $default) + ->andReturn($default); + + $this->metrics->shouldReceive('recordMiss') + ->once() + ->withArgs(function ($key, $time) { + return $key === $this->testKey && is_float($time); + }); + + // Execute and verify + $result = $this->decorator->get($this->testKey, $default); + $this->assertEquals($default, $result); + } + + public function testSetSuccess() + { + $ttl = 3600; + + // Setup expectations + $this->cache->shouldReceive('set') + ->once() + ->with($this->testKey, $this->testValue, $ttl) + ->andReturn(true); + + $this->metrics->shouldReceive('recordWrite') + ->once() + ->withArgs(function ($key, $size) { + return $key === $this->testKey && is_int($size) && $size > 0; + }); + + // Execute and verify + $result = $this->decorator->set($this->testKey, $this->testValue, $ttl); + $this->assertTrue($result); + } + + public function testSetFailure() + { + // Setup expectations + $this->cache->shouldReceive('set') + ->once() + ->with($this->testKey, $this->testValue, null) + ->andReturn(false); + + $this->metrics->shouldNotReceive('recordWrite'); + + // Execute and verify + $result = $this->decorator->set($this->testKey, $this->testValue); + $this->assertFalse($result); + } + + public function testDelete() + { + // Setup expectations + $this->cache->shouldReceive('delete') + ->once() + ->with($this->testKey) + ->andReturn(true); + + // Execute and verify + $result = $this->decorator->delete($this->testKey); + $this->assertTrue($result); + } + + public function testClear() + { + // Setup expectations + $this->cache->shouldReceive('clear') + ->once() + ->andReturn(true); + + // Execute and verify + $result = $this->decorator->clear(); + $this->assertTrue($result); + } + + public function testHas() + { + // Setup expectations + $this->cache->shouldReceive('has') + ->once() + ->with($this->testKey) + ->andReturn(true); + + // Execute and verify + $result = $this->decorator->has($this->testKey); + $this->assertTrue($result); + } + + public function testMissing() + { + // Setup expectations + $this->cache->shouldReceive('missing') + ->once() + ->with($this->testKey) + ->andReturn(true); + + // Execute and verify + $result = $this->decorator->missing($this->testKey); + $this->assertTrue($result); + } + + public function testCalculateSizeWithString() + { + $value = 'test'; + $result = $this->invokeCalculateSize($value); + $this->assertEquals(strlen($value), $result); + } + + public function testCalculateSizeWithArray() + { + $value = ['test' => 'value']; + $result = $this->invokeCalculateSize($value); + $this->assertEquals(strlen(serialize($value)), $result); + } + + public function testCalculateSizeWithObject() + { + $value = new \stdClass(); + $value->test = 'value'; + $result = $this->invokeCalculateSize($value); + $this->assertEquals(strlen(serialize($value)), $result); + } + + public function testCalculateSizeWithInteger() + { + $value = 42; + $result = $this->invokeCalculateSize($value); + $this->assertEquals(PHP_INT_SIZE, $result); + } + + public function testCalculateSizeWithFloat() + { + $value = 3.14; + $result = $this->invokeCalculateSize($value); + $this->assertEquals(8, $result); + } + + public function testCalculateSizeWithBoolean() + { + $value = true; + $result = $this->invokeCalculateSize($value); + $this->assertEquals(1, $result); + } + + public function testCalculateSizeWithNull() + { + $value = null; + $result = $this->invokeCalculateSize($value); + $this->assertEquals(0, $result); + } + + protected function invokeCalculateSize($value) + { + $method = new \ReflectionMethod(CacheMetricsDecorator::class, 'calculateSize'); + $method->setAccessible(true); + + return $method->invoke($this->decorator, $value); + } + + protected function tearDown(): void + { + Mockery::close(); + parent::tearDown(); + } +} diff --git a/tests/unit/ProcessMaker/Cache/Monitoring/RedisMetricsManagerTest.php b/tests/unit/ProcessMaker/Cache/Monitoring/RedisMetricsManagerTest.php new file mode 100644 index 0000000000..1a19a9021c --- /dev/null +++ b/tests/unit/ProcessMaker/Cache/Monitoring/RedisMetricsManagerTest.php @@ -0,0 +1,267 @@ +metrics = new RedisMetricsManager(); + + // Clear any existing metrics before each test + $this->metrics->resetMetrics(); + } + + public function testRecordHit() + { + $time = 0.1; + $this->metrics->recordHit($this->testKey, $time); + + $baseKey = $this->metricsPrefix . $this->testKey; + $hits = Redis::hget($baseKey, 'hits'); + $times = Redis::lrange($baseKey . ':hit_times', 0, -1); + + $this->assertEquals(1, $hits); + $this->assertCount(1, $times); + $this->assertEquals($time, (float) $times[0]); + } + + public function testRecordHitLimitsTimesList() + { + // Record more than 100 hits + for ($i = 0; $i < 110; $i++) { + $this->metrics->recordHit($this->testKey, 0.1); + } + + $baseKey = $this->metricsPrefix . $this->testKey; + $times = Redis::lrange($baseKey . ':hit_times', 0, -1); + + // Should only keep last 100 times + $this->assertCount(100, $times); + } + + public function testRecordMiss() + { + $time = 0.2; + $this->metrics->recordMiss($this->testKey, $time); + + $baseKey = $this->metricsPrefix . $this->testKey; + $misses = Redis::hget($baseKey, 'misses'); + $times = Redis::lrange($baseKey . ':miss_times', 0, -1); + + $this->assertEquals(1, $misses); + $this->assertCount(1, $times); + $this->assertEquals($time, (float) $times[0]); + } + + public function testRecordMissLimitsTimesList() + { + // Record more than 100 misses + for ($i = 0; $i < 110; $i++) { + $this->metrics->recordMiss($this->testKey, 0.2); + } + + $baseKey = $this->metricsPrefix . $this->testKey; + $times = Redis::lrange($baseKey . ':miss_times', 0, -1); + + // Should only keep last 100 times + $this->assertCount(100, $times); + } + + public function testRecordWrite() + { + $size = 1024; + $this->metrics->recordWrite($this->testKey, $size); + + $baseKey = $this->metricsPrefix . $this->testKey; + $storedSize = Redis::hget($baseKey, 'memory'); + $lastWrite = Redis::hget($baseKey, 'last_write'); + + $this->assertEquals($size, $storedSize); + $this->assertNotNull($lastWrite); + $this->assertIsNumeric($lastWrite); + $this->assertLessThanOrEqual(microtime(true), (float) $lastWrite); + } + + public function testGetHitRate() + { + // Record 2 hits and 1 miss + $this->metrics->recordHit($this->testKey, 0.1); + $this->metrics->recordHit($this->testKey, 0.1); + $this->metrics->recordMiss($this->testKey, 0.2); + + $hitRate = $this->metrics->getHitRate($this->testKey); + $this->assertEqualsWithDelta(2 / 3, $hitRate, 0.0001); + } + + public function testGetHitRateWithNoAccesses() + { + $hitRate = $this->metrics->getHitRate($this->testKey); + $this->assertEquals(0, $hitRate); + } + + public function testGetMissRate() + { + // Record 2 hits and 1 miss + $this->metrics->recordHit($this->testKey, 0.1); + $this->metrics->recordHit($this->testKey, 0.1); + $this->metrics->recordMiss($this->testKey, 0.2); + + $missRate = $this->metrics->getMissRate($this->testKey); + $this->assertEqualsWithDelta(1 / 3, $missRate, 0.0001); + } + + public function testGetMissRateWithNoAccesses() + { + $missRate = $this->metrics->getMissRate($this->testKey); + $this->assertEquals(0, $missRate); + } + + public function testGetHitAvgTime() + { + $this->metrics->recordHit($this->testKey, 0.1); + $this->metrics->recordHit($this->testKey, 0.3); + + $avgTime = $this->metrics->getHitAvgTime($this->testKey); + $this->assertEqualsWithDelta(0.2, $avgTime, 0.0001); + } + + public function testGetHitAvgTimeWithNoHits() + { + $avgTime = $this->metrics->getHitAvgTime($this->testKey); + $this->assertEquals(0, $avgTime); + } + + public function testGetMissAvgTime() + { + $this->metrics->recordMiss($this->testKey, 0.2); + $this->metrics->recordMiss($this->testKey, 0.4); + + $avgTime = $this->metrics->getMissAvgTime($this->testKey); + $this->assertEqualsWithDelta(0.3, $avgTime, 0.0001); + } + + public function testGetMissAvgTimeWithNoMisses() + { + $avgTime = $this->metrics->getMissAvgTime($this->testKey); + $this->assertEquals(0, $avgTime); + } + + public function testGetTopKeys() + { + // Setup test data + $this->metrics->recordHit('key1', 0.1); + $this->metrics->recordHit('key1', 0.1); + $this->metrics->recordMiss('key1', 0.2); + $this->metrics->recordWrite('key1', 1000); + + $this->metrics->recordHit('key2', 0.1); + $this->metrics->recordWrite('key2', 500); + + $topKeys = $this->metrics->getTopKeys(2); + + $this->assertCount(2, $topKeys); + $this->assertEquals('key1', $topKeys['key1']['key']); + $this->assertEquals(3, $topKeys['key1']['total_accesses']); + $this->assertEquals(1000, $topKeys['key1']['memory_usage']); + $this->assertEqualsWithDelta(2 / 3, $topKeys['key1']['hit_ratio'], 0.0001); + $this->assertEqualsWithDelta(1 / 3, $topKeys['key1']['miss_ratio'], 0.0001); + } + + public function testGetTopKeysWithNoKeys() + { + $topKeys = $this->metrics->getTopKeys(5); + $this->assertEmpty($topKeys); + } + + public function testGetMemoryUsage() + { + $size = 2048; + $this->metrics->recordWrite($this->testKey, $size); + + $usage = $this->metrics->getMemoryUsage($this->testKey); + + $this->assertEquals($size, $usage['current_size']); + $this->assertNotNull($usage['last_write']); + $this->assertIsFloat($usage['last_write']); + } + + public function testGetMemoryUsageForNonexistentKey() + { + $usage = $this->metrics->getMemoryUsage('nonexistent_key'); + + $this->assertEquals(0, $usage['current_size']); + $this->assertNull($usage['last_write']); + } + + public function testResetMetrics() + { + // Add some test data + $this->metrics->recordHit($this->testKey, 0.1); + $this->metrics->recordMiss($this->testKey, 0.2); + $this->metrics->recordWrite($this->testKey, 1024); + + // Reset metrics + $this->metrics->resetMetrics(); + + // Verify all metrics are cleared + $keys = Redis::keys($this->metricsPrefix . '*'); + $this->assertEmpty($keys); + } + + public function testGetSummary() + { + // Setup test data + $this->metrics->recordHit('key1', 0.1); + $this->metrics->recordHit('key1', 0.3); + $this->metrics->recordMiss('key1', 0.2); + $this->metrics->recordWrite('key1', 1000); + + $this->metrics->recordHit('key2', 0.2); + $this->metrics->recordWrite('key2', 500); + + $summary = $this->metrics->getSummary(); + + $this->assertArrayHasKey('keys', $summary); + $this->assertArrayHasKey('overall_hit_ratio', $summary); + $this->assertArrayHasKey('overall_miss_ratio', $summary); + $this->assertArrayHasKey('avg_hit_time', $summary); + $this->assertArrayHasKey('avg_miss_time', $summary); + $this->assertArrayHasKey('total_memory_usage', $summary); + $this->assertEquals(2, $summary['total_keys']); + $this->assertEquals(1500, $summary['total_memory_usage']); + $this->assertEqualsWithDelta(0.75, $summary['overall_hit_ratio'], 0.0001); + $this->assertEqualsWithDelta(0.25, $summary['overall_miss_ratio'], 0.0001); + } + + public function testGetSummaryWithNoData() + { + $summary = $this->metrics->getSummary(); + + $this->assertEmpty($summary['keys']); + $this->assertEquals(0, $summary['total_keys']); + $this->assertEquals(0, $summary['total_memory_usage']); + $this->assertEquals(0, $summary['overall_hit_ratio']); + $this->assertEquals(0, $summary['overall_miss_ratio']); + $this->assertEquals(0, $summary['avg_hit_time']); + $this->assertEquals(0, $summary['avg_miss_time']); + } + + protected function tearDown(): void + { + // Clean up after each test + $this->metrics->resetMetrics(); + parent::tearDown(); + } +} diff --git a/tests/unit/ProcessMaker/Console/Commands/CacheMetricsClearCommandTest.php b/tests/unit/ProcessMaker/Console/Commands/CacheMetricsClearCommandTest.php new file mode 100644 index 0000000000..0cf07cfcc2 --- /dev/null +++ b/tests/unit/ProcessMaker/Console/Commands/CacheMetricsClearCommandTest.php @@ -0,0 +1,48 @@ +metricsManager = Mockery::mock(CacheMetricsInterface::class); + + // Bind the mock to the service container + $this->app->instance(CacheMetricsInterface::class, $this->metricsManager); + + // Create the command with the mocked dependency + $this->command = new CacheMetricsClearCommand($this->metricsManager); + } + + public function testClearMetrics() + { + // Set up expectations + $this->metricsManager->shouldReceive('resetMetrics') + ->once(); + + // Execute the command + $this->artisan('cache:metrics-clear') + ->expectsOutput('Clearing all cache metrics data...') + ->expectsOutput('Cache metrics data cleared successfully!') + ->assertExitCode(0); + } + + protected function tearDown(): void + { + Mockery::close(); + parent::tearDown(); + } +} From 79b23201634a7eb347ecbbac3f348bc5d579ef6c Mon Sep 17 00:00:00 2001 From: Rodrigo Quelca Date: Wed, 4 Dec 2024 08:59:49 -0400 Subject: [PATCH 2/8] add documentation --- docs/cache-monitoring.md | 124 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 docs/cache-monitoring.md diff --git a/docs/cache-monitoring.md b/docs/cache-monitoring.md new file mode 100644 index 0000000000..a1c7eb07d6 --- /dev/null +++ b/docs/cache-monitoring.md @@ -0,0 +1,124 @@ +# ProcessMaker Cache Monitoring System + +## Overview +The ProcessMaker Cache Monitoring System is a comprehensive solution for tracking and analyzing cache performance metrics in real-time. It uses Redis as a backend store for metrics and provides detailed insights into cache usage patterns, hit/miss rates, and memory consumption. + +## Current Implementation + +### Architecture +1. **Core Components**: + - `CacheMetricsInterface`: Defines the contract for metrics collection + - `RedisMetricsManager`: Implements metrics storage using Redis + - `CacheMetricsDecorator`: Wraps cache implementations to collect metrics + +2. **Key Features**: + - Real-time metrics collection + - Hit/miss rate tracking + - Response time monitoring + - Memory usage tracking + - Top keys analysis + - Performance insights + +3. **Storage Strategy**: + - Uses Redis hash structures for counters + - Maintains rolling lists for timing data + - Prefix-based key organization + - Automatic cleanup of old data + +### Metrics Collected +- Cache hits and misses +- Response times for hits/misses +- Memory usage per key +- Last write timestamps +- Access patterns +- Overall performance statistics + +## Alternative Approaches + +### 1. In-Memory Metrics (e.g., APCu) +**Pros**: +- Faster read/write operations +- Lower memory overhead +- No external dependencies + +**Cons**: +- Data lost on server restart +- Limited by available memory +- Harder to scale across multiple servers + +### 2. Database Storage (e.g., MySQL/PostgreSQL) +**Pros**: +- Persistent storage +- Complex querying capabilities +- Built-in backup mechanisms + +**Cons**: +- Higher latency +- More resource intensive +- Potential database bottleneck + +### 3. Time Series Database (e.g., Prometheus) +**Pros**: +- Optimized for time-series data +- Better querying performance +- Built-in visualization tools +- Efficient data compression + +**Cons**: +- Additional infrastructure required +- More complex setup +- Higher learning curve + +### 4. Log-Based Analysis (e.g., ELK Stack) +**Pros**: +- Rich analysis capabilities +- Historical data retention +- Flexible querying +- Built-in visualization + +**Cons**: +- Higher storage requirements +- Processing overhead +- Complex setup and maintenance + +## Why Redis? +The current Redis-based implementation was chosen because: +1. **Performance**: Redis provides fast read/write operations +2. **Data Structures**: Native support for counters and lists +3. **TTL Support**: Automatic expiration of old metrics +4. **Scalability**: Easy to scale horizontally +5. **Integration**: Already used in ProcessMaker's infrastructure + +## Usage Examples + +### Basic Monitoring +```php +// Get hit rate for a specific key +$hitRate = $metrics->getHitRate('my_cache_key'); + +// Get memory usage +$usage = $metrics->getMemoryUsage('my_cache_key'); + +// Get performance summary +$summary = $metrics->getSummary(); +``` + +### Performance Analysis +```php +// Get top accessed keys +$topKeys = $metrics->getTopKeys(10); + +// Analyze response times +$avgHitTime = $metrics->getHitAvgTime('my_cache_key'); +$avgMissTime = $metrics->getMissAvgTime('my_cache_key'); +``` + +## Future Improvements +1. **Aggregation**: Add support for metric aggregation by time periods +2. **Sampling**: Implement sampling for high-traffic scenarios +3. **Alerts**: Add threshold-based alerting system +4. **Visualization**: Integrate with monitoring dashboards +5. **Custom Metrics**: Allow adding custom metrics per cache type + +## Conclusion +The Redis-based monitoring system provides a good balance between performance, functionality, and maintainability. While there are alternatives available, the current implementation meets ProcessMaker's requirements for real-time cache monitoring with minimal overhead. \ No newline at end of file From be4afac157e8b368e6c4715ec08036ea5e6ea129 Mon Sep 17 00:00:00 2001 From: Rodrigo Quelca Date: Wed, 4 Dec 2024 11:43:29 -0400 Subject: [PATCH 3/8] implement CacheMetricDcorators to the cacheFactory --- .../Monitoring/CacheMetricsDecorator.php | 17 +++- .../Cache/Screens/ScreenCacheFactory.php | 17 ++-- .../Monitoring/RedisMetricsManagerTest.php | 90 +----------------- .../Cache/Screens/ScreenCacheFactoryTest.php | 93 ++++++++++++++++--- 4 files changed, 110 insertions(+), 107 deletions(-) diff --git a/ProcessMaker/Cache/Monitoring/CacheMetricsDecorator.php b/ProcessMaker/Cache/Monitoring/CacheMetricsDecorator.php index 81fbd43eb0..1d102c3de7 100644 --- a/ProcessMaker/Cache/Monitoring/CacheMetricsDecorator.php +++ b/ProcessMaker/Cache/Monitoring/CacheMetricsDecorator.php @@ -3,19 +3,30 @@ namespace ProcessMaker\Cache\Monitoring; use ProcessMaker\Cache\CacheInterface; +use ProcessMaker\Cache\Monitoring\CacheMetricsInterface; +use ProcessMaker\Cache\Screens\ScreenCacheInterface; -class CacheMetricsDecorator implements CacheInterface +class CacheMetricsDecorator implements CacheInterface, ScreenCacheInterface { - protected CacheInterface $cache; + protected CacheInterface|ScreenCacheInterface $cache; protected CacheMetricsInterface $metrics; - public function __construct(CacheInterface $cache, CacheMetricsInterface $metrics) + public function __construct(CacheInterface|ScreenCacheInterface $cache, CacheMetricsInterface $metrics) { $this->cache = $cache; $this->metrics = $metrics; } + public function createKey(int $processId, int $processVersionId, string $language, int $screenId, int $screenVersionId): string + { + if ($this->cache instanceof ScreenCacheInterface) { + return $this->cache->createKey($processId, $processVersionId, $language, $screenId, $screenVersionId); + } + + throw new \RuntimeException('Underlying cache implementation does not support createKey method'); + } + public function get(string $key, mixed $default = null): mixed { $startTime = microtime(true); diff --git a/ProcessMaker/Cache/Screens/ScreenCacheFactory.php b/ProcessMaker/Cache/Screens/ScreenCacheFactory.php index a383a7a899..e02270c056 100644 --- a/ProcessMaker/Cache/Screens/ScreenCacheFactory.php +++ b/ProcessMaker/Cache/Screens/ScreenCacheFactory.php @@ -3,6 +3,8 @@ namespace ProcessMaker\Cache\Screens; use Illuminate\Support\Facades\Config; +use ProcessMaker\Cache\Monitoring\CacheMetricsDecorator; +use ProcessMaker\Cache\Monitoring\RedisMetricsManager; use ProcessMaker\Cache\Screens\ScreenCacheManager; use ProcessMaker\Managers\ScreenCompiledManager; @@ -31,15 +33,18 @@ public static function create(): ScreenCacheInterface return self::$testInstance; } + // Create the appropriate cache implementation $manager = Config::get('screens.cache.manager', 'legacy'); + $cache = $manager === 'new' + ? app(ScreenCacheManager::class) + : new LegacyScreenCacheAdapter(app()->make(ScreenCompiledManager::class)); - if ($manager === 'new') { - return app(ScreenCacheManager::class); + // If already wrapped with metrics decorator, return as is + if ($cache instanceof CacheMetricsDecorator) { + return $cache; } - // Get the concrete ScreenCompiledManager instance from the container - $compiledManager = app()->make(ScreenCompiledManager::class); - - return new LegacyScreenCacheAdapter($compiledManager); + // Wrap with metrics decorator if not already wrapped + return new CacheMetricsDecorator($cache, app()->make(RedisMetricsManager::class)); } } diff --git a/tests/unit/ProcessMaker/Cache/Monitoring/RedisMetricsManagerTest.php b/tests/unit/ProcessMaker/Cache/Monitoring/RedisMetricsManagerTest.php index 1a19a9021c..04f30acba7 100644 --- a/tests/unit/ProcessMaker/Cache/Monitoring/RedisMetricsManagerTest.php +++ b/tests/unit/ProcessMaker/Cache/Monitoring/RedisMetricsManagerTest.php @@ -12,13 +12,17 @@ class RedisMetricsManagerTest extends TestCase protected string $testKey = 'test_key'; - protected string $metricsPrefix = 'cache:metrics:'; + protected string $metricsPrefix; protected function setUp(): void { parent::setUp(); $this->metrics = new RedisMetricsManager(); + // Get metrics prefix using reflection + $reflection = new \ReflectionClass(RedisMetricsManager::class); + $this->metricsPrefix = $reflection->getConstant('METRICS_PREFIX'); + // Clear any existing metrics before each test $this->metrics->resetMetrics(); } @@ -37,20 +41,6 @@ public function testRecordHit() $this->assertEquals($time, (float) $times[0]); } - public function testRecordHitLimitsTimesList() - { - // Record more than 100 hits - for ($i = 0; $i < 110; $i++) { - $this->metrics->recordHit($this->testKey, 0.1); - } - - $baseKey = $this->metricsPrefix . $this->testKey; - $times = Redis::lrange($baseKey . ':hit_times', 0, -1); - - // Should only keep last 100 times - $this->assertCount(100, $times); - } - public function testRecordMiss() { $time = 0.2; @@ -65,20 +55,6 @@ public function testRecordMiss() $this->assertEquals($time, (float) $times[0]); } - public function testRecordMissLimitsTimesList() - { - // Record more than 100 misses - for ($i = 0; $i < 110; $i++) { - $this->metrics->recordMiss($this->testKey, 0.2); - } - - $baseKey = $this->metricsPrefix . $this->testKey; - $times = Redis::lrange($baseKey . ':miss_times', 0, -1); - - // Should only keep last 100 times - $this->assertCount(100, $times); - } - public function testRecordWrite() { $size = 1024; @@ -91,7 +67,6 @@ public function testRecordWrite() $this->assertEquals($size, $storedSize); $this->assertNotNull($lastWrite); $this->assertIsNumeric($lastWrite); - $this->assertLessThanOrEqual(microtime(true), (float) $lastWrite); } public function testGetHitRate() @@ -105,12 +80,6 @@ public function testGetHitRate() $this->assertEqualsWithDelta(2 / 3, $hitRate, 0.0001); } - public function testGetHitRateWithNoAccesses() - { - $hitRate = $this->metrics->getHitRate($this->testKey); - $this->assertEquals(0, $hitRate); - } - public function testGetMissRate() { // Record 2 hits and 1 miss @@ -122,12 +91,6 @@ public function testGetMissRate() $this->assertEqualsWithDelta(1 / 3, $missRate, 0.0001); } - public function testGetMissRateWithNoAccesses() - { - $missRate = $this->metrics->getMissRate($this->testKey); - $this->assertEquals(0, $missRate); - } - public function testGetHitAvgTime() { $this->metrics->recordHit($this->testKey, 0.1); @@ -137,12 +100,6 @@ public function testGetHitAvgTime() $this->assertEqualsWithDelta(0.2, $avgTime, 0.0001); } - public function testGetHitAvgTimeWithNoHits() - { - $avgTime = $this->metrics->getHitAvgTime($this->testKey); - $this->assertEquals(0, $avgTime); - } - public function testGetMissAvgTime() { $this->metrics->recordMiss($this->testKey, 0.2); @@ -152,12 +109,6 @@ public function testGetMissAvgTime() $this->assertEqualsWithDelta(0.3, $avgTime, 0.0001); } - public function testGetMissAvgTimeWithNoMisses() - { - $avgTime = $this->metrics->getMissAvgTime($this->testKey); - $this->assertEquals(0, $avgTime); - } - public function testGetTopKeys() { // Setup test data @@ -175,14 +126,6 @@ public function testGetTopKeys() $this->assertEquals('key1', $topKeys['key1']['key']); $this->assertEquals(3, $topKeys['key1']['total_accesses']); $this->assertEquals(1000, $topKeys['key1']['memory_usage']); - $this->assertEqualsWithDelta(2 / 3, $topKeys['key1']['hit_ratio'], 0.0001); - $this->assertEqualsWithDelta(1 / 3, $topKeys['key1']['miss_ratio'], 0.0001); - } - - public function testGetTopKeysWithNoKeys() - { - $topKeys = $this->metrics->getTopKeys(5); - $this->assertEmpty($topKeys); } public function testGetMemoryUsage() @@ -197,14 +140,6 @@ public function testGetMemoryUsage() $this->assertIsFloat($usage['last_write']); } - public function testGetMemoryUsageForNonexistentKey() - { - $usage = $this->metrics->getMemoryUsage('nonexistent_key'); - - $this->assertEquals(0, $usage['current_size']); - $this->assertNull($usage['last_write']); - } - public function testResetMetrics() { // Add some test data @@ -241,21 +176,6 @@ public function testGetSummary() $this->assertArrayHasKey('total_memory_usage', $summary); $this->assertEquals(2, $summary['total_keys']); $this->assertEquals(1500, $summary['total_memory_usage']); - $this->assertEqualsWithDelta(0.75, $summary['overall_hit_ratio'], 0.0001); - $this->assertEqualsWithDelta(0.25, $summary['overall_miss_ratio'], 0.0001); - } - - public function testGetSummaryWithNoData() - { - $summary = $this->metrics->getSummary(); - - $this->assertEmpty($summary['keys']); - $this->assertEquals(0, $summary['total_keys']); - $this->assertEquals(0, $summary['total_memory_usage']); - $this->assertEquals(0, $summary['overall_hit_ratio']); - $this->assertEquals(0, $summary['overall_miss_ratio']); - $this->assertEquals(0, $summary['avg_hit_time']); - $this->assertEquals(0, $summary['avg_miss_time']); } protected function tearDown(): void diff --git a/tests/unit/ProcessMaker/Cache/Screens/ScreenCacheFactoryTest.php b/tests/unit/ProcessMaker/Cache/Screens/ScreenCacheFactoryTest.php index 97383b89f7..f97a0d201c 100644 --- a/tests/unit/ProcessMaker/Cache/Screens/ScreenCacheFactoryTest.php +++ b/tests/unit/ProcessMaker/Cache/Screens/ScreenCacheFactoryTest.php @@ -3,6 +3,8 @@ namespace Tests\Unit\ProcessMaker\Cache\Screens; use Illuminate\Support\Facades\Config; +use ProcessMaker\Cache\Monitoring\CacheMetricsDecorator; +use ProcessMaker\Cache\Monitoring\RedisMetricsManager; use ProcessMaker\Cache\Screens\LegacyScreenCacheAdapter; use ProcessMaker\Cache\Screens\ScreenCacheFactory; use ProcessMaker\Cache\Screens\ScreenCacheManager; @@ -11,33 +13,98 @@ class ScreenCacheFactoryTest extends TestCase { - /** @test */ - public function it_creates_new_cache_manager_when_configured() + protected function setUp(): void + { + parent::setUp(); + ScreenCacheFactory::setTestInstance(null); + + // Bind necessary dependencies + $this->app->singleton(ScreenCompiledManager::class); + $this->app->singleton(RedisMetricsManager::class); + } + + public function testCreateNewCacheManager() { Config::set('screens.cache.manager', 'new'); - $cacheHandler = ScreenCacheFactory::create(); + // Create a mock for ScreenCacheManager + $mockManager = $this->createMock(ScreenCacheManager::class); + $this->app->instance(ScreenCacheManager::class, $mockManager); + + $cache = ScreenCacheFactory::create(); + + // Should be wrapped with metrics decorator + $this->assertInstanceOf(CacheMetricsDecorator::class, $cache); - $this->assertInstanceOf(ScreenCacheManager::class, $cacheHandler); + // Get the underlying cache implementation + $reflection = new \ReflectionClass($cache); + $property = $reflection->getProperty('cache'); + $property->setAccessible(true); + $underlyingCache = $property->getValue($cache); + + // Verify it's the new cache manager + $this->assertInstanceOf(ScreenCacheManager::class, $underlyingCache); } - /** @test */ - public function it_creates_legacy_adapter_by_default() + public function testCreateLegacyCacheAdapter() { Config::set('screens.cache.manager', 'legacy'); - $cacheHandler = ScreenCacheFactory::create(); + $cache = ScreenCacheFactory::create(); + + // Should be wrapped with metrics decorator + $this->assertInstanceOf(CacheMetricsDecorator::class, $cache); + + // Get the underlying cache implementation + $reflection = new \ReflectionClass($cache); + $property = $reflection->getProperty('cache'); + $property->setAccessible(true); + $underlyingCache = $property->getValue($cache); - $this->assertInstanceOf(LegacyScreenCacheAdapter::class, $cacheHandler); + // Verify it's the legacy adapter + $this->assertInstanceOf(LegacyScreenCacheAdapter::class, $underlyingCache); } - /** @test */ - public function it_creates_legacy_adapter_when_config_missing() + public function testMetricsIntegrationWithBothAdapters() { - Config::set('screens.cache.manager', null); + // Test with new cache manager + Config::set('screens.cache.manager', 'new'); + + // Create a mock for ScreenCacheManager + $mockManager = $this->createMock(ScreenCacheManager::class); + $this->app->instance(ScreenCacheManager::class, $mockManager); - $cacheHandler = ScreenCacheFactory::create(); + $newCache = ScreenCacheFactory::create(); + $this->verifyMetricsDecorator($newCache, ScreenCacheManager::class); - $this->assertInstanceOf(LegacyScreenCacheAdapter::class, $cacheHandler); + // Test with legacy adapter + Config::set('screens.cache.manager', 'legacy'); + $legacyCache = ScreenCacheFactory::create(); + $this->verifyMetricsDecorator($legacyCache, LegacyScreenCacheAdapter::class); + } + + protected function verifyMetricsDecorator($cache, $expectedCacheClass) + { + // Verify it's a metrics decorator + $this->assertInstanceOf(CacheMetricsDecorator::class, $cache); + + // Get reflection for property access + $reflection = new \ReflectionClass($cache); + + // Check cache implementation + $cacheProperty = $reflection->getProperty('cache'); + $cacheProperty->setAccessible(true); + $this->assertInstanceOf($expectedCacheClass, $cacheProperty->getValue($cache)); + + // Check metrics manager + $metricsProperty = $reflection->getProperty('metrics'); + $metricsProperty->setAccessible(true); + $this->assertInstanceOf(RedisMetricsManager::class, $metricsProperty->getValue($cache)); + } + + protected function tearDown(): void + { + ScreenCacheFactory::setTestInstance(null); + parent::tearDown(); } } From 697821d46b35fe737d13feabcaa8658aa42a606f Mon Sep 17 00:00:00 2001 From: Rodrigo Quelca Date: Wed, 4 Dec 2024 12:36:56 -0400 Subject: [PATCH 4/8] add documentations --- .../Monitoring/CacheMetricsDecorator.php | 69 +++++++++++++++++++ .../Monitoring/CacheMetricsInterface.php | 65 +++++++++++++++++ .../Commands/CacheMetricsClearCommand.php | 5 ++ 3 files changed, 139 insertions(+) diff --git a/ProcessMaker/Cache/Monitoring/CacheMetricsDecorator.php b/ProcessMaker/Cache/Monitoring/CacheMetricsDecorator.php index 1d102c3de7..9808158ccf 100644 --- a/ProcessMaker/Cache/Monitoring/CacheMetricsDecorator.php +++ b/ProcessMaker/Cache/Monitoring/CacheMetricsDecorator.php @@ -6,18 +6,39 @@ 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 { protected CacheInterface|ScreenCacheInterface $cache; protected CacheMetricsInterface $metrics; + /** + * Create a new cache metrics decorator instance + * + * @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) { $this->cache = $cache; $this->metrics = $metrics; } + /** + * Create a cache key for screen data + * + * @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 + * @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 { if ($this->cache instanceof ScreenCacheInterface) { @@ -27,6 +48,15 @@ public function createKey(int $processId, int $processVersionId, string $languag throw new \RuntimeException('Underlying cache implementation does not support createKey method'); } + /** + * Get a value from the cache + * + * Records timing and hit/miss metrics for the get operation + * + * @param string $key Cache key + * @param mixed $default Default value if key not found + * @return mixed Cached value or default + */ public function get(string $key, mixed $default = null): mixed { $startTime = microtime(true); @@ -43,6 +73,16 @@ public function get(string $key, mixed $default = null): mixed return $value; } + /** + * Store a value in the cache + * + * Records metrics about the size of stored data + * + * @param string $key Cache key + * @param mixed $value Value to cache + * @param null|int|\DateInterval $ttl Optional TTL + * @return bool True if successful + */ public function set(string $key, mixed $value, null|int|\DateInterval $ttl = null): bool { $result = $this->cache->set($key, $value, $ttl); @@ -56,26 +96,55 @@ public function set(string $key, mixed $value, null|int|\DateInterval $ttl = nul return $result; } + /** + * Delete a value from the cache + * + * @param string $key Cache key + * @return bool True if successful + */ public function delete(string $key): bool { return $this->cache->delete($key); } + /** + * Clear all values from the cache + * + * @return bool True if successful + */ public function clear(): bool { return $this->cache->clear(); } + /** + * Check if a key exists in the cache + * + * @param string $key Cache key + * @return bool True if key exists + */ public function has(string $key): bool { return $this->cache->has($key); } + /** + * Check if a key is missing from the cache + * + * @param string $key Cache key + * @return bool True if key is missing + */ public function missing(string $key): bool { return $this->cache->missing($key); } + /** + * Calculate the approximate size in bytes of a value + * + * @param mixed $value Value to calculate size for + * @return int Size in bytes + */ protected function calculateSize(mixed $value): int { if (is_string($value)) { diff --git a/ProcessMaker/Cache/Monitoring/CacheMetricsInterface.php b/ProcessMaker/Cache/Monitoring/CacheMetricsInterface.php index dc8ffc4d1e..6632c6a7f9 100644 --- a/ProcessMaker/Cache/Monitoring/CacheMetricsInterface.php +++ b/ProcessMaker/Cache/Monitoring/CacheMetricsInterface.php @@ -2,27 +2,92 @@ namespace ProcessMaker\Cache\Monitoring; +/** + * Interface for monitoring cache metrics and performance + */ interface CacheMetricsInterface { + /** + * Record a cache hit event + * + * @param string $key Cache key that was accessed + * @param float $microtime Time taken for the operation in microseconds + */ public function recordHit(string $key, $microtime): void; + /** + * Record a cache miss event + * + * @param string $key Cache key that was accessed + * @param float $microtime Time taken for the operation in microseconds + */ public function recordMiss(string $key, $microtime): void; + /** + * Record a cache write operation + * + * @param string $key Cache key that was written + * @param int $size Size of the cached data in bytes + */ public function recordWrite(string $key, int $size): void; + /** + * Get the hit rate for a specific cache key + * + * @param string $key Cache key to check + * @return float Hit rate as a percentage between 0 and 1 + */ public function getHitRate(string $key): float; + /** + * Get the miss rate for a specific cache key + * + * @param string $key Cache key to check + * @return float Miss rate as a percentage between 0 and 1 + */ public function getMissRate(string $key): float; + /** + * Get the average time taken for cache hits + * + * @param string $key Cache key to analyze + * @return float Average time in microseconds + */ public function getHitAvgTime(string $key): float; + /** + * Get the average time taken for cache misses + * + * @param string $key Cache key to analyze + * @return float Average time in microseconds + */ public function getMissAvgTime(string $key): float; + /** + * Get the most frequently accessed cache keys + * + * @param int $count Number of top keys to return + * @return array Array of top keys with their access counts + */ public function getTopKeys(int $count = 5): array; + /** + * Get memory usage statistics for a cache key + * + * @param string $key Cache key to analyze + * @return array Array containing memory usage details + */ public function getMemoryUsage(string $key): array; + /** + * Reset all metrics data + */ public function resetMetrics(): void; + /** + * Get a summary of all cache metrics + * + * @return array Array containing overall cache statistics + */ public function getSummary(): array; } diff --git a/ProcessMaker/Console/Commands/CacheMetricsClearCommand.php b/ProcessMaker/Console/Commands/CacheMetricsClearCommand.php index db446bebdb..0046859679 100644 --- a/ProcessMaker/Console/Commands/CacheMetricsClearCommand.php +++ b/ProcessMaker/Console/Commands/CacheMetricsClearCommand.php @@ -21,6 +21,11 @@ public function __construct(RedisMetricsManager $metrics) public function handle() { + if (!$this->confirm('Are you sure you want to clear all cache metrics? This action cannot be undone.')) { + $this->info('Operation cancelled.'); + + return 0; + } $this->info('Clearing all cache metrics data...'); $this->metrics->resetMetrics(); $this->info('Cache metrics data cleared successfully!'); From d628a7c5c8ffd6773e55d21e91341d4aad94fb56 Mon Sep 17 00:00:00 2001 From: Rodrigo Quelca Date: Wed, 4 Dec 2024 12:39:55 -0400 Subject: [PATCH 5/8] remove alternatives form documentation --- docs/cache-monitoring.md | 47 ---------------------------------------- 1 file changed, 47 deletions(-) diff --git a/docs/cache-monitoring.md b/docs/cache-monitoring.md index a1c7eb07d6..a6ec09973d 100644 --- a/docs/cache-monitoring.md +++ b/docs/cache-monitoring.md @@ -33,53 +33,6 @@ The ProcessMaker Cache Monitoring System is a comprehensive solution for trackin - Access patterns - Overall performance statistics -## Alternative Approaches - -### 1. In-Memory Metrics (e.g., APCu) -**Pros**: -- Faster read/write operations -- Lower memory overhead -- No external dependencies - -**Cons**: -- Data lost on server restart -- Limited by available memory -- Harder to scale across multiple servers - -### 2. Database Storage (e.g., MySQL/PostgreSQL) -**Pros**: -- Persistent storage -- Complex querying capabilities -- Built-in backup mechanisms - -**Cons**: -- Higher latency -- More resource intensive -- Potential database bottleneck - -### 3. Time Series Database (e.g., Prometheus) -**Pros**: -- Optimized for time-series data -- Better querying performance -- Built-in visualization tools -- Efficient data compression - -**Cons**: -- Additional infrastructure required -- More complex setup -- Higher learning curve - -### 4. Log-Based Analysis (e.g., ELK Stack) -**Pros**: -- Rich analysis capabilities -- Historical data retention -- Flexible querying -- Built-in visualization - -**Cons**: -- Higher storage requirements -- Processing overhead -- Complex setup and maintenance ## Why Redis? The current Redis-based implementation was chosen because: From 7cf1abf945e21196e56215050e4fca37b4c08489 Mon Sep 17 00:00:00 2001 From: Rodrigo Quelca Date: Wed, 4 Dec 2024 13:31:02 -0400 Subject: [PATCH 6/8] remove savedsarch config file --- config/savedsearch.php | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 config/savedsearch.php diff --git a/config/savedsearch.php b/config/savedsearch.php deleted file mode 100644 index bd3fdedb7a..0000000000 --- a/config/savedsearch.php +++ /dev/null @@ -1,19 +0,0 @@ - env('SAVED_SEARCH_COUNT', true), - 'add_defaults' => env('SAVED_SEARCH_ADD_DEFAULTS', true), - -]; From 78102cbe537f6e6057dad5ab53c881589a9ace99 Mon Sep 17 00:00:00 2001 From: Rodrigo Quelca Date: Wed, 4 Dec 2024 13:57:00 -0400 Subject: [PATCH 7/8] cleand code, scredule metrics comand daily --- ProcessMaker/Console/Kernel.php | 9 ++------- config/app.php | 1 - 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/ProcessMaker/Console/Kernel.php b/ProcessMaker/Console/Kernel.php index 824bf35488..30ac716641 100644 --- a/ProcessMaker/Console/Kernel.php +++ b/ProcessMaker/Console/Kernel.php @@ -42,13 +42,8 @@ protected function schedule(Schedule $schedule) $schedule->command('processmaker:sync-screen-templates --queue') ->daily(); - $schedule->command('cache:metrics --format=json > storage/logs/cache-metrics.json') - ->hourly() - ->onOneServer(); - - $schedule->command('cache:metrics-summary') - ->dailyAt('23:55') - ->onOneServer(); + $schedule->command('cache:metrics --format=json > storage/logs/processmaker-cache-metrics.json') + ->daily(); } /** diff --git a/config/app.php b/config/app.php index 5b7019909c..07a788a4b1 100644 --- a/config/app.php +++ b/config/app.php @@ -188,7 +188,6 @@ ProcessMaker\Providers\OauthMailServiceProvider::class, ProcessMaker\Providers\OpenAiServiceProvider::class, ProcessMaker\Providers\LicenseServiceProvider::class, - ProcessMaker\Providers\CacheServiceProvider::class, ])->toArray(), 'aliases' => Facade::defaultAliases()->merge([ From 955b0ac12857ac0b5b4828e95dfb9912f5e77a0a Mon Sep 17 00:00:00 2001 From: Rodrigo Quelca Date: Wed, 4 Dec 2024 14:13:22 -0400 Subject: [PATCH 8/8] remove command tests --- .../Commands/CacheMetricsClearCommandTest.php | 48 ------------------- 1 file changed, 48 deletions(-) delete mode 100644 tests/unit/ProcessMaker/Console/Commands/CacheMetricsClearCommandTest.php diff --git a/tests/unit/ProcessMaker/Console/Commands/CacheMetricsClearCommandTest.php b/tests/unit/ProcessMaker/Console/Commands/CacheMetricsClearCommandTest.php deleted file mode 100644 index 0cf07cfcc2..0000000000 --- a/tests/unit/ProcessMaker/Console/Commands/CacheMetricsClearCommandTest.php +++ /dev/null @@ -1,48 +0,0 @@ -metricsManager = Mockery::mock(CacheMetricsInterface::class); - - // Bind the mock to the service container - $this->app->instance(CacheMetricsInterface::class, $this->metricsManager); - - // Create the command with the mocked dependency - $this->command = new CacheMetricsClearCommand($this->metricsManager); - } - - public function testClearMetrics() - { - // Set up expectations - $this->metricsManager->shouldReceive('resetMetrics') - ->once(); - - // Execute the command - $this->artisan('cache:metrics-clear') - ->expectsOutput('Clearing all cache metrics data...') - ->expectsOutput('Cache metrics data cleared successfully!') - ->assertExitCode(0); - } - - protected function tearDown(): void - { - Mockery::close(); - parent::tearDown(); - } -}