Open juslintek opened 4 years ago
@juslintek The member properties of your task can only be in the limited types (boolean/integer/float/string/null/array/object), callable/resource cannot be serialized.
[2020-05-29 18:40:33] [ERROR] Uncaught exception 'Exception': [0]Serialization of 'Closure' is not allowed called in /var/www/html/vendor/hhxsv5/laravel-s/src/Swoole/Task/BaseTask.php:93
BTW, xdebug
conflicts with swoole
, they cannot be enabled at the same time.
I use sdebug the swoole compatible one. But one way or another it doesn't work... Is there any tool like xdebug for swoole?
P.S. problem is not the error I get. It is obvious. Its that processes after reload is not connecting to websocket again. Seems like callback is not happening anymore after reload or error. Here is process and task codes:
Process:
<?php
declare(strict_types=1);
namespace App\Processes;
use App\Services\DataloyApiClient;
use App\Tasks\FrameToNotificationConversion;
use Hhxsv5\LaravelS\Swoole\Process\CustomProcessInterface;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Swoole\Http\Server;
use Swoole\Process;
use Swoole\WebSocket\Frame;
use Swoole\Coroutine\HTTP\Client;
class WebSocketClientProcess implements CustomProcessInterface
{
/**
* @var bool Quit tag for Reload updates
*/
private static bool $quit = false;
private static string $accessToken = '';
private static Client $client;
public static function callback(Server $swoole, Process $process): void
{
$dataloyClient = new DataloyApiClient();
if (self::$accessToken === '') {
self::$accessToken = $dataloyClient->getAccessToken();
}
self::$client = new Client(
'socket-server',
80
);
self::$client->setHeaders([
'Host' => 'socket-server',
'User-Agent' => 'Chrome/49.0.2587.3',
'Accept' => 'application/json',
'Accept-Encoding' => 'gzip',
]);
self::$client->set(['timeout' => 1]);
$upgraded = self::$client->upgrade('/ws/rest/WebSocketAlertScriptRaw/COA/' . self::$accessToken);
if (!$upgraded) {
return;
}
while (!self::$quit) {
$frame = self::$client->recv();
if ($frame instanceof Frame) {
$task = new FrameToNotificationConversion($frame);
$task::deliver($task);
}
}
}
// Requirements: LaravelS >= v3.4.0 & callback() must be async non-blocking program.
public static function onReload(Server $swoole, Process $process): void
{
\Log::info('WebSocket Client process: reloading (' . self::$client->host . ')');
self::$quit = true;
self::$client->close();
}
}
Task:
<?php
declare(strict_types = 1);
namespace App\Tasks;
use App\Company;
use App\Notifications\ShipmentArrival;
use App\Services\DataloyApiClient;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\Notification;
use Swoole\WebSocket\Frame;
class FrameToNotificationConversion extends Task
{
private array $data;
private array $result = [];
public function __construct(Frame $frame)
{
$this->data = json_decode($frame->data, true);
}
// The logic of task handling, run in task process, CAN NOT deliver task
public function handle(): void
{
\Log::info(__CLASS__ . ':handle start', [$this->data]);
foreach ($this->data['cargos'] as $cargo) {
$cargoKey = $cargo['key'];
try {
$cargo = (new DataloyApiClient())
->get('Cargo', $cargoKey)
->fields(json_encode(
[
'cargoReference' => '',
'charterer' => [
'businessPartnerCode' => '',
'businessPartnerName' => '',
],
'cargoPorts' => [
'port' => [
'portName' => '',
],
'reasonForCall' => [
'reasonForCall' => '',
],
],
],
JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT
))
->fetch();
} catch (\Exception $exception) {
\Log::error($exception->getMessage() . '. CargoKey:' . $cargoKey);
continue;
}
try {
$company = Company::where(
'dataloy_code',
$cargo->charterer->businessPartnerCode
)
->firstOrFail();
} catch (ModelNotFoundException $modelNotFoundException) {
\Log::notice(
$modelNotFoundException->getMessage()
. ' with dataloy_code = '
. $cargo->charterer->businessPartnerCode
. ' in '
. $modelNotFoundException->getFile()
. ':'
. $modelNotFoundException->getLine()
);
continue;
}
$messageData = [
'port_name' => $cargo->cargoPorts[1]->port->portName,
'shipment_id' => $cargo->cargoReference,
'action_url' => '/shipment/' . $cargo->cargoReference,
'new_eta' => $this->data['newETA'],
];
$this->result[] = $messageData;
Notification::send($company->users, new ShipmentArrival($messageData));
}
}
// Optional, finish event, the logic of after task handling, run in worker process, CAN deliver task
public function finish(): void
{
\Log::info(__CLASS__ . ':finish start', [$this->result]);
}
}
As far as I know, currently only sdebug. About the reload problem, I do not reproduce.
class WebSocketService implements WebSocketHandlerInterface
{
public function __construct()
{
}
public function onOpen(Server $server, Request $request)
{
$server->push($request->fd, 'Welcome to LaravelS[main]#' . $request->fd);
}
public function onMessage(Server $server, Frame $frame)
{
$server->push($frame->fd, 'LaravelS: ' . $frame->data);
}
public function onClose(Server $server, $fd, $reactorId)
{
}
}
class TestProcess implements CustomProcessInterface
{
protected static $quit = false;
public static function callback(Server $swoole, Process $process)
{
\Log::info(__METHOD__ . ':start');
$cli = new Client('127.0.0.1', 5200);
$ret = $cli->upgrade('/');
if ($ret) {
while (!self::$quit) {
$cli->push('hello ' . date('Y-m-d H:i:s'));
$frame = $cli->recv();
Task::deliver(new TestWebSocketTask($frame));
Coroutine::sleep(1);
}
}
\Log::info(__METHOD__ . ':quit');
}
public static function onReload(Server $swoole, Process $process)
{
\Log::info('custom process: reloading');
self::$quit = true;
}
}
class TestWebSocketTask extends Task
{
private $frame;
private $result;
public function __construct(Frame $frame)
{
$this->frame = $frame;
}
public function handle()
{
\Log::info(__METHOD__, [$this->frame->data]);
$this->result = 'the result: ' . microtime(true);
}
public function finish()
{
\Log::info(__METHOD__, [$this->result]);
}
}
bin/laravels reload at 2020-05-31 10:26:27.
[2020-05-31 10:26:10] local.INFO: App\Tasks\TestWebSocketTask::handle ["LaravelS: hello 2020-05-31 10:26:09"]
[2020-05-31 10:26:16] local.INFO: App\Processes\TestProcess::callback:start
[2020-05-31 10:26:16] local.INFO: App\Tasks\TestWebSocketTask::handle ["Welcome to LaravelS[main]#1"]
[2020-05-31 10:26:17] local.INFO: App\Tasks\TestWebSocketTask::handle ["LaravelS: hello 2020-05-31 10:26:16"]
[2020-05-31 10:26:18] local.INFO: App\Tasks\TestWebSocketTask::handle ["LaravelS: hello 2020-05-31 10:26:17"]
[2020-05-31 10:26:19] local.INFO: App\Tasks\TestWebSocketTask::handle ["LaravelS: hello 2020-05-31 10:26:18"]
[2020-05-31 10:26:20] local.INFO: App\Tasks\TestWebSocketTask::handle ["LaravelS: hello 2020-05-31 10:26:19"]
[2020-05-31 10:26:21] local.INFO: App\Tasks\TestWebSocketTask::handle ["LaravelS: hello 2020-05-31 10:26:20"]
[2020-05-31 10:26:22] local.INFO: App\Tasks\TestWebSocketTask::handle ["LaravelS: hello 2020-05-31 10:26:21"]
[2020-05-31 10:26:23] local.INFO: App\Tasks\TestWebSocketTask::handle ["LaravelS: hello 2020-05-31 10:26:22"]
[2020-05-31 10:26:24] local.INFO: App\Tasks\TestWebSocketTask::handle ["LaravelS: hello 2020-05-31 10:26:23"]
[2020-05-31 10:26:25] local.INFO: App\Tasks\TestWebSocketTask::handle ["LaravelS: hello 2020-05-31 10:26:24"]
[2020-05-31 10:26:26] local.INFO: App\Tasks\TestWebSocketTask::handle ["LaravelS: hello 2020-05-31 10:26:25"]
[2020-05-31 10:26:27] local.INFO: App\Tasks\TestWebSocketTask::handle ["LaravelS: hello 2020-05-31 10:26:26"]
[2020-05-31 10:26:27] local.INFO: custom process: reloading
[2020-05-31 10:26:28] local.INFO: App\Processes\TestProcess::callback:quit
[2020-05-31 10:26:35] local.INFO: App\Processes\TestProcess::callback:start
[2020-05-31 10:26:35] local.INFO: App\Tasks\TestWebSocketTask::handle ["Welcome to LaravelS[main]#2"]
[2020-05-31 10:26:36] local.INFO: App\Tasks\TestWebSocketTask::handle ["LaravelS: hello 2020-05-31 10:26:35"]
[2020-05-31 10:26:37] local.INFO: App\Tasks\TestWebSocketTask::handle ["LaravelS: hello 2020-05-31 10:26:36"]
[2020-05-31 10:26:38] local.INFO: App\Tasks\TestWebSocketTask::handle ["LaravelS: hello 2020-05-31 10:26:37"]
[2020-05-31 10:26:39] local.INFO: App\Tasks\TestWebSocketTask::handle ["LaravelS: hello 2020-05-31 10:26:38"]
@hhxsv5, But is WebSocket Service websocket client like new Websocket on javascript? Or WebSocket Server, like laravel-echo-server?
Because I currently need WebSocket as a client, not a service.
Maybe your wrapper cannot handle WebSocket clients?
Can you use my code, just without the processing complexity of data?
You can use this server for testing: https://www.websocket.org/echo.html
wss://echo.websocket.org
you can send any messages via the form at their website.
P.S. Have you used my Dockerfile?
I am adding composer.json in case:
{
"name": "juslintek/laravels-websocket-client",
"type": "project",
"description": "Swoole websocket client in laravel",
"keywords": [
"framework",
"laravel"
],
"license": "MIT",
"require": {
"php": "^7.4",
"ext-json": "*",
"fideloper/proxy": "^4.0",
"hhxsv5/laravel-s": "^3.7",
"laravel/framework": "^6.2",
"laravel/horizon": "^3.4",
"laravel/passport": "^8.0",
"laravel/socialite": "^4.3",
"laravel/tinker": "^1.0",
"predis/predis": "^1.1",
"rennokki/laravel-eloquent-query-cache": "^1.3",
"socialiteproviders/microsoft-azure": "^3.0",
"spatie/enum": "^2.3",
"spatie/laravel-responsecache": "^6.5"
},
"require-dev": {
"barryvdh/laravel-ide-helper": "^2.6",
"codedungeon/phpunit-result-printer": "^0.26.2",
"eaglewu/swoole-ide-helper": "dev-master",
"facade/ignition": "^1.4",
"fzaninotto/faker": "^1.4",
"laravel/homestead": "^9.4",
"laravel/telescope": "^3.5",
"laravel/ui": "^1.1",
"mockery/mockery": "^1.0",
"nunomaduro/collision": "^3.0",
"nunomaduro/larastan": "^0.5.8",
"nunomaduro/phpinsights": "^1.9",
"phpmd/phpmd": "^2.8",
"phpstan/phpstan": "^0.12",
"phpunit/phpunit": "^8.0",
"symplify/easy-coding-standard": "^7.0",
"yurunsoft/guzzle-swoole": "^2.1"
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true
},
"extra": {
"laravel": {
"dont-discover": [
"laravel/socialite"
]
}
},
"autoload": {
"psr-4": {
"App\\": "app/"
},
"classmap": [
"database/seeds",
"database/factories"
]
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"minimum-stability": "dev",
"prefer-stable": true,
"scripts": {
"phpstan:test": "phpstan analyze --ansi --memory-limit=1G",
"phpunit:test": "phpunit --colors=always",
"ecs:test": "ecs check app tests",
"ecs:fix": "ecs check --fix app tests",
"php-cs-fixer:fix": "php-cs-fixer fix --config=.php_cs -v --using-cache=no ./app ./tests ./routes ./database",
"insights:test": "php artisan insights --no-interaction",
"test": [
"@insights:test",
"@phpunit:test"
],
"ide-helper:generate": [
"php artisan ide-helper:generate",
"php artisan ide-helper:meta",
"php artisan ide-helper:models --nowrite"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi"
],
"post-update-cmd": "@ide-helper:generate"
}
}
And as well .env file:
APP_PORT=80
MYSQL_PORT=3306
IGNITION_THEME=dark
APP_NAME=Laravel
APP_ENV=local
APP_KEY= # generate your own code
APP_DEBUG=true
APP_URL=http://localhost
#APP_RUNNING_IN_CONSOLE=false
MIX_SENTRY_DSN_PUBLIC="localhost"
LOG_CHANNEL=stack
# run MySQL container for that.
DB_CONNECTION=mysql
DB_HOST=mysql #docker MySQL container
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=secret
REDIS_CLIENT=phpredis
REDIS_HOST=redis #docker redis container
REDIS_PASSWORD=null
REDIS_PORT=6379
REDIS_CACHE_DB=2
REDIS_QUEUES_DB=3
REDIS_HORIZON_CONNECTION=default
REDIS_PREFIX=
BROADCAST_DRIVER=redis
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
SESSION_LIFETIME=120
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
TELESCOPE_ENABLED=true
PASSPORT_LOGIN_URL=${APP_URL}/oauth/token
PASSPORT_CLIENT_ID= #genrate your own
PASSPORT_CLIENT_SECRET= #genrate your own
PHP_IDE=phpstorm
LARAVEL_ECHO_SERVER_AUTH_HOST=http://localhost
LARAVEL_ECHO_SERVER_REDIS_HOST=${REDIS_HOST}
LARAVEL_ECHO_SERVER_REDIS_PORT=${REDIS_PORT}
MIX_SOCKETIO_APP_ID=
MIX_SOCKETIO_APP_KEY=
HORIZON_BASIC_AUTH_USERNAME=
HORIZON_BASIC_AUTH_PASSWORD=
LARAVELS_INOTIFY_RELOAD=true
LARAVELS_LISTEN_IP=0.0.0.0
LARAVELS_LISTEN_PORT=80
LARAVELS_TIMER=true
SWOOLE_SOCKET_TYPE=1
LARAVELS_REACTOR_NUM=2
LARAVELS_WORKER_NUM=4
LARAVELS_TASK_WORKER_NUM=4
UPDATE Seems like it stops listening after the 60s or something and you have to manually restart service in order for socket to continue listening.
UPDATE: Guess it was more of a swoole thing. Solved it by changing heartbeat check interval and hearbeat_idle_time: config/laravels.php
'swoole' => [
// ...
'heartbeat_idle_time' => 60,
'heartbeat_check_interval' => 10,
// ...
],
Guess to prevent also tried this in the loop:
while (!self::$quit) {
$frame = self::$client->recv();
if ($frame instanceof Frame) {
$task = new FrameToNotificationConversion($frame);
$task::deliver($task);
}
if (!self::$client->connected) {
self::$quit = true;
}
}
and it worked. This one creates rapid reconnect, can change networks whatever you want. Very persistent. :-) Even better than health checks. :-) Basically reruns process a new. Can even put your computer to sleep run back again and bam its running and listening to socket. :D
Super awesome.
@juslintek According to the Swoole documentation, after 4.x, the connected
property is no longer updated in real time and is no longer reliable. So I suggest to use errCode
as the connection status judgment.
while (!self::$quit) {
$frame = self::$client->recv();
if (!self::$client->errCode) {
Log::error('An error occurred', [$client->errCode, $client->errMsg]);
self::$quit = true;
}
if ($frame instanceof Frame) {
$task = new FrameToNotificationConversion($frame);
$task::deliver($task);
}
}
@hhxsv5 thank you very much, I will change code accordingly. :-)
@hhxsv5 When this process runs for a very long time, ./bin/laravels
stop doesn't kill it. It continues working in the background unless you add timeouts to the connection, then it waits for a connection to timeout and after that gets killed, but if the connection is continuous like WebSocket, then does not get interrupted and killed. So the only way to kill it is with:
kill -9 PID
I thought that onStop it will get killed. But it doesn't.
I see one solution is to use the client as a static property for the whole process. And onStop, close its connection.
What do you think? Do I need to send some keepalive or ping pong to check if the connection is running? I basically need to stop it instantly on-demand.
I'm running laravel-s and trying to implement sdebug, could you guys assist on this?
Its seems to be connecting to my vscode but it drops right after. is there any specific configuration I need to change?
@juber-ivre, Sdebug works only with phpunit. :-) Or first request in requests sequence or I assume it gets clogged somewhere and cannot trace requests. You can check my issues related to sdebug. Basically if you retry request from dev tools network it usually hits the breakpoint. But you do page reload it gets ignored. After it gets ignored you have to restart laravels process. Then again you can catch sdebug responses on breakpoints.
Run inside a docker container of ubuntu:latest
xdebug.ini
supervisor.config
+---------------------------+---------+ | Component | Version | +---------------------------+---------+ | PHP | 7.4.3 | | Swoole | 4.5.1 | | LaravelS | 3.7.3 | | Laravel Framework [local] | 6.18.16 | +---------------------------+---------+
If a Task or process gets the following error, the process is not restarted even if it should do that after 5s of abnormal exit.
reproducible
code blocks andsteps
No code necessary, just do configs like this:
and create processes with task, run inside task closure after error remove it and try var dumping something. It will not happen, service will be stuck.
I want to make swoole to always listen to WebSocket. Seems like after some time it becomes unresponsive and that's it. Websocket frames are sent, but swoole coroutine client is not picking up any frames anymore...