m6w6 / ext-pq

PostgreSQL client library (libpq) binding
BSD 2-Clause "Simplified" License
39 stars 7 forks source link

New async API for PHP 7 #12

Closed ParkFramework closed 8 years ago

ParkFramework commented 8 years ago

The use of callbacks, there are disadvantages

  1. You can not catch exceptions
  2. It is difficult to controls (is progress, is done, cancel) the current state executing query
  3. Not API to return value

Callback Hell :) http://callbackhell.com/

In other languages, it is easy to solve, async/await - $result = await $db->exec($sql), when there will be this in PHP is not known.

Here's what I suggest, is the draft, but it works:

class PrototypeAsync extends pq\Connection
{
    public function execAsync($query, $callable = null)
    {
        if(is_string($query)) // for backward compatibility
        {
            return parent::execAsync($query);
        }

        $coroutine = $query();

        do
        {
            $sql = $coroutine->current();

            try
            {
                $result = parent::exec($sql); // Must be in reality - async work in PQ

                $coroutine->send($result);
            }
            catch(Throwable $e)
            {
                $coroutine->throw($e);
            }
        }
        while($coroutine->valid());

        return $coroutine;
    }
}

//------------ User Land ----------------

$db = new PrototypeAsync($dsn);

$coroutine = $db->execAsync(function()
{
    return (yield 'SELECT 1')->fetchRow();
});

if($coroutine->valid()) // is true - executing in progress
{
    // await in EventLoop
    //....
    // or cancel
    // $coroutine->throw(new Exception);
}
else // is executed, get result
{
    $coroutine->getReturn(); // Array([0] => 1)
}

// Full example - three queries and catching exceptions
$coroutine = $db->execAsync(function()
{
    (yield 'SELECT 1')->fetchCol($one);

    echo "Result: $one\r\n";

    try
    {
        (yield 'SELECT throw')->fetchCol($value);
    }
    catch(Throwable $e)
    {
        echo 'Catch error: '.$e->getCode()."\r\n";
    }

    (yield 'SELECT 2')->fetchCol($two);

    echo "Result: $two\r\n";

    return $one + $two;
});

$ret = $coroutine->getReturn();

echo "Return: $ret\r\n";

Print

Result: 1
Catch error: 8
Result: 2
Return: 3

What do you think about the use coroutine?

This does not solve the problem of independent queries queue, this for only related queries or single query.

Thank.

ParkFramework commented 8 years ago

We use a simpler API - $result = yield $db->execAsync($sql), but it requires an asynchronous PHP framework.

Here is an example https://github.com/icicleio/postgres/blob/master/examples/test.php

MySQL https://github.com/amphp/mysql

m6w6 commented 8 years ago

Looks interesting!

ParkFramework commented 8 years ago

Looks interesting!

I think useful to do with API integration to EventLoop (Ev, Event, etc..), I'll write later working examples to see how best to make of integration to EventLoop.

Sample php userland:

// runtime before async...

$result = yield $db->execAsync($sql); // pause runtime

// runtime after async...

This solves the problem of queues because the code is executed sequentially and async.

You did not think to move all *async methods to new class pq\ConnectionAsync, so it would be nice?

$db = new pq\ConnectionAsync($dsn) // async connection
$db->exec($sql); // execAsync
$db->prepare($sql); // prepareAsync
$db->listen($sql); // listenAsync
//... 
m6w6 commented 8 years ago

I think to make it truly concurrent, I'd have to yield from within ext-pq, which does not work.

ParkFramework commented 8 years ago

I'm busy now, this all implementing in userland, i later show you :)

m6w6 commented 8 years ago
            $result = parent::exec($sql); // Must be in reality - async work in PQ

AFAICT I'd have to yield in there, else it would not be really asynchronous/concurrent.

ParkFramework commented 8 years ago

AFAICT I'd have to yield in there, else it would not be really asynchronous/concurrent.

$result = yield $db->execAsync($sql); // userland framework - save this generator in global var $coroutine

//...

$loop = EvLoop::defaultLoop();

$io = $loop->io($db->socket, Ev::READ, function($w) use($db)
{
    global $coroutine; // this last generator

    $db->poll();
    $r = $db->getResult();

    if($r === null)
    {
        return;
    }

    if($r->status === pq\Result::TUPLES_OK)
    {
        $coroutine->send($r);
    }
    else
    {
        $coroutine->throw(new Exception($r->errorMessage));
    }
});

$loop->run();
ParkFramework commented 8 years ago

I done working example, pq asynchronous calls, via coroutine, like async/await https://github.com/ParkFramework/pq-async/blob/master/src/

Use eventloop - EvIo https://github.com/ParkFramework/pq-async/blob/master/src/DB.php#L23

DaveRandom commented 8 years ago

I'm picking this up again in amphp and I will have a public working implementation in the next week or two.

TL;DR having co-routine support directly in the extension doesn't make a lot of sense until it exists in the language as a whole (using generators is a hack, a working hack but a hack nontheless). Callback-driven APIs can be adapted to work with co-routines via the promise pattern.

I agree that this would be a nice thing to have directly supported by the extension API, but now is not the right time.

IMHO.

ParkFramework commented 8 years ago

I agree that this would be a nice thing to have directly supported by the extension API, but now is not the right time.

Yes, i agree.

ParkFramework commented 8 years ago

I think you can close, up the best times and ideas :)