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

Asynchronous invocation #25

Open
thekid opened this issue Jun 3, 2022 · 2 comments
Open

Asynchronous invocation #25

thekid opened this issue Jun 3, 2022 · 2 comments
Labels
enhancement New feature or request help wanted Extra attention is needed

Comments

@thekid
Copy link
Member

thekid commented Jun 3, 2022

If used inside a single-threaded xp-forge/web web application, waiting will block the entire server. We should also have an asynchronous method to execute requests and wait for them.

Blocks server until API call returns

Current functionality

function($req, $res) use($endpoint) {
  $user= $endpoint->resource('/users/me')->get()->value();
  
  $res->answer(200);
  $res->write('Hello '.$user['displayName'], 'text/plain');
}

Asynchronous handler function

Idea: Defer reading from request until any of RestResponse's methods are called, add await():

function($req, $res) use($endpoint) {
  $user= (yield $endpoint->resource('/users/me')->get()->await())->value();
  
  $res->answer(200);
  $res->write('Hello '.$user['displayName'], 'text/plain');
}
@thekid
Copy link
Member Author

thekid commented Aug 23, 2022

This is how it would work with xp-framework/networking#22 extended by the following patch:

diff --git a/src/main/php/peer/server/AsyncServer.class.php b/src/main/php/peer/server/AsyncServer.class.php
index 13a55f5..af25f68 100755
--- a/src/main/php/peer/server/AsyncServer.class.php
+++ b/src/main/php/peer/server/AsyncServer.class.php
@@ -3,7 +3,7 @@
 use Throwable;
 use lang\IllegalStateException;
 use peer\server\protocol\SocketAcceptHandler;
-use peer\{ServerSocket, SocketException, SocketTimeoutException};
+use peer\{Socket, ServerSocket, SocketException, SocketTimeoutException};
 
 /**
  * Asynchronous TCP/IP Server
@@ -140,7 +140,26 @@ class AsyncServer extends Server {
       }
     });
     return $i;
-  } 
+  }
+
+  /**
+   * Returns a slot to watch for a given awaitable
+   *
+   * @param  var $awaitable
+   * @param  int $slot the slot to signal
+   * @return int
+   */
+  private function watch($awaitable, $slot) {
+    if ($awaitable instanceof Socket) {
+      $new= $this->select ? array_key_last($this->select) + 1 : 1;
+      $this->select[$new]= $awaitable;
+      $this->continuation[$new]= new Continuation(function() use($slot) { yield 'signal' => $slot; });
+      return $new;
+    }
+
+    // No awaitable given, watch the current slot
+    return $slot;
+  }
 
   /**
    * Runs service until shutdown() is called.
@@ -193,11 +212,15 @@ class AsyncServer extends Server {
           continue;
         }
 
-        // `yield 'accept' => $socket`: Check for being able to read from socket
-        // `yield 'read' => $_`: Continue as soon as the socket becomes readable
-        // `yield 'write' => $_`: Continue as soon as the socket becomes writeable
-        // `yield 'delay' => $millis`: Wait a specified number of milliseconds
-        // `yield`: Continue at the next possible execution slot (`delay => 0`)
+        // Internal use:
+        // * `yield 'accept' => $socket`: Check for being able to read from socket
+        // * `yield 'signal' => $n`: Finish signalling task, continue slot #n immediately
+        //
+        // Public use:
+        // * `yield 'read' => $awaitable`: Continue as soon as the awaitable becomes readable
+        // * `yield 'write' => $awaitable`: Continue as soon as the awaitable becomes writeable
+        // * `yield 'delay' => $millis`: Wait a specified number of milliseconds
+        // * `yield`: Continue at the next possible execution slot (`delay => 0`)
         switch ($execute->key()) {
           case 'accept':
             $socket= $execute->current();
@@ -206,13 +229,21 @@ class AsyncServer extends Server {
             $wait[]= $socket->getTimeout();
             break;
 
+          case 'signal':
+            unset($this->tasks[$i], $this->select[$i], $this->continuation[$i], $write[$i]);
+            $waitable[$execute->current()]= true;
+            $wait[]= 0;
+            break;
+
           case 'write':
+            $i= $this->watch($execute->current(), $i);
             $write[$i]= true;
             $writeable[$i]= $this->select[$i];
             $wait[]= $this->select[$i]->getTimeout();
             break;
 
           case 'read':
+            $i= $this->watch($execute->current(), $i);
             unset($write[$i]);
             $readable[$i]= $this->select[$i];
             $wait[]= $this->select[$i]->getTimeout();

Now, code could also pass sockets to select on via yield:

$s= new Socket('thekid.de', 80);
$s->connect();

yield 'write' => $s;
$s->write("GET / HTTP/1.1\r\nHost: thekid.de\r\nConnection: close\r\n\r\n");

do {
  yield 'read' => $s;
  $buffer= $s->read();
  yield 'write' => $response;
  $response->write($buffer);
} while (!$s->eof());

$s->close();

This could be extracted into a library as follows:

class HttpConnection {
  private $target;

  public function __construct($target) {
    $this->target= $target;
  }

  public function get($uri= '/') {
    $s= new Socket($this->target, 80);
    $s->connect();

    yield 'write' => $s;
    $s->write("GET {$uri} HTTP/1.1\r\nHost: {$this->target}\r\nConnection: close\r\n\r\n");

    return $s;
  }
}

// Usage:
$c= new HttpConnection('thekid.de');
$s= yield from $c->get('/');

do {
  // Same as above
} while (!$s->eof());

$s->close();

However, yield from get() would not be usable in a non-asynchronous context, like inside a script, yielding: Compile error (The "yield" expression can only be used inside a function) (same as JavaScript's top-level await topic). This could be addressed by the following inside the script executor (and a similar version inside the class.php XP runner entrypoint).

$r= (function() use($argc, $argv) {
  // Put all top-level statements here
})();

if ($r instanceof Generator) {
  while ($r->valid()) $r->next();
  return $r->getReturn();
} else {
  return $r;
}

The last problem is forgetting the yield from, which might happen frequently in the beginning (see here):

$c= new HttpConnection('thekid.de');
$s= $c->get('/');

// Call to undefined method Generator::read()
$s->read();

There are a couple of options here:

  • Provide get() (the synchronous version, blocks and returns directly) alongside with getAsync() (asynchronous version, requires usage of yield from) but has ugly naming
  • Refactor get() to return an intermediate object with has sync() and async() methods, which then actually trigger the HTTP request but this breaks all existing code
  • Provide an HttpConnection class inside a different namespace along with the synchronous version, migrating means a) changing the import statements and b) adding yield from but this means a lot of potentially duplicated code
  • Explore use of fibers but that would limit our codebase to PHP 8.1+

@thekid
Copy link
Member Author

thekid commented Aug 23, 2022

XP compiler could throw errors for async method invocations without yield from, but that would require understanding the type flow quite a bit more than it currently does.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

1 participant