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:
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorBoundaries\Async;
|
||||
|
||||
use App\Framework\Exception\FrameworkException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Exception thrown when both async operation and fallback fail
|
||||
*/
|
||||
final class AsyncBoundaryFailedException extends FrameworkException
|
||||
{
|
||||
public function __construct(
|
||||
string $message,
|
||||
private readonly string $boundaryName,
|
||||
private readonly Throwable $originalException,
|
||||
private readonly Throwable $fallbackException,
|
||||
int $code = 0,
|
||||
?Throwable $previous = null
|
||||
) {
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the boundary name where failure occurred
|
||||
*/
|
||||
public function getBoundaryName(): string
|
||||
{
|
||||
return $this->boundaryName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the original operation exception
|
||||
*/
|
||||
public function getOriginalException(): Throwable
|
||||
{
|
||||
return $this->originalException;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fallback exception
|
||||
*/
|
||||
public function getFallbackException(): Throwable
|
||||
{
|
||||
return $this->fallbackException;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get detailed error information
|
||||
*/
|
||||
public function getErrorDetails(): array
|
||||
{
|
||||
return [
|
||||
'boundary_name' => $this->boundaryName,
|
||||
'original_error' => [
|
||||
'class' => $this->originalException::class,
|
||||
'message' => $this->originalException->getMessage(),
|
||||
'file' => $this->originalException->getFile(),
|
||||
'line' => $this->originalException->getLine(),
|
||||
],
|
||||
'fallback_error' => [
|
||||
'class' => $this->fallbackException::class,
|
||||
'message' => $this->fallbackException->getMessage(),
|
||||
'file' => $this->fallbackException->getFile(),
|
||||
'line' => $this->fallbackException->getLine(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
171
src/Framework/ErrorBoundaries/Async/AsyncBulkResult.php
Normal file
171
src/Framework/ErrorBoundaries/Async/AsyncBulkResult.php
Normal file
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorBoundaries\Async;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Percentage;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Result object for async bulk operations
|
||||
*/
|
||||
final readonly class AsyncBulkResult
|
||||
{
|
||||
public function __construct(
|
||||
public array $results,
|
||||
public array $errors,
|
||||
public int $totalOperations,
|
||||
public int $successfulOperations,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get success rate as percentage
|
||||
*/
|
||||
public function getSuccessRate(): Percentage
|
||||
{
|
||||
if ($this->totalOperations === 0) {
|
||||
return Percentage::fromFloat(0.0);
|
||||
}
|
||||
|
||||
return Percentage::fromFloat($this->successfulOperations / $this->totalOperations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error rate as percentage
|
||||
*/
|
||||
public function getErrorRate(): Percentage
|
||||
{
|
||||
if ($this->totalOperations === 0) {
|
||||
return Percentage::fromFloat(0.0);
|
||||
}
|
||||
|
||||
return Percentage::fromFloat($this->getErrorCount() / $this->totalOperations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get number of errors
|
||||
*/
|
||||
public function getErrorCount(): int
|
||||
{
|
||||
return count($this->errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any operations succeeded
|
||||
*/
|
||||
public function hasSuccesses(): bool
|
||||
{
|
||||
return $this->successfulOperations > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any operations failed
|
||||
*/
|
||||
public function hasErrors(): bool
|
||||
{
|
||||
return count($this->errors) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if all operations succeeded
|
||||
*/
|
||||
public function isCompleteSuccess(): bool
|
||||
{
|
||||
return $this->successfulOperations === $this->totalOperations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if all operations failed
|
||||
*/
|
||||
public function isCompleteFailure(): bool
|
||||
{
|
||||
return count($this->errors) === $this->totalOperations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all successful results
|
||||
*/
|
||||
public function getSuccessfulResults(): array
|
||||
{
|
||||
return $this->results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all errors
|
||||
*/
|
||||
public function getErrors(): array
|
||||
{
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error by key
|
||||
*/
|
||||
public function getError(string $key): ?Throwable
|
||||
{
|
||||
return $this->errors[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get result by key
|
||||
*/
|
||||
public function getResult(string $key): mixed
|
||||
{
|
||||
return $this->results[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if key has error
|
||||
*/
|
||||
public function hasError(string $key): bool
|
||||
{
|
||||
return isset($this->errors[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if key has result
|
||||
*/
|
||||
public function hasResult(string $key): bool
|
||||
{
|
||||
return isset($this->results[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get summary statistics
|
||||
*/
|
||||
public function getSummary(): array
|
||||
{
|
||||
return [
|
||||
'total_operations' => $this->totalOperations,
|
||||
'successful_operations' => $this->successfulOperations,
|
||||
'failed_operations' => $this->getErrorCount(),
|
||||
'success_rate' => $this->getSuccessRate()->toFloat(),
|
||||
'error_rate' => $this->getErrorRate()->toFloat(),
|
||||
'is_complete_success' => $this->isCompleteSuccess(),
|
||||
'is_complete_failure' => $this->isCompleteFailure(),
|
||||
'has_partial_success' => $this->hasSuccesses() && $this->hasErrors(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'results' => $this->results,
|
||||
'errors' => array_map(
|
||||
fn (Throwable $e) => [
|
||||
'class' => $e::class,
|
||||
'message' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
],
|
||||
$this->errors
|
||||
),
|
||||
'summary' => $this->getSummary(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorBoundaries\Async;
|
||||
|
||||
use App\Framework\Exception\FrameworkException;
|
||||
|
||||
/**
|
||||
* Exception thrown when async circuit breaker is open
|
||||
*/
|
||||
final class AsyncCircuitBreakerOpenException extends FrameworkException
|
||||
{
|
||||
public function __construct(
|
||||
string $message,
|
||||
int $code = 0,
|
||||
?\Throwable $previous = null
|
||||
) {
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
382
src/Framework/ErrorBoundaries/Async/AsyncErrorBoundary.php
Normal file
382
src/Framework/ErrorBoundaries/Async/AsyncErrorBoundary.php
Normal file
@@ -0,0 +1,382 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorBoundaries\Async;
|
||||
|
||||
use App\Framework\Async\AsyncPromise;
|
||||
use App\Framework\Async\FiberManager;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\DateTime\Clock;
|
||||
use App\Framework\DateTime\Timer;
|
||||
use App\Framework\ErrorBoundaries\BoundaryConfig;
|
||||
use App\Framework\ErrorBoundaries\CircuitBreaker\BoundaryCircuitBreakerManager;
|
||||
use App\Framework\ErrorBoundaries\Events\BoundaryEventPublisher;
|
||||
use App\Framework\ErrorBoundaries\Events\BoundaryExecutionFailed;
|
||||
use App\Framework\ErrorBoundaries\Events\BoundaryExecutionSucceeded;
|
||||
use App\Framework\ErrorBoundaries\Events\BoundaryFallbackExecuted;
|
||||
use App\Framework\ErrorBoundaries\Events\BoundaryTimeoutOccurred;
|
||||
use App\Framework\Logging\Logger;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Async Error Boundary using framework's async components
|
||||
*/
|
||||
final readonly class AsyncErrorBoundary
|
||||
{
|
||||
public function __construct(
|
||||
private string $boundaryName,
|
||||
private BoundaryConfig $config,
|
||||
private FiberManager $fiberManager,
|
||||
private Clock $clock,
|
||||
private Timer $timer,
|
||||
private ?Logger $logger = null,
|
||||
private ?BoundaryCircuitBreakerManager $circuitBreakerManager = null,
|
||||
private ?BoundaryEventPublisher $eventPublisher = null,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute async operation with error boundary
|
||||
*
|
||||
* @template T
|
||||
* @param callable(): T $operation
|
||||
* @param callable(): T|null $fallback
|
||||
* @return AsyncPromise<T>
|
||||
*/
|
||||
public function executeAsync(callable $operation, ?callable $fallback = null): AsyncPromise
|
||||
{
|
||||
return AsyncPromise::create(function () use ($operation, $fallback) {
|
||||
$startTime = $this->clock->time();
|
||||
|
||||
try {
|
||||
$result = $this->executeWithRetryAsync($operation);
|
||||
$executionTime = $startTime->age($this->clock);
|
||||
|
||||
// Publish success event
|
||||
$this->publishEvent(new BoundaryExecutionSucceeded(
|
||||
boundaryName: $this->boundaryName,
|
||||
executionTime: $executionTime,
|
||||
message: 'Async operation completed successfully',
|
||||
));
|
||||
|
||||
return $result;
|
||||
|
||||
} catch (Throwable $e) {
|
||||
$executionTime = $startTime->age($this->clock);
|
||||
|
||||
// Publish failure event
|
||||
$this->publishEvent(new BoundaryExecutionFailed(
|
||||
boundaryName: $this->boundaryName,
|
||||
exception: $e,
|
||||
executionTime: $executionTime,
|
||||
willRetry: false,
|
||||
message: 'Async operation failed',
|
||||
));
|
||||
|
||||
return $this->handleFailureAsync($e, $fallback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute multiple async operations concurrently with individual boundaries
|
||||
*
|
||||
* @param array<string, callable> $operations
|
||||
* @param array<string, callable>|null $fallbacks
|
||||
* @return AsyncPromise<array>
|
||||
*/
|
||||
public function executeConcurrent(array $operations, ?array $fallbacks = null): AsyncPromise
|
||||
{
|
||||
$promises = [];
|
||||
|
||||
foreach ($operations as $name => $operation) {
|
||||
$fallback = $fallbacks[$name] ?? null;
|
||||
$promises[$name] = $this->executeAsync($operation, $fallback);
|
||||
}
|
||||
|
||||
return AsyncPromise::all($promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute with timeout using framework components
|
||||
*
|
||||
* @template T
|
||||
* @param callable(): T $operation
|
||||
* @param Duration $timeout
|
||||
* @param callable(): T|null $fallback
|
||||
* @return AsyncPromise<T>
|
||||
*/
|
||||
public function executeWithTimeout(
|
||||
callable $operation,
|
||||
Duration $timeout,
|
||||
?callable $fallback = null
|
||||
): AsyncPromise {
|
||||
return AsyncPromise::create(function () use ($operation, $timeout, $fallback) {
|
||||
try {
|
||||
return $this->fiberManager->withTimeoutDuration($operation, $timeout);
|
||||
} catch (Throwable $e) {
|
||||
$this->publishEvent(new BoundaryTimeoutOccurred(
|
||||
boundaryName: $this->boundaryName,
|
||||
timeoutThreshold: $timeout,
|
||||
actualExecutionTime: $timeout, // We don't know the exact time here
|
||||
fallbackExecuted: $fallback !== null,
|
||||
message: "Async operation timed out after {$timeout->toHumanReadable()}",
|
||||
));
|
||||
|
||||
if ($fallback !== null) {
|
||||
return $fallback();
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute with circuit breaker support
|
||||
*
|
||||
* @template T
|
||||
* @param callable(): T $operation
|
||||
* @param callable(): T|null $fallback
|
||||
* @return AsyncPromise<T>
|
||||
*/
|
||||
public function executeWithCircuitBreaker(callable $operation, ?callable $fallback = null): AsyncPromise
|
||||
{
|
||||
if ($this->config->circuitBreakerEnabled && $this->circuitBreakerManager) {
|
||||
if ($this->circuitBreakerManager->isCircuitOpen($this->boundaryName, $this->config)) {
|
||||
$this->log('info', 'Circuit breaker is open, using fallback');
|
||||
|
||||
if ($fallback !== null) {
|
||||
return AsyncPromise::resolve($fallback());
|
||||
}
|
||||
|
||||
return AsyncPromise::reject(new AsyncCircuitBreakerOpenException(
|
||||
"Circuit breaker is open for boundary '{$this->boundaryName}'"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return AsyncPromise::create(function () use ($operation, $fallback) {
|
||||
try {
|
||||
$result = $operation();
|
||||
|
||||
if ($this->config->circuitBreakerEnabled && $this->circuitBreakerManager) {
|
||||
$this->circuitBreakerManager->recordSuccess($this->boundaryName, $this->config);
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
||||
} catch (Throwable $e) {
|
||||
if ($this->config->circuitBreakerEnabled && $this->circuitBreakerManager) {
|
||||
$this->circuitBreakerManager->recordFailure($this->boundaryName, $this->config);
|
||||
}
|
||||
|
||||
return $this->handleFailureAsync($e, $fallback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute batch operations with individual error boundaries
|
||||
*
|
||||
* @template T
|
||||
* @param array<T> $items
|
||||
* @param callable(T): mixed $processor
|
||||
* @param int $maxConcurrency
|
||||
* @return AsyncPromise<AsyncBulkResult>
|
||||
*/
|
||||
public function executeBatch(
|
||||
array $items,
|
||||
callable $processor,
|
||||
int $maxConcurrency = 10
|
||||
): AsyncPromise {
|
||||
return AsyncPromise::create(function () use ($items, $processor, $maxConcurrency) {
|
||||
$operations = [];
|
||||
|
||||
foreach ($items as $key => $item) {
|
||||
$operations[$key] = fn () => $processor($item);
|
||||
}
|
||||
|
||||
$results = $this->fiberManager->throttled($operations, $maxConcurrency);
|
||||
|
||||
$successful = [];
|
||||
$errors = [];
|
||||
|
||||
foreach ($results as $key => $result) {
|
||||
if ($result instanceof Throwable) {
|
||||
$errors[$key] = $result;
|
||||
} else {
|
||||
$successful[$key] = $result;
|
||||
}
|
||||
}
|
||||
|
||||
return new AsyncBulkResult(
|
||||
results: $successful,
|
||||
errors: $errors,
|
||||
totalOperations: count($items),
|
||||
successfulOperations: count($successful),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute operation with retry using framework components
|
||||
*/
|
||||
private function executeWithRetryAsync(callable $operation): mixed
|
||||
{
|
||||
$lastException = null;
|
||||
|
||||
for ($attempt = 1; $attempt <= $this->config->maxRetries; $attempt++) {
|
||||
try {
|
||||
return $operation();
|
||||
} catch (Throwable $e) {
|
||||
$lastException = $e;
|
||||
|
||||
// Don't retry certain types of errors
|
||||
if (! $this->shouldRetry($e)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($attempt < $this->config->maxRetries) {
|
||||
$delay = $this->calculateRetryDelay($attempt);
|
||||
$this->log('info', "Async attempt {$attempt} failed, retrying in {$delay}ms", [
|
||||
'exception' => $e->getMessage(),
|
||||
'attempt' => $attempt,
|
||||
'delay' => $delay,
|
||||
]);
|
||||
|
||||
// Use framework's timer for async sleep
|
||||
$this->timer->sleep(Duration::fromMilliseconds($delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw $lastException;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle failure with async fallback
|
||||
*/
|
||||
private function handleFailureAsync(Throwable $exception, ?callable $fallback): mixed
|
||||
{
|
||||
$this->logFailure($exception, 'Async operation failed');
|
||||
|
||||
if ($fallback === null) {
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $fallback();
|
||||
|
||||
// Publish fallback executed event
|
||||
$this->publishEvent(new BoundaryFallbackExecuted(
|
||||
boundaryName: $this->boundaryName,
|
||||
originalException: $exception,
|
||||
fallbackReason: 'Async operation failed: ' . $exception->getMessage(),
|
||||
message: 'Async fallback executed successfully',
|
||||
));
|
||||
|
||||
return $result;
|
||||
|
||||
} catch (Throwable $fallbackException) {
|
||||
$this->log('error', 'Async fallback also failed', [
|
||||
'original_exception' => $exception->getMessage(),
|
||||
'fallback_exception' => $fallbackException->getMessage(),
|
||||
]);
|
||||
|
||||
throw new AsyncBoundaryFailedException(
|
||||
"Both async operation and fallback failed in boundary '{$this->boundaryName}'",
|
||||
$this->boundaryName,
|
||||
$exception,
|
||||
$fallbackException
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if exception should trigger retry
|
||||
*/
|
||||
private function shouldRetry(Throwable $e): bool
|
||||
{
|
||||
// Don't retry async-specific errors
|
||||
if ($e instanceof AsyncCircuitBreakerOpenException) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use same retry logic as synchronous boundary
|
||||
return true; // Simplified for this example
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate retry delay
|
||||
*/
|
||||
private function calculateRetryDelay(int $attempt): int
|
||||
{
|
||||
$baseMs = (int) $this->config->baseDelay->toMilliseconds();
|
||||
$maxMs = (int) $this->config->maxDelay->toMilliseconds();
|
||||
|
||||
return match ($this->config->retryStrategy) {
|
||||
default => min($baseMs * $attempt, $maxMs),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get async circuit breaker health
|
||||
*/
|
||||
public function getCircuitHealthAsync(): AsyncPromise
|
||||
{
|
||||
return AsyncPromise::create(function () {
|
||||
if (! $this->config->circuitBreakerEnabled || ! $this->circuitBreakerManager) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->circuitBreakerManager->getCircuitHealth($this->boundaryName);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset circuit breaker asynchronously
|
||||
*/
|
||||
public function resetCircuitAsync(): AsyncPromise
|
||||
{
|
||||
return AsyncPromise::create(function () {
|
||||
if ($this->config->circuitBreakerEnabled && $this->circuitBreakerManager) {
|
||||
$this->circuitBreakerManager->resetCircuit($this->boundaryName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function logFailure(Throwable $e, string $message): void
|
||||
{
|
||||
$this->log('error', $message, [
|
||||
'exception' => $e::class,
|
||||
'message' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function log(string $level, string $message, array $context = []): void
|
||||
{
|
||||
if ($this->logger) {
|
||||
$context['boundary'] = $this->boundaryName;
|
||||
$context['async'] = true;
|
||||
|
||||
match ($level) {
|
||||
'debug' => $this->logger->debug("[AsyncBoundary] {$message}", $context),
|
||||
'info' => $this->logger->info("[AsyncBoundary] {$message}", $context),
|
||||
'warning' => $this->logger->warning("[AsyncBoundary] {$message}", $context),
|
||||
'error' => $this->logger->error("[AsyncBoundary] {$message}", $context),
|
||||
default => $this->logger->info("[AsyncBoundary] {$message}", $context),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private function publishEvent($event): void
|
||||
{
|
||||
if ($this->eventPublisher !== null) {
|
||||
$this->eventPublisher->publish($event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorBoundaries\Async;
|
||||
|
||||
use App\Framework\Async\FiberManager;
|
||||
use App\Framework\DateTime\Clock;
|
||||
use App\Framework\DateTime\Timer;
|
||||
use App\Framework\ErrorBoundaries\BoundaryConfig;
|
||||
use App\Framework\ErrorBoundaries\CircuitBreaker\BoundaryCircuitBreakerManager;
|
||||
use App\Framework\ErrorBoundaries\Events\BoundaryEventPublisher;
|
||||
use App\Framework\EventBus\EventBus;
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\StateManagement\StateManagerFactory;
|
||||
|
||||
/**
|
||||
* Factory for creating async error boundaries
|
||||
*/
|
||||
final readonly class AsyncErrorBoundaryFactory
|
||||
{
|
||||
public function __construct(
|
||||
private FiberManager $fiberManager,
|
||||
private Clock $clock,
|
||||
private Timer $timer,
|
||||
private ?Logger $logger = null,
|
||||
private ?StateManagerFactory $stateManagerFactory = null,
|
||||
private ?EventBus $eventBus = null,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create async error boundary
|
||||
*/
|
||||
public function create(string $name, BoundaryConfig $config): AsyncErrorBoundary
|
||||
{
|
||||
$circuitBreakerManager = null;
|
||||
|
||||
if ($config->circuitBreakerEnabled && $this->stateManagerFactory) {
|
||||
$stateManager = $this->stateManagerFactory->createForErrorBoundary();
|
||||
$circuitBreakerManager = new BoundaryCircuitBreakerManager($stateManager, $this->logger);
|
||||
}
|
||||
|
||||
$eventPublisher = new BoundaryEventPublisher($this->eventBus, $this->logger);
|
||||
|
||||
return new AsyncErrorBoundary(
|
||||
boundaryName: $name,
|
||||
config: $config,
|
||||
fiberManager: $this->fiberManager,
|
||||
clock: $this->clock,
|
||||
timer: $this->timer,
|
||||
logger: $this->logger,
|
||||
circuitBreakerManager: $circuitBreakerManager,
|
||||
eventPublisher: $eventPublisher,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create async boundary for external service calls
|
||||
*/
|
||||
public function createForExternalService(string $serviceName): AsyncErrorBoundary
|
||||
{
|
||||
return $this->create("async_external_{$serviceName}", BoundaryConfig::externalService());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create async boundary for database operations
|
||||
*/
|
||||
public function createForDatabase(string $operation = 'database'): AsyncErrorBoundary
|
||||
{
|
||||
return $this->create("async_{$operation}", BoundaryConfig::database());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create async boundary for background jobs
|
||||
*/
|
||||
public function createForBackgroundJob(string $jobName): AsyncErrorBoundary
|
||||
{
|
||||
return $this->create("async_job_{$jobName}", BoundaryConfig::backgroundJob());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create async boundary for critical operations
|
||||
*/
|
||||
public function createForCriticalOperation(string $operationName): AsyncErrorBoundary
|
||||
{
|
||||
return $this->create("async_critical_{$operationName}", BoundaryConfig::critical());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create async boundary for batch processing
|
||||
*/
|
||||
public function createForBatchProcessing(string $batchName): AsyncErrorBoundary
|
||||
{
|
||||
$config = new BoundaryConfig(
|
||||
maxRetries: 2,
|
||||
retryStrategy: \App\Framework\ErrorBoundaries\RetryStrategy::EXPONENTIAL_JITTER,
|
||||
baseDelay: \App\Framework\Core\ValueObjects\Duration::fromMilliseconds(100),
|
||||
maxDelay: \App\Framework\Core\ValueObjects\Duration::fromSeconds(5),
|
||||
circuitBreakerEnabled: true,
|
||||
circuitBreakerThreshold: 10, // Higher threshold for batch operations
|
||||
circuitBreakerTimeout: \App\Framework\Core\ValueObjects\Duration::fromMinutes(2),
|
||||
enableMetrics: true,
|
||||
maxBulkErrorRate: 0.2, // Allow 20% error rate in batch
|
||||
);
|
||||
|
||||
return $this->create("async_batch_{$batchName}", $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create async boundary for high-throughput operations
|
||||
*/
|
||||
public function createForHighThroughput(string $operationName): AsyncErrorBoundary
|
||||
{
|
||||
$config = new BoundaryConfig(
|
||||
maxRetries: 1, // Fewer retries for high throughput
|
||||
retryStrategy: \App\Framework\ErrorBoundaries\RetryStrategy::FIXED,
|
||||
baseDelay: \App\Framework\Core\ValueObjects\Duration::fromMilliseconds(50),
|
||||
maxDelay: \App\Framework\Core\ValueObjects\Duration::fromMilliseconds(200),
|
||||
circuitBreakerEnabled: true,
|
||||
circuitBreakerThreshold: 20, // Higher threshold
|
||||
circuitBreakerTimeout: \App\Framework\Core\ValueObjects\Duration::fromSeconds(30),
|
||||
enableMetrics: true,
|
||||
);
|
||||
|
||||
return $this->create("async_throughput_{$operationName}", $config);
|
||||
}
|
||||
}
|
||||
266
src/Framework/ErrorBoundaries/Async/README.md
Normal file
266
src/Framework/ErrorBoundaries/Async/README.md
Normal file
@@ -0,0 +1,266 @@
|
||||
# Async ErrorBoundary System
|
||||
|
||||
Complete async error boundary implementation using the framework's async components (`FiberManager`, `AsyncPromise`, etc.).
|
||||
|
||||
## Components
|
||||
|
||||
### Core Classes
|
||||
|
||||
- **`AsyncErrorBoundary`** - Main async error boundary using framework's `FiberManager` and `AsyncPromise`
|
||||
- **`AsyncErrorBoundaryFactory`** - Factory for creating async boundaries with different configurations
|
||||
- **`AsyncBulkResult`** - Result object for batch async operations with success/error tracking
|
||||
- **`AsyncBoundaryFailedException`** - Exception for when both operation and fallback fail
|
||||
- **`AsyncCircuitBreakerOpenException`** - Exception for circuit breaker open state
|
||||
|
||||
## Framework Integration
|
||||
|
||||
### Uses Framework Async Components
|
||||
- **`FiberManager`** - Fiber-based async execution with batching and throttling
|
||||
- **`AsyncPromise`** - Promise-based async/await pattern
|
||||
- **`Clock`/`Timer`** - Time management using framework value objects
|
||||
- **`Duration`/`Timestamp`** - Proper time value objects
|
||||
|
||||
### ErrorBoundary Integration
|
||||
- Circuit breaker state management via `BoundaryCircuitBreakerManager`
|
||||
- Event publishing via `BoundaryEventPublisher`
|
||||
- Metrics and observability support
|
||||
- Same retry strategies and configuration as sync boundaries
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Async Operation
|
||||
|
||||
```php
|
||||
$factory = new AsyncErrorBoundaryFactory($fiberManager, $clock, $timer);
|
||||
$boundary = $factory->createForExternalService('payment_api');
|
||||
|
||||
$promise = $boundary->executeAsync(
|
||||
operation: fn() => $paymentApi->processPayment($request),
|
||||
fallback: fn() => $this->createOfflinePaymentRecord($request)
|
||||
);
|
||||
|
||||
$result = $promise->await();
|
||||
```
|
||||
|
||||
### Concurrent Operations
|
||||
|
||||
```php
|
||||
$operations = [
|
||||
'user_data' => fn() => $userService->getUserData($userId),
|
||||
'preferences' => fn() => $preferenceService->getPreferences($userId),
|
||||
'notifications' => fn() => $notificationService->getUnread($userId),
|
||||
];
|
||||
|
||||
$promise = $boundary->executeConcurrent($operations);
|
||||
$results = $promise->await();
|
||||
|
||||
// Results: ['user_data' => ..., 'preferences' => ..., 'notifications' => ...]
|
||||
```
|
||||
|
||||
### Batch Processing
|
||||
|
||||
```php
|
||||
$batchBoundary = $factory->createForBatchProcessing('email_sending');
|
||||
|
||||
$promise = $batchBoundary->executeBatch(
|
||||
items: $emailQueue,
|
||||
processor: fn($email) => $mailService->send($email),
|
||||
maxConcurrency: 5
|
||||
);
|
||||
|
||||
$bulkResult = $promise->await();
|
||||
|
||||
echo "Success rate: " . $bulkResult->getSuccessRate()->toFloat() * 100 . "%\n";
|
||||
echo "Processed: {$bulkResult->successfulOperations}/{$bulkResult->totalOperations}\n";
|
||||
```
|
||||
|
||||
### Timeout Handling
|
||||
|
||||
```php
|
||||
$promise = $boundary->executeWithTimeout(
|
||||
operation: fn() => $heavyComputation->process($data),
|
||||
timeout: Duration::fromSeconds(30),
|
||||
fallback: fn() => $this->getCachedResult($data)
|
||||
);
|
||||
|
||||
try {
|
||||
$result = $promise->await();
|
||||
} catch (AsyncTimeoutException $e) {
|
||||
// Handle timeout
|
||||
}
|
||||
```
|
||||
|
||||
### Circuit Breaker with Async
|
||||
|
||||
```php
|
||||
$promise = $boundary->executeWithCircuitBreaker(
|
||||
operation: fn() => $externalApi->call($request),
|
||||
fallback: fn() => $this->getDefaultResponse()
|
||||
);
|
||||
|
||||
$result = $promise->await();
|
||||
```
|
||||
|
||||
## Factory Configurations
|
||||
|
||||
### External Service Boundary
|
||||
```php
|
||||
$boundary = $factory->createForExternalService('payment_service');
|
||||
```
|
||||
- Retry strategy with exponential backoff
|
||||
- Circuit breaker enabled
|
||||
- Appropriate timeouts for external calls
|
||||
|
||||
### Database Boundary
|
||||
```php
|
||||
$boundary = $factory->createForDatabase('user_queries');
|
||||
```
|
||||
- Fast fail for database issues
|
||||
- Connection pooling aware
|
||||
- Optimized for database operation patterns
|
||||
|
||||
### Background Job Boundary
|
||||
```php
|
||||
$boundary = $factory->createForBackgroundJob('email_processor');
|
||||
```
|
||||
- Higher retry counts
|
||||
- Longer timeouts
|
||||
- Batch error tolerance
|
||||
|
||||
### High Throughput Boundary
|
||||
```php
|
||||
$boundary = $factory->createForHighThroughput('api_endpoints');
|
||||
```
|
||||
- Minimal retries for speed
|
||||
- Higher circuit breaker thresholds
|
||||
- Optimized for high-volume operations
|
||||
|
||||
## Async Result Types
|
||||
|
||||
### AsyncBulkResult
|
||||
|
||||
```php
|
||||
$bulkResult = $promise->await();
|
||||
|
||||
// Statistics
|
||||
$successRate = $bulkResult->getSuccessRate(); // Percentage object
|
||||
$errorRate = $bulkResult->getErrorRate(); // Percentage object
|
||||
|
||||
// Individual results
|
||||
$userResult = $bulkResult->getResult('user_1');
|
||||
$userError = $bulkResult->getError('user_1');
|
||||
|
||||
// Status checks
|
||||
$hasAnyErrors = $bulkResult->hasErrors();
|
||||
$isCompleteSuccess = $bulkResult->isCompleteSuccess();
|
||||
```
|
||||
|
||||
## Event Integration
|
||||
|
||||
Async boundaries publish the same events as sync boundaries:
|
||||
- `BoundaryExecutionSucceeded` - Successful async operation
|
||||
- `BoundaryExecutionFailed` - Failed async operation
|
||||
- `BoundaryFallbackExecuted` - Fallback was used
|
||||
- `BoundaryTimeoutOccurred` - Operation timed out
|
||||
|
||||
## Promise Chaining
|
||||
|
||||
```php
|
||||
$promise = $boundary->executeAsync(fn() => $service->getData())
|
||||
->then(fn($data) => $this->processData($data))
|
||||
->then(fn($processed) => $this->saveData($processed))
|
||||
->catch(fn($error) => $this->handleError($error))
|
||||
->finally(fn() => $this->cleanup());
|
||||
|
||||
$result = $promise->await();
|
||||
```
|
||||
|
||||
## Concurrent with Fallbacks
|
||||
|
||||
```php
|
||||
$operations = [
|
||||
'primary_data' => fn() => $primaryService->getData(),
|
||||
'backup_data' => fn() => $backupService->getData(),
|
||||
];
|
||||
|
||||
$fallbacks = [
|
||||
'primary_data' => fn() => $cache->get('primary_data'),
|
||||
'backup_data' => fn() => $cache->get('backup_data'),
|
||||
];
|
||||
|
||||
$promise = $boundary->executeConcurrent($operations, $fallbacks);
|
||||
$results = $promise->await();
|
||||
```
|
||||
|
||||
## Health Monitoring
|
||||
|
||||
```php
|
||||
// Async health check
|
||||
$healthPromise = $boundary->getCircuitHealthAsync();
|
||||
$health = $healthPromise->await();
|
||||
|
||||
if ($health && !$health['is_healthy']) {
|
||||
$this->alerting->sendAlert("Boundary {$boundary->boundaryName} is unhealthy");
|
||||
}
|
||||
|
||||
// Async circuit reset
|
||||
$resetPromise = $boundary->resetCircuitAsync();
|
||||
$resetPromise->await();
|
||||
```
|
||||
|
||||
## Performance Benefits
|
||||
|
||||
### Framework Integration Benefits
|
||||
- **Fiber-based**: Uses PHP 8.1+ Fibers for true async execution
|
||||
- **Batching**: Framework's `FiberManager` handles optimal batching
|
||||
- **Throttling**: Built-in concurrency control
|
||||
- **Timeout Management**: Framework's timeout handling with proper cleanup
|
||||
|
||||
### Error Boundary Benefits
|
||||
- **Graceful Degradation**: Async fallbacks prevent service interruption
|
||||
- **Circuit Breaking**: Prevents cascade failures in async operations
|
||||
- **Retry Logic**: Async retry with proper delays
|
||||
- **Observability**: Events and metrics for async operations
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Concurrency Management
|
||||
```php
|
||||
// Good: Use factory methods for appropriate concurrency
|
||||
$batchBoundary = $factory->createForBatchProcessing('data_sync');
|
||||
$bulkResult = $batchBoundary->executeBatch($items, $processor, 10);
|
||||
|
||||
// Good: Throttle concurrent operations
|
||||
$results = $fiberManager->throttled($operations, 5);
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
```php
|
||||
// Good: Provide meaningful fallbacks
|
||||
$promise = $boundary->executeAsync(
|
||||
operation: fn() => $externalService->getData(),
|
||||
fallback: fn() => $this->getCachedData() // Always have a fallback
|
||||
);
|
||||
|
||||
// Good: Handle different error types
|
||||
try {
|
||||
$result = $promise->await();
|
||||
} catch (AsyncCircuitBreakerOpenException $e) {
|
||||
// Circuit breaker is open
|
||||
} catch (AsyncBoundaryFailedException $e) {
|
||||
// Both operation and fallback failed
|
||||
$originalError = $e->getOriginalException();
|
||||
$fallbackError = $e->getFallbackException();
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Optimization
|
||||
```php
|
||||
// Good: Use appropriate configurations
|
||||
$highThroughputBoundary = $factory->createForHighThroughput('api_calls');
|
||||
$batchBoundary = $factory->createForBatchProcessing('data_processing');
|
||||
|
||||
// Good: Monitor performance
|
||||
$bulkResult = $batchBoundary->executeBatch($items, $processor);
|
||||
$this->metrics->record('batch_success_rate', $bulkResult->getSuccessRate());
|
||||
```
|
||||
Reference in New Issue
Block a user