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:
141
src/Framework/Async/AsyncBarrier.php
Normal file
141
src/Framework/Async/AsyncBarrier.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
183
src/Framework/Async/AsyncChannel.php
Normal file
183
src/Framework/Async/AsyncChannel.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
142
src/Framework/Async/AsyncEventLoop.php
Normal file
142
src/Framework/Async/AsyncEventLoop.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
131
src/Framework/Async/AsyncMutex.php
Normal file
131
src/Framework/Async/AsyncMutex.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
128
src/Framework/Async/AsyncOperationFactory.php
Normal file
128
src/Framework/Async/AsyncOperationFactory.php
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
115
src/Framework/Async/AsyncPool.php
Normal file
115
src/Framework/Async/AsyncPool.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
289
src/Framework/Async/AsyncPromise.php
Normal file
289
src/Framework/Async/AsyncPromise.php
Normal 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),
|
||||
];
|
||||
}
|
||||
}
|
||||
143
src/Framework/Async/AsyncQueue.php
Normal file
143
src/Framework/Async/AsyncQueue.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
193
src/Framework/Async/AsyncScheduler.php
Normal file
193
src/Framework/Async/AsyncScheduler.php
Normal 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')),
|
||||
];
|
||||
}
|
||||
}
|
||||
120
src/Framework/Async/AsyncSemaphore.php
Normal file
120
src/Framework/Async/AsyncSemaphore.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
141
src/Framework/Async/AsyncService.php
Normal file
141
src/Framework/Async/AsyncService.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
30
src/Framework/Async/AsyncServiceInitializer.php
Normal file
30
src/Framework/Async/AsyncServiceInitializer.php
Normal 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);
|
||||
}
|
||||
}
|
||||
235
src/Framework/Async/AsyncStream.php
Normal file
235
src/Framework/Async/AsyncStream.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
19
src/Framework/Async/AsyncTimeoutException.php
Normal file
19
src/Framework/Async/AsyncTimeoutException.php
Normal 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);
|
||||
}
|
||||
}
|
||||
309
src/Framework/Async/AsyncTimer.php
Normal file
309
src/Framework/Async/AsyncTimer.php
Normal 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];
|
||||
}
|
||||
}
|
||||
110
src/Framework/Async/BackgroundJob.php
Normal file
110
src/Framework/Async/BackgroundJob.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
180
src/Framework/Async/BackgroundJobProcessor.php
Normal file
180
src/Framework/Async/BackgroundJobProcessor.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
28
src/Framework/Async/Contracts/AsyncCapable.php
Normal file
28
src/Framework/Async/Contracts/AsyncCapable.php
Normal 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;
|
||||
}
|
||||
40
src/Framework/Async/Contracts/AsyncOperation.php
Normal file
40
src/Framework/Async/Contracts/AsyncOperation.php
Normal 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;
|
||||
}
|
||||
336
src/Framework/Async/FiberManager.php
Normal file
336
src/Framework/Async/FiberManager.php
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user