Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Long-running tasks blocking the WebSocket process #1058

Open
programarivm opened this issue Oct 1, 2024 · 3 comments
Open

Long-running tasks blocking the WebSocket process #1058

programarivm opened this issue Oct 1, 2024 · 3 comments

Comments

@programarivm
Copy link

programarivm commented Oct 1, 2024

Until recently, we had implemented our chess functionality on two different servers: The WebSocket server and the web server. The former was intended to run real-time tasks while the latter hosted a REST-like API for long-running tasks like database queries, ad hoc reportings, and so on, which would take a few seconds to run.

This separation of concerns was perfeclty fine.

However, at some point it was decided to get rid of the web server and try out an implementation completely based on WebSockets. All functionality, which is to say real-time operations and long-running operations, was moved to the WebSocket server. The reason being was mainly because this setup looked simpler and cheaper.

The thing is, whether using Workerman or Ratchet, the staging server has demonstrated that there is something wrong with this setup.

If two users are playing chess online (real-time) while another user is generating an ad hoc report (long-running) the two users playing online will experience a bottleneck because the report generation seems to be blocking the WebScocket process for a few seconds.

The current WebSocket server is pretty much unusable if there are a few users connected at the same time:

Are we missing something?

Could you please provide some guidance on how to implement long-running tasks with WebSockets? Or should we get back to the previous API implementation for the long-running tasks?

🙏 Thank you for the help, it is very much appreciated!

@joanhey
Copy link
Contributor

joanhey commented Oct 1, 2024

Try to use Timer::add() to run the report generation.
Using it, the process will be queued to the event loop.
It worked ok for me in some apps.

You can find more information here: #937

@programarivm
Copy link
Author

🙏 Thank you @joanhey for the help.

The /heuristic command may take a few seconds to run. This is how it is being implemented now.

<?php

namespace ChessServer\Command\Game;

use Chess\SanHeuristic;
use Chess\Function\CompleteFunction;
use Chess\Variant\Chess960\Board as Chess960Board;
use Chess\Variant\Chess960\FEN\StrToBoard as Chess960FenStrToBoard;
use Chess\Variant\Classical\Board as ClassicalBoard;
use Chess\Variant\Classical\FEN\StrToBoard as ClassicalFenStrToBoard;
use ChessServer\Command\AbstractCommand;
use ChessServer\Socket\AbstractSocket;

class HeuristicCommand extends AbstractCommand
{
    public function __construct()
    {
        $this->name = '/heuristic';
        $this->description = 'Balance of a chess heuristic.';
        $this->params = [
            'params' => '<string>',
        ];
    }

    public function validate(array $argv)
    {
        return count($argv) - 1 === count($this->params);
    }

    public function run(AbstractSocket $socket, array $argv, int $id)
    {
        $params = json_decode(stripslashes($argv[1]), true);

        if ($params['variant'] === Chess960Board::VARIANT) {
            $startPos = str_split($params['startPos']);
            $board = isset($params['fen'])
                ? (new Chess960FenStrToBoard($params['fen'], $startPos))->create()
                : new Chess960Board($startPos);
        } elseif ($params['variant'] === ClassicalBoard::VARIANT) {
            $board = isset($params['fen'])
                ? (new ClassicalFenStrToBoard($params['fen']))->create()
                : new ClassicalBoard();
        }

        $balance = (new SanHeuristic(
            new CompleteFunction(),
            $params['name'],
            $params['movetext'],
            $board
        ))->getBalance();

        return $socket->getClientStorage()->send([$id], [
            $this->name => $balance,
        ]);
    }
}

And this is how I'm trying to defer it without much success.

<?php

namespace ChessServer\Command\Game;

use Chess\SanHeuristic;
use Chess\Function\CompleteFunction;
use Chess\Variant\Chess960\Board as Chess960Board;
use Chess\Variant\Chess960\FEN\StrToBoard as Chess960FenStrToBoard;
use Chess\Variant\Classical\Board as ClassicalBoard;
use Chess\Variant\Classical\FEN\StrToBoard as ClassicalFenStrToBoard;
use ChessServer\Command\AbstractCommand;
use ChessServer\Socket\AbstractSocket;
use Workerman\Timer;

class HeuristicCommand extends AbstractCommand
{
    public function __construct()
    {
        $this->name = '/heuristic';
        $this->description = 'Balance of a chess heuristic.';
        $this->params = [
            'params' => '<string>',
        ];
    }

    public function validate(array $argv)
    {
        return count($argv) - 1 === count($this->params);
    }

    public function run(AbstractSocket $socket, array $argv, int $id)
    {
        $params = json_decode(stripslashes($argv[1]), true);

        if ($params['variant'] === Chess960Board::VARIANT) {
            $startPos = str_split($params['startPos']);
            $board = isset($params['fen'])
                ? (new Chess960FenStrToBoard($params['fen'], $startPos))->create()
                : new Chess960Board($startPos);
        } elseif ($params['variant'] === ClassicalBoard::VARIANT) {
            $board = isset($params['fen'])
                ? (new ClassicalFenStrToBoard($params['fen']))->create()
                : new ClassicalBoard();
        }

        Timer::add(0.001, function() use ($params, $board, $socket, $id) {
            $balance = (new SanHeuristic(
                new CompleteFunction(),
                $params['name'],
                $params['movetext'],
                $board
            ))->getBalance();

            return $socket->getClientStorage()->send([$id], [
                $this->name => $balance,
            ]);
        }, persistent: false);
    }
}

Is this correct or am I wrong in doing so?

        // ...  

        Timer::add(0.001, function() use ($params, $board, $socket, $id) {
            $balance = (new SanHeuristic(
                new CompleteFunction(),
                $params['name'],
                $params['movetext'],
                $board
            ))->getBalance();

            return $socket->getClientStorage()->send([$id], [
                $this->name => $balance,
            ]);
        }, persistent: false);

        // ...

Happy coding.

@programarivm
Copy link
Author

PHP Chess Server is a flexible asynchronous PHP chess server allowing support for multiple async PHP frameworks.

At the moment is using Workerman and Ratchet with the default one being Workerman. Also we'd want to support AMPHP. This is made possible thanks to a polymorphic, object-oriented WebSocket implementation that is providing chess functionality.

We're thinking along the lines of solving the concurrency issue using PCNTL functions with the help of spatie/async agnostically to the async PHP frameworks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants