- 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.
397 lines
15 KiB
PHP
397 lines
15 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Cache\CacheItem;
|
|
use App\Framework\Cache\CacheKey;
|
|
use App\Framework\Cache\CacheResult;
|
|
use App\Framework\Cache\Compression\NoCompression;
|
|
use App\Framework\Cache\Driver\InMemoryCache;
|
|
use App\Framework\Cache\GeneralCache;
|
|
use App\Framework\Cache\MultiLevelCache;
|
|
use App\Framework\Core\ValueObjects\Duration;
|
|
use App\Framework\Serializer\Php\PhpSerializer;
|
|
|
|
describe('MultiLevelCache', function () {
|
|
beforeEach(function () {
|
|
// Fast cache: In-memory for quick access
|
|
$fastDriver = new InMemoryCache();
|
|
$serializer = new PhpSerializer();
|
|
$compression = new NoCompression();
|
|
$this->fastCache = new GeneralCache($fastDriver, $serializer, $compression);
|
|
|
|
// Slow cache: Another in-memory cache to simulate slower storage
|
|
$slowDriver = new InMemoryCache();
|
|
$this->slowCache = new GeneralCache($slowDriver, $serializer, $compression);
|
|
|
|
// Multi-level cache combining both
|
|
$this->cache = new MultiLevelCache($this->fastCache, $this->slowCache);
|
|
});
|
|
|
|
describe('Basic Operations', function () {
|
|
it('sets items in both fast and slow cache for small values', function () {
|
|
$key = CacheKey::fromString('small-value');
|
|
$value = 'Small text';
|
|
|
|
$success = $this->cache->set(CacheItem::forSet($key, $value));
|
|
|
|
expect($success)->toBeTrue();
|
|
|
|
// Verify in fast cache
|
|
$fastResult = $this->fastCache->get($key);
|
|
expect($fastResult->isHit)->toBeTrue();
|
|
expect($fastResult->value)->toBe('Small text');
|
|
|
|
// Verify in slow cache
|
|
$slowResult = $this->slowCache->get($key);
|
|
expect($slowResult->isHit)->toBeTrue();
|
|
expect($slowResult->value)->toBe('Small text');
|
|
});
|
|
|
|
it('sets large values only in slow cache', function () {
|
|
$key = CacheKey::fromString('large-value');
|
|
// Create a large value (> 1KB)
|
|
$value = str_repeat('Large data content ', 100); // > 1KB
|
|
|
|
$success = $this->cache->set(CacheItem::forSet($key, $value));
|
|
|
|
expect($success)->toBeTrue();
|
|
|
|
// Should NOT be in fast cache (too large)
|
|
$fastResult = $this->fastCache->get($key);
|
|
expect($fastResult->isHit)->toBeFalse();
|
|
|
|
// Should be in slow cache
|
|
$slowResult = $this->slowCache->get($key);
|
|
expect($slowResult->isHit)->toBeTrue();
|
|
});
|
|
|
|
it('retrieves from fast cache when available', function () {
|
|
$key = CacheKey::fromString('fast-key');
|
|
|
|
// Set directly in fast cache
|
|
$this->fastCache->set(CacheItem::forSet($key, 'Fast value'));
|
|
|
|
$result = $this->cache->get($key);
|
|
|
|
expect($result->isHit)->toBeTrue();
|
|
expect($result->value)->toBe('Fast value');
|
|
});
|
|
|
|
it('falls back to slow cache on fast cache miss', function () {
|
|
$key = CacheKey::fromString('slow-key');
|
|
|
|
// Set only in slow cache
|
|
$this->slowCache->set(CacheItem::forSet($key, 'Slow value'));
|
|
|
|
$result = $this->cache->get($key);
|
|
|
|
expect($result->isHit)->toBeTrue();
|
|
expect($result->value)->toBe('Slow value');
|
|
});
|
|
|
|
it('warms up fast cache when fetching from slow cache', function () {
|
|
$key = CacheKey::fromString('warmup-key');
|
|
$value = 'Small value for warmup';
|
|
|
|
// Set only in slow cache
|
|
$this->slowCache->set(CacheItem::forSet($key, $value));
|
|
|
|
// Fast cache should be empty initially
|
|
expect($this->fastCache->get($key)->isHit)->toBeFalse();
|
|
|
|
// Get from multi-level cache (will warm up fast cache)
|
|
$result = $this->cache->get($key);
|
|
expect($result->isHit)->toBeTrue();
|
|
|
|
// Now fast cache should have it
|
|
$fastResult = $this->fastCache->get($key);
|
|
expect($fastResult->isHit)->toBeTrue();
|
|
expect($fastResult->value)->toBe($value);
|
|
});
|
|
});
|
|
|
|
describe('Multi-Key Operations', function () {
|
|
it('gets multiple keys from mixed sources', function () {
|
|
$key1 = CacheKey::fromString('fast-1');
|
|
$key2 = CacheKey::fromString('slow-1');
|
|
$key3 = CacheKey::fromString('missing-1');
|
|
|
|
// key1 in fast cache
|
|
$this->fastCache->set(CacheItem::forSet($key1, 'Value from fast'));
|
|
|
|
// key2 only in slow cache
|
|
$this->slowCache->set(CacheItem::forSet($key2, 'Value from slow'));
|
|
|
|
// key3 nowhere
|
|
|
|
$result = $this->cache->get($key1, $key2, $key3);
|
|
|
|
expect($result->count())->toBe(3);
|
|
expect($result->getValue($key1))->toBe('Value from fast');
|
|
expect($result->getValue($key2))->toBe('Value from slow');
|
|
expect($result->getItem($key3)->isHit)->toBeFalse();
|
|
});
|
|
|
|
it('sets multiple items with appropriate cache levels', function () {
|
|
$item1 = CacheItem::forSet(CacheKey::fromString('multi-1'), 'Small');
|
|
$item2 = CacheItem::forSet(CacheKey::fromString('multi-2'), 'Another small');
|
|
|
|
$success = $this->cache->set($item1, $item2);
|
|
|
|
expect($success)->toBeTrue();
|
|
|
|
// Both should be in both caches (small values)
|
|
expect($this->fastCache->get(CacheKey::fromString('multi-1'))->isHit)->toBeTrue();
|
|
expect($this->fastCache->get(CacheKey::fromString('multi-2'))->isHit)->toBeTrue();
|
|
expect($this->slowCache->get(CacheKey::fromString('multi-1'))->isHit)->toBeTrue();
|
|
expect($this->slowCache->get(CacheKey::fromString('multi-2'))->isHit)->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('Forget Operations', function () {
|
|
it('forgets items from both caches', function () {
|
|
$key = CacheKey::fromString('forget-me');
|
|
|
|
// Set in multi-level cache
|
|
$this->cache->set(CacheItem::forSet($key, 'Temporary value'));
|
|
|
|
// Verify it exists
|
|
expect($this->cache->get($key)->isHit)->toBeTrue();
|
|
|
|
// Forget
|
|
$success = $this->cache->forget($key);
|
|
|
|
expect($success)->toBeTrue();
|
|
expect($this->fastCache->get($key)->isHit)->toBeFalse();
|
|
expect($this->slowCache->get($key)->isHit)->toBeFalse();
|
|
expect($this->cache->get($key)->isHit)->toBeFalse();
|
|
});
|
|
|
|
it('forgets multiple keys from both caches', function () {
|
|
$key1 = CacheKey::fromString('forget-1');
|
|
$key2 = CacheKey::fromString('forget-2');
|
|
|
|
$this->cache->set(
|
|
CacheItem::forSet($key1, 'Value 1'),
|
|
CacheItem::forSet($key2, 'Value 2')
|
|
);
|
|
|
|
$this->cache->forget($key1, $key2);
|
|
|
|
expect($this->cache->get($key1)->isHit)->toBeFalse();
|
|
expect($this->cache->get($key2)->isHit)->toBeFalse();
|
|
});
|
|
});
|
|
|
|
describe('Clear Operations', function () {
|
|
it('clears both fast and slow caches', function () {
|
|
$this->cache->set(
|
|
CacheItem::forSet(CacheKey::fromString('clear-1'), 'Data 1'),
|
|
CacheItem::forSet(CacheKey::fromString('clear-2'), 'Data 2')
|
|
);
|
|
|
|
$success = $this->cache->clear();
|
|
|
|
expect($success)->toBeTrue();
|
|
expect($this->cache->get(CacheKey::fromString('clear-1'))->isHit)->toBeFalse();
|
|
expect($this->cache->get(CacheKey::fromString('clear-2'))->isHit)->toBeFalse();
|
|
expect($this->fastCache->get(CacheKey::fromString('clear-1'))->isHit)->toBeFalse();
|
|
expect($this->slowCache->get(CacheKey::fromString('clear-1'))->isHit)->toBeFalse();
|
|
});
|
|
});
|
|
|
|
describe('Has Operations', function () {
|
|
it('checks existence in fast cache first', function () {
|
|
$key = CacheKey::fromString('has-fast');
|
|
$this->fastCache->set(CacheItem::forSet($key, 'Fast data'));
|
|
|
|
$results = $this->cache->has($key);
|
|
|
|
expect($results['has-fast'])->toBeTrue();
|
|
});
|
|
|
|
it('checks slow cache on fast cache miss', function () {
|
|
$key = CacheKey::fromString('has-slow');
|
|
$this->slowCache->set(CacheItem::forSet($key, 'Slow data'));
|
|
|
|
$results = $this->cache->has($key);
|
|
|
|
expect($results['has-slow'])->toBeTrue();
|
|
});
|
|
|
|
it('warms up fast cache during has() check', function () {
|
|
$key = CacheKey::fromString('has-warmup');
|
|
$value = 'Small value';
|
|
$this->slowCache->set(CacheItem::forSet($key, $value));
|
|
|
|
// Fast cache empty initially
|
|
expect($this->fastCache->get($key)->isHit)->toBeFalse();
|
|
|
|
// Check existence (triggers warmup)
|
|
$results = $this->cache->has($key);
|
|
expect($results['has-warmup'])->toBeTrue();
|
|
|
|
// Fast cache should now have it
|
|
expect($this->fastCache->get($key)->isHit)->toBeTrue();
|
|
});
|
|
|
|
it('returns false for non-existent keys', function () {
|
|
$results = $this->cache->has(CacheKey::fromString('not-exists'));
|
|
|
|
expect($results['not-exists'])->toBeFalse();
|
|
});
|
|
});
|
|
|
|
describe('Remember Pattern', function () {
|
|
it('executes callback and caches result on miss', function () {
|
|
$key = CacheKey::fromString('remember-key');
|
|
$callbackExecuted = false;
|
|
|
|
$callback = function () use (&$callbackExecuted) {
|
|
$callbackExecuted = true;
|
|
return 'Computed value';
|
|
};
|
|
|
|
$result = $this->cache->remember($key, $callback);
|
|
|
|
expect($callbackExecuted)->toBeTrue();
|
|
expect($result->value)->toBe('Computed value');
|
|
|
|
// Verify it's cached
|
|
$cachedResult = $this->cache->get($key);
|
|
expect($cachedResult->isHit)->toBeTrue();
|
|
expect($cachedResult->value)->toBe('Computed value');
|
|
});
|
|
|
|
it('returns cached value without executing callback on hit', function () {
|
|
$key = CacheKey::fromString('remember-cached');
|
|
|
|
// Pre-populate cache
|
|
$this->cache->set(CacheItem::forSet($key, 'Cached value'));
|
|
|
|
$callbackExecuted = false;
|
|
$callback = function () use (&$callbackExecuted) {
|
|
$callbackExecuted = true;
|
|
return 'Should not execute';
|
|
};
|
|
|
|
$result = $this->cache->remember($key, $callback);
|
|
|
|
expect($callbackExecuted)->toBeFalse();
|
|
expect($result->value)->toBe('Cached value');
|
|
});
|
|
});
|
|
|
|
describe('Size-Based Cache Distribution', function () {
|
|
it('caches primitives in both layers', function () {
|
|
$key = CacheKey::fromString('primitive');
|
|
$this->cache->set(CacheItem::forSet($key, 42));
|
|
|
|
expect($this->fastCache->get($key)->isHit)->toBeTrue();
|
|
expect($this->slowCache->get($key)->isHit)->toBeTrue();
|
|
});
|
|
|
|
it('does not cache objects in fast layer', function () {
|
|
$key = CacheKey::fromString('object');
|
|
$object = new stdClass();
|
|
$object->data = 'test';
|
|
|
|
$this->cache->set(CacheItem::forSet($key, $object));
|
|
|
|
// Object should only be in slow cache
|
|
expect($this->fastCache->get($key)->isHit)->toBeFalse();
|
|
expect($this->slowCache->get($key)->isHit)->toBeTrue();
|
|
});
|
|
|
|
it('caches small arrays in both layers', function () {
|
|
$key = CacheKey::fromString('small-array');
|
|
$value = ['a' => 1, 'b' => 2, 'c' => 3];
|
|
|
|
$this->cache->set(CacheItem::forSet($key, $value));
|
|
|
|
expect($this->fastCache->get($key)->isHit)->toBeTrue();
|
|
expect($this->slowCache->get($key)->isHit)->toBeTrue();
|
|
});
|
|
|
|
it('caches large arrays only in slow layer', function () {
|
|
$key = CacheKey::fromString('large-array');
|
|
// Create large array (> 1KB estimated)
|
|
$value = array_fill(0, 100, str_repeat('data', 10));
|
|
|
|
$this->cache->set(CacheItem::forSet($key, $value));
|
|
|
|
expect($this->fastCache->get($key)->isHit)->toBeFalse();
|
|
expect($this->slowCache->get($key)->isHit)->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('TTL Management', function () {
|
|
it('respects TTL in slow cache', function () {
|
|
$key = CacheKey::fromString('ttl-test');
|
|
$item = CacheItem::forSet($key, 'TTL value', Duration::fromMinutes(10));
|
|
|
|
$this->cache->set($item);
|
|
|
|
// Should be cached
|
|
expect($this->slowCache->get($key)->isHit)->toBeTrue();
|
|
});
|
|
|
|
it('uses shorter TTL for fast cache', function () {
|
|
$key = CacheKey::fromString('fast-ttl');
|
|
// Set with 10 minute TTL
|
|
$item = CacheItem::forSet($key, 'Short value', Duration::fromMinutes(10));
|
|
|
|
$this->cache->set($item);
|
|
|
|
// Fast cache should have shorter TTL (5 minutes max by default)
|
|
// We can't directly verify TTL, but we verify it's cached
|
|
expect($this->fastCache->get($key)->isHit)->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases', function () {
|
|
it('handles empty operations gracefully', function () {
|
|
expect($this->cache->set())->toBeTrue();
|
|
expect($this->cache->forget())->toBeTrue();
|
|
expect($this->cache->has())->toBe([]);
|
|
expect($this->cache->get())->toBeInstanceOf(CacheResult::class);
|
|
});
|
|
|
|
it('handles null values correctly', function () {
|
|
$key = CacheKey::fromString('null-value');
|
|
$this->cache->set(CacheItem::forSet($key, null));
|
|
|
|
$result = $this->cache->get($key);
|
|
expect($result->isHit)->toBeTrue();
|
|
expect($result->value)->toBeNull();
|
|
});
|
|
|
|
it('handles false values correctly', function () {
|
|
$key = CacheKey::fromString('false-value');
|
|
$this->cache->set(CacheItem::forSet($key, false));
|
|
|
|
$result = $this->cache->get($key);
|
|
expect($result->isHit)->toBeTrue();
|
|
expect($result->value)->toBeFalse();
|
|
});
|
|
});
|
|
|
|
describe('Cache Layer Access', function () {
|
|
it('provides access to fast cache layer', function () {
|
|
$fastCache = $this->cache->getFastCache();
|
|
expect($fastCache)->toBeInstanceOf(GeneralCache::class);
|
|
});
|
|
|
|
it('provides access to slow cache layer', function () {
|
|
$slowCache = $this->cache->getSlowCache();
|
|
expect($slowCache)->toBeInstanceOf(GeneralCache::class);
|
|
});
|
|
|
|
it('provides driver access through slow cache', function () {
|
|
$driver = $this->cache->getDriver();
|
|
expect($driver)->toBeInstanceOf(InMemoryCache::class);
|
|
});
|
|
});
|
|
});
|