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:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -0,0 +1,222 @@
<?php
declare(strict_types=1);
namespace App\Framework\StateManagement;
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheKey;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Logging\Logger;
/**
* Cache-based implementation of state manager
*
* @template T of SerializableState
* @implements StateManager<T>
*/
final class CacheBasedStateManager implements StateManager
{
private int $hitCount = 0;
private int $missCount = 0;
private int $setCount = 0;
private int $removeCount = 0;
private int $updateCount = 0;
private Duration $totalSetTime;
private Duration $totalGetTime;
public function __construct(
private readonly Cache $cache,
private readonly string $keyPrefix,
private readonly string $stateClass,
private readonly ?Duration $defaultTtl = null,
private readonly ?Logger $logger = null,
private readonly string $namespace = 'default',
) {
$this->totalSetTime = Duration::zero();
$this->totalGetTime = Duration::zero();
}
public function getState(string $key): mixed
{
$startTime = Timestamp::now();
try {
$cacheKey = $this->getCacheKey($key);
$cacheItem = $this->cache->get($cacheKey);
if (! $cacheItem->isHit) {
$this->missCount++;
$this->log('debug', "State miss for key: {$key}");
return null;
}
$data = $cacheItem->value;
if (! is_array($data)) {
$this->missCount++;
$this->log('warning', "Invalid state data for key: {$key}");
return null;
}
/** @var T $state */
$state = call_user_func([$this->stateClass, 'fromArray'], $data);
$this->hitCount++;
$this->log('debug', "State hit for key: {$key}");
return $state;
} finally {
$elapsed = Timestamp::now()->diff($startTime);
$this->totalGetTime = $this->totalGetTime->add($elapsed);
}
}
public function setState(string $key, mixed $state, ?Duration $ttl = null): void
{
$startTime = Timestamp::now();
try {
if (! $state instanceof SerializableState) {
throw new \InvalidArgumentException("State must implement SerializableState interface");
}
$cacheKey = $this->getCacheKey($key);
$effectiveTtl = $ttl ?? $this->defaultTtl ?? Duration::fromHours(1);
$this->cache->set($cacheKey, $state->toArray(), $effectiveTtl);
$this->setCount++;
$this->log('debug', "State set for key: {$key}", ['ttl' => $effectiveTtl->toSeconds()]);
} finally {
$elapsed = Timestamp::now()->diff($startTime);
$this->totalSetTime = $this->totalSetTime->add($elapsed);
}
}
public function hasState(string $key): bool
{
$cacheKey = $this->getCacheKey($key);
$cacheItem = $this->cache->get($cacheKey);
return $cacheItem->isHit;
}
public function removeState(string $key): void
{
$cacheKey = $this->getCacheKey($key);
$this->cache->forget($cacheKey);
$this->removeCount++;
$this->log('debug', "State removed for key: {$key}");
}
public function updateState(string $key, callable $updater, ?Duration $ttl = null): mixed
{
$this->updateCount++;
// Get current state
$currentState = $this->getState($key);
// Apply update function
$newState = $updater($currentState);
// Save updated state
$this->setState($key, $newState, $ttl);
$this->log('debug', "State updated for key: {$key}");
return $newState;
}
public function getAllStates(): array
{
// Note: This would require cache enumeration support
// For now, return empty array - could be enhanced with cache tag support
$this->log('warning', "getAllStates() not supported by cache-based implementation");
return [];
}
public function clearAll(): void
{
// Note: This would require cache enumeration or tag support
// For now, we'll just log - could be enhanced with cache prefix clearing
$this->log('warning', "clearAll() not fully supported by cache-based implementation");
}
public function getStatistics(): StateManagerStatistics
{
$totalGets = $this->hitCount + $this->missCount;
$averageGetTime = $totalGets > 0
? Duration::fromMilliseconds($this->totalGetTime->toMilliseconds() / $totalGets)
: Duration::zero();
$averageSetTime = $this->setCount > 0
? Duration::fromMilliseconds($this->totalSetTime->toMilliseconds() / $this->setCount)
: Duration::zero();
return new StateManagerStatistics(
totalKeys: 0, // Would require cache enumeration
hitCount: $this->hitCount,
missCount: $this->missCount,
setCount: $this->setCount,
removeCount: $this->removeCount,
updateCount: $this->updateCount,
averageSetTime: $averageSetTime,
averageGetTime: $averageGetTime,
expiredKeys: 0, // Would require cache inspection
memoryUsage: Byte::fromBytes(0), // Would require cache size information
);
}
/**
* Create state manager for specific state type
*
* @template U of SerializableState
* @param class-string<U> $stateClass
* @return CacheBasedStateManager<U>
*/
public static function for(
Cache $cache,
string $keyPrefix,
string $stateClass,
?Duration $defaultTtl = null,
?Logger $logger = null,
string $namespace = 'default',
): self {
return new self($cache, $keyPrefix, $stateClass, $defaultTtl, $logger, $namespace);
}
private function getCacheKey(string $key): CacheKey
{
$fullKey = "{$this->keyPrefix}:{$this->namespace}:{$key}";
return CacheKey::from($fullKey);
}
private function log(string $level, string $message, array $context = []): void
{
if ($this->logger === null) {
return;
}
$context['namespace'] = $this->namespace;
$context['key_prefix'] = $this->keyPrefix;
$context['state_class'] = $this->stateClass;
match ($level) {
'debug' => $this->logger->debug("[StateManager] {$message}", $context),
'info' => $this->logger->info("[StateManager] {$message}", $context),
'warning' => $this->logger->warning("[StateManager] {$message}", $context),
'error' => $this->logger->error("[StateManager] {$message}", $context),
default => $this->logger->info("[StateManager] {$message}", $context),
};
}
}

View File

@@ -0,0 +1,215 @@
<?php
declare(strict_types=1);
namespace App\Framework\StateManagement;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* In-memory implementation of state manager (for testing and simple use cases)
*
* @template T of SerializableState
* @implements StateManager<T>
*/
final class InMemoryStateManager implements StateManager
{
private array $states = [];
private array $expirationTimes = [];
private int $hitCount = 0;
private int $missCount = 0;
private int $setCount = 0;
private int $removeCount = 0;
private int $updateCount = 0;
private Duration $totalSetTime;
private Duration $totalGetTime;
public function __construct(
private readonly string $stateClass,
) {
$this->totalSetTime = Duration::zero();
$this->totalGetTime = Duration::zero();
}
public function getState(string $key): mixed
{
$startTime = Timestamp::now();
try {
// Check if key exists and is not expired
if (! $this->isValidKey($key)) {
$this->missCount++;
return null;
}
$data = $this->states[$key];
/** @var T $state */
$state = call_user_func([$this->stateClass, 'fromArray'], $data);
$this->hitCount++;
return $state;
} finally {
$elapsed = Timestamp::now()->diff($startTime);
$this->totalGetTime = $this->totalGetTime->add($elapsed);
}
}
public function setState(string $key, mixed $state, ?Duration $ttl = null): void
{
$startTime = Timestamp::now();
try {
if (! $state instanceof SerializableState) {
throw new \InvalidArgumentException("State must implement SerializableState interface");
}
$this->states[$key] = $state->toArray();
// Set expiration time if TTL provided
if ($ttl !== null && ! $ttl->isZero()) {
$this->expirationTimes[$key] = Timestamp::now()->add($ttl)->toFloat();
} else {
unset($this->expirationTimes[$key]);
}
$this->setCount++;
} finally {
$elapsed = Timestamp::now()->diff($startTime);
$this->totalSetTime = $this->totalSetTime->add($elapsed);
}
}
public function hasState(string $key): bool
{
return $this->isValidKey($key);
}
public function removeState(string $key): void
{
unset($this->states[$key], $this->expirationTimes[$key]);
$this->removeCount++;
}
public function updateState(string $key, callable $updater, ?Duration $ttl = null): mixed
{
$this->updateCount++;
// Get current state
$currentState = $this->getState($key);
// Apply update function
$newState = $updater($currentState);
// Save updated state
$this->setState($key, $newState, $ttl);
return $newState;
}
public function getAllStates(): array
{
$validStates = [];
foreach ($this->states as $key => $data) {
if ($this->isValidKey($key)) {
/** @var T $state */
$state = call_user_func([$this->stateClass, 'fromArray'], $data);
$validStates[$key] = $state;
}
}
return $validStates;
}
public function clearAll(): void
{
$this->states = [];
$this->expirationTimes = [];
}
public function getStatistics(): StateManagerStatistics
{
$this->cleanupExpiredKeys(); // Clean up before calculating stats
$totalGets = $this->hitCount + $this->missCount;
$averageGetTime = $totalGets > 0
? Duration::fromMilliseconds($this->totalGetTime->toMilliseconds() / $totalGets)
: Duration::zero();
$averageSetTime = $this->setCount > 0
? Duration::fromMilliseconds($this->totalSetTime->toMilliseconds() / $this->setCount)
: Duration::zero();
// Estimate memory usage
$memoryUsage = Byte::fromBytes(
strlen(serialize($this->states)) + strlen(serialize($this->expirationTimes))
);
return new StateManagerStatistics(
totalKeys: count($this->states),
hitCount: $this->hitCount,
missCount: $this->missCount,
setCount: $this->setCount,
removeCount: $this->removeCount,
updateCount: $this->updateCount,
averageSetTime: $averageSetTime,
averageGetTime: $averageGetTime,
expiredKeys: 0, // We clean up expired keys immediately
memoryUsage: $memoryUsage,
);
}
/**
* Create state manager for specific state type
*
* @template U of SerializableState
* @param class-string<U> $stateClass
* @return InMemoryStateManager<U>
*/
public static function for(string $stateClass): self
{
return new self($stateClass);
}
private function isValidKey(string $key): bool
{
if (! isset($this->states[$key])) {
return false;
}
// Check expiration
if (isset($this->expirationTimes[$key])) {
$now = Timestamp::now()->toFloat();
if ($now > $this->expirationTimes[$key]) {
$this->removeState($key);
return false;
}
}
return true;
}
private function cleanupExpiredKeys(): void
{
$now = Timestamp::now()->toFloat();
foreach ($this->expirationTimes as $key => $expirationTime) {
if ($now > $expirationTime) {
$this->removeState($key);
}
}
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Framework\StateManagement;
/**
* Interface for states that can be serialized for persistence
*/
interface SerializableState
{
/**
* Convert state to array for serialization
*/
public function toArray(): array;
/**
* Create state from array (deserialization)
*/
public static function fromArray(array $data): static;
}

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Framework\StateManagement;
use App\Framework\Core\ValueObjects\Duration;
/**
* Generic interface for state management
*
* @template T
*/
interface StateManager
{
/**
* Get state for given key
*
* @param string $key
* @return T|null
*/
public function getState(string $key): mixed;
/**
* Set state for given key
*
* @param string $key
* @param T $state
* @param Duration|null $ttl
* @return void
*/
public function setState(string $key, mixed $state, ?Duration $ttl = null): void;
/**
* Check if state exists for given key
*/
public function hasState(string $key): bool;
/**
* Remove state for given key
*/
public function removeState(string $key): void;
/**
* Update state atomically using callback
*
* @param string $key
* @param callable(T|null): T $updater
* @param Duration|null $ttl
* @return T
*/
public function updateState(string $key, callable $updater, ?Duration $ttl = null): mixed;
/**
* Get all states (if supported by implementation)
*
* @return array<string, T>
*/
public function getAllStates(): array;
/**
* Clear all states
*/
public function clearAll(): void;
/**
* Get state statistics
*/
public function getStatistics(): StateManagerStatistics;
}

View File

@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace App\Framework\StateManagement;
use App\Framework\Cache\Cache;
use App\Framework\CircuitBreaker\CircuitBreakerMetrics;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\ErrorBoundaries\CircuitBreaker\BoundaryCircuitBreakerState;
use App\Framework\Logging\Logger;
use App\Framework\RateLimit\TokenBucket;
/**
* Factory for creating state managers
*/
final readonly class StateManagerFactory
{
public function __construct(
private Cache $cache,
private ?Logger $logger = null,
) {
}
/**
* Create cache-based state manager
*
* @template T of SerializableState
* @param class-string<T> $stateClass
* @return CacheBasedStateManager<T>
*/
public function createCacheBased(
string $keyPrefix,
string $stateClass,
?Duration $defaultTtl = null,
string $namespace = 'default',
): CacheBasedStateManager {
return CacheBasedStateManager::for(
cache: $this->cache,
keyPrefix: $keyPrefix,
stateClass: $stateClass,
defaultTtl: $defaultTtl,
logger: $this->logger,
namespace: $namespace,
);
}
/**
* Create in-memory state manager (for testing)
*
* @template T of SerializableState
* @param class-string<T> $stateClass
* @return InMemoryStateManager<T>
*/
public function createInMemory(string $stateClass): InMemoryStateManager
{
return InMemoryStateManager::for($stateClass);
}
/**
* Create state manager for CircuitBreaker states
*/
public function createForCircuitBreaker(string $namespace = 'default'): CacheBasedStateManager
{
return $this->createCacheBased(
keyPrefix: 'circuit_breaker_state',
stateClass: CircuitBreakerMetrics::class,
defaultTtl: Duration::fromHours(1),
namespace: $namespace,
);
}
/**
* Create state manager for ErrorBoundary states
*/
public function createForErrorBoundary(string $namespace = 'default'): CacheBasedStateManager
{
return $this->createCacheBased(
keyPrefix: 'error_boundary_state',
stateClass: BoundaryCircuitBreakerState::class,
defaultTtl: Duration::fromHours(1),
namespace: $namespace,
);
}
/**
* Create state manager for RateLimiter (future use)
*/
public function createForRateLimiter(string $namespace = 'default'): CacheBasedStateManager
{
return $this->createCacheBased(
keyPrefix: 'rate_limiter_state',
stateClass: TokenBucket::class, // Assuming this will implement SerializableState
defaultTtl: Duration::fromHours(1),
namespace: $namespace,
);
}
}

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace App\Framework\StateManagement;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Percentage;
/**
* Statistics for state manager operations
*/
final readonly class StateManagerStatistics
{
public function __construct(
public int $totalKeys = 0,
public int $hitCount = 0,
public int $missCount = 0,
public int $setCount = 0,
public int $removeCount = 0,
public int $updateCount = 0,
public Duration $averageSetTime = new Duration(0.0),
public Duration $averageGetTime = new Duration(0.0),
public int $expiredKeys = 0,
public Byte $memoryUsage = new Byte(0),
) {
}
/**
* Calculate hit rate
*/
public function getHitRate(): Percentage
{
$total = $this->hitCount + $this->missCount;
if ($total === 0) {
return Percentage::fromFloat(0.0);
}
return Percentage::fromFloat($this->hitCount / $total);
}
/**
* Calculate miss rate
*/
public function getMissRate(): Percentage
{
$hitRate = $this->getHitRate();
return Percentage::fromFloat(1.0 - $hitRate->toFloat());
}
/**
* Get total operations
*/
public function getTotalOperations(): int
{
return $this->hitCount + $this->missCount + $this->setCount + $this->removeCount + $this->updateCount;
}
/**
* Convert to array for serialization
*/
public function toArray(): array
{
return [
'total_keys' => $this->totalKeys,
'hit_count' => $this->hitCount,
'miss_count' => $this->missCount,
'set_count' => $this->setCount,
'remove_count' => $this->removeCount,
'update_count' => $this->updateCount,
'hit_rate' => $this->getHitRate()->toFloat(),
'hit_rate_percentage' => $this->getHitRate()->toString(),
'miss_rate' => $this->getMissRate()->toFloat(),
'miss_rate_percentage' => $this->getMissRate()->toString(),
'total_operations' => $this->getTotalOperations(),
'average_set_time_ms' => $this->averageSetTime->toMilliseconds(),
'average_get_time_ms' => $this->averageGetTime->toMilliseconds(),
'expired_keys' => $this->expiredKeys,
'memory_usage_bytes' => $this->memoryUsage->toBytes(),
'memory_usage_mb' => $this->memoryUsage->toMegabytes(),
'memory_usage_human' => $this->memoryUsage->toHumanReadable(),
];
}
}