feat(Production): Complete production deployment infrastructure

- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -5,166 +5,191 @@ declare(strict_types=1);
namespace App\Framework\Async;
use Fiber;
use SplQueue;
use Throwable;
/**
* Channel für Kommunikation zwischen Fibers
* Go-Style Channel für asynchrone Producer-Consumer Patterns
*
* Ermöglicht sichere Kommunikation zwischen Fibers mit automatischer
* Blockierung bei vollen/leeren Channels.
*/
final class AsyncChannel
{
/** @var array<mixed> */
private array $buffer = [];
/** @var SplQueue<mixed> */
private SplQueue $buffer;
/** @var array<Fiber> */
private array $waitingSenders = [];
/** @var array<Fiber> Wartende Producer */
private array $waitingProducers = [];
/** @var array<Fiber> */
private array $waitingReceivers = [];
/** @var array<Fiber> Wartende Consumer */
private array $waitingConsumers = [];
private bool $closed = false;
public function __construct(
private readonly int $bufferSize = 0 // 0 = unbuffered (synchronous)
private readonly int $capacity = 0 // 0 = unbuffered (synchronous)
) {
$this->buffer = new SplQueue();
}
/**
* Sendet einen Wert über den Channel
* Sendet einen Wert in den Channel
*
* Blockiert, wenn der Channel voll ist (buffered) oder kein Consumer wartet (unbuffered)
*
* @throws ChannelClosedException|Throwable wenn Channel geschlossen ist
*/
public function send(mixed $value): bool
public function send(mixed $value): void
{
if ($this->closed) {
return false;
throw new ChannelClosedException('Cannot send on closed channel');
}
// 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;
}
// Buffered channel: Warte bis Platz frei
if ($this->capacity > 0) {
while ($this->buffer->count() >= $this->capacity && !$this->closed) {
$this->waitingProducers[] = Fiber::getCurrent();
Fiber::suspend();
}
// 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));
}
if ($this->closed) {
throw new ChannelClosedException('Channel was closed while waiting');
}
return true;
}
$this->buffer->enqueue($value);
return false; // Buffer voll
// Wecke wartende Consumer auf
$this->resumeWaitingConsumers();
} else {
// Unbuffered channel: Direkter Transfer
if (empty($this->waitingConsumers)) {
// Kein Consumer wartet, Producer muss warten
$this->buffer->enqueue($value);
$this->waitingProducers[] = Fiber::getCurrent();
Fiber::suspend();
} else {
// Consumer wartet, direkter Transfer
$consumer = array_shift($this->waitingConsumers);
$this->buffer->enqueue($value);
if ($consumer instanceof Fiber && $consumer->isSuspended()) {
$consumer->resume();
}
}
}
}
/**
* Empfängt einen Wert vom Channel
*
* Blockiert wenn Channel leer ist
*
* @throws ChannelClosedException wenn Channel geschlossen und leer ist
*/
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) {
// Buffered channel: Warte auf Daten
if ($this->capacity > 0) {
while ($this->buffer->isEmpty() && !$this->closed) {
$this->waitingConsumers[] = Fiber::getCurrent();
Fiber::suspend();
}
if (! empty($this->buffer)) {
return array_shift($this->buffer);
if ($this->buffer->isEmpty() && $this->closed) {
throw new ChannelClosedException('Channel is closed and empty');
}
return null;
});
$value = $this->buffer->dequeue();
$this->waitingReceivers[] = $fiber;
$fiber->start();
// Wecke wartende Producer auf
$this->resumeWaitingProducers();
return $fiber->getReturn();
return $value;
} else {
// Unbuffered channel: Warte auf Producer
while ($this->buffer->isEmpty() && !$this->closed) {
$this->waitingConsumers[] = Fiber::getCurrent();
Fiber::suspend();
}
if ($this->buffer->isEmpty() && $this->closed) {
throw new ChannelClosedException('Channel is closed and empty');
}
$value = $this->buffer->dequeue();
// Wecke wartende Producer auf
$this->resumeWaitingProducers();
return $value;
}
}
/**
* Versucht zu empfangen (non-blocking)
* Versucht einen Wert zu empfangen ohne zu blockieren
*
* @return array{success: bool, value: mixed}
*/
public function tryReceive(): mixed
public function tryReceive(): array
{
if (empty($this->buffer)) {
return null;
if ($this->buffer->isEmpty()) {
return ['success' => false, 'value' => null];
}
return array_shift($this->buffer);
$value = $this->buffer->dequeue();
$this->resumeWaitingProducers();
return ['success' => true, 'value' => $value];
}
/**
* Versucht einen Wert zu senden ohne zu blockieren
*/
public function trySend(mixed $value): bool
{
if ($this->closed) {
throw new ChannelClosedException('Cannot send on closed channel');
}
if ($this->capacity > 0 && $this->buffer->count() >= $this->capacity) {
return false; // Channel voll
}
$this->buffer->enqueue($value);
$this->resumeWaitingConsumers();
return true;
}
/**
* Schließt den Channel
*
* Weitere send() Operationen werden fehlschlagen
* Wartende Fibers werden aufgeweckt
*/
public function close(): void
{
$this->closed = true;
// Wecke alle wartenden Fibers auf
foreach ($this->waitingSenders as $sender) {
if (! $sender->isTerminated()) {
$sender->resume();
}
}
$this->resumeWaitingProducers();
$this->resumeWaitingConsumers();
}
foreach ($this->waitingReceivers as $receiver) {
if (! $receiver->isTerminated()) {
$receiver->resume(null);
}
}
/**
* Prüft ob Channel geschlossen ist
*/
public function isClosed(): bool
{
return $this->closed;
}
$this->waitingSenders = [];
$this->waitingReceivers = [];
/**
* Gibt die aktuelle Anzahl der Elemente im Buffer zurück
*/
public function count(): int
{
return $this->buffer->count();
}
/**
@@ -173,11 +198,42 @@ final class AsyncChannel
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,
'capacity' => $this->capacity,
'buffered_items' => $this->buffer->count(),
'waiting_producers' => count($this->waitingProducers),
'waiting_consumers' => count($this->waitingConsumers),
'is_closed' => $this->closed,
];
}
private function resumeWaitingConsumers(): void
{
while (!empty($this->waitingConsumers) && !$this->buffer->isEmpty()) {
$consumer = array_shift($this->waitingConsumers);
if ($consumer instanceof Fiber && $consumer->isSuspended()) {
$consumer->resume();
}
}
}
private function resumeWaitingProducers(): void
{
if ($this->capacity > 0) {
// Buffered: Wecke auf wenn Platz frei
while (!empty($this->waitingProducers) && $this->buffer->count() < $this->capacity) {
$producer = array_shift($this->waitingProducers);
if ($producer instanceof Fiber && $producer->isSuspended()) {
$producer->resume();
}
}
} else {
// Unbuffered: Wecke erste wartende Producer auf
if (!empty($this->waitingProducers)) {
$producer = array_shift($this->waitingProducers);
if ($producer instanceof Fiber && $producer->isSuspended()) {
$producer->resume();
}
}
}
}
}

View File

@@ -17,7 +17,7 @@ final class AsyncMutex
/** @var SplQueue<Fiber> */
private SplQueue $waitingFibers;
private ?string $owner = null;
private ?int $owner = null;
public function __construct(
private readonly string $name = ''

View File

@@ -22,8 +22,8 @@ final class AsyncPool
private array $results = [];
public function __construct(
private readonly int $maxConcurrency = 10,
private readonly FiberManager $fiberManager = new FiberManager()
private readonly FiberManager $fiberManager,
private readonly int $maxConcurrency = 10
) {
$this->pendingOperations = new SplQueue();
}
@@ -65,8 +65,13 @@ final class AsyncPool
private function startFiber(string $id, callable $operation): void
{
$fiber = $this->fiberManager->async($operation, $id);
$this->activeFibers[$id] = $fiber;
try {
$fiber = $this->fiberManager->async($operation, $id);
$this->activeFibers[$id] = $fiber;
} catch (\Throwable $e) {
// If fiber throws immediately on start, store as result
$this->results[$id] = $e;
}
}
private function collectCompletedFibers(): void

View File

@@ -25,16 +25,16 @@ final class AsyncPromise
private array $finallyCallbacks = [];
public function __construct(
private readonly FiberManager $fiberManager = new FiberManager()
private readonly FiberManager $fiberManager
) {
}
/**
* Erstellt ein resolved Promise
*/
public static function resolve(mixed $value): self
public static function resolve(mixed $value, FiberManager $fiberManager): self
{
$promise = new self();
$promise = new self($fiberManager);
$promise->result = $value;
$promise->resolved = true;
@@ -44,9 +44,9 @@ final class AsyncPromise
/**
* Erstellt ein rejected Promise
*/
public static function reject(\Throwable $exception): self
public static function reject(\Throwable $exception, FiberManager $fiberManager): self
{
$promise = new self();
$promise = new self($fiberManager);
$promise->exception = $exception;
$promise->resolved = true;
@@ -56,9 +56,9 @@ final class AsyncPromise
/**
* Erstellt Promise aus Callable
*/
public static function create(callable $executor): self
public static function create(callable $executor, FiberManager $fiberManager): self
{
$promise = new self();
$promise = new self($fiberManager);
$promise->fiberManager->async(function () use ($promise, $executor) {
try {
@@ -75,9 +75,9 @@ final class AsyncPromise
/**
* Wartet auf alle Promises
*/
public static function all(array $promises): self
public static function all(array $promises, FiberManager $fiberManager): self
{
$allPromise = new self();
$allPromise = new self($fiberManager);
$allPromise->fiberManager->async(function () use ($allPromise, $promises) {
try {
@@ -101,9 +101,9 @@ final class AsyncPromise
/**
* Wartet auf das erste erfolgreiche Promise
*/
public static function race(array $promises): self
public static function race(array $promises, FiberManager $fiberManager): self
{
$racePromise = new self();
$racePromise = new self($fiberManager);
foreach ($promises as $promise) {
if ($promise instanceof self) {

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace App\Framework\Async;
/**
* Factory für AsyncPromise Instanzen mit proper Dependency Injection
*/
final readonly class AsyncPromiseFactory
{
public function __construct(
private FiberManager $fiberManager
) {
}
/**
* Erstellt ein resolved Promise
*/
public function resolve(mixed $value): AsyncPromise
{
return AsyncPromise::resolve($value, $this->fiberManager);
}
/**
* Erstellt ein rejected Promise
*/
public function reject(\Throwable $exception): AsyncPromise
{
return AsyncPromise::reject($exception, $this->fiberManager);
}
/**
* Erstellt Promise aus Callable
*/
public function create(callable $executor): AsyncPromise
{
return AsyncPromise::create($executor, $this->fiberManager);
}
/**
* Wartet auf alle Promises (variadic)
*/
public function all(AsyncPromise ...$promises): AsyncPromise
{
return AsyncPromise::all($promises, $this->fiberManager);
}
/**
* Wartet auf das erste erfolgreiche Promise (variadic)
*/
public function race(AsyncPromise ...$promises): AsyncPromise
{
return AsyncPromise::race($promises, $this->fiberManager);
}
/**
* Erstellt ein Promise mit direktem FiberManager Zugriff
* Für fortgeschrittene Use-Cases
*/
public function createWithManager(callable $executor): AsyncPromise
{
return new AsyncPromise($this->fiberManager);
}
}

View File

@@ -16,6 +16,7 @@ final readonly class AsyncService
{
public function __construct(
private FiberManager $fiberManager,
private AsyncPromiseFactory $promiseFactory,
private AsyncTimer $asyncTimer,
private Clock $clock,
private Timer $timer
@@ -35,20 +36,20 @@ final readonly class AsyncService
*/
public function promise(callable $operation): AsyncPromise
{
return AsyncPromise::create($operation);
return $this->promiseFactory->create($operation);
}
/**
* Run multiple operations in parallel
* Run multiple operations in parallel (variadic)
*/
public function parallel(array $operations): AsyncPromise
public function parallel(callable ...$operations): AsyncPromise
{
$promises = [];
foreach ($operations as $key => $operation) {
$promises[$key] = $this->promise($operation);
foreach ($operations as $operation) {
$promises[] = $this->promiseFactory->create($operation);
}
return AsyncPromise::all($promises);
return $this->promiseFactory->all(...$promises);
}
/**
@@ -121,9 +122,9 @@ final readonly class AsyncService
}
/**
* Batch operations with concurrency control
* Batch operations with concurrency control (variadic)
*/
public function batch(array $operations, int $maxConcurrency = 10): array
public function batch(int $maxConcurrency = 10, callable ...$operations): array
{
return $this->fiberManager->throttled($operations, $maxConcurrency);
}

View File

@@ -23,8 +23,15 @@ final readonly class AsyncServiceInitializer
public function __invoke(): AsyncService
{
$fiberManager = new FiberManager($this->clock, $this->timer);
$promiseFactory = new AsyncPromiseFactory($fiberManager);
$asyncTimer = new AsyncTimer($fiberManager, $this->clock, $this->timer);
return new AsyncService($fiberManager, $asyncTimer, $this->clock, $this->timer);
return new AsyncService(
$fiberManager,
$promiseFactory,
$asyncTimer,
$this->clock,
$this->timer
);
}
}

View File

@@ -4,112 +4,96 @@ declare(strict_types=1);
namespace App\Framework\Async;
use Fiber;
use Generator;
use Iterator;
/**
* Stream für kontinuierliche asynchrone Datenverarbeitung
* Async Stream für deklarative Stream-Processing-Pipeline
*
* Ermöglicht funktionale Transformationen auf großen Datenmengen
* mit Lazy Evaluation und optionaler paralleler Verarbeitung.
*/
final class AsyncStream
final readonly class AsyncStream
{
/** @var array<callable> */
private array $processors = [];
private bool $closed = false;
public function __construct(
private readonly FiberManager $fiberManager = new FiberManager()
private function __construct(
private Iterator|Generator $source,
private FiberManager $fiberManager
) {
}
/**
* Erstellt einen Stream aus einem Iterator oder Array
*/
public static function from(Iterator|array $source, FiberManager $fiberManager): self
{
$iterator = is_array($source) ? new \ArrayIterator($source) : $source;
return new self($iterator, $fiberManager);
}
/**
* Erstellt einen Stream aus einem Generator
*/
public static function fromGenerator(Generator $generator): self
public static function fromGenerator(callable $generator, FiberManager $fiberManager): self
{
$stream = new self();
return new self($generator(), $fiberManager);
}
$stream->fiberManager->async(function () use ($stream, $generator) {
foreach ($generator as $item) {
if ($stream->closed) {
break;
/**
* Erstellt einen Stream aus einem Range
*/
public static function range(int $start, int $end, FiberManager $fiberManager): self
{
$generator = function () use ($start, $end) {
for ($i = $start; $i <= $end; $i++) {
yield $i;
}
};
return new self($generator(), $fiberManager);
}
/**
* Erstellt einen Stream aus einem AsyncChannel
*/
public static function fromChannel(AsyncChannel $channel, FiberManager $fiberManager): self
{
$generator = function () use ($channel) {
try {
while (true) {
yield $channel->receive();
}
$stream->emit($item);
} catch (ChannelClosedException) {
// Channel geschlossen, Stream endet
}
$stream->close();
});
return $stream;
};
return new self($generator(), $fiberManager);
}
/**
* 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
* Wendet eine Transformation auf jedes Element an
*/
public function map(callable $transformer): self
{
return $this->pipe($transformer);
$generator = function () use ($transformer) {
foreach ($this->source as $item) {
yield $transformer($item);
}
};
return new self($generator(), $this->fiberManager);
}
/**
* Filtert Elemente basierend auf einem Prädikat
*/
public function filter(callable $predicate): self
{
$generator = function () use ($predicate) {
foreach ($this->source as $item) {
if ($predicate($item)) {
yield $item;
}
}
};
return new self($generator(), $this->fiberManager);
}
/**
@@ -117,18 +101,17 @@ final class AsyncStream
*/
public function take(int $count): self
{
$taken = 0;
return $this->pipe(function ($item) use (&$taken, $count) {
if ($taken < $count) {
$generator = function () use ($count) {
$taken = 0;
foreach ($this->source as $item) {
if ($taken >= $count) {
break;
}
yield $item;
$taken++;
return $item;
}
$this->close();
return null;
});
};
return new self($generator(), $this->fiberManager);
}
/**
@@ -136,100 +119,243 @@ final class AsyncStream
*/
public function skip(int $count): self
{
$skipped = 0;
$generator = function () use ($count) {
$skipped = 0;
foreach ($this->source as $item) {
if ($skipped < $count) {
$skipped++;
continue;
}
yield $item;
}
};
return new self($generator(), $this->fiberManager);
}
return $this->pipe(function ($item) use (&$skipped, $count) {
if ($skipped < $count) {
$skipped++;
/**
* Teilt Stream in Chunks fester Größe
*/
public function chunk(int $size): self
{
$generator = function () use ($size) {
$chunk = [];
foreach ($this->source as $item) {
$chunk[] = $item;
if (count($chunk) >= $size) {
yield $chunk;
$chunk = [];
}
}
// Letzter Chunk (falls nicht voll)
if (!empty($chunk)) {
yield $chunk;
}
};
return new self($generator(), $this->fiberManager);
}
return null;
/**
* Verarbeitet Stream parallel mit N Workers
*/
public function parallel(int $workers): self
{
$generator = function () use ($workers) {
$pool = new AsyncPool($this->fiberManager, maxConcurrency: $workers);
$results = [];
$index = 0;
foreach ($this->source as $item) {
$currentIndex = $index++;
$pool->add(function () use ($item) {
return $item;
}, (string) $currentIndex);
}
return $item;
});
}
$processedResults = $pool->execute();
/**
* Sammelt alle Stream-Elemente in einem Array
*/
public function collect(): Fiber
{
return $this->fiberManager->async(function () {
$collected = [];
// Sortiere Ergebnisse nach Original-Reihenfolge
ksort($processedResults);
$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;
foreach ($processedResults as $result) {
yield $result;
}
};
return new self($generator(), $this->fiberManager);
}
/**
* Flat-mapped nested structures
*/
public function flatMap(callable $transformer): self
{
$generator = function () use ($transformer) {
foreach ($this->source as $item) {
$transformed = $transformer($item);
if (is_iterable($transformed)) {
foreach ($transformed as $subItem) {
yield $subItem;
}
} else {
yield $transformed;
}
}
};
return new self($generator(), $this->fiberManager);
}
/**
* Führt eine Aktion für jedes Element aus (Terminal Operation)
*/
public function forEach(callable $action): void
{
foreach ($this->source as $item) {
$action($item);
}
}
/**
* Schließt den Stream
* Reduziert den Stream auf einen einzelnen Wert (Terminal Operation)
*/
public function close(): void
public function reduce(callable $reducer, mixed $initial = null): mixed
{
$this->closed = true;
$accumulator = $initial;
foreach ($this->source as $item) {
$accumulator = $reducer($accumulator, $item);
}
return $accumulator;
}
/**
* Prüft ob Stream geschlossen ist
* Sammelt alle Elemente in ein Array (Terminal Operation)
*/
public function isClosed(): bool
public function toArray(): array
{
return $this->closed;
return iterator_to_array($this->source, false);
}
/**
* Gibt Stream-Statistiken zurück
* Zählt die Anzahl der Elemente (Terminal Operation)
*/
public function getStats(): array
public function count(): int
{
return [
'processors' => count($this->processors),
'closed' => $this->closed,
];
$count = 0;
foreach ($this->source as $_) {
$count++;
}
return $count;
}
/**
* Prüft ob mindestens ein Element das Prädikat erfüllt (Terminal Operation)
*/
public function any(callable $predicate): bool
{
foreach ($this->source as $item) {
if ($predicate($item)) {
return true;
}
}
return false;
}
/**
* Prüft ob alle Elemente das Prädikat erfüllen (Terminal Operation)
*/
public function all(callable $predicate): bool
{
foreach ($this->source as $item) {
if (!$predicate($item)) {
return false;
}
}
return true;
}
/**
* Findet das erste Element das Prädikat erfüllt (Terminal Operation)
*/
public function first(?callable $predicate = null): mixed
{
foreach ($this->source as $item) {
if ($predicate === null || $predicate($item)) {
return $item;
}
}
return null;
}
/**
* Sendet alle Elemente in einen AsyncChannel (Terminal Operation)
*/
public function toChannel(AsyncChannel $channel): void
{
foreach ($this->source as $item) {
$channel->send($item);
}
$channel->close();
}
/**
* Gruppiert Elemente nach Key-Funktion (Terminal Operation)
*/
public function groupBy(callable $keySelector): array
{
$groups = [];
foreach ($this->source as $item) {
$key = $keySelector($item);
if (!isset($groups[$key])) {
$groups[$key] = [];
}
$groups[$key][] = $item;
}
return $groups;
}
/**
* Sortiert Stream (Terminal Operation, lädt alle Elemente in Memory)
*/
public function sorted(?callable $comparator = null): self
{
$items = $this->toArray();
if ($comparator !== null) {
usort($items, $comparator);
} else {
sort($items);
}
return self::from($items, $this->fiberManager);
}
/**
* Entfernt Duplikate basierend auf Wert oder Key-Funktion
*/
public function distinct(?callable $keySelector = null): self
{
$generator = function () use ($keySelector) {
$seen = [];
foreach ($this->source as $item) {
$key = $keySelector !== null ? $keySelector($item) : $item;
$keyString = is_scalar($key) ? (string) $key : serialize($key);
if (!isset($seen[$keyString])) {
$seen[$keyString] = true;
yield $item;
}
}
};
return new self($generator(), $this->fiberManager);
}
/**
* Führt eine Aktion für jedes Element aus ohne Stream zu ändern
*/
public function tap(callable $action): self
{
$generator = function () use ($action) {
foreach ($this->source as $item) {
$action($item);
yield $item;
}
};
return new self($generator(), $this->fiberManager);
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Framework\Async;
use RuntimeException;
/**
* Exception für geschlossene Channels
*/
final class ChannelClosedException extends RuntimeException
{
}

View File

@@ -44,9 +44,9 @@ final class FiberManager
/**
* Führt eine asynchrone Operation aus und gibt sofort einen Fiber zurück
*/
public function async(callable $operation, ?string $operationId = null): Fiber
public function async(callable $operation, string|int|null $operationId = null): Fiber
{
$operationId ??= uniqid('fiber_', true);
$operationId = $operationId !== null ? (string) $operationId : uniqid('fiber_', true);
$startTime = $this->clock->time();
$fiber = new Fiber(function () use ($operation, $operationId, $startTime) {
@@ -183,6 +183,9 @@ final class FiberManager
/**
* Execute operation with timeout using Duration
*
* Note: This only works reliably with cooperative operations that use Fiber::suspend()
* For blocking operations, the timeout check cannot interrupt execution.
*/
public function withTimeoutDuration(callable $operation, Duration $timeout): mixed
{
@@ -206,6 +209,90 @@ final class FiberManager
return $fiber->getReturn();
}
/**
* Execute cooperative operation with timeout support
*
* The operation can use Fiber::suspend() to yield control, allowing timeout checks.
* This enables proper timeout handling for long-running operations.
*/
public function asyncCooperative(callable $operation, string|int|null $operationId = null): Fiber
{
$operationId = $operationId !== null ? (string) $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;
// Don't start immediately - let caller control execution
return $fiber;
}
/**
* Execute cooperative operation with timeout using Duration
*
* The operation must use Fiber::suspend() to allow timeout checks.
* Example:
*
* $result = $manager->withTimeoutCooperative(function() {
* for ($i = 0; $i < 100; $i++) {
* doWork($i);
* if ($i % 10 === 0) {
* Fiber::suspend(); // Allow timeout check
* }
* }
* return 'done';
* }, Duration::fromSeconds(5));
*/
public function withTimeoutCooperative(callable $operation, Duration $timeout): mixed
{
$startTime = $this->clock->time();
$endTime = Timestamp::fromFloat($startTime->toFloat() + $timeout->toSeconds());
$fiber = $this->asyncCooperative($operation);
// Start fiber
$fiber->start();
// Process fiber with timeout checks
while (! $fiber->isTerminated()) {
// Check timeout
if ($this->clock->time()->isAfter($endTime)) {
$elapsed = $startTime->age($this->clock);
throw new AsyncTimeoutException(
"Operation exceeded timeout of {$timeout->toHumanReadable()} (elapsed: {$elapsed->toHumanReadable()})"
);
}
// Resume suspended fiber
if ($fiber->isSuspended()) {
$fiber->resume();
}
// Small delay to prevent CPU spinning
$this->timer->sleep(Duration::fromMilliseconds(1));
}
return $fiber->getReturn();
}
/**
* Kombiniert mehrere Fibers zu einem einzigen
*/