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,141 @@
<?php
declare(strict_types=1);
namespace App\Framework\Async;
use Fiber;
/**
* Barrier für Synchronisation mehrerer Fibers an einem Punkt
*/
final class AsyncBarrier
{
private int $waitingCount = 0;
/** @var array<Fiber> */
private array $waitingFibers = [];
private bool $broken = false;
public function __construct(
private readonly int $parties,
private readonly mixed $barrierAction = null
) {
}
/**
* Wartet bis alle Parties die Barrier erreichen
*/
public function await(): void
{
if ($this->broken) {
throw new \RuntimeException("Barrier is broken");
}
$this->waitingCount++;
if ($this->waitingCount < $this->parties) {
// Noch nicht alle da, warten
$fiber = new Fiber(function () {
while ($this->waitingCount < $this->parties && ! $this->broken) {
Fiber::suspend();
}
if ($this->broken) {
throw new \RuntimeException("Barrier was broken while waiting");
}
});
$this->waitingFibers[] = $fiber;
$fiber->start();
$fiber->getReturn();
} else {
// Alle sind da, führe Barrier-Action aus und wecke alle auf
if ($this->barrierAction) {
try {
($this->barrierAction)();
} catch (\Throwable $e) {
$this->broken = true;
$this->resumeAllWithException($e);
throw $e;
}
}
$this->resumeAll();
$this->reset();
}
}
/**
* Bricht die Barrier (alle wartenden Fibers bekommen Exception)
*/
public function breakBarrier(): void
{
$this->broken = true;
$this->resumeAllWithException(new \RuntimeException("Barrier broken"));
$this->reset();
}
/**
* Prüft ob Barrier gebrochen ist
*/
public function isBroken(): bool
{
return $this->broken;
}
/**
* Gibt Anzahl wartender Parties zurück
*/
public function getNumberWaiting(): int
{
return $this->waitingCount;
}
/**
* Gibt Anzahl benötigter Parties zurück
*/
public function getParties(): int
{
return $this->parties;
}
private function resumeAll(): void
{
foreach ($this->waitingFibers as $fiber) {
if (! $fiber->isTerminated()) {
$fiber->resume();
}
}
}
private function resumeAllWithException(\Throwable $exception): void
{
foreach ($this->waitingFibers as $fiber) {
if (! $fiber->isTerminated()) {
$fiber->throw($exception);
}
}
}
private function reset(): void
{
$this->waitingCount = 0;
$this->waitingFibers = [];
}
/**
* Gibt Barrier-Statistiken zurück
*/
public function getStats(): array
{
return [
'parties' => $this->parties,
'waiting_count' => $this->waitingCount,
'waiting_fibers' => count($this->waitingFibers),
'broken' => $this->broken,
];
}
}

View File

@@ -0,0 +1,183 @@
<?php
declare(strict_types=1);
namespace App\Framework\Async;
use Fiber;
/**
* Channel für Kommunikation zwischen Fibers
*/
final class AsyncChannel
{
/** @var array<mixed> */
private array $buffer = [];
/** @var array<Fiber> */
private array $waitingSenders = [];
/** @var array<Fiber> */
private array $waitingReceivers = [];
private bool $closed = false;
public function __construct(
private readonly int $bufferSize = 0 // 0 = unbuffered (synchronous)
) {
}
/**
* Sendet einen Wert über den Channel
*/
public function send(mixed $value): bool
{
if ($this->closed) {
return false;
}
// Unbuffered channel - direkter Transfer
if ($this->bufferSize === 0) {
if (! empty($this->waitingReceivers)) {
$receiver = array_shift($this->waitingReceivers);
if (! $receiver->isTerminated()) {
$receiver->resume($value);
return true;
}
}
// Kein wartender Receiver - Sender muss warten
$fiber = new Fiber(function () use ($value) {
while (empty($this->waitingReceivers) && ! $this->closed) {
Fiber::suspend();
}
if (! empty($this->waitingReceivers)) {
$receiver = array_shift($this->waitingReceivers);
if (! $receiver->isTerminated()) {
$receiver->resume($value);
return true;
}
}
return false;
});
$this->waitingSenders[] = $fiber;
$fiber->start();
return $fiber->getReturn();
}
// Buffered channel
if (count($this->buffer) < $this->bufferSize) {
$this->buffer[] = $value;
// Wecke wartende Receiver auf
if (! empty($this->waitingReceivers)) {
$receiver = array_shift($this->waitingReceivers);
if (! $receiver->isTerminated()) {
$receiver->resume(array_shift($this->buffer));
}
}
return true;
}
return false; // Buffer voll
}
/**
* Empfängt einen Wert vom Channel
*/
public function receive(): mixed
{
if (! empty($this->buffer)) {
$value = array_shift($this->buffer);
// Wecke wartende Sender auf
if (! empty($this->waitingSenders)) {
$sender = array_shift($this->waitingSenders);
if (! $sender->isTerminated()) {
$sender->resume();
}
}
return $value;
}
if ($this->closed) {
return null;
}
// Warte auf Wert
$fiber = new Fiber(function () {
while (empty($this->buffer) && ! $this->closed) {
Fiber::suspend();
}
if (! empty($this->buffer)) {
return array_shift($this->buffer);
}
return null;
});
$this->waitingReceivers[] = $fiber;
$fiber->start();
return $fiber->getReturn();
}
/**
* Versucht zu empfangen (non-blocking)
*/
public function tryReceive(): mixed
{
if (empty($this->buffer)) {
return null;
}
return array_shift($this->buffer);
}
/**
* Schließt den Channel
*/
public function close(): void
{
$this->closed = true;
// Wecke alle wartenden Fibers auf
foreach ($this->waitingSenders as $sender) {
if (! $sender->isTerminated()) {
$sender->resume();
}
}
foreach ($this->waitingReceivers as $receiver) {
if (! $receiver->isTerminated()) {
$receiver->resume(null);
}
}
$this->waitingSenders = [];
$this->waitingReceivers = [];
}
/**
* Gibt Channel-Statistiken zurück
*/
public function getStats(): array
{
return [
'buffer_size' => $this->bufferSize,
'buffered_items' => count($this->buffer),
'waiting_senders' => count($this->waitingSenders),
'waiting_receivers' => count($this->waitingReceivers),
'closed' => $this->closed,
];
}
}

View File

@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
namespace App\Framework\Async;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\DateTime\Clock;
use App\Framework\DateTime\Timer;
use Fiber;
/**
* Event Loop für kontinuierliche asynchrone Verarbeitung
*/
final class AsyncEventLoop
{
/** @var array<string, callable> */
private array $scheduledTasks = [];
/** @var array<string, Duration> */
private array $taskIntervals = [];
/** @var array<string, float> */
private array $lastExecution = [];
private bool $running = false;
public function __construct(
private readonly FiberManager $fiberManager,
private readonly Clock $clock,
private readonly Timer $timer
) {
}
/**
* Startet den Event Loop
*/
public function run(): void
{
$this->running = true;
while ($this->running) {
$this->processTasks();
$this->timer->sleep(Duration::fromMilliseconds(1)); // 1ms zwischen Zyklen
}
}
/**
* Stoppt den Event Loop
*/
public function stop(): void
{
$this->running = false;
}
/**
* Plant eine wiederkehrende Aufgabe
* @deprecated Use scheduleDuration() instead
*/
public function schedule(string $id, callable $task, float $intervalSeconds): void
{
$this->scheduleDuration($id, $task, Duration::fromSeconds($intervalSeconds));
}
/**
* Schedule a recurring task using Duration
*/
public function scheduleDuration(string $id, callable $task, Duration $interval): void
{
$this->scheduledTasks[$id] = $task;
$this->taskIntervals[$id] = $interval;
$this->lastExecution[$id] = 0;
}
/**
* Entfernt eine geplante Aufgabe
*/
public function unschedule(string $id): void
{
unset($this->scheduledTasks[$id], $this->taskIntervals[$id], $this->lastExecution[$id]);
}
/**
* Plant eine einmalige Aufgabe nach Verzögerung
* @deprecated Use setTimeoutDuration() instead
*/
public function setTimeout(callable $task, float $delaySeconds): void
{
$this->setTimeoutDuration($task, Duration::fromSeconds($delaySeconds));
}
/**
* Schedule a one-time task after a delay using Duration
*/
public function setTimeoutDuration(callable $task, Duration $delay): void
{
$executeAt = $this->clock->time()->toFloat() + $delay->toSeconds();
$id = uniqid('timeout_', true);
$this->fiberManager->async(function () use ($task, $executeAt, $id) {
while ($this->clock->time()->toFloat() < $executeAt) {
$this->timer->sleep(Duration::fromMilliseconds(1)); // 1ms
}
$task();
});
}
/**
* Führt eine Aufgabe sofort asynchron aus
*/
public function nextTick(callable $task): Fiber
{
return $this->fiberManager->async($task);
}
private function processTasks(): void
{
$currentTime = $this->clock->time()->toFloat();
foreach ($this->scheduledTasks as $id => $task) {
$interval = $this->taskIntervals[$id];
$lastExecution = $this->lastExecution[$id];
if ($currentTime - $lastExecution >= $interval->toSeconds()) {
$this->fiberManager->async($task);
$this->lastExecution[$id] = $currentTime;
}
}
}
/**
* Gibt Event Loop Statistiken zurück
*/
public function getStats(): array
{
return [
'running' => $this->running,
'scheduled_tasks' => count($this->scheduledTasks),
'fiber_stats' => $this->fiberManager->getStats(),
];
}
}

View File

@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace App\Framework\Async;
use Fiber;
use SplQueue;
/**
* Mutex für Thread-sichere Operationen zwischen Fibers
*/
final class AsyncMutex
{
private bool $locked = false;
/** @var SplQueue<Fiber> */
private SplQueue $waitingFibers;
private ?string $owner = null;
public function __construct(
private readonly string $name = ''
) {
$this->waitingFibers = new SplQueue();
}
/**
* Erwirbt das Lock (blockierend)
*/
public function acquire(): void
{
$currentFiber = Fiber::getCurrent();
$fiberId = spl_object_id($currentFiber);
if (! $this->locked) {
$this->locked = true;
$this->owner = $fiberId;
return;
}
// Wenn bereits gelockt, warte in der Queue
$fiber = new Fiber(function () use ($fiberId) {
while ($this->locked) {
Fiber::suspend();
}
$this->locked = true;
$this->owner = $fiberId;
});
$this->waitingFibers->enqueue($fiber);
$fiber->start();
$fiber->getReturn();
}
/**
* Versucht das Lock zu erwerben (non-blocking)
*/
public function tryAcquire(): bool
{
if ($this->locked) {
return false;
}
$currentFiber = Fiber::getCurrent();
$this->locked = true;
$this->owner = spl_object_id($currentFiber);
return true;
}
/**
* Gibt das Lock frei
*/
public function release(): void
{
$currentFiber = Fiber::getCurrent();
$fiberId = spl_object_id($currentFiber);
if ($this->owner !== $fiberId) {
throw new \RuntimeException("Cannot release mutex owned by different fiber");
}
$this->locked = false;
$this->owner = null;
// Wecke nächsten wartenden Fiber auf
if (! $this->waitingFibers->isEmpty()) {
$nextFiber = $this->waitingFibers->dequeue();
if (! $nextFiber->isTerminated()) {
$nextFiber->resume();
}
}
}
/**
* Führt eine Funktion mit automatischem Lock aus
*/
public function synchronized(callable $callback): mixed
{
$this->acquire();
try {
return $callback();
} finally {
$this->release();
}
}
/**
* Prüft ob das Lock gehalten wird
*/
public function isLocked(): bool
{
return $this->locked;
}
/**
* Gibt Mutex-Statistiken zurück
*/
public function getStats(): array
{
return [
'name' => $this->name,
'locked' => $this->locked,
'owner' => $this->owner,
'waiting_fibers' => $this->waitingFibers->count(),
];
}
}

View File

@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
namespace App\Framework\Async;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\DateTime\Clock;
use App\Framework\DateTime\Timer;
use Fiber;
/**
* Factory für häufig verwendete asynchrone Operationen
*/
final readonly class AsyncOperationFactory
{
public function __construct(
private FiberManager $fiberManager,
private Clock $clock,
private Timer $timer
) {
}
/**
* Erstellt eine asynchrone Dateileseoperation
*/
public function readFile(string $path): Fiber
{
return $this->fiberManager->async(
fn () => file_get_contents($path) ?: throw new \RuntimeException("Failed to read file: $path")
);
}
/**
* Erstellt eine asynchrone Dateischreiboperation
*/
public function writeFile(string $path, string $content): Fiber
{
return $this->fiberManager->async(
fn () => file_put_contents($path, $content) ?: throw new \RuntimeException("Failed to write file: $path")
);
}
/**
* Erstellt eine asynchrone Verzeichnisleseoperation
*/
public function listDirectory(string $path): Fiber
{
return $this->fiberManager->async(
fn () => scandir($path) ?: throw new \RuntimeException("Failed to list directory: $path")
);
}
/**
* Erstellt eine asynchrone HTTP-Request-Operation (Platzhalter)
*/
public function httpRequest(string $url, array $options = []): Fiber
{
return $this->fiberManager->async(function () use ($url, $options) {
// Hier würde eine echte HTTP-Client-Integration stehen
// Für jetzt nur ein Platzhalter
return ['url' => $url, 'options' => $options, 'response' => 'async response'];
});
}
/**
* Erstellt eine asynchrone Datenbank-Query-Operation (Platzhalter)
*/
public function databaseQuery(string $query, array $params = []): Fiber
{
return $this->fiberManager->async(function () use ($query, $params) {
// Hier würde eine echte Datenbank-Integration stehen
return ['query' => $query, 'params' => $params, 'result' => 'async db result'];
});
}
/**
* Erstellt eine asynchrone Sleep-Operation
* @deprecated Use sleepDuration() instead
*/
public function sleep(float $seconds): Fiber
{
return $this->sleepDuration(Duration::fromSeconds($seconds));
}
/**
* Create async sleep operation using Duration
*/
public function sleepDuration(Duration $duration): Fiber
{
return $this->fiberManager->async(function () use ($duration) {
$this->timer->sleep($duration);
return $duration;
});
}
/**
* Measure execution time of an operation
*/
public function measureExecution(callable $operation): Fiber
{
return $this->fiberManager->async(function () use ($operation) {
$startTime = $this->clock->time();
$result = $operation();
$endTime = $this->clock->time();
$duration = $startTime->diff($endTime);
return [
'result' => $result,
'duration' => $duration,
'start_time' => $startTime,
'end_time' => $endTime,
'milliseconds' => $duration->toMilliseconds(),
];
});
}
/**
* Create operation with timeout
*/
public function withTimeout(callable $operation, Duration $timeout): Fiber
{
return $this->fiberManager->async(function () use ($operation, $timeout) {
return $this->fiberManager->withTimeoutDuration($operation, $timeout);
});
}
}

View File

@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace App\Framework\Async;
use Fiber;
use SplQueue;
/**
* Pool für begrenzte parallele Fiber-Ausführung
*/
final class AsyncPool
{
/** @var SplQueue<callable> */
private SplQueue $pendingOperations;
/** @var array<string, Fiber> */
private array $activeFibers = [];
/** @var array<string, mixed> */
private array $results = [];
public function __construct(
private readonly int $maxConcurrency = 10,
private readonly FiberManager $fiberManager = new FiberManager()
) {
$this->pendingOperations = new SplQueue();
}
/**
* Fügt eine Operation zum Pool hinzu
*/
public function add(callable $operation, ?string $id = null): string
{
$id ??= uniqid('pool_', true);
$this->pendingOperations->enqueue(['id' => $id, 'operation' => $operation]);
return $id;
}
/**
* Führt alle Operationen mit begrenzter Parallelität aus
*/
public function execute(): array
{
while (! $this->pendingOperations->isEmpty() || ! empty($this->activeFibers)) {
// Starte neue Fibers bis zur maximalen Parallelität
while (count($this->activeFibers) < $this->maxConcurrency && ! $this->pendingOperations->isEmpty()) {
$task = $this->pendingOperations->dequeue();
$this->startFiber($task['id'], $task['operation']);
}
// Sammle abgeschlossene Fibers
$this->collectCompletedFibers();
// Kurze Pause um CPU zu schonen
if (! empty($this->activeFibers)) {
usleep(100); // 0.1ms
}
}
return $this->results;
}
private function startFiber(string $id, callable $operation): void
{
$fiber = $this->fiberManager->async($operation, $id);
$this->activeFibers[$id] = $fiber;
}
private function collectCompletedFibers(): void
{
foreach ($this->activeFibers as $id => $fiber) {
if ($fiber->isTerminated()) {
try {
$this->results[$id] = $fiber->getReturn();
} catch (\Throwable $e) {
$this->results[$id] = $e;
}
unset($this->activeFibers[$id]);
}
}
}
/**
* Wartet auf ein spezifisches Ergebnis
*/
public function await(string $id): mixed
{
while (! isset($this->results[$id])) {
if (isset($this->activeFibers[$id])) {
$this->collectCompletedFibers();
usleep(100);
} else {
throw new \RuntimeException("Operation with ID '$id' not found");
}
}
return $this->results[$id];
}
/**
* Gibt aktuelle Pool-Statistiken zurück
*/
public function getStats(): array
{
return [
'pending' => $this->pendingOperations->count(),
'active' => count($this->activeFibers),
'completed' => count($this->results),
'max_concurrency' => $this->maxConcurrency,
];
}
}

View File

@@ -0,0 +1,289 @@
<?php
declare(strict_types=1);
namespace App\Framework\Async;
/**
* Promise für async/await-ähnliche Programmierung
*/
final class AsyncPromise
{
private mixed $result = null;
private ?\Throwable $exception = null;
private bool $resolved = false;
/** @var array<callable> */
private array $thenCallbacks = [];
/** @var array<callable> */
private array $catchCallbacks = [];
/** @var array<callable> */
private array $finallyCallbacks = [];
public function __construct(
private readonly FiberManager $fiberManager = new FiberManager()
) {
}
/**
* Erstellt ein resolved Promise
*/
public static function resolve(mixed $value): self
{
$promise = new self();
$promise->result = $value;
$promise->resolved = true;
return $promise;
}
/**
* Erstellt ein rejected Promise
*/
public static function reject(\Throwable $exception): self
{
$promise = new self();
$promise->exception = $exception;
$promise->resolved = true;
return $promise;
}
/**
* Erstellt Promise aus Callable
*/
public static function create(callable $executor): self
{
$promise = new self();
$promise->fiberManager->async(function () use ($promise, $executor) {
try {
$result = $executor();
$promise->doResolve($result);
} catch (\Throwable $e) {
$promise->doReject($e);
}
});
return $promise;
}
/**
* Wartet auf alle Promises
*/
public static function all(array $promises): self
{
$allPromise = new self();
$allPromise->fiberManager->async(function () use ($allPromise, $promises) {
try {
$results = [];
foreach ($promises as $key => $promise) {
if ($promise instanceof self) {
$results[$key] = $promise->await();
} else {
$results[$key] = $promise;
}
}
$allPromise->doResolve($results);
} catch (\Throwable $e) {
$allPromise->doReject($e);
}
});
return $allPromise;
}
/**
* Wartet auf das erste erfolgreiche Promise
*/
public static function race(array $promises): self
{
$racePromise = new self();
foreach ($promises as $promise) {
if ($promise instanceof self) {
$promise->fiberManager->async(function () use ($racePromise, $promise) {
try {
if (! $racePromise->resolved) {
$result = $promise->await();
$racePromise->doResolve($result);
}
} catch (\Throwable $e) {
if (! $racePromise->resolved) {
$racePromise->doReject($e);
}
}
});
}
}
return $racePromise;
}
/**
* Fügt Then-Handler hinzu
*/
public function then(?callable $onFulfilled = null, ?callable $onRejected = null): self
{
$newPromise = new self($this->fiberManager);
if ($this->resolved) {
$this->executeThen($onFulfilled, $onRejected, $newPromise);
} else {
$this->thenCallbacks[] = [$onFulfilled, $onRejected, $newPromise];
}
return $newPromise;
}
/**
* Fügt Catch-Handler hinzu
*/
public function catch(callable $onRejected): self
{
return $this->then(null, $onRejected);
}
/**
* Fügt Finally-Handler hinzu
*/
public function finally(callable $callback): self
{
$newPromise = new self($this->fiberManager);
if ($this->resolved) {
$this->executeFinally($callback, $newPromise);
} else {
$this->finallyCallbacks[] = [$callback, $newPromise];
}
return $newPromise;
}
/**
* Wartet synchron auf das Ergebnis
*/
public function await(): mixed
{
while (! $this->resolved) {
usleep(1000); // 1ms
}
if ($this->exception) {
throw $this->exception;
}
return $this->result;
}
/**
* Resolved das Promise
*/
private function doResolve(mixed $value): void
{
if ($this->resolved) {
return;
}
$this->result = $value;
$this->resolved = true;
foreach ($this->thenCallbacks as [$onFulfilled, $onRejected, $promise]) {
$this->executeThen($onFulfilled, $onRejected, $promise);
}
foreach ($this->finallyCallbacks as [$callback, $promise]) {
$this->executeFinally($callback, $promise);
}
}
/**
* Rejected das Promise
*/
private function doReject(\Throwable $exception): void
{
if ($this->resolved) {
return;
}
$this->exception = $exception;
$this->resolved = true;
foreach ($this->thenCallbacks as [$onFulfilled, $onRejected, $promise]) {
$this->executeThen($onFulfilled, $onRejected, $promise);
}
foreach ($this->finallyCallbacks as [$callback, $promise]) {
$this->executeFinally($callback, $promise);
}
}
private function executeThen(?callable $onFulfilled, ?callable $onRejected, self $promise): void
{
$this->fiberManager->async(function () use ($onFulfilled, $onRejected, $promise) {
try {
if ($this->exception) {
if ($onRejected) {
$result = $onRejected($this->exception);
$promise->doResolve($result);
} else {
$promise->doReject($this->exception);
}
} else {
if ($onFulfilled) {
$result = $onFulfilled($this->result);
$promise->doResolve($result);
} else {
$promise->doResolve($this->result);
}
}
} catch (\Throwable $e) {
$promise->doReject($e);
}
});
}
private function executeFinally(callable $callback, self $promise): void
{
$this->fiberManager->async(function () use ($callback, $promise) {
try {
$callback();
if ($this->exception) {
$promise->doReject($this->exception);
} else {
$promise->doResolve($this->result);
}
} catch (\Throwable $e) {
$promise->doReject($e);
}
});
}
/**
* Prüft ob Promise resolved ist
*/
public function isResolved(): bool
{
return $this->resolved;
}
/**
* Gibt Promise-Statistiken zurück
*/
public function getStats(): array
{
return [
'resolved' => $this->resolved,
'has_result' => $this->result !== null,
'has_exception' => $this->exception !== null,
'then_callbacks' => count($this->thenCallbacks),
'catch_callbacks' => count($this->catchCallbacks),
'finally_callbacks' => count($this->finallyCallbacks),
];
}
}

View File

@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
namespace App\Framework\Async;
use Fiber;
use SplQueue;
/**
* Asynchrone Queue für Producer-Consumer Pattern
*/
final class AsyncQueue
{
/** @var SplQueue<mixed> */
private SplQueue $items;
/** @var array<Fiber> */
private array $waitingConsumers = [];
private bool $closed = false;
public function __construct(
private readonly int $maxSize = 1000
) {
$this->items = new SplQueue();
}
/**
* Fügt ein Element zur Queue hinzu
*/
public function enqueue(mixed $item): bool
{
if ($this->closed) {
return false;
}
if ($this->items->count() >= $this->maxSize) {
return false; // Queue voll
}
$this->items->enqueue($item);
// Wecke wartende Consumer auf
if (! empty($this->waitingConsumers)) {
$consumer = array_shift($this->waitingConsumers);
if (! $consumer->isTerminated()) {
$consumer->resume($item);
}
}
return true;
}
/**
* Nimmt ein Element aus der Queue (blockierend)
*/
public function dequeue(): mixed
{
if (! $this->items->isEmpty()) {
return $this->items->dequeue();
}
if ($this->closed) {
return null;
}
// Warte auf neues Element
$fiber = new Fiber(function () {
while ($this->items->isEmpty() && ! $this->closed) {
Fiber::suspend();
}
if (! $this->items->isEmpty()) {
return $this->items->dequeue();
}
return null;
});
$this->waitingConsumers[] = $fiber;
$fiber->start();
return $fiber->getReturn();
}
/**
* Versucht ein Element zu nehmen (non-blocking)
*/
public function tryDequeue(): mixed
{
if ($this->items->isEmpty()) {
return null;
}
return $this->items->dequeue();
}
/**
* Schließt die Queue
*/
public function close(): void
{
$this->closed = true;
// Wecke alle wartenden Consumer auf
foreach ($this->waitingConsumers as $consumer) {
if (! $consumer->isTerminated()) {
$consumer->resume(null);
}
}
$this->waitingConsumers = [];
}
/**
* Prüft ob Queue leer ist
*/
public function isEmpty(): bool
{
return $this->items->isEmpty();
}
/**
* Gibt aktuelle Queue-Größe zurück
*/
public function size(): int
{
return $this->items->count();
}
/**
* Gibt Queue-Statistiken zurück
*/
public function getStats(): array
{
return [
'size' => $this->items->count(),
'max_size' => $this->maxSize,
'waiting_consumers' => count($this->waitingConsumers),
'closed' => $this->closed,
];
}
}

View File

@@ -0,0 +1,193 @@
<?php
declare(strict_types=1);
namespace App\Framework\Async;
use DateTime;
use DateTimeInterface;
use Fiber;
/**
* Scheduler für zeitbasierte asynchrone Aufgaben
*/
final class AsyncScheduler
{
/** @var array<string, array> */
private array $jobs = [];
private bool $running = false;
public function __construct(
private readonly FiberManager $fiberManager = new FiberManager()
) {
}
/**
* Plant eine täglich wiederkehrende Aufgabe
*/
public function daily(string $id, callable $task, string $time = '00:00'): void
{
$this->jobs[$id] = [
'task' => $task,
'type' => 'daily',
'time' => $time,
'last_run' => null,
];
}
/**
* Plant eine stündlich wiederkehrende Aufgabe
*/
public function hourly(string $id, callable $task, int $minute = 0): void
{
$this->jobs[$id] = [
'task' => $task,
'type' => 'hourly',
'minute' => $minute,
'last_run' => null,
];
}
/**
* Plant eine minütlich wiederkehrende Aufgabe
*/
public function everyMinute(string $id, callable $task): void
{
$this->jobs[$id] = [
'task' => $task,
'type' => 'minute',
'last_run' => null,
];
}
/**
* Plant eine Aufgabe mit Intervall in Sekunden
*/
public function every(string $id, callable $task, int $seconds): void
{
$this->jobs[$id] = [
'task' => $task,
'type' => 'interval',
'interval' => $seconds,
'last_run' => null,
];
}
/**
* Plant eine einmalige Aufgabe zu bestimmter Zeit
*/
public function at(string $id, callable $task, DateTimeInterface $when): void
{
$this->jobs[$id] = [
'task' => $task,
'type' => 'once',
'when' => $when,
'executed' => false,
];
}
/**
* Startet den Scheduler
*/
public function start(): Fiber
{
return $this->fiberManager->async(function () {
$this->running = true;
while ($this->running) {
$now = new DateTime();
foreach ($this->jobs as $id => $job) {
if ($this->shouldRun($job, $now)) {
$this->executeJob($id, $job);
}
}
// Prüfe jede Sekunde
sleep(1);
}
});
}
/**
* Stoppt den Scheduler
*/
public function stop(): void
{
$this->running = false;
}
/**
* Entfernt einen Job
*/
public function unschedule(string $id): void
{
unset($this->jobs[$id]);
}
private function shouldRun(array $job, DateTime $now): bool
{
switch ($job['type']) {
case 'daily':
$lastRun = $job['last_run'] ? new DateTime($job['last_run']) : null;
$targetTime = DateTime::createFromFormat('H:i', $job['time']);
return $now->format('H:i') === $job['time'] &&
(! $lastRun || $lastRun->format('Y-m-d') !== $now->format('Y-m-d'));
case 'hourly':
$lastRun = $job['last_run'] ? new DateTime($job['last_run']) : null;
return $now->format('i') == sprintf('%02d', $job['minute']) &&
(! $lastRun || $lastRun->format('Y-m-d H') !== $now->format('Y-m-d H'));
case 'minute':
$lastRun = $job['last_run'] ? new DateTime($job['last_run']) : null;
return ! $lastRun || $lastRun->format('Y-m-d H:i') !== $now->format('Y-m-d H:i');
case 'interval':
$lastRun = $job['last_run'] ? new DateTime($job['last_run']) : null;
return ! $lastRun || ($now->getTimestamp() - $lastRun->getTimestamp()) >= $job['interval'];
case 'once':
return ! $job['executed'] && $now >= $job['when'];
default:
return false;
}
}
private function executeJob(string $id, array &$job): void
{
$this->fiberManager->async(function () use ($job) {
try {
$job['task']();
} catch (\Throwable $e) {
// Log error
error_log("Scheduled job failed: " . $e->getMessage());
}
});
// Update last run time
if ($job['type'] === 'once') {
$this->jobs[$id]['executed'] = true;
} else {
$this->jobs[$id]['last_run'] = (new DateTime())->format('Y-m-d H:i:s');
}
}
/**
* Gibt Scheduler-Statistiken zurück
*/
public function getStats(): array
{
return [
'running' => $this->running,
'total_jobs' => count($this->jobs),
'job_types' => array_count_values(array_column($this->jobs, 'type')),
];
}
}

View File

@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace App\Framework\Async;
use Fiber;
use SplQueue;
/**
* Semaphore für begrenzte Ressourcen-Zugriffe
*/
final class AsyncSemaphore
{
private int $currentCount;
/** @var SplQueue<Fiber> */
private SplQueue $waitingFibers;
public function __construct(
private readonly int $maxCount,
private readonly string $name = ''
) {
$this->currentCount = $maxCount;
$this->waitingFibers = new SplQueue();
}
/**
* Erwirbt eine Semaphore-Erlaubnis (blockierend)
*/
public function acquire(): void
{
if ($this->currentCount > 0) {
$this->currentCount--;
return;
}
// Wenn keine Erlaubnisse verfügbar, warte
$fiber = new Fiber(function () {
while ($this->currentCount <= 0) {
Fiber::suspend();
}
$this->currentCount--;
});
$this->waitingFibers->enqueue($fiber);
$fiber->start();
$fiber->getReturn();
}
/**
* Versucht eine Erlaubnis zu erwerben (non-blocking)
*/
public function tryAcquire(): bool
{
if ($this->currentCount <= 0) {
return false;
}
$this->currentCount--;
return true;
}
/**
* Gibt eine Erlaubnis zurück
*/
public function release(): void
{
if ($this->currentCount >= $this->maxCount) {
throw new \RuntimeException("Cannot release more permits than maximum");
}
$this->currentCount++;
// Wecke wartende Fibers auf
if (! $this->waitingFibers->isEmpty()) {
$nextFiber = $this->waitingFibers->dequeue();
if (! $nextFiber->isTerminated()) {
$nextFiber->resume();
}
}
}
/**
* Führt eine Funktion mit automatischer Erlaubnis aus
*/
public function withPermit(callable $callback): mixed
{
$this->acquire();
try {
return $callback();
} finally {
$this->release();
}
}
/**
* Gibt verfügbare Erlaubnisse zurück
*/
public function availablePermits(): int
{
return $this->currentCount;
}
/**
* Gibt Semaphore-Statistiken zurück
*/
public function getStats(): array
{
return [
'name' => $this->name,
'max_count' => $this->maxCount,
'current_count' => $this->currentCount,
'waiting_fibers' => $this->waitingFibers->count(),
];
}
}

View File

@@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace App\Framework\Async;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\DateTime\Clock;
use App\Framework\DateTime\Timer;
use Fiber;
/**
* Service für asynchrone Operationen mit Composition-Pattern
*/
final readonly class AsyncService
{
public function __construct(
private FiberManager $fiberManager,
private AsyncTimer $asyncTimer,
private Clock $clock,
private Timer $timer
) {
}
/**
* Run operation asynchronously
*/
public function async(callable $operation): Fiber
{
return $this->fiberManager->async($operation);
}
/**
* Run operation asynchronously with promise
*/
public function promise(callable $operation): AsyncPromise
{
return AsyncPromise::create($operation);
}
/**
* Run multiple operations in parallel
*/
public function parallel(array $operations): AsyncPromise
{
$promises = [];
foreach ($operations as $key => $operation) {
$promises[$key] = $this->promise($operation);
}
return AsyncPromise::all($promises);
}
/**
* Run operation with timeout
*/
public function withTimeout(callable $operation, Duration $timeout): mixed
{
return $this->fiberManager->withTimeoutDuration($operation, $timeout);
}
/**
* Delay execution
*/
public function delay(Duration $duration): Fiber
{
return $this->asyncTimer->sleepDuration($duration);
}
/**
* Measure execution time
*/
public function measure(callable $operation): AsyncPromise
{
$start = $this->clock->time();
return $this->promise($operation)->then(function ($result) use ($start) {
$duration = $start->age($this->clock);
return [
'result' => $result,
'duration' => $duration,
'milliseconds' => $duration->toMilliseconds(),
];
});
}
/**
* Wait for condition
*/
public function waitFor(
callable $condition,
?Duration $timeout = null,
?Duration $checkInterval = null
): Fiber {
return $this->asyncTimer->waitForDuration($condition, $timeout, $checkInterval);
}
/**
* Schedule callback after delay
*/
public function schedule(callable $callback, Duration $delay): string
{
return $this->asyncTimer->setTimeoutDuration($callback, $delay);
}
/**
* Schedule recurring callback
*/
public function repeat(callable $callback, Duration $interval): string
{
return $this->asyncTimer->setIntervalDuration($callback, $interval);
}
/**
* Cancel scheduled operation
*/
public function cancel(string $id): bool
{
return $this->asyncTimer->clear($id);
}
/**
* Batch operations with concurrency control
*/
public function batch(array $operations, int $maxConcurrency = 10): array
{
return $this->fiberManager->throttled($operations, $maxConcurrency);
}
/**
* Get async statistics
*/
public function getStats(): array
{
return [
'fiber_manager' => $this->fiberManager->getStats(),
'async_timer' => $this->asyncTimer->getStats(),
];
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Framework\Async;
use App\Framework\DateTime\Clock;
use App\Framework\DateTime\Timer;
use App\Framework\DI\Initializer;
/**
* Initializer für AsyncService
*/
final readonly class AsyncServiceInitializer
{
public function __construct(
private Clock $clock,
private Timer $timer
) {
}
#[Initializer]
public function __invoke(): AsyncService
{
$fiberManager = new FiberManager($this->clock, $this->timer);
$asyncTimer = new AsyncTimer($fiberManager, $this->clock, $this->timer);
return new AsyncService($fiberManager, $asyncTimer, $this->clock, $this->timer);
}
}

View File

@@ -0,0 +1,235 @@
<?php
declare(strict_types=1);
namespace App\Framework\Async;
use Fiber;
use Generator;
/**
* Stream für kontinuierliche asynchrone Datenverarbeitung
*/
final class AsyncStream
{
/** @var array<callable> */
private array $processors = [];
private bool $closed = false;
public function __construct(
private readonly FiberManager $fiberManager = new FiberManager()
) {
}
/**
* Erstellt einen Stream aus einem Generator
*/
public static function fromGenerator(Generator $generator): self
{
$stream = new self();
$stream->fiberManager->async(function () use ($stream, $generator) {
foreach ($generator as $item) {
if ($stream->closed) {
break;
}
$stream->emit($item);
}
$stream->close();
});
return $stream;
}
/**
* Erstellt einen Stream aus einem Array
*/
public static function fromArray(array $items): self
{
$stream = new self();
$stream->fiberManager->async(function () use ($stream, $items) {
foreach ($items as $item) {
if ($stream->closed) {
break;
}
$stream->emit($item);
}
$stream->close();
});
return $stream;
}
/**
* Erstellt einen Interval-Stream
*/
public static function interval(float $intervalSeconds, ?int $count = null): self
{
$stream = new self();
$stream->fiberManager->async(function () use ($stream, $intervalSeconds, $count) {
$emitted = 0;
while (! $stream->closed && ($count === null || $emitted < $count)) {
$stream->emit($emitted);
$emitted++;
usleep($intervalSeconds * 1_000_000);
}
$stream->close();
});
return $stream;
}
/**
* Fügt einen Processor zum Stream hinzu
*/
public function pipe(callable $processor): self
{
$this->processors[] = $processor;
return $this;
}
/**
* Filtert Stream-Elemente
*/
public function filter(callable $predicate): self
{
return $this->pipe(function ($item) use ($predicate) {
return $predicate($item) ? $item : null;
});
}
/**
* Transformiert Stream-Elemente
*/
public function map(callable $transformer): self
{
return $this->pipe($transformer);
}
/**
* Nimmt nur die ersten N Elemente
*/
public function take(int $count): self
{
$taken = 0;
return $this->pipe(function ($item) use (&$taken, $count) {
if ($taken < $count) {
$taken++;
return $item;
}
$this->close();
return null;
});
}
/**
* Überspringt die ersten N Elemente
*/
public function skip(int $count): self
{
$skipped = 0;
return $this->pipe(function ($item) use (&$skipped, $count) {
if ($skipped < $count) {
$skipped++;
return null;
}
return $item;
});
}
/**
* Sammelt alle Stream-Elemente in einem Array
*/
public function collect(): Fiber
{
return $this->fiberManager->async(function () {
$collected = [];
$this->subscribe(function ($item) use (&$collected) {
if ($item !== null) {
$collected[] = $item;
}
});
return $collected;
});
}
/**
* Reduziert den Stream zu einem einzelnen Wert
*/
public function reduce(callable $reducer, mixed $initial = null): Fiber
{
return $this->fiberManager->async(function () use ($reducer, $initial) {
$accumulator = $initial;
$this->subscribe(function ($item) use (&$accumulator, $reducer) {
if ($item !== null) {
$accumulator = $reducer($accumulator, $item);
}
});
return $accumulator;
});
}
/**
* Abonniert den Stream
*/
public function subscribe(callable $subscriber): void
{
$this->processors[] = $subscriber;
}
/**
* Emittiert ein Element an alle Subscriber
*/
private function emit(mixed $item): void
{
foreach ($this->processors as $processor) {
$result = $processor($item);
if ($result !== null) {
$item = $result;
}
}
}
/**
* Schließt den Stream
*/
public function close(): void
{
$this->closed = true;
}
/**
* Prüft ob Stream geschlossen ist
*/
public function isClosed(): bool
{
return $this->closed;
}
/**
* Gibt Stream-Statistiken zurück
*/
public function getStats(): array
{
return [
'processors' => count($this->processors),
'closed' => $this->closed,
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Framework\Async;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;
/**
* Exception für Timeout bei asynchronen Operationen
*/
class AsyncTimeoutException extends FrameworkException
{
public function __construct(string $message = 'Async operation timed out', int $code = 0, ?\Throwable $previous = null)
{
parent::__construct($message, ExceptionContext::empty(), $code, $previous);
}
}

View File

@@ -0,0 +1,309 @@
<?php
declare(strict_types=1);
namespace App\Framework\Async;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\DateTime\Clock;
use App\Framework\DateTime\Timer;
use Fiber;
/**
* Timer-System für asynchrone zeitbasierte Operationen mit Value Objects
*/
final class AsyncTimer
{
/** @var array<string, array{callback: callable, executeAt: Timestamp, type: string, interval?: Duration}> */
private array $timers = [];
/** @var array<string, array{callback: callable, interval: Duration, executeAt: Timestamp, type: string}> */
private array $intervals = [];
private bool $running = false;
public function __construct(
private readonly FiberManager $fiberManager,
private readonly Clock $clock,
private readonly Timer $timer
) {
}
/**
* Führt eine Funktion nach einer Verzögerung aus
* @deprecated Use setTimeoutDuration() instead
*/
public function setTimeout(callable $callback, float $delaySeconds, ?string $id = null): string
{
return $this->setTimeoutDuration($callback, Duration::fromSeconds($delaySeconds), $id);
}
/**
* Schedule a callback after a delay using Duration
*/
public function setTimeoutDuration(callable $callback, Duration $delay, ?string $id = null): string
{
$id ??= uniqid('timeout_', true);
$executeAt = $this->calculateExecuteTime($delay);
$this->timers[$id] = [
'callback' => $callback,
'executeAt' => $executeAt,
'type' => 'timeout',
];
return $id;
}
/**
* Führt eine Funktion wiederholt in Intervallen aus
* @deprecated Use setIntervalDuration() instead
*/
public function setInterval(callable $callback, float $intervalSeconds, ?string $id = null): string
{
return $this->setIntervalDuration($callback, Duration::fromSeconds($intervalSeconds), $id);
}
/**
* Schedule a recurring callback using Duration
*/
public function setIntervalDuration(callable $callback, Duration $interval, ?string $id = null): string
{
$id ??= uniqid('interval_', true);
$executeAt = $this->calculateExecuteTime($interval);
$this->intervals[$id] = [
'callback' => $callback,
'interval' => $interval,
'executeAt' => $executeAt,
'type' => 'interval',
];
return $id;
}
/**
* Entfernt einen Timer oder Interval
*/
public function clear(string $id): bool
{
if (isset($this->timers[$id])) {
unset($this->timers[$id]);
return true;
}
if (isset($this->intervals[$id])) {
unset($this->intervals[$id]);
return true;
}
return false;
}
/**
* Startet den Timer-Loop
*/
public function start(): Fiber
{
return $this->fiberManager->async(function () {
$this->running = true;
while ($this->running) {
$this->processTasks();
$this->timer->sleep(Duration::fromMilliseconds(1)); // 1ms Auflösung
}
});
}
/**
* Stoppt den Timer-Loop
*/
public function stop(): void
{
$this->running = false;
}
/**
* Wartet asynchron für eine bestimmte Zeit
* @deprecated Use sleepDuration() instead
*/
public function sleep(float $seconds): Fiber
{
return $this->sleepDuration(Duration::fromSeconds($seconds));
}
/**
* Sleep for a duration
*/
public function sleepDuration(Duration $duration): Fiber
{
return $this->fiberManager->async(function () use ($duration) {
$endTime = $this->calculateExecuteTime($duration);
while ($this->clock->time()->isBefore($endTime)) {
$this->timer->sleep(Duration::fromMilliseconds(1));
}
});
}
/**
* Wartet auf eine Bedingung mit Timeout
* @deprecated Use waitForDuration() instead
*/
public function waitFor(callable $condition, float $timeoutSeconds = 10, float $checkIntervalSeconds = 0.1): Fiber
{
return $this->waitForDuration(
$condition,
Duration::fromSeconds($timeoutSeconds),
Duration::fromSeconds($checkIntervalSeconds)
);
}
/**
* Wait for a condition with timeout using Duration
*/
public function waitForDuration(
callable $condition,
?Duration $timeout = null,
?Duration $checkInterval = null
): Fiber {
$timeout ??= Duration::fromSeconds(10);
$checkInterval ??= Duration::fromMilliseconds(100);
return $this->fiberManager->async(function () use ($condition, $timeout, $checkInterval) {
$startTime = $this->clock->time();
$endTime = $this->calculateExecuteTime($timeout);
while ($this->clock->time()->isBefore($endTime)) {
if ($condition()) {
return true;
}
$this->timer->sleep($checkInterval);
}
$elapsed = $startTime->age($this->clock);
throw new AsyncTimeoutException(
"Condition not met within {$elapsed->toHumanReadable()}"
);
});
}
private function processTasks(): void
{
$currentTime = $this->clock->time();
// Verarbeite Timeouts
foreach ($this->timers as $id => $timer) {
if ($currentTime->isAfter($timer['executeAt']) || $currentTime->equals($timer['executeAt'])) {
$this->fiberManager->async($timer['callback']);
unset($this->timers[$id]);
}
}
// Verarbeite Intervals
foreach ($this->intervals as $id => &$interval) {
if ($currentTime->isAfter($interval['executeAt']) || $currentTime->equals($interval['executeAt'])) {
$this->fiberManager->async($interval['callback']);
$interval['executeAt'] = $this->calculateExecuteTime($interval['interval']);
}
}
}
/**
* Calculate execution time based on current time and delay
*/
private function calculateExecuteTime(Duration $delay): Timestamp
{
$now = $this->clock->time();
$futureTime = $now->toFloat() + $delay->toSeconds();
return Timestamp::fromFloat($futureTime);
}
/**
* Schedule a callback at a specific timestamp
*/
public function scheduleAt(callable $callback, Timestamp $timestamp, ?string $id = null): string
{
$id ??= uniqid('scheduled_', true);
$this->timers[$id] = [
'callback' => $callback,
'executeAt' => $timestamp,
'type' => 'scheduled',
];
return $id;
}
/**
* Get next execution time for a timer
*/
public function getNextExecution(string $id): ?Timestamp
{
if (isset($this->timers[$id])) {
return $this->timers[$id]['executeAt'];
}
if (isset($this->intervals[$id])) {
return $this->intervals[$id]['executeAt'];
}
return null;
}
/**
* Get time until next execution
*/
public function timeUntilExecution(string $id): ?Duration
{
$nextExecution = $this->getNextExecution($id);
return $nextExecution?->timeUntil($this->clock);
}
/**
* Gibt Timer-Statistiken zurück
*/
public function getStats(): array
{
$nextExecution = $this->getNextExecutionTime();
return [
'running' => $this->running,
'timeouts' => count($this->timers),
'intervals' => count($this->intervals),
'next_execution' => $nextExecution?->format('Y-m-d H:i:s.u'),
'time_until_next' => $nextExecution?->timeUntil($this->clock)->toHumanReadable(),
];
}
/**
* Get the next execution time across all timers
*/
private function getNextExecutionTime(): ?Timestamp
{
$times = [];
foreach ($this->timers as $timer) {
$times[] = $timer['executeAt'];
}
foreach ($this->intervals as $interval) {
$times[] = $interval['executeAt'];
}
if (empty($times)) {
return null;
}
// Find earliest time
usort($times, fn ($a, $b) => $a->isBefore($b) ? -1 : 1);
return $times[0];
}
}

View File

@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace App\Framework\Async;
/**
* Background Job für asynchrone Verarbeitung
*/
final readonly class BackgroundJob
{
public function __construct(
public string $id,
public string $type,
public array $payload,
public int $priority = 0,
public ?int $delay = null,
public int $maxRetries = 3,
public int $retryCount = 0,
public ?int $timeout = null,
public array $metadata = []
) {
}
/**
* Erstellt einen neuen Job
*/
public static function create(string $type, array $payload = [], array $options = []): self
{
return new self(
id: uniqid('job_', true),
type: $type,
payload: $payload,
priority: $options['priority'] ?? 0,
delay: $options['delay'] ?? null,
maxRetries: $options['max_retries'] ?? 3,
timeout: $options['timeout'] ?? null,
metadata: $options['metadata'] ?? []
);
}
/**
* Erstellt einen Retry-Job
*/
public function retry(\Throwable $exception): self
{
return new self(
id: $this->id,
type: $this->type,
payload: $this->payload,
priority: $this->priority,
delay: $this->calculateRetryDelay(),
maxRetries: $this->maxRetries,
retryCount: $this->retryCount + 1,
timeout: $this->timeout,
metadata: array_merge($this->metadata, [
'last_error' => $exception->getMessage(),
'retry_at' => time() + $this->calculateRetryDelay(),
])
);
}
/**
* Prüft ob Job noch retried werden kann
*/
public function canRetry(): bool
{
return $this->retryCount < $this->maxRetries;
}
/**
* Berechnet Retry-Delay (exponential backoff)
*/
private function calculateRetryDelay(): int
{
return min(60, pow(2, $this->retryCount)) + rand(0, 10);
}
/**
* Prüft ob Job bereit zur Ausführung ist
*/
public function isReady(): bool
{
if ($this->delay === null) {
return true;
}
$retryAt = $this->metadata['retry_at'] ?? time();
return time() >= $retryAt;
}
/**
* Konvertiert zu Array
*/
public function toArray(): array
{
return [
'id' => $this->id,
'type' => $this->type,
'payload' => $this->payload,
'priority' => $this->priority,
'delay' => $this->delay,
'max_retries' => $this->maxRetries,
'retry_count' => $this->retryCount,
'timeout' => $this->timeout,
'metadata' => $this->metadata,
];
}
}

View File

@@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
namespace App\Framework\Async;
use Fiber;
/**
* Processor für Background Jobs
*/
final class BackgroundJobProcessor
{
/** @var array<string, callable> */
private array $handlers = [];
private bool $running = false;
/** @var array<string, BackgroundJob> */
private array $activeJobs = [];
public function __construct(
private readonly AsyncQueue $jobQueue,
private readonly FiberManager $fiberManager = new FiberManager(),
private readonly int $maxConcurrentJobs = 10
) {
}
/**
* Registriert einen Job-Handler
*/
public function registerHandler(string $jobType, callable $handler): void
{
$this->handlers[$jobType] = $handler;
}
/**
* Fügt einen Job zur Queue hinzu
*/
public function enqueue(BackgroundJob $job): bool
{
return $this->jobQueue->enqueue($job);
}
/**
* Startet den Job-Processor
*/
public function start(): Fiber
{
return $this->fiberManager->async(function () {
$this->running = true;
while ($this->running) {
if (count($this->activeJobs) < $this->maxConcurrentJobs) {
$job = $this->jobQueue->tryDequeue();
if ($job instanceof BackgroundJob && $job->isReady()) {
$this->processJob($job);
}
}
$this->cleanupCompletedJobs();
usleep(100000); // 100ms
}
});
}
/**
* Stoppt den Job-Processor
*/
public function stop(): void
{
$this->running = false;
// Warte bis alle aktiven Jobs beendet sind
while (! empty($this->activeJobs)) {
$this->cleanupCompletedJobs();
usleep(100000);
}
}
/**
* Verarbeitet einen einzelnen Job
*/
private function processJob(BackgroundJob $job): void
{
$fiber = $this->fiberManager->async(function () use ($job) {
try {
if (! isset($this->handlers[$job->type])) {
throw new \RuntimeException("No handler registered for job type: {$job->type}");
}
$handler = $this->handlers[$job->type];
$startTime = microtime(true);
// Timeout-Handling
if ($job->timeout) {
$result = $this->fiberManager->withTimeout(
fn () => $handler($job->payload, $job),
$job->timeout
);
} else {
$result = $handler($job->payload, $job);
}
$executionTime = microtime(true) - $startTime;
$this->onJobCompleted($job, $result, $executionTime);
} catch (\Throwable $e) {
$this->onJobFailed($job, $e);
}
});
$this->activeJobs[$job->id] = $job;
}
/**
* Handler für erfolgreich abgeschlossene Jobs
*/
private function onJobCompleted(BackgroundJob $job, mixed $result, float $executionTime): void
{
// Log successful completion
error_log("Job {$job->id} completed successfully in {$executionTime}s");
// Kann erweitert werden für Callbacks, Metrics, etc.
}
/**
* Handler für fehlgeschlagene Jobs
*/
private function onJobFailed(BackgroundJob $job, \Throwable $exception): void
{
error_log("Job {$job->id} failed: " . $exception->getMessage());
if ($job->canRetry()) {
$retryJob = $job->retry($exception);
$this->enqueue($retryJob);
error_log("Job {$job->id} scheduled for retry #{$retryJob->retryCount}");
} else {
error_log("Job {$job->id} failed permanently after {$job->retryCount} retries");
$this->onJobDeadLetter($job, $exception);
}
}
/**
* Handler für permanent fehlgeschlagene Jobs
*/
private function onJobDeadLetter(BackgroundJob $job, \Throwable $exception): void
{
// Hier können Dead Letter Queue, Alerting, etc. implementiert werden
error_log("Job {$job->id} moved to dead letter queue");
}
/**
* Räumt abgeschlossene Jobs auf
*/
private function cleanupCompletedJobs(): void
{
foreach ($this->activeJobs as $jobId => $job) {
// Job ist fertig wenn er nicht mehr in running fibers ist
// (vereinfachte Implementierung)
unset($this->activeJobs[$jobId]);
}
}
/**
* Gibt Processor-Statistiken zurück
*/
public function getStats(): array
{
return [
'running' => $this->running,
'active_jobs' => count($this->activeJobs),
'max_concurrent_jobs' => $this->maxConcurrentJobs,
'registered_handlers' => array_keys($this->handlers),
'queue_stats' => $this->jobQueue->getStats(),
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Framework\Async\Contracts;
use App\Framework\Async\AsyncPromise;
/**
* Interface for components that can execute operations asynchronously
*/
interface AsyncCapable
{
/**
* Execute operation asynchronously
*/
public function async(): AsyncPromise;
/**
* Execute operation synchronously
*/
public function sync(): mixed;
/**
* Check if async execution is available
*/
public function supportsAsync(): bool;
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Framework\Async\Contracts;
use App\Framework\Async\AsyncPromise;
use App\Framework\Core\ValueObjects\Duration;
use Fiber;
/**
* Interface for async operations with timeout and cancellation support
*/
interface AsyncOperation
{
/**
* Execute the operation synchronously
*/
public function execute(): mixed;
/**
* Execute the operation asynchronously
*/
public function executeAsync(): Fiber;
/**
* Execute with timeout
*/
public function executeWithTimeout(Duration $timeout): AsyncPromise;
/**
* Check if operation can be cancelled
*/
public function isCancellable(): bool;
/**
* Cancel the operation if possible
*/
public function cancel(): void;
}

View File

@@ -0,0 +1,336 @@
<?php
declare(strict_types=1);
namespace App\Framework\Async;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\DateTime\Clock;
use App\Framework\DateTime\Timer;
use Fiber;
use Generator;
use Throwable;
/**
* Zentraler Manager für Fiber-basierte asynchrone Operationen
*
* Koordiniert die Ausführung von Fibers und bietet High-Level-APIs
* für asynchrone Programmierung im Framework.
*/
final class FiberManager
{
/** @var array<string, Fiber> */
private array $runningFibers = [];
/** @var array<string, mixed> */
private array $fiberResults = [];
/** @var array<string, Throwable> */
private array $fiberErrors = [];
/** @var array<string, Timestamp> */
private array $fiberStartTimes = [];
/** @var array<string, Timestamp> */
private array $fiberEndTimes = [];
public function __construct(
private readonly Clock $clock,
private readonly Timer $timer
) {
}
/**
* Führt eine asynchrone Operation aus und gibt sofort einen Fiber zurück
*/
public function async(callable $operation, ?string $operationId = null): Fiber
{
$operationId ??= uniqid('fiber_', true);
$startTime = $this->clock->time();
$fiber = new Fiber(function () use ($operation, $operationId, $startTime) {
$this->fiberStartTimes[$operationId] = $startTime;
try {
$result = $operation();
$this->fiberResults[$operationId] = $result;
$this->fiberEndTimes[$operationId] = $this->clock->time();
return $result;
} catch (Throwable $e) {
$this->fiberErrors[$operationId] = $e;
$this->fiberEndTimes[$operationId] = $this->clock->time();
throw $e;
} finally {
unset($this->runningFibers[$operationId]);
}
});
$this->runningFibers[$operationId] = $fiber;
$fiber->start();
return $fiber;
}
/**
* Führt mehrere Operationen parallel aus
*
* @param array<string, callable> $operations
* @return array<string, mixed>
*/
public function batch(array $operations): array
{
$fibers = [];
$results = [];
// Starte alle Fibers parallel
foreach ($operations as $id => $operation) {
$fibers[$id] = $this->async($operation, $id);
}
// Sammle alle Ergebnisse
foreach ($fibers as $id => $fiber) {
try {
$results[$id] = $fiber->getReturn();
} catch (Throwable $e) {
$results[$id] = $e;
}
}
return $results;
}
/**
* Führt eine Generator-basierte asynchrone Operation aus
*/
public function asyncGenerator(Generator $generator): Fiber
{
return new Fiber(function () use ($generator) {
$result = null;
while ($generator->valid()) {
$current = $generator->current();
// Wenn current ein Fiber ist, warte darauf
if ($current instanceof Fiber) {
if (! $current->isStarted()) {
$current->start();
}
if (! $current->isTerminated()) {
$result = $current->getReturn();
}
} else {
$result = $current;
}
$generator->send($result);
$generator->next();
}
return $generator->getReturn();
});
}
/**
* Wartet auf die Fertigstellung aller laufenden Fibers
*/
public function waitForAll(): array
{
$results = [];
foreach ($this->runningFibers as $id => $fiber) {
try {
if (! $fiber->isTerminated()) {
$results[$id] = $fiber->getReturn();
}
} catch (Throwable $e) {
$results[$id] = $e;
}
}
return $results;
}
/**
* Führt Operationen mit konfigurierbarer Parallelität aus
*
* @param array<callable> $operations
* @param int $maxConcurrency Maximale Anzahl paralleler Fibers
* @return array<mixed>
*/
public function throttled(array $operations, int $maxConcurrency = 10): array
{
$results = [];
$chunks = array_chunk($operations, $maxConcurrency, true);
foreach ($chunks as $chunk) {
$chunkResults = $this->batch($chunk);
$results = array_merge($results, $chunkResults);
}
return $results;
}
/**
* Führt Operationen mit Timeout aus
* @deprecated Use withTimeoutDuration() instead
*/
public function withTimeout(callable $operation, float $timeoutSeconds): mixed
{
return $this->withTimeoutDuration($operation, Duration::fromSeconds($timeoutSeconds));
}
/**
* Execute operation with timeout using Duration
*/
public function withTimeoutDuration(callable $operation, Duration $timeout): mixed
{
$startTime = $this->clock->time();
$endTime = Timestamp::fromFloat($startTime->toFloat() + $timeout->toSeconds());
$fiber = $this->async($operation);
while (! $fiber->isTerminated()) {
if ($this->clock->time()->isAfter($endTime)) {
$elapsed = $startTime->age($this->clock);
throw new AsyncTimeoutException(
"Operation exceeded timeout of {$timeout->toHumanReadable()} (elapsed: {$elapsed->toHumanReadable()})"
);
}
// Kurze Pause um CPU nicht zu blockieren
$this->timer->sleep(Duration::fromMilliseconds(1));
}
return $fiber->getReturn();
}
/**
* Kombiniert mehrere Fibers zu einem einzigen
*/
public function combine(array $fibers): Fiber
{
return new Fiber(function () use ($fibers) {
$results = [];
foreach ($fibers as $key => $fiber) {
if ($fiber instanceof Fiber) {
$results[$key] = $fiber->getReturn();
}
}
return $results;
});
}
/**
* Führt Operationen sequenziell aber asynchron aus
*/
public function sequence(array $operations): Fiber
{
return new Fiber(function () use ($operations) {
$results = [];
foreach ($operations as $key => $operation) {
$fiber = $this->async($operation);
$results[$key] = $fiber->getReturn();
}
return $results;
});
}
/**
* Reset des Managers (für Tests und Cleanup)
*/
public function reset(): void
{
$this->runningFibers = [];
$this->fiberResults = [];
$this->fiberErrors = [];
$this->fiberStartTimes = [];
$this->fiberEndTimes = [];
}
/**
* Get execution duration for a completed fiber
*/
public function getFiberDuration(string $operationId): ?Duration
{
$startTime = $this->fiberStartTimes[$operationId] ?? null;
$endTime = $this->fiberEndTimes[$operationId] ?? null;
if ($startTime && $endTime) {
return $startTime->diff($endTime);
}
return null;
}
/**
* Get running time for an active fiber
*/
public function getFiberRunningTime(string $operationId): ?Duration
{
$startTime = $this->fiberStartTimes[$operationId] ?? null;
if ($startTime && isset($this->runningFibers[$operationId])) {
return $startTime->age($this->clock);
}
return null;
}
/**
* Gibt Statistiken über laufende Fibers zurück
*/
public function getStats(): array
{
$currentTime = $this->clock->time();
$averageDuration = $this->calculateAverageDuration();
$totalExecutionTime = $this->calculateTotalExecutionTime();
return [
'running_fibers' => count($this->runningFibers),
'completed_results' => count($this->fiberResults),
'errors' => count($this->fiberErrors),
'fiber_ids' => array_keys($this->runningFibers),
'average_duration_ms' => $averageDuration?->toMilliseconds(),
'total_execution_time' => $totalExecutionTime->toHumanReadable(),
'current_time' => $currentTime->format('Y-m-d H:i:s.u'),
];
}
private function calculateAverageDuration(): ?Duration
{
$durations = [];
foreach (array_keys($this->fiberResults) as $id) {
$duration = $this->getFiberDuration($id);
if ($duration) {
$durations[] = $duration->toSeconds();
}
}
if (empty($durations)) {
return null;
}
$average = array_sum($durations) / count($durations);
return Duration::fromSeconds($average);
}
private function calculateTotalExecutionTime(): Duration
{
$total = 0;
foreach (array_keys($this->fiberResults) as $id) {
$duration = $this->getFiberDuration($id);
if ($duration) {
$total += $duration->toSeconds();
}
}
return Duration::fromSeconds($total);
}
}