krzysztofzylka/event-loop

There is no license information available for the latest version (1.0.0) of this package.

Installs: 3

Dependents: 1

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/krzysztofzylka/event-loop

1.0.0 2025-10-27 17:21 UTC

This package is auto-updated.

Last update: 2025-10-27 17:23:34 UTC


README

Lekka, szybka i w pełni asynchroniczna pętla zdarzeń napisana w czystym PHP 8.3+, inspirowana ReactPHP oraz Node.js.
Bazuje na stream_select(), dzięki czemu działa bez zewnętrznych rozszerzeń, a teraz dodatkowo potrafi wykonywać kosztowne zadania w osobnych procesach.

Wymagania

  • PHP >= 8.3
  • Opcjonalnie ext-pcntl – wymagane do timerów procesowych (GNU/Linux, macOS)

Instalacja

composer require krzysztofzylka/event-loop

Najważniejsze funkcje

  • Asynchroniczne I/O oparte na stream_select().
  • Timery jednorazowe i cykliczne z priorytetami (HIGH, NORMAL, LOW).
  • Future ticks – natychmiastowe callbacki uruchamiane po bieżącej iteracji pętli.
  • Obsługa strumieni odczytu i zapisu.
  • Obsługa sygnałów systemowych (SIGINT, SIGTERM itd.).
  • Timery procesowe – uruchamianie ciężkich zadań w osobnych procesach (jednorazowo i cyklicznie).
  • Konfigurowalne sterowniki pętli (LoopConfig) oraz tagowanie timerów i strumieni.
  • Brak zewnętrznych zależności – działa nawet na shared hostingu.

Szybki start

<?php

require __DIR__ . '/vendor/autoload.php';

use Async\Loop;

Loop::futureTick(fn() => print "Start loop\n");

Loop::addTimer(1, fn() => print "Minęła 1 sekunda\n");
Loop::addPeriodicTimer(2, fn() => print "Ping co 2 sekundy\n");

Loop::addTimer(6, fn() => Loop::stop());

Loop::run();

Wynik:

Start loop
Minęła 1 sekunda
Ping co 2 sekundy
Ping co 2 sekundy

Timery procesowe

Procesowe timery pozwalają uruchamiać kod w osobnym procesie i odebrać wynik po zakończeniu pracy – bez blokowania głównej pętli. Nadadzą się do zadań CPU-intensywnych, integracji ze starymi bibliotekami wykonującymi sleep() itp.

use Async\Loop;
use Async\ProcessTimerException;
use Async\Timer;

Loop::addProcessTimer(
    0.0,
    fn() => ['pid' => getmypid(), 'checksum' => array_sum(range(1, 1_000_000))],
    function (array $result, Timer $timer, int $pid): void {
        printf("Child %d finished with checksum %d\n", $pid, $result['checksum']);
    },
    function (ProcessTimerException $exception): void {
        fprintf(STDERR, "Process failed: %s\n", $exception->getMessage());
    }
);

Loop::run();

Właściwości

  • Loop::addProcessTimer() – jednorazowe zadanie.
  • Loop::addPeriodicProcessTimer() – zadanie cykliczne (uruchamia nowe dziecko przy każdej iteracji).
  • W callbacku możesz otrzymać wynik ($onResult) lub błąd ($onError).
  • Włącza się tylko, jeśli dostępne jest pcntl_fork() oraz pcntl_waitpid() – w innym wypadku rzucany jest RuntimeException.
  • Dane z procesów buforowane są w katalogach tymczasowych (kolejno): /tmp, <projekt>/tmp, <projekt>/.async-process-*.

Pełny przykład znajdziesz w examples/process_timers.php.

Konfiguracja i sterowniki

Domyślnie pętla korzysta z drivera opartego na stream_select(). Możesz jednak utworzyć własną instancję z innymi parametrami (np. zmienionym interwałem odpytywania procesów) i przekazać ją do Loop::configure():

use Async\Loop;
use Async\LoopConfig;

$config = LoopConfig::default()
    ->withProcessPollInterval(0.005)
    ->withIdleSleepUsec(500);

Loop::configure($config);
Loop::run();

Jeżeli napiszesz własny driver (np. oparty o epoll), zaimplementuj Async\Driver\LoopDriverInterface i przekaż go do konfiguracji metodą withDriver(...).

Pula procesów (prefork)

Pula procesów pozwala utrzymać stałą liczbę pracowników uruchomionych z wyprzedzeniem i zlecać im zadania bez kosztu fork() przy każdej operacji.

use Async\Loop;

$pool = Loop::createProcessPool(2);

$pool->submit('strtoupper', ['hello'], fn($value) => print("Upper: $value\n"));
$pool->submit('strlen', ['async'], fn($value) => print("Length: $value\n"));

Loop::addTimer(0.5, function () use ($pool) {
    $pool->shutdown();
    Loop::stop();
});

Loop::run();

Każde zadanie wykonuje statyczny callable (function lub [ClassName::class, 'method']) z podanymi argumentami. Wyniki oraz błędy można obsłużyć przez przekazane callbacki. Przykład znajdziesz w examples/process_pool.php, a test obciążeniowy w benchmarks/process_pool.php.

Metryki

Pętla udostępnia podstawowe statystyki przez Loop::getMetrics() (liczba timerów, strumieni, procesów, średni czas ticku itd.):

$metrics = Loop::getMetrics();
print_r($metrics);

Metryki można wykorzystać np. do monitoringu lub prostej diagnostyki w trakcie działania aplikacji.

API klasy Loop

Metoda Opis
Loop::addTimer(float $sec, callable $cb, Priority $priority = Priority::NORMAL) Jednorazowy timer
Loop::addPeriodicTimer(float $sec, callable $cb, Priority $priority = Priority::NORMAL) Timer cykliczny
Loop::addProcessTimer(float $sec, callable $process, ?callable $onResult = null, ?callable $onError = null, Priority $priority = Priority::NORMAL) Jednorazowe zadanie w podprocesie
Loop::addPeriodicProcessTimer(float $sec, callable $process, ?callable $onResult = null, ?callable $onError = null, Priority $priority = Priority::NORMAL) Cykliczne zadanie w podprocesie
Loop::cancelTimer(int $id) Anuluje timer (również procesowy)
Loop::futureTick(callable $cb) Callback po zakończeniu bieżącej iteracji
Loop::addReadStream($stream, callable $cb) Rejestruje strumień do odczytu
Loop::addWriteStream($stream, callable $cb) Rejestruje strumień do zapisu
Loop::addSignal(int $signal, callable $cb) Obsługa sygnałów systemowych
Loop::run() Uruchamia pętlę zdarzeń
Loop::stop() Zatrzymuje pętlę
Loop::configure(?LoopConfig $config = null) Tworzy nową instancję pętli na podstawie konfiguracji
Loop::tagTimer(int $id, string ...$tags) Dodaje tagi do timera
Loop::cancelTimersByTag(string $tag) Anuluje wszystkie timery mające dany tag
Loop::tagReadStream($stream, string ...$tags) Oznacza strumień do odczytu tagami
Loop::tagWriteStream($stream, string ...$tags) Oznacza strumień do zapisu tagami
Loop::removeStreamsByTag(string $tag) Usuwa wszystkie strumienie przypisane do tagu

Priorytety timerów

use Async\Loop;
use Async\Priority;

Loop::addTimer(1, fn() => print "[LOW] niski\n", Priority::LOW);
Loop::addTimer(1, fn() => print "[NORMAL] normalny\n", Priority::NORMAL);
Loop::addTimer(1, fn() => print "[HIGH] wysoki\n", Priority::HIGH);
Loop::addTimer(2, fn() => Loop::stop());
Loop::run();

Wynik:

[HIGH] wysoki
[NORMAL] normalny
[LOW] niski

Priorytet decyduje o kolejności wykonania timerów zaplanowanych na ten sam moment.

Echo serwer TCP

use Async\Loop;

$server = stream_socket_server("tcp://127.0.0.1:9000", $errno, $errstr);
if (!$server) {
    die("Błąd: $errstr ($errno)\n");
}

echo "Serwer działa: 127.0.0.1:9000\n";

Loop::addReadStream($server, function ($server) {
    $client = @stream_socket_accept($server, 0);
    if ($client) {
        echo "[+] Nowe połączenie\n";
        stream_set_blocking($client, false);

        Loop::addReadStream($client, function ($client) {
            $data = fread($client, 1024);
            if ($data === '' || $data === false) {
                Loop::removeReadStream($client);
                fclose($client);
                echo "[-] Rozłączono\n";
            } else {
                fwrite($client, "Echo: $data");
            }
        });
    }
});

Loop::addSignal(SIGINT, fn() => print("\nSIGINT — zatrzymuję serwer\n") || Loop::stop());
Loop::run();

Test:

telnet 127.0.0.1 9000
> hello
Echo: hello

Przykłady

Katalog examples/ zawiera gotowe scenariusze:

  • process_timers.php – demonstracja timerów procesowych (jednorazowy + cykliczny).
  • configuration.php – uruchomienie pętli z niestandardowym LoopConfig.
  • tags.php – praca z tagami timerów i strumieni.
  • process_pool.php – użycie puli procesów prefork.

Uruchomienie:

php examples/process_timers.php
php examples/configuration.php
php examples/tags.php
php examples/process_pool.php

Testy

composer install
vendor/bin/phpunit

Testy dotyczące timerów procesowych wymagają dostępności pcntl.