Files
michaelschiemer/tests/Unit/Framework/Cache/Strategies/CacheStrategiesTest.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- 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.
2025-10-25 19:18:37 +02:00

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);
});
});
});