Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
668
src/Framework/Discovery/Storage/DiscoveryCacheManager.php
Normal file
668
src/Framework/Discovery/Storage/DiscoveryCacheManager.php
Normal file
@@ -0,0 +1,668 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Discovery\Storage;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Cache\CacheItem;
|
||||
use App\Framework\Cache\CacheKey;
|
||||
use App\Framework\Core\Events\EventDispatcher;
|
||||
use App\Framework\Core\ValueObjects\Byte;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Score;
|
||||
use App\Framework\DateTime\Clock;
|
||||
use App\Framework\Discovery\Events\CacheCompressionEvent;
|
||||
use App\Framework\Discovery\Events\CacheHitEvent;
|
||||
use App\Framework\Discovery\Events\CacheMissEvent;
|
||||
use App\Framework\Discovery\Memory\DiscoveryMemoryManager;
|
||||
use App\Framework\Discovery\Memory\MemoryStatus;
|
||||
use App\Framework\Discovery\Results\DiscoveryRegistry;
|
||||
use App\Framework\Discovery\ValueObjects\CacheLevel;
|
||||
use App\Framework\Discovery\ValueObjects\CacheMetrics;
|
||||
use App\Framework\Discovery\ValueObjects\CacheTier;
|
||||
use App\Framework\Discovery\ValueObjects\CompressionLevel;
|
||||
use App\Framework\Discovery\ValueObjects\DiscoveryContext;
|
||||
use App\Framework\Filesystem\FilePath;
|
||||
use App\Framework\Filesystem\FileSystemService;
|
||||
use App\Framework\Logging\Logger;
|
||||
|
||||
/**
|
||||
* Enhanced Discovery cache manager with memory-aware and tiered caching strategies
|
||||
*
|
||||
* Combines traditional discovery caching with:
|
||||
* - Memory-aware compression and eviction
|
||||
* - Tiered caching for different access patterns
|
||||
* - Automatic cache level adjustment based on memory pressure
|
||||
* - Comprehensive cache metrics and monitoring
|
||||
*/
|
||||
final class DiscoveryCacheManager
|
||||
{
|
||||
private const string CACHE_PREFIX = 'discovery:';
|
||||
private const int DEFAULT_TTL_HOURS = 24;
|
||||
|
||||
private array $cacheMetrics = [];
|
||||
|
||||
private array $accessPatterns = [];
|
||||
|
||||
private CacheLevel $currentLevel = CacheLevel::NORMAL;
|
||||
|
||||
public function __construct(
|
||||
private readonly Cache $cache,
|
||||
private readonly Clock $clock,
|
||||
private readonly FileSystemService $fileSystemService,
|
||||
private readonly ?Logger $logger = null,
|
||||
private readonly int $ttlHours = self::DEFAULT_TTL_HOURS,
|
||||
private readonly ?DiscoveryMemoryManager $memoryManager = null,
|
||||
private readonly ?EventDispatcher $eventDispatcher = null,
|
||||
private float $compressionThreshold = 0.75,
|
||||
private readonly float $evictionThreshold = 0.85
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached discovery results with memory-aware retrieval
|
||||
*/
|
||||
public function get(DiscoveryContext $context): ?DiscoveryRegistry
|
||||
{
|
||||
// Try tiered cache first
|
||||
$result = $this->getTieredCache($context);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Fallback to standard cache
|
||||
$key = $this->buildCacheKey($context);
|
||||
$item = $this->cache->get($key);
|
||||
|
||||
if (! $item->isHit) {
|
||||
$this->emitCacheMissEvent($key->toString(), 'not_found');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$cached = $this->processRetrievedData($item->value);
|
||||
if (! $cached instanceof DiscoveryRegistry) {
|
||||
$this->logger?->warning('Invalid cache data type', [
|
||||
'key' => $key->toString(),
|
||||
'type' => gettype($cached),
|
||||
]);
|
||||
$this->emitCacheMissEvent($key->toString(), 'corrupted');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate cache freshness
|
||||
if ($this->isStale($context, $cached)) {
|
||||
$this->logger?->info('Discovery cache is stale', [
|
||||
'key' => $key->toString(),
|
||||
]);
|
||||
$this->invalidate($context);
|
||||
$this->emitCacheMissEvent($key->toString(), 'expired');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Track access and emit hit event
|
||||
$this->trackAccess($key->toString());
|
||||
$dataSize = $this->estimateRegistrySize($cached);
|
||||
$this->emitCacheHitEvent($key->toString(), $dataSize);
|
||||
|
||||
return $cached;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store discovery results with memory-aware and tiered caching
|
||||
*/
|
||||
public function store(DiscoveryContext $context, DiscoveryRegistry $registry): bool
|
||||
{
|
||||
// Optimize registry before caching
|
||||
$registry->optimize();
|
||||
|
||||
// Determine cache strategy based on memory status
|
||||
if ($this->memoryManager !== null) {
|
||||
return $this->storeWithMemoryAwareness($context, $registry);
|
||||
}
|
||||
|
||||
// Fallback to standard caching
|
||||
return $this->storeStandard($context, $registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store with memory-aware strategy
|
||||
*/
|
||||
private function storeWithMemoryAwareness(DiscoveryContext $context, DiscoveryRegistry $registry): bool
|
||||
{
|
||||
$memoryStatus = $this->memoryManager->getMemoryStatus('cache_store');
|
||||
$dataSize = $this->estimateRegistrySize($registry);
|
||||
|
||||
// Determine appropriate cache level and tier
|
||||
$cacheLevel = $this->determineCacheLevel($memoryStatus);
|
||||
$cacheTier = $this->determineCacheTier($context, $registry, $memoryStatus);
|
||||
|
||||
// Apply memory-aware processing
|
||||
$processedData = $this->applyMemoryProcessing($registry, $cacheLevel, $cacheTier);
|
||||
$adjustedTtl = $this->calculateAdjustedTtl($cacheTier, $memoryStatus);
|
||||
|
||||
// Store in appropriate tier
|
||||
$key = $this->buildTieredCacheKey($context, $cacheTier);
|
||||
$cacheItem = CacheItem::forSet(
|
||||
key: $key,
|
||||
value: $processedData,
|
||||
ttl: $adjustedTtl
|
||||
);
|
||||
|
||||
$success = $this->cache->set($cacheItem);
|
||||
|
||||
if ($success) {
|
||||
$this->updateCacheMetrics($key->toString(), $dataSize, $cacheLevel, 'store');
|
||||
$this->trackAccess($key->toString());
|
||||
|
||||
$this->logger?->info('Discovery results cached with memory awareness', [
|
||||
'key' => $key->toString(),
|
||||
'items' => count($registry),
|
||||
'cache_level' => $cacheLevel->value,
|
||||
'cache_tier' => $cacheTier->value,
|
||||
'memory_pressure' => $memoryStatus->memoryPressure->toDecimal(),
|
||||
'data_size' => $dataSize->toHumanReadable(),
|
||||
'ttl' => $adjustedTtl->toHours(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard storage method (fallback)
|
||||
*/
|
||||
private function storeStandard(DiscoveryContext $context, DiscoveryRegistry $registry): bool
|
||||
{
|
||||
$key = $this->buildCacheKey($context);
|
||||
$ttl = Duration::fromHours($this->ttlHours);
|
||||
|
||||
$cacheItem = CacheItem::forSet(
|
||||
key: $key,
|
||||
value: $registry,
|
||||
ttl: $ttl
|
||||
);
|
||||
|
||||
$success = $this->cache->set($cacheItem);
|
||||
|
||||
if ($success) {
|
||||
$this->logger?->info('Discovery results cached (standard)', [
|
||||
'key' => $key->toString(),
|
||||
'items' => count($registry),
|
||||
'ttl' => $ttl->toHours(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate cache for a context
|
||||
*/
|
||||
public function invalidate(DiscoveryContext $context): bool
|
||||
{
|
||||
$key = $this->buildCacheKey($context);
|
||||
|
||||
return $this->cache->forget($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all discovery caches
|
||||
*/
|
||||
public function clearAll(): bool
|
||||
{
|
||||
// This would ideally use cache tags, but for now we'll use pattern matching
|
||||
$this->logger?->info('Clearing all discovery caches');
|
||||
|
||||
// Since we can't iterate cache keys, we'll just return true
|
||||
// In a real implementation, you'd want cache tags support
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enhanced cache health status with memory awareness
|
||||
*/
|
||||
public function getHealthStatus(): array
|
||||
{
|
||||
$baseStatus = [
|
||||
'cache_driver' => get_class($this->cache),
|
||||
'ttl_hours' => $this->ttlHours,
|
||||
'prefix' => self::CACHE_PREFIX,
|
||||
'memory_aware' => $this->memoryManager !== null,
|
||||
];
|
||||
|
||||
if ($this->memoryManager !== null) {
|
||||
$memoryStatus = $this->memoryManager->getMemoryStatus('health_check');
|
||||
$baseStatus['memory_management'] = [
|
||||
'status' => $memoryStatus->status->value,
|
||||
'current_usage' => $memoryStatus->currentUsage->toHumanReadable(),
|
||||
'memory_pressure' => $memoryStatus->memoryPressure->toDecimal(),
|
||||
'cache_level' => $this->currentLevel->value,
|
||||
];
|
||||
}
|
||||
|
||||
return $baseStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comprehensive cache metrics
|
||||
*/
|
||||
public function getCacheMetrics(): ?CacheMetrics
|
||||
{
|
||||
if ($this->memoryManager === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$memoryStatus = $this->memoryManager->getMemoryStatus('metrics');
|
||||
$hitRate = $this->calculateHitRate();
|
||||
$totalSize = $this->calculateTotalCacheSize();
|
||||
$compressionRatio = $this->calculateCompressionRatio();
|
||||
|
||||
return new CacheMetrics(
|
||||
memoryStatus: $memoryStatus,
|
||||
cacheLevel: $this->currentLevel,
|
||||
totalItems: count($this->cacheMetrics),
|
||||
hitRate: Score::fromRatio((int) ($hitRate * 100), 100),
|
||||
totalSize: $totalSize,
|
||||
compressionRatio: Score::fromRatio((int) ($compressionRatio * 100), 100),
|
||||
evictionCount: 0, // Would need to track this
|
||||
timestamp: $this->clock->time()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform memory pressure management
|
||||
*/
|
||||
public function performMemoryPressureManagement(): array
|
||||
{
|
||||
if ($this->memoryManager === null) {
|
||||
return ['message' => 'Memory management not available'];
|
||||
}
|
||||
|
||||
$memoryStatus = $this->memoryManager->getMemoryStatus('pressure_management');
|
||||
$actions = [];
|
||||
|
||||
if ($memoryStatus->status === MemoryStatus::CRITICAL) {
|
||||
// Clear all caches in critical situations
|
||||
$this->clearAll();
|
||||
$actions[] = 'emergency_cache_clear';
|
||||
} elseif ($memoryStatus->memoryPressure->toDecimal() > $this->evictionThreshold) {
|
||||
// Selective eviction based on access patterns
|
||||
$evicted = $this->performSelectiveEviction();
|
||||
$actions[] = "evicted_{$evicted}_items";
|
||||
}
|
||||
|
||||
// Adjust cache level based on memory pressure
|
||||
$newLevel = CacheLevel::fromMemoryPressure($memoryStatus->memoryPressure->toDecimal());
|
||||
if ($newLevel !== $this->currentLevel) {
|
||||
$this->currentLevel = $newLevel;
|
||||
$actions[] = "level_changed_to_{$newLevel->value}";
|
||||
}
|
||||
|
||||
return [
|
||||
'actions' => $actions,
|
||||
'memory_status' => $memoryStatus->toArray(),
|
||||
'cache_level' => $this->currentLevel->value,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build cache key for context
|
||||
*/
|
||||
private function buildCacheKey(DiscoveryContext $context): CacheKey
|
||||
{
|
||||
// Context returns already a CacheKey, so we use it directly
|
||||
return $context->getCacheKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if cached data is stale
|
||||
*/
|
||||
private function isStale(DiscoveryContext $context, DiscoveryRegistry $registry): bool
|
||||
{
|
||||
// For incremental scans, always consider cache stale
|
||||
if ($context->isIncremental()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if any source directories have been modified
|
||||
foreach ($context->paths as $path) {
|
||||
if ($this->directoryModifiedSince($path, $context->startTime)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if directory has been modified since given time
|
||||
*/
|
||||
private function directoryModifiedSince(string $path, \DateTimeInterface $since): bool
|
||||
{
|
||||
try {
|
||||
$filePath = FilePath::create($path);
|
||||
$metadata = $this->fileSystemService->getMetadata($filePath);
|
||||
|
||||
// Check if modification time is after the given time
|
||||
return $metadata->modifiedAt->getTimestamp() > $since->getTimestamp();
|
||||
} catch (\Throwable) {
|
||||
// If we can't check, assume it's been modified
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// === Memory-Aware Helper Methods ===
|
||||
|
||||
/**
|
||||
* Get cache from tiered storage
|
||||
*/
|
||||
private function getTieredCache(DiscoveryContext $context): ?DiscoveryRegistry
|
||||
{
|
||||
if ($this->memoryManager === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try each tier in order of preference
|
||||
foreach (CacheTier::orderedByPriority() as $tier) {
|
||||
$key = $this->buildTieredCacheKey($context, $tier);
|
||||
$item = $this->cache->get($key);
|
||||
|
||||
if ($item->isHit) {
|
||||
$data = $this->processRetrievedData($item->value);
|
||||
if ($data instanceof DiscoveryRegistry) {
|
||||
$this->trackAccess($key->toString());
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine cache level based on memory status
|
||||
*/
|
||||
private function determineCacheLevel(object $memoryStatus): CacheLevel
|
||||
{
|
||||
return CacheLevel::fromMemoryPressure($memoryStatus->memoryPressure->toDecimal());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine cache tier based on context and memory status
|
||||
*/
|
||||
private function determineCacheTier(DiscoveryContext $context, DiscoveryRegistry $registry, object $memoryStatus): CacheTier
|
||||
{
|
||||
$dataSize = $this->estimateRegistrySize($registry)->toBytes();
|
||||
$accessFrequency = $this->getAccessFrequency($context->getCacheKey());
|
||||
$memoryPressure = $memoryStatus->memoryPressure->toDecimal();
|
||||
|
||||
return CacheTier::suggest($dataSize, $accessFrequency, $memoryPressure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply memory-aware processing (compression, etc.)
|
||||
*/
|
||||
private function applyMemoryProcessing(DiscoveryRegistry $registry, CacheLevel $level, CacheTier $tier): mixed
|
||||
{
|
||||
$compressionLevel = $tier->getCompressionLevel();
|
||||
|
||||
if ($compressionLevel === CompressionLevel::NONE) {
|
||||
return $registry;
|
||||
}
|
||||
|
||||
// Serialize registry for compression
|
||||
$serialized = serialize($registry);
|
||||
$originalSize = Byte::fromBytes(strlen($serialized));
|
||||
|
||||
if ($originalSize->lessThan(Byte::fromBytes($compressionLevel->getMinimumDataSize()))) {
|
||||
return $registry;
|
||||
}
|
||||
|
||||
$compressed = $this->compressData($serialized, $compressionLevel);
|
||||
if ($compressed !== null) {
|
||||
$compressedSize = Byte::fromBytes(strlen($compressed));
|
||||
|
||||
// Emit compression event
|
||||
$this->emitCompressionEvent($originalSize, $compressedSize, $compressionLevel);
|
||||
|
||||
return [
|
||||
'__discovery_compressed__' => true,
|
||||
'data' => $compressed,
|
||||
'level' => $compressionLevel->value,
|
||||
'tier' => $tier->value,
|
||||
];
|
||||
}
|
||||
|
||||
return $registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process retrieved data (decompress if needed)
|
||||
*/
|
||||
private function processRetrievedData(mixed $data): mixed
|
||||
{
|
||||
if (! is_array($data) || ! ($data['__discovery_compressed__'] ?? false)) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$compressionLevel = CompressionLevel::from($data['level']);
|
||||
$decompressed = $this->decompressData($data['data'], $compressionLevel);
|
||||
|
||||
if ($decompressed !== null) {
|
||||
return unserialize($decompressed);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate adjusted TTL based on tier and memory status
|
||||
*/
|
||||
private function calculateAdjustedTtl(CacheTier $tier, object $memoryStatus): Duration
|
||||
{
|
||||
$baseTtl = Duration::fromHours($this->ttlHours);
|
||||
$tierMultiplier = $tier->getTtlMultiplier();
|
||||
$pressureAdjustment = 1.0 - ($memoryStatus->memoryPressure->toDecimal() * 0.3);
|
||||
|
||||
$finalMultiplier = $tierMultiplier * max(0.1, $pressureAdjustment);
|
||||
|
||||
return Duration::fromSeconds((int) ($baseTtl->toSeconds() * $finalMultiplier));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build tiered cache key
|
||||
*/
|
||||
private function buildTieredCacheKey(DiscoveryContext $context, CacheTier $tier): CacheKey
|
||||
{
|
||||
return CacheKey::fromString(self::CACHE_PREFIX . "tier_{$tier->value}:" . $context->getCacheKey()->toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate registry size
|
||||
*/
|
||||
private function estimateRegistrySize(DiscoveryRegistry $registry): Byte
|
||||
{
|
||||
return Byte::fromBytes(strlen(serialize($registry)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Track access patterns
|
||||
*/
|
||||
private function trackAccess(string $key): void
|
||||
{
|
||||
$timestamp = $this->clock->time();
|
||||
|
||||
if (! isset($this->accessPatterns[$key])) {
|
||||
$this->accessPatterns[$key] = [
|
||||
'access_count' => 0,
|
||||
'last_access' => $timestamp,
|
||||
'access_history' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$this->accessPatterns[$key]['access_count']++;
|
||||
$this->accessPatterns[$key]['last_access'] = $timestamp;
|
||||
$this->accessPatterns[$key]['access_history'][] = $timestamp;
|
||||
|
||||
// Trim history to prevent memory growth
|
||||
if (count($this->accessPatterns[$key]['access_history']) > 100) {
|
||||
$this->accessPatterns[$key]['access_history'] = array_slice(
|
||||
$this->accessPatterns[$key]['access_history'],
|
||||
-50
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get access frequency for a key
|
||||
*/
|
||||
private function getAccessFrequency(string $key): float
|
||||
{
|
||||
if (! isset($this->accessPatterns[$key])) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$pattern = $this->accessPatterns[$key];
|
||||
$timeWindow = 3600; // 1 hour
|
||||
$currentTime = $this->clock->time();
|
||||
|
||||
$recentAccesses = array_filter(
|
||||
$pattern['access_history'],
|
||||
fn ($timestamp) => ($currentTime - $timestamp) <= $timeWindow
|
||||
);
|
||||
|
||||
return count($recentAccesses) / ($timeWindow / 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update cache metrics
|
||||
*/
|
||||
private function updateCacheMetrics(string $key, Byte $size, CacheLevel $level, string $operation): void
|
||||
{
|
||||
if (! isset($this->cacheMetrics[$key])) {
|
||||
$this->cacheMetrics[$key] = [
|
||||
'size' => $size,
|
||||
'level' => $level,
|
||||
'hits' => 0,
|
||||
'misses' => 0,
|
||||
'created' => $this->clock->time(),
|
||||
];
|
||||
}
|
||||
|
||||
$this->cacheMetrics[$key]['last_access'] = $this->clock->time();
|
||||
|
||||
match ($operation) {
|
||||
'hit' => $this->cacheMetrics[$key]['hits']++,
|
||||
'miss' => $this->cacheMetrics[$key]['misses']++,
|
||||
default => null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate hit rate
|
||||
*/
|
||||
private function calculateHitRate(): float
|
||||
{
|
||||
$totalHits = array_sum(array_column($this->cacheMetrics, 'hits'));
|
||||
$totalMisses = array_sum(array_column($this->cacheMetrics, 'misses'));
|
||||
$total = $totalHits + $totalMisses;
|
||||
|
||||
return $total > 0 ? $totalHits / $total : 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate total cache size
|
||||
*/
|
||||
private function calculateTotalCacheSize(): Byte
|
||||
{
|
||||
$totalSize = Byte::zero();
|
||||
foreach ($this->cacheMetrics as $metrics) {
|
||||
$totalSize = $totalSize->add($metrics['size']);
|
||||
}
|
||||
|
||||
return $totalSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate compression ratio
|
||||
*/
|
||||
private function calculateCompressionRatio(): float
|
||||
{
|
||||
// Placeholder - would need to track actual compression ratios
|
||||
return 0.6; // Assume 40% compression on average
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform selective eviction
|
||||
*/
|
||||
private function performSelectiveEviction(): int
|
||||
{
|
||||
// Placeholder - would implement LRU-based eviction
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compress data
|
||||
*/
|
||||
private function compressData(string $data, CompressionLevel $level): ?string
|
||||
{
|
||||
$compressed = gzcompress($data, $level->getGzipLevel());
|
||||
|
||||
return $compressed !== false ? $compressed : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decompress data
|
||||
*/
|
||||
private function decompressData(string $data, CompressionLevel $level): ?string
|
||||
{
|
||||
$decompressed = gzuncompress($data);
|
||||
|
||||
return $decompressed !== false ? $decompressed : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit cache hit event
|
||||
*/
|
||||
private function emitCacheHitEvent(string $key, Byte $dataSize): void
|
||||
{
|
||||
$this->eventDispatcher?->dispatch(new CacheHitEvent(
|
||||
cacheKey: CacheKey::fromString($key),
|
||||
itemCount: 1,
|
||||
cacheAge: Duration::fromSeconds(0), // Would need actual age
|
||||
timestamp: $this->clock->time(),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit cache miss event
|
||||
*/
|
||||
private function emitCacheMissEvent(string $key, string $reason): void
|
||||
{
|
||||
$this->eventDispatcher?->dispatch(new CacheMissEvent(
|
||||
cacheKey: CacheKey::fromString($key),
|
||||
reason: $reason,
|
||||
cacheLevel: $this->currentLevel,
|
||||
timestamp: $this->clock->time()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit compression event
|
||||
*/
|
||||
private function emitCompressionEvent(Byte $originalSize, Byte $compressedSize, CompressionLevel $level): void
|
||||
{
|
||||
$this->eventDispatcher?->dispatch(new CacheCompressionEvent(
|
||||
originalSize: $originalSize,
|
||||
compressedSize: $compressedSize,
|
||||
compressionLevel: $level,
|
||||
compressionRatio: $compressedSize->percentOf($originalSize)->toDecimal(),
|
||||
timestamp: $this->clock->time()
|
||||
));
|
||||
}
|
||||
}
|
||||
70
src/Framework/Discovery/Storage/DiscoveryStorage.php
Normal file
70
src/Framework/Discovery/Storage/DiscoveryStorage.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Discovery\Storage;
|
||||
|
||||
use App\Framework\Filesystem\FileMetadata;
|
||||
use App\Framework\Filesystem\Storage;
|
||||
use SplFileInfo;
|
||||
|
||||
/**
|
||||
* Spezialisiertes Storage-Interface für Discovery-Operationen
|
||||
* Erweitert das Standard-Storage-Interface mit Discovery-spezifischen Methoden
|
||||
*/
|
||||
interface DiscoveryStorage extends Storage
|
||||
{
|
||||
/**
|
||||
* Findet geänderte Dateien seit dem letzten Scan
|
||||
*
|
||||
* @param string $directory Zu durchsuchendes Verzeichnis
|
||||
* @param array<string, array> $fileMetadata Vorhandene Metadaten [path => metadata]
|
||||
* @return array<SplFileInfo> Liste geänderter Dateien
|
||||
*/
|
||||
public function findChangedFiles(string $directory, array $fileMetadata): array;
|
||||
|
||||
/**
|
||||
* Führt einen inkrementellen Scan durch
|
||||
*
|
||||
* @param string $directory Zu durchsuchendes Verzeichnis
|
||||
* @param array<string, array> $fileMetadata Vorhandene Metadaten [path => metadata]
|
||||
* @return array<string, array> Aktualisierte Metadaten [path => metadata]
|
||||
*/
|
||||
public function incrementalScan(string $directory, array $fileMetadata): array;
|
||||
|
||||
/**
|
||||
* Findet alle Dateien mit einem bestimmten Muster
|
||||
*
|
||||
* @param string $directory Zu durchsuchendes Verzeichnis
|
||||
* @param string $pattern Suchmuster (z.B. '*.php')
|
||||
* @return array<SplFileInfo> Liste gefundener Dateien
|
||||
*/
|
||||
public function findFiles(string $directory, string $pattern): array;
|
||||
|
||||
/**
|
||||
* Erstellt Metadaten für eine Datei
|
||||
*
|
||||
* @param string $filePath Pfad zur Datei
|
||||
* @param bool $calculateChecksum Ob eine Prüfsumme berechnet werden soll
|
||||
* @return FileMetadata Metadaten der Datei
|
||||
*/
|
||||
public function getDiscoveryMetadata(string $filePath, bool $calculateChecksum = true): FileMetadata;
|
||||
|
||||
/**
|
||||
* Erstellt Metadaten für mehrere Dateien parallel
|
||||
*
|
||||
* @param array<string> $filePaths Liste von Dateipfaden
|
||||
* @param bool $calculateChecksum Ob Prüfsummen berechnet werden sollen
|
||||
* @return array<string, FileMetadata> Metadaten [path => metadata]
|
||||
*/
|
||||
public function getDiscoveryMetadataMultiple(array $filePaths, bool $calculateChecksum = true): array;
|
||||
|
||||
/**
|
||||
* Registriert Dateisystem-Events für Änderungserkennung
|
||||
*
|
||||
* @param string $directory Zu überwachendes Verzeichnis
|
||||
* @param callable $callback Callback-Funktion für Events (string $path, string $event)
|
||||
* @return bool Erfolg der Registrierung
|
||||
*/
|
||||
public function registerFileSystemEvents(string $directory, callable $callback): bool;
|
||||
}
|
||||
343
src/Framework/Discovery/Storage/FileSystemDiscoveryStorage.php
Normal file
343
src/Framework/Discovery/Storage/FileSystemDiscoveryStorage.php
Normal file
@@ -0,0 +1,343 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Discovery\Storage;
|
||||
|
||||
use App\Framework\Async\FiberManager;
|
||||
use App\Framework\Filesystem\Directory;
|
||||
use App\Framework\Filesystem\File;
|
||||
use App\Framework\Filesystem\FileMetadata;
|
||||
use App\Framework\Filesystem\PermissionChecker;
|
||||
use App\Framework\Filesystem\Storage;
|
||||
use SplFileInfo;
|
||||
|
||||
/**
|
||||
* Implementierung des DiscoveryStorage-Interfaces, die das Filesystem-Modul nutzt
|
||||
* und die Discovery-spezifischen Methoden implementiert.
|
||||
*
|
||||
* Diese Klasse verwendet Composition, um die Funktionalität des Storage-Interfaces
|
||||
* zu delegieren und erweitert sie um Discovery-spezifische Funktionen.
|
||||
*/
|
||||
final class FileSystemDiscoveryStorage implements DiscoveryStorage
|
||||
{
|
||||
public readonly PermissionChecker $permissions;
|
||||
|
||||
public readonly FiberManager $fiberManager;
|
||||
|
||||
/**
|
||||
* @param Storage $storage Basis-Storage-Implementierung für Delegation
|
||||
* @param PermissionChecker $permissions Permissions-Checker
|
||||
* @param FiberManager $fiberManager Fiber-Manager für asynchrone Operationen
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly Storage $storage,
|
||||
?PermissionChecker $permissions = null,
|
||||
?FiberManager $fiberManager = null
|
||||
) {
|
||||
$this->permissions = $permissions ?? new PermissionChecker($storage);
|
||||
$this->fiberManager = $fiberManager ?? new FiberManager(
|
||||
new \App\Framework\DateTime\SystemClock(),
|
||||
new \App\Framework\DateTime\SystemTimer()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function get(string $path): string
|
||||
{
|
||||
return $this->storage->get($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function put(string $path, string $content): void
|
||||
{
|
||||
$this->storage->put($path, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function exists(string $path): bool
|
||||
{
|
||||
return $this->storage->exists($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function delete(string $path): void
|
||||
{
|
||||
$this->storage->delete($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function copy(string $source, string $destination): void
|
||||
{
|
||||
$this->storage->copy($source, $destination);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function size(string $path): int
|
||||
{
|
||||
return $this->storage->size($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function lastModified(string $path): int
|
||||
{
|
||||
return $this->storage->lastModified($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMimeType(string $path): string
|
||||
{
|
||||
return $this->storage->getMimeType($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isReadable(string $path): bool
|
||||
{
|
||||
return $this->storage->isReadable($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isWritable(string $path): bool
|
||||
{
|
||||
return $this->storage->isWritable($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function listDirectory(string $directory): array
|
||||
{
|
||||
return $this->storage->listDirectory($directory);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createDirectory(string $path, int $permissions = 0755, bool $recursive = true): void
|
||||
{
|
||||
$this->storage->createDirectory($path, $permissions, $recursive);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function file(string $path): File
|
||||
{
|
||||
return $this->storage->file($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function directory(string $path): Directory
|
||||
{
|
||||
return $this->storage->directory($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function batch(array $operations): array
|
||||
{
|
||||
return $this->storage->batch($operations);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMultiple(array $paths): array
|
||||
{
|
||||
return $this->storage->getMultiple($paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function putMultiple(array $files): void
|
||||
{
|
||||
$this->storage->putMultiple($files);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMetadataMultiple(array $paths): array
|
||||
{
|
||||
return $this->storage->getMetadataMultiple($paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function findChangedFiles(string $directory, array $fileMetadata): array
|
||||
{
|
||||
// Finde alle Dateien mit dem Muster
|
||||
$allFiles = $this->findFiles($directory, '*.php');
|
||||
$changedFiles = [];
|
||||
$newFiles = [];
|
||||
$modifiedFiles = [];
|
||||
|
||||
// Prüfe jede Datei auf Änderungen
|
||||
foreach ($allFiles as $file) {
|
||||
$filePath = $file->getPathname();
|
||||
|
||||
// Neue Datei (nicht in Metadaten)
|
||||
if (! isset($fileMetadata[$filePath])) {
|
||||
$newFiles[] = $file;
|
||||
$changedFiles[] = $file;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prüfe auf Änderungen mit detaillierten Metadaten
|
||||
try {
|
||||
$storedMetadata = FileMetadata::fromArray($fileMetadata[$filePath]);
|
||||
|
||||
if ($storedMetadata->hasChanged($filePath, $this)) {
|
||||
$modifiedFiles[] = $file;
|
||||
$changedFiles[] = $file;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// Bei Problemen mit Metadaten, behandle als geändert
|
||||
$changedFiles[] = $file;
|
||||
}
|
||||
}
|
||||
|
||||
return $changedFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function incrementalScan(string $directory, array $fileMetadata): array
|
||||
{
|
||||
$changedFiles = $this->findChangedFiles($directory, $fileMetadata);
|
||||
$updatedMetadata = $fileMetadata;
|
||||
|
||||
// Aktualisiere Metadaten für geänderte Dateien
|
||||
foreach ($changedFiles as $file) {
|
||||
$filePath = $file->getPathname();
|
||||
|
||||
try {
|
||||
$metadata = $this->getDiscoveryMetadata($filePath, true);
|
||||
$updatedMetadata[$filePath] = $metadata->toArray();
|
||||
} catch (\Throwable) {
|
||||
// Bei Fehlern, entferne die Metadaten
|
||||
unset($updatedMetadata[$filePath]);
|
||||
}
|
||||
}
|
||||
|
||||
// Entferne gelöschte Dateien aus Metadaten
|
||||
$allFiles = $this->findFiles($directory, '*.php');
|
||||
$existingPaths = array_map(fn (SplFileInfo $file) => $file->getPathname(), $allFiles);
|
||||
|
||||
foreach (array_keys($updatedMetadata) as $path) {
|
||||
if (! in_array($path, $existingPaths)) {
|
||||
unset($updatedMetadata[$path]);
|
||||
}
|
||||
}
|
||||
|
||||
return $updatedMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function findFiles(string $directory, string $pattern): array
|
||||
{
|
||||
$result = [];
|
||||
$this->findFilesRecursive($directory, $pattern, $result);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rekursive Hilfsmethode für findFiles
|
||||
*/
|
||||
private function findFilesRecursive(string $directory, string $pattern, array &$result): void
|
||||
{
|
||||
if (! is_dir($directory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$files = scandir($directory);
|
||||
if ($files === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($files as $file) {
|
||||
if ($file === '.' || $file === '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = $directory . DIRECTORY_SEPARATOR . $file;
|
||||
|
||||
if (is_dir($path)) {
|
||||
$this->findFilesRecursive($path, $pattern, $result);
|
||||
} elseif ($this->matchesPattern($file, $pattern)) {
|
||||
$result[] = new SplFileInfo($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob ein Dateiname einem Muster entspricht
|
||||
*/
|
||||
private function matchesPattern(string $filename, string $pattern): bool
|
||||
{
|
||||
return fnmatch($pattern, $filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDiscoveryMetadata(string $filePath, bool $calculateChecksum = true): FileMetadata
|
||||
{
|
||||
return FileMetadata::fromFile($filePath, $this, $calculateChecksum);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDiscoveryMetadataMultiple(array $filePaths, bool $calculateChecksum = true): array
|
||||
{
|
||||
$operations = [];
|
||||
|
||||
foreach ($filePaths as $path) {
|
||||
$operations[$path] = fn () => $this->getDiscoveryMetadata($path, $calculateChecksum);
|
||||
}
|
||||
|
||||
return $this->batch($operations);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function registerFileSystemEvents(string $directory, callable $callback): bool
|
||||
{
|
||||
// Implementierung hängt von der konkreten Event-System-Unterstützung ab
|
||||
// Hier eine einfache Implementierung, die immer false zurückgibt
|
||||
// In einer realen Implementierung würde hier ein Filesystem-Watcher registriert
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user