PHP asynchronicznie - prosty sposób na długo wykonujące się zadania

Krzysztof Gał
PHP asynchronicznie - prosty sposób na długo wykonujące się zadania

Czasami w starych aplikacjach jak np. Prestashop zachodzi potrzeba wykonania jakiegoś zadania które w zasadzie nie powinno mieć wpływu na odpowiedź jaką dostaje użytkownik strony od serwera.

Takim zadaniem może być np. wysłanie e-maila lub też wygenerowanie jakiegoś PDF itp... Po co użytkownik ma czekać tak długi czas, jeżeli można takie zadania wykonać po tym jak klient otrzyma odpowiedź z serwera.

Jak zatem można w łatwy sposób zaimplementować mechanizm, który nam to umożliwi.

Po pierwsze potrzebujemy jakiegoś Process Managera, do którego będziemy zapisywać nasze zadania i który będzie je później uruchamiać. Nasz ProcessManager będzie Singletonem.

class ProcessManager
{
    private static $instance = null;
    private static $process = null;

    public static function getInstance()
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct()
    {
        self::$process = [];
    }

    public function attach(callable $callable)
    {
        self::$process[] = $callable;
    }

    public function process()
    {
        foreach (self::$process as $callable) {
            call_user_func($callable);
        }
    }

    public function hasAny()
    {
        return (bool) self::$process;
    }
}

Mając takiego Process Managera możemy dodawać do niego zadania w postaci funkcji anonimowych (callable). Na przykład wysyłamy emaila z potwierdzeniem zamówienia.

$processManager = ProcessManager::getInstance();

$sendOrderEmail = function() use ($orderObject) {
    //Wyśli emaila używająć danych z $orderObject
};

$processManager->attach($sendOrderEmail);

Skoro mamy już Process Managera oraz zarejestrowaliśmy zadanie do wykonania, pozostaje nam już tylko w odpowiednim miejscu wywołać jego wykonanie.

Najlepszym miejscem do wywołania naszego Process Managera będzie jakiś wspólny punkt wejścia do aplikacji, czyli zwykle jakiś front controller lub też po prostu jakiś index.php.

W tym miejscu może pojawić się pewien problem, jak bowiem wywołać Process Managera kiedy gdzieś (często) w kodzie, aplikacja wychodzi bezpardonowo funkcją exit() i de facto aby go uruchomić musielibyśmy wstawić jego wywołanie przed wywołaniem exit() a aby zrobić to globalnie dla całej aplikacji musielibyśmy go uruchomić przed wywołaniem Kontrolera czy co tam mamy w naszej aplikacji (oczywiście to jest nonsens), tak było w moim przypadku w pracy z Prestashop.

Otóż okazuje się, że PHP posiada wbudowane rozwiązanie, które uratuje nasz pomysł z takiej sytuacji. Funkcja register_shutdown_function() przyjmuje za argument tak jak nasz Process Manager callable i uruchamia wszystkie zapisane w niej funkcje przy zakończeniu działania aplikacji, w tym po wywołaniu np. exit(). W ten sposób oto możemy uruchomić naszego Process Managera zawsze przy zakończeniu działania aplikacji.

Dodajemy więc wywołanie naszego ProcessManager do wejścia naszej aplikacji (index.php).

$longRunningTask = function() {
    $processManager = ProcessManager::getInstance();
    $processManager->process();
};

register_shutdown_function($longRunningTask);

//wywołanie kontrolera aplikacji

No i Voilà* :) Tyle, że to nie działa zgodnie z oczekiwaniami, gdyż register_shutdown_function() działa dokładnie tak jakbyśmy ten kod uruchomili normalnie w dowolnym innym miejscu, czyli w żaden sposób nie przyspiesza to odpowiedzi do klienta, wszystkie zadania przetwarzają się zanim klient otrzyma od nas response.

W tym miejscu poraz kolejny znajdujemy w dokumentacji PHP to czego potrzebujemy. Funkcja fastcgi_finish_request() jest zasadniczo punktem kulminacyjnym naszego Process Managera i jedyną rzeczą, która pozwoli wykorzystać go zgodnie z oczekiwaniami. Funkcja zgodnie z dokumentacją "flushes all response data to the client and finishes the request. This allows for time consuming tasks to be performed without leaving the connection to the client open", czyli jej wywołanie wymusza zakończenie requesta i zwrócenie odpowiedzi.

W dokumentacji znajdziemy również poradę aby przed wywołaniem tej funkcji, wywołać session_write_close() aby nie blokować możliwości kolejnego połączenia klienta z serwerem.

W czasie testów z implementacją tegoż rozwiązania do systemu Prestashop, napotkałem problem z wykonaniem operacji takich jak logowanie, wylogowanie spowodowany działaniem funkcji fastcgi_finish_request() dlatego też do ostatecznego rozwiązania dodałem sprawdzenie czy jest jakieś zadanie do wykonania, jeżeli jest, to tylko wtedy wymuszamy zwrócenie odpowiedzi do klienta.

$longRunningTask = function() {
    $processManager = ProcessManager::getInstance();

    if ($processManager->hasAny()) {
        session_write_close();
        fastcgi_finish_request();
        $processManager->process();
    }
};

register_shutdown_function($longRunningTask);

//wywołanie kontrolera aplikacji

Takim oto sposobem do aplikacji, która odstaje kodem od jakichkolwiek dzisiejszych standardów, dodałem nutkę asynchroniczności, a klient przy złożeniu zamówienia, dostaje stronę z powiedzeniem w 3 sekundy a email wraz z wygenerowanym PDF wysyła się około 15 sekund później.

Może znacie jakieś inne sposoby na ożywienie przestarzałych i powolnych aplikacji.

Blog Comments powered by Disqus.