swoole-src icon indicating copy to clipboard operation
swoole-src copied to clipboard

Coroutine suspending & resuming (like "Promises") across different HTTP requests?

Open kingIZZZY2 opened this issue 2 years ago • 10 comments

Question

How can I make one incoming Swoole\Http\Server::on('Request') to suspend/wait/yield indefinitely, and then another Swoole\Http\Server::on('Request') or even Swoole\WebSocket\Server::on('Message') can resume/unblock/continue that waiting HTTP request?

Kind of like a "Promise"

  • dispatch and begin waiting on a promise in Http\Server Request A, so this request has not yet responded and is still "loading"
  • meanwhile a parallel Http\Server Request B or WebSocket\Server Message B "resolves" the promise
  • now the pending/loading Request A completes and responds and finishes

1. What did you do? If possible, provide a simple script for reproducing the error.

Tried using Swoole\Coroutine::suspend(); and Swoole\Coroutine::resume(); within an HTTP server across different HTTP requests

<?php 
$server = new Swoole\WebSocket\Server('0.0.0.0',getenv('PORT'));

$server->on('Request', function(Swoole\Http\Request $request, Swoole\Http\Response $response)
{
    $response->header('Content-Type', 'text/plain');
    $response->header('Cache-Control', 'no-cache');

    $cid = Swoole\Coroutine::getCid();
    switch ($request->get['whatdo'] ?? '') {

        case 'suspend':
            error_log("suspending cid:$cid");
            Swoole\Coroutine::suspend();
            error_log("resumed after suspending $cid");
            break;

        case 'resume':
            error_log("resuming othercid:{$request->get['cid']} cid:$cid");
            if ($request->get['cid'] && Swoole\Coroutine::exists((int)$request->get['cid'])) {
                Swoole\Coroutine::resume((int)$request->get['cid']);
                error_log("resumed othercid:{$request->get['cid']} cid:$cid");
            }
            break;
    }

    $response->end(print_r(compact('cid','request'), true));
});

$server->start();
> browse in [Tab A] http://example.com/?whatdo=suspend
suspending cid:2
[Tab A] is still "🔃loading loading..." as expected

> meanwhile browse in [Tab B] http://example.com/?whatdo=resume&cid=2
resuming othercid:2 cid:4
[Tab B] Request Complete - did NOT call ::resume(2) - cid 2 does not ::exists(2)

... some time later ...
[Tab A] Request Timeout - "/?whatdo=suspend" - 30000ms - status=503

2. What did you expect to see?

Isn't swoole able to suspend/yield one request coroutine indefinitely, and resume it from another HTTP request? Similar to Promises, begin the promise from Request A, suspend the request without responding yet, then later Request B "resolves" the promise and somehow affects Request A to resume from suspension/yielding.

3. What did you see instead?

Coroutine IDs seem to be completely unrelated between Request A and Request B, sometimes even Request A and Request B both have the same identical cid! It seems they do not share a coroutine context?

4. What version of Swoole are you using (show your php --ri swoole)?

swoole

Swoole => enabled
Author => Swoole Team <[email protected]>
Version => 4.8.6
Built => Jan 14 2022 17:04:36
coroutine => enabled with boost asm context
epoll => enabled
eventfd => enabled
signalfd => enabled
cpu_affinity => enabled
spinlock => enabled
rwlock => enabled
sockets => enabled
openssl => OpenSSL 1.1.1f  31 Mar 2020
dtls => enabled
http2 => enabled
json => enabled
curl-native => enabled
pcre => enabled
zlib => 1.2.11
mutex_timedlock => enabled
pthread_barrier => enabled
futex => enabled
mysqlnd => enabled
async_redis => enabled

Directive => Local Value => Master Value
swoole.enable_coroutine => On => On
swoole.enable_library => On => On
swoole.enable_preemptive_scheduler => Off => Off
swoole.display_errors => On => On
swoole.use_shortname => On => On
swoole.unixsock_buffer_size => 8388608 => 8388608

5. What is your machine environment used (show your uname -a & php -v & gcc -v) ?

Linux 1dcfc2b2-3bfd-4682-93c7-06a62241e54a 4.4.0-1103-aws #108-Ubuntu SMP Mon Mar 28 22:40:07 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux

PHP 8.1.6 (cli) (built: May 18 2022 14:31:08) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.6, Copyright (c) Zend Technologies
    with Zend OPcache v8.1.6, Copyright (c), by Zend Technologies
Using built-in specs.

COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/9/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none:hsa
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 9.4.0-1ubuntu1~20.04.1' --with-bugurl=file:///usr/share/doc/gcc-9/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++,gm2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-9 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-9-Av3uEd/gcc-9-9.4.0/debian/tmp-nvptx/usr,hsa --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.1)

kingIZZZY2 avatar May 19 '22 09:05 kingIZZZY2

$server = new Swoole\WebSocket\Server('0.0.0.0',getenv('PORT'),SWOOLE_BASE);

or

$server = new Swoole\WebSocket\Server('0.0.0.0',getenv('PORT'));
$server->set(['worker_num' => 1]);

or

$server = new Swoole\WebSocket\Server('0.0.0.0',getenv('PORT'));
$server->set(['dispatch_mode' => 4]);

It will only take effect in a single process because of process isolation.

NathanFreeman avatar May 19 '22 13:05 NathanFreeman

I believe I was already in an environment of 1 single worker/process, and it was still not working..

Is there some way to get worker ID or get process ID or whatever so i can prove by log / print to output whether things are happening in 1 single worker/process or not?

EDIT: OK i found how to do this. Indeed my environment would usually run 8 workers by default. If I limited to 1 then suspending & resuming works as expected! Please see the big follow-up question below

kingIZZZY2 avatar May 19 '22 16:05 kingIZZZY2

But really even across multi-worker / multi-process, how to achieve this same "promise/future" effect between any 2 Requests or even WebSocket messages even across other fibers/threads/workers/processes?

If co::suspend() / resume() / WaitGroup::wait() / done() won't work because of process isolation, is there some other way to make Request A (no matter which worker or process) to yield to other coroutines & not block other requests or other fibers/workers/processes from functioning non-blocking, and only whenever any other code decides to "resolve" some condition EVEN from a separate isolated worker or process - then it resumes & unblocks Request A to continue and respond & end the request?

kingIZZZY2 avatar May 19 '22 17:05 kingIZZZY2

Coroutine IDs seem to be completely unrelated between Request A and Request B, sometimes even Request A and Request B both have the same identical cid! It seems they do not share a coroutine context?

For multi-process servers, you may need to use redis and its subscription feature.

twose avatar May 20 '22 01:05 twose

Doesn't Swoole itself have any technologies which can help for this need, without even needing 3rd party stuff like redis? Anything with IPC or something like that maybe? Something with swoole events and file descriptors and epoll_wait or something? Any other cross-worker "Promise/await/resolve" system anyone implemented based on swoole?...

kingIZZZY2 avatar May 20 '22 01:05 kingIZZZY2

Maybe you can use Swoole\Table to solve it. Swoole\table can be shared by multi-process.

NathanFreeman avatar May 20 '22 14:05 NathanFreeman

But Swoole table cannot persist objects & instances of complex classes, only primitives int/string/float (unless i'm wrong about that?)

kingIZZZY2 avatar May 20 '22 15:05 kingIZZZY2

Ever since you stopped thinking about the single-process mode, you've started thinking about its scalability, so we might as well think a little further. PHP + Swoole can do almost everything you can think of, including a database with a subscription feature. But I think it might be a bit troublesome for you... So, I still recommend you choose redis.

twose avatar May 21 '22 07:05 twose

@twose Can you please explain just a little bit more how do you suggest I use redis

If I subscribe to a redis channel then how can I make my "Coroutine A" wait until a message comes from that channel? Do I loop in a while(){ ... } loop and check if a redis pub/sub message arrived? How is it better than checking for a swoole table value?

Is there maybe a more performant way how to achieve a similar effect using the swoole Event API maybe, by using some temporary file descriptor as a suspend/resume mechanism waiting for file descriptor events?

kingIZZZY2 avatar May 24 '22 06:05 kingIZZZY2

@kingIZZZY2 This is unrelated to Swoole, but you should look at redis blocking commands. The idea is that each thread can have a coroutine subscribed to a redis blocking command, say BLPOP, and when it gets some data, start a new coroutine to process it and block on a BLPOP command again.

That way, you have threads across different servers waiting for data in some redis list, and once an item shows up there, some thread will pop that item and process it.

ValiDrv avatar Jul 09 '22 18:07 ValiDrv