- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
579 lines
20 KiB
PHP
579 lines
20 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Cache\CacheKey;
|
|
use App\Framework\Cache\Strategies\AdaptiveTtlCacheStrategy;
|
|
use App\Framework\Cache\Strategies\HeatMapCacheStrategy;
|
|
use App\Framework\Cache\Strategies\PredictiveCacheStrategy;
|
|
use App\Framework\Core\ValueObjects\Duration;
|
|
use App\Framework\Core\ValueObjects\Timestamp;
|
|
|
|
describe('AdaptiveTtlCacheStrategy', function () {
|
|
beforeEach(function () {
|
|
$this->strategy = new AdaptiveTtlCacheStrategy(
|
|
enabled: true,
|
|
learningWindow: 10,
|
|
minTtl: Duration::fromSeconds(30),
|
|
maxTtl: Duration::fromHours(2)
|
|
);
|
|
});
|
|
|
|
describe('Basic Operations', function () {
|
|
it('is enabled by default', function () {
|
|
expect($this->strategy->isEnabled())->toBeTrue();
|
|
expect($this->strategy->getName())->toBe('adaptive_ttl');
|
|
});
|
|
|
|
it('tracks cache access patterns', function () {
|
|
$key = CacheKey::fromString('test-key');
|
|
|
|
// Record multiple accesses
|
|
for ($i = 0; $i < 5; $i++) {
|
|
$this->strategy->onCacheAccess($key, true);
|
|
}
|
|
|
|
$stats = $this->strategy->getStats();
|
|
|
|
expect($stats['strategy'])->toBe('AdaptiveTtlCacheStrategy');
|
|
expect($stats['total_tracked_keys'])->toBe(1);
|
|
expect($stats['enabled'])->toBeTrue();
|
|
});
|
|
|
|
it('provides default TTL for new keys', function () {
|
|
$key = CacheKey::fromString('new-key');
|
|
$originalTtl = Duration::fromMinutes(10);
|
|
|
|
$adaptedTtl = $this->strategy->onCacheSet($key, 'value', $originalTtl);
|
|
|
|
expect($adaptedTtl)->toBeInstanceOf(Duration::class);
|
|
expect($adaptedTtl->toSeconds())->toBeGreaterThan(29); // >= Min TTL (30)
|
|
expect($adaptedTtl->toSeconds())->toBeLessThan(7201); // <= Max TTL (7200)
|
|
});
|
|
|
|
it('clears all tracking data', function () {
|
|
$key = CacheKey::fromString('test-key');
|
|
$this->strategy->onCacheAccess($key, true);
|
|
|
|
expect($this->strategy->getStats()['total_tracked_keys'])->toBe(1);
|
|
|
|
$this->strategy->clear();
|
|
|
|
expect($this->strategy->getStats()['total_tracked_keys'])->toBe(0);
|
|
});
|
|
|
|
it('forgets key-specific data', function () {
|
|
$key1 = CacheKey::fromString('key1');
|
|
$key2 = CacheKey::fromString('key2');
|
|
|
|
$this->strategy->onCacheAccess($key1, true);
|
|
$this->strategy->onCacheAccess($key2, true);
|
|
|
|
expect($this->strategy->getStats()['total_tracked_keys'])->toBe(2);
|
|
|
|
$this->strategy->onCacheForget($key1);
|
|
|
|
expect($this->strategy->getStats()['total_tracked_keys'])->toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('TTL Adaptation', function () {
|
|
it('extends TTL for frequently accessed keys', function () {
|
|
$key = CacheKey::fromString('hot-key');
|
|
|
|
// Simulate high access count with good hit rate
|
|
for ($i = 0; $i < 15; $i++) {
|
|
$this->strategy->onCacheAccess($key, true); // High hit rate
|
|
}
|
|
|
|
$originalTtl = Duration::fromMinutes(30);
|
|
$adaptedTtl = $this->strategy->onCacheSet($key, 'value', $originalTtl);
|
|
|
|
// Should extend TTL for frequently accessed keys
|
|
expect($adaptedTtl->toSeconds())->toBeGreaterThan($originalTtl->toSeconds() - 1);
|
|
});
|
|
|
|
it('reduces TTL for rarely accessed keys', function () {
|
|
$key = CacheKey::fromString('cold-key');
|
|
|
|
// Simulate low access count with poor hit rate
|
|
$this->strategy->onCacheAccess($key, false);
|
|
$this->strategy->onCacheAccess($key, false);
|
|
|
|
$originalTtl = Duration::fromMinutes(30);
|
|
$adaptedTtl = $this->strategy->onCacheSet($key, 'value', $originalTtl);
|
|
|
|
// Should reduce or maintain TTL for rarely accessed keys
|
|
expect($adaptedTtl->toSeconds())->toBeLessThan($originalTtl->toSeconds() + 1);
|
|
});
|
|
|
|
it('enforces minimum TTL bounds', function () {
|
|
$key = CacheKey::fromString('min-bound-key');
|
|
$veryShortTtl = Duration::fromSeconds(5); // Below minimum
|
|
|
|
$adaptedTtl = $this->strategy->onCacheSet($key, 'value', $veryShortTtl);
|
|
|
|
// Should be at least the minimum TTL
|
|
expect($adaptedTtl->toSeconds())->toBeGreaterThan(29);
|
|
expect($adaptedTtl->toSeconds())->toBeLessThan(61); // Shouldn't be much higher
|
|
});
|
|
|
|
it('enforces maximum TTL bounds', function () {
|
|
$key = CacheKey::fromString('max-bound-key');
|
|
$veryLongTtl = Duration::fromHours(24); // Above maximum
|
|
|
|
$adaptedTtl = $this->strategy->onCacheSet($key, 'value', $veryLongTtl);
|
|
|
|
// Should be capped at maximum TTL (2 hours = 7200 seconds)
|
|
expect($adaptedTtl->toSeconds())->toBeLessThan(7201);
|
|
expect($adaptedTtl->toSeconds())->toBeGreaterThan(7199);
|
|
});
|
|
});
|
|
|
|
describe('Statistics and Insights', function () {
|
|
it('provides comprehensive statistics', function () {
|
|
$key1 = CacheKey::fromString('key1');
|
|
$key2 = CacheKey::fromString('key2');
|
|
|
|
// Simulate access patterns
|
|
for ($i = 0; $i < 5; $i++) {
|
|
$this->strategy->onCacheAccess($key1, true);
|
|
}
|
|
for ($i = 0; $i < 3; $i++) {
|
|
$this->strategy->onCacheAccess($key2, false);
|
|
}
|
|
|
|
$stats = $this->strategy->getStats();
|
|
|
|
expect($stats)->toHaveKeys([
|
|
'strategy',
|
|
'enabled',
|
|
'total_tracked_keys',
|
|
'learning_window',
|
|
'ttl_bounds',
|
|
'adaptation_factors',
|
|
'key_patterns'
|
|
]);
|
|
});
|
|
|
|
it('tracks hit rate per key', function () {
|
|
$key = CacheKey::fromString('mixed-key');
|
|
|
|
// 3 hits, 2 misses = 60% hit rate
|
|
$this->strategy->onCacheAccess($key, true);
|
|
$this->strategy->onCacheAccess($key, true);
|
|
$this->strategy->onCacheAccess($key, true);
|
|
$this->strategy->onCacheAccess($key, false);
|
|
$this->strategy->onCacheAccess($key, false);
|
|
|
|
$stats = $this->strategy->getStats();
|
|
|
|
expect($stats['key_patterns'])->toHaveKey('mixed-key');
|
|
expect($stats['key_patterns']['mixed-key']['hit_rate'])->toBe(0.6);
|
|
expect($stats['key_patterns']['mixed-key']['total_requests'])->toBe(5);
|
|
});
|
|
});
|
|
|
|
describe('Disabled Strategy', function () {
|
|
it('does nothing when disabled', function () {
|
|
$disabledStrategy = new AdaptiveTtlCacheStrategy(enabled: false);
|
|
$key = CacheKey::fromString('disabled-key');
|
|
|
|
$disabledStrategy->onCacheAccess($key, true);
|
|
|
|
$stats = $disabledStrategy->getStats();
|
|
|
|
expect($stats['enabled'])->toBeFalse();
|
|
expect($stats['total_tracked_keys'])->toBe(0);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('HeatMapCacheStrategy', function () {
|
|
beforeEach(function () {
|
|
$this->strategy = new HeatMapCacheStrategy(
|
|
enabled: true,
|
|
maxTrackedKeys: 100,
|
|
hotThreshold: 10,
|
|
coldThreshold: 1,
|
|
analysisWindowHours: 2
|
|
);
|
|
});
|
|
|
|
describe('Basic Operations', function () {
|
|
it('is enabled by default', function () {
|
|
expect($this->strategy->isEnabled())->toBeTrue();
|
|
expect($this->strategy->getName())->toBe('heat_map');
|
|
});
|
|
|
|
it('tracks cache access patterns', function () {
|
|
$key = CacheKey::fromString('test-key');
|
|
|
|
$this->strategy->onCacheAccess($key, true, Duration::fromMilliseconds(50));
|
|
|
|
$stats = $this->strategy->getStats();
|
|
|
|
expect($stats['strategy'])->toBe('HeatMapCacheStrategy');
|
|
expect($stats['total_tracked_keys'])->toBe(1);
|
|
});
|
|
|
|
it('respects max tracked keys limit', function () {
|
|
// Create more keys than the limit
|
|
for ($i = 0; $i < 150; $i++) {
|
|
$key = CacheKey::fromString("key-{$i}");
|
|
$this->strategy->onCacheAccess($key, true);
|
|
}
|
|
|
|
$stats = $this->strategy->getStats();
|
|
|
|
expect($stats['total_tracked_keys'])->toBeLessThan(101);
|
|
});
|
|
|
|
it('clears all heat map data', function () {
|
|
$key = CacheKey::fromString('test-key');
|
|
$this->strategy->onCacheAccess($key, true);
|
|
|
|
expect($this->strategy->getStats()['total_tracked_keys'])->toBe(1);
|
|
|
|
$this->strategy->clear();
|
|
|
|
expect($this->strategy->getStats()['total_tracked_keys'])->toBe(0);
|
|
});
|
|
|
|
it('forgets key-specific data', function () {
|
|
$key1 = CacheKey::fromString('key1');
|
|
$key2 = CacheKey::fromString('key2');
|
|
|
|
$this->strategy->onCacheAccess($key1, true);
|
|
$this->strategy->onCacheAccess($key2, true);
|
|
|
|
expect($this->strategy->getStats()['total_tracked_keys'])->toBe(2);
|
|
|
|
$this->strategy->onCacheForget($key1);
|
|
|
|
expect($this->strategy->getStats()['total_tracked_keys'])->toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('Heat Map Analysis', function () {
|
|
it('identifies hot keys', function () {
|
|
$hotKey = CacheKey::fromString('hot-key');
|
|
|
|
// Simulate frequent access (above hot threshold)
|
|
for ($i = 0; $i < 25; $i++) {
|
|
$this->strategy->onCacheAccess($hotKey, true, Duration::fromMilliseconds(10));
|
|
}
|
|
|
|
$hotKeys = $this->strategy->getHotKeys(10);
|
|
|
|
expect($hotKeys)->toHaveKey('hot-key');
|
|
expect($hotKeys['hot-key'])->toBeGreaterThan(10);
|
|
});
|
|
|
|
it('provides heat map analysis', function () {
|
|
$key = CacheKey::fromString('test-key');
|
|
|
|
for ($i = 0; $i < 5; $i++) {
|
|
$this->strategy->onCacheAccess($key, true, Duration::fromMilliseconds(10));
|
|
}
|
|
|
|
$analysis = $this->strategy->getHeatMapAnalysis();
|
|
|
|
expect($analysis)->toHaveKeys([
|
|
'total_tracked_keys',
|
|
'hot_keys',
|
|
'cold_keys',
|
|
'performance_insights',
|
|
'analysis_window_hours'
|
|
]);
|
|
expect($analysis['total_tracked_keys'])->toBe(1);
|
|
});
|
|
|
|
it('tracks retrieval time statistics', function () {
|
|
$key = CacheKey::fromString('timed-key');
|
|
|
|
$this->strategy->onCacheAccess($key, true, Duration::fromMilliseconds(150));
|
|
$this->strategy->onCacheAccess($key, true, Duration::fromMilliseconds(100));
|
|
|
|
$stats = $this->strategy->getStats();
|
|
|
|
expect($stats['total_tracked_keys'])->toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('Performance Bottlenecks', function () {
|
|
it('identifies slow retrieval bottlenecks', function () {
|
|
$slowKey = CacheKey::fromString('slow-key');
|
|
|
|
// Simulate slow retrieval times (>100ms)
|
|
for ($i = 0; $i < 10; $i++) {
|
|
$this->strategy->onCacheAccess($slowKey, true, Duration::fromMilliseconds(250));
|
|
}
|
|
|
|
$bottlenecks = $this->strategy->getPerformanceBottlenecks();
|
|
|
|
// Should identify slow-key as a bottleneck
|
|
expect($bottlenecks)->toBeArray();
|
|
if (!empty($bottlenecks)) {
|
|
expect($bottlenecks[0]['key'])->toBe('slow-key');
|
|
expect($bottlenecks[0])->toHaveKeys([
|
|
'key',
|
|
'impact_score',
|
|
'type',
|
|
'avg_retrieval_time_ms',
|
|
'hit_rate',
|
|
'access_count',
|
|
'recommendation'
|
|
]);
|
|
}
|
|
});
|
|
|
|
it('identifies low hit rate bottlenecks', function () {
|
|
$missKey = CacheKey::fromString('miss-key');
|
|
|
|
// Simulate low hit rate (<50%)
|
|
for ($i = 0; $i < 10; $i++) {
|
|
$this->strategy->onCacheAccess($missKey, false, Duration::fromMilliseconds(50));
|
|
}
|
|
|
|
$bottlenecks = $this->strategy->getPerformanceBottlenecks();
|
|
|
|
expect($bottlenecks)->toBeArray();
|
|
});
|
|
});
|
|
|
|
describe('Statistics and Insights', function () {
|
|
it('provides comprehensive statistics', function () {
|
|
$key = CacheKey::fromString('stats-key');
|
|
|
|
for ($i = 0; $i < 5; $i++) {
|
|
$this->strategy->onCacheAccess($key, true, Duration::fromMilliseconds(30));
|
|
}
|
|
|
|
$stats = $this->strategy->getStats();
|
|
|
|
expect($stats)->toHaveKeys([
|
|
'strategy',
|
|
'enabled',
|
|
'total_tracked_keys',
|
|
'max_tracked_keys',
|
|
'thresholds',
|
|
'analysis',
|
|
'performance_bottlenecks'
|
|
]);
|
|
});
|
|
|
|
it('tracks write operations', function () {
|
|
$key = CacheKey::fromString('write-key');
|
|
|
|
$this->strategy->onCacheSet($key, 'some value', Duration::fromMinutes(5));
|
|
|
|
// Write operations are tracked but don't affect key count
|
|
$stats = $this->strategy->getStats();
|
|
expect($stats['total_tracked_keys'])->toBe(0); // No access yet
|
|
});
|
|
});
|
|
|
|
describe('Disabled Strategy', function () {
|
|
it('does nothing when disabled', function () {
|
|
$disabledStrategy = new HeatMapCacheStrategy(enabled: false);
|
|
$key = CacheKey::fromString('disabled-key');
|
|
|
|
$disabledStrategy->onCacheAccess($key, true);
|
|
|
|
$stats = $disabledStrategy->getStats();
|
|
|
|
expect($stats['enabled'])->toBeFalse();
|
|
expect($stats['total_tracked_keys'])->toBe(0);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('PredictiveCacheStrategy', function () {
|
|
beforeEach(function () {
|
|
$this->strategy = new PredictiveCacheStrategy(
|
|
enabled: true,
|
|
predictionWindowHours: 24,
|
|
confidenceThreshold: 0.7,
|
|
maxConcurrentWarming: 5
|
|
);
|
|
});
|
|
|
|
describe('Basic Operations', function () {
|
|
it('is enabled by default', function () {
|
|
expect($this->strategy->isEnabled())->toBeTrue();
|
|
expect($this->strategy->getName())->toBe('predictive');
|
|
});
|
|
|
|
it('tracks cache access patterns', function () {
|
|
$key = CacheKey::fromString('test-key');
|
|
|
|
$this->strategy->onCacheAccess($key, true);
|
|
$this->strategy->onCacheAccess($key, true);
|
|
|
|
$stats = $this->strategy->getStats();
|
|
|
|
expect($stats['strategy'])->toBe('PredictiveCacheStrategy');
|
|
expect($stats['total_patterns'])->toBe(1);
|
|
});
|
|
|
|
it('does not modify TTL', function () {
|
|
$key = CacheKey::fromString('test-key');
|
|
$originalTtl = Duration::fromMinutes(10);
|
|
|
|
$resultTtl = $this->strategy->onCacheSet($key, 'value', $originalTtl);
|
|
|
|
expect($resultTtl->toSeconds())->toBe($originalTtl->toSeconds());
|
|
});
|
|
|
|
it('clears pattern data', function () {
|
|
$key = CacheKey::fromString('test-key');
|
|
$this->strategy->onCacheAccess($key, true);
|
|
|
|
expect($this->strategy->getStats()['total_patterns'])->toBe(1);
|
|
|
|
$this->strategy->clear();
|
|
|
|
expect($this->strategy->getStats()['total_patterns'])->toBe(0);
|
|
});
|
|
|
|
it('forgets key-specific data', function () {
|
|
$key1 = CacheKey::fromString('key1');
|
|
$key2 = CacheKey::fromString('key2');
|
|
|
|
$this->strategy->onCacheAccess($key1, true);
|
|
$this->strategy->onCacheAccess($key2, true);
|
|
|
|
expect($this->strategy->getStats()['total_patterns'])->toBe(2);
|
|
|
|
$this->strategy->onCacheForget($key1);
|
|
|
|
expect($this->strategy->getStats()['total_patterns'])->toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('Pattern Learning', function () {
|
|
it('records access patterns with context', function () {
|
|
$key = CacheKey::fromString('pattern-key');
|
|
|
|
$this->strategy->recordAccess($key, [
|
|
'is_hit' => true,
|
|
'retrieval_time_ms' => 50
|
|
]);
|
|
|
|
$stats = $this->strategy->getStats();
|
|
|
|
expect($stats['total_patterns'])->toBe(1);
|
|
});
|
|
|
|
it('tracks cache dependencies', function () {
|
|
$primaryKey = CacheKey::fromString('primary-key');
|
|
$dependentKey = CacheKey::fromString('dependent-key');
|
|
|
|
$this->strategy->recordDependency($primaryKey, $dependentKey);
|
|
|
|
$stats = $this->strategy->getStats();
|
|
|
|
expect($stats['total_patterns'])->toBe(1);
|
|
});
|
|
|
|
it('generates predictions based on patterns', function () {
|
|
$key = CacheKey::fromString('predictable-key');
|
|
|
|
// Record multiple accesses to establish pattern
|
|
for ($i = 0; $i < 5; $i++) {
|
|
$this->strategy->onCacheAccess($key, true);
|
|
}
|
|
|
|
$predictions = $this->strategy->generatePredictions();
|
|
|
|
expect($predictions)->toBeArray();
|
|
});
|
|
});
|
|
|
|
describe('Warming Callbacks', function () {
|
|
it('registers warming callback for key', function () {
|
|
$key = CacheKey::fromString('warming-key');
|
|
$callbackExecuted = false;
|
|
|
|
$callback = function () use (&$callbackExecuted) {
|
|
$callbackExecuted = true;
|
|
return 'warmed value';
|
|
};
|
|
|
|
$this->strategy->registerWarmingCallback($key, $callback);
|
|
|
|
$stats = $this->strategy->getStats();
|
|
|
|
expect($stats['total_patterns'])->toBe(1);
|
|
});
|
|
|
|
it('performs predictive warming with callback', function () {
|
|
$key = CacheKey::fromString('warm-key');
|
|
$callbackExecuted = false;
|
|
|
|
$callback = function () use (&$callbackExecuted) {
|
|
$callbackExecuted = true;
|
|
return 'warmed value';
|
|
};
|
|
|
|
$this->strategy->registerWarmingCallback($key, $callback);
|
|
|
|
// Record accesses to build confidence
|
|
for ($i = 0; $i < 10; $i++) {
|
|
$this->strategy->onCacheAccess($key, true);
|
|
}
|
|
|
|
$results = $this->strategy->performPredictiveWarming();
|
|
|
|
expect($results)->toBeArray();
|
|
});
|
|
});
|
|
|
|
describe('Statistics and Insights', function () {
|
|
it('provides comprehensive statistics', function () {
|
|
$key = CacheKey::fromString('stats-key');
|
|
|
|
for ($i = 0; $i < 3; $i++) {
|
|
$this->strategy->onCacheAccess($key, true);
|
|
}
|
|
|
|
$stats = $this->strategy->getStats();
|
|
|
|
expect($stats)->toHaveKeys([
|
|
'strategy',
|
|
'enabled',
|
|
'total_patterns',
|
|
'active_warming_jobs',
|
|
'completed_warming_operations',
|
|
'successful_warming_operations',
|
|
'warming_success_rate',
|
|
'avg_warming_time_ms',
|
|
'confidence_threshold',
|
|
'prediction_window_hours'
|
|
]);
|
|
});
|
|
|
|
it('tracks warming success rate', function () {
|
|
$stats = $this->strategy->getStats();
|
|
|
|
expect($stats['warming_success_rate'])->toBeFloat();
|
|
expect($stats['avg_warming_time_ms'])->toBeFloat();
|
|
});
|
|
});
|
|
|
|
describe('Disabled Strategy', function () {
|
|
it('does nothing when disabled', function () {
|
|
$disabledStrategy = new PredictiveCacheStrategy(enabled: false);
|
|
$key = CacheKey::fromString('disabled-key');
|
|
|
|
$disabledStrategy->onCacheAccess($key, true);
|
|
|
|
$stats = $disabledStrategy->getStats();
|
|
|
|
expect($stats['enabled'])->toBeFalse();
|
|
expect($stats['total_patterns'])->toBe(0);
|
|
});
|
|
});
|
|
});
|