EventHttpConnection doesn't work in EventHttp's setDefaultCallback

Issue #63 wontfix
eechen created an issue

Environment:

Ubuntu 20.04, PHP 7.4.5, event 2.5.7

php --ri event

Event support => enabled
Sockets support => enabled
Debug support => disabled
Extra functionality support including HTTP, DNS, and RPC => enabled
OpenSSL support => enabled
Thread safety support => disabled
Extension version => 2.5.7
libevent2 headers version => 2.1.11-stable

<?php

$base = new EventBase();

// curl http://127.0.0.1:8888
$http = new EventHttp($base);
$http->bind('0.0.0.0', 8888);
$http->setDefaultCallback(function($req) {
    $buf = new EventBuffer();
    $req->addHeader('Content-Type', 'text/html; charset=utf-8', EventHttpRequest::OUTPUT_HEADER);
    $buf->add('<html>Hello World</html>');
    $req->sendReply(200, 'OK', $buf);

    call_user_func(function(){
        global $base;

        echo 'foo' . "\n";

        // EventHttpConnection doesn't work in EventHttp's setDefaultCallback
        $conn = new EventHttpConnection($base, null, '127.0.0.1', '8888');
        $conn->setTimeout(5);
        $req = new EventHttpRequest(function($req){
            if ($req) {
                $code = $req->getResponseCode();
                $buf = $req->getInputBuffer();
                $body = $buf->read($buf->length);
                echo "{$code}:{$body}\n";
            }
        });
        $req->addHeader('Host', '127.0.0.1', EventHttpRequest::OUTPUT_HEADER);
        $req->addHeader('Content-Length', '0', EventHttpRequest::OUTPUT_HEADER);
        $conn->makeRequest($req, EventHttpRequest::CMD_GET, '/');

        echo 'bar' . "\n";
    });

});

$base->loop();

Comments (11)

  1. Ruslan Osmanov repo owner

    I don't get the point of making recursive requests, sorry. But the recursive call is not the problem in itself. The problem is that the instance of EventHttpConnection is destroyed when it leaves the scope of the request handler callback. When the connection object is destroyed, there is no chance for the associated request of being sent.

    The problem could be solved by creating an instance of secondary connection in the scope of $base, for example. Then that connection could be used in the default callback.

  2. eechen reporter

    @Ruslan Osmanov

    I just want to make some different domain(address) HTTP request (like use curl_exec) and get the results, then return the results to the browser.
    If I create the EventHttpConnection in the EventBase's scope, I can not change the domain(address).
    Or I should create multi EventHttpConnection in the EventBase's scope?
    Thanks for your reply.

  3. Ruslan Osmanov repo owner

    @eechen, If the goal is to make a number of independent parallel HTTP requests, then it would be a good idea to create all EventHttpConnections before running the loop. Then you would launch them all and collect the results as soon as they are ready. In this case, the server (EventHttp) is not needed at all, especially if all of this stuff is performed in web (non-CLI) context. The server is intended strictly for the use in long running CLI processes. Actually, the whole pecl-event extension was designed for CLI. Although, I don’t entirely exclude the possibility of using it in the web context. But, again, I wouldn’t run any HTTP requests in a web controller, if possible. The reason is that I/O operations are so heavy that they should be scheduled for execution in background. But that’s only my vision.

    However, if one request depends on another, then there is no sense in doing requests asynchronously – just use the blocking curl_exec.

  4. eechen reporter

    @Ruslan Osmanov

    Node.js can make requests in a server's callback function easily, so I think Event should support it necessarily.

    var http = require("http");
    http.createServer(function (req, res) {
        http.get("http://127.0.0.1:8080/", resp => {
            let data = "";
            resp.on("data", function(chunk) {
                data += chunk;
            });
            resp.on("end", () => {
                res.writeHead(200, {"Content-Type": "application/json"});
                res.end(data);
            });
            resp.on("error", err => {
                console.log(err.message);
            });
        });
    }).listen(8181, "0.0.0.0");
    

  5. Ruslan Osmanov repo owner

    @eechen, Well, Node.js has its own rules on closures, scopes and the whole idea of asynchronous execution. So let’s keep it aside.

    I agree on the point that there is a difficulty in keeping track of the objects in asynchronous code, and the fact that the objects get destroyed when they go out of scope is sometimes undesirable. But this can be handled by storing references to these objects somewhere. For example,

    <?php
    class MyServer {
        private string $host;
        private int $port;
        private EventBase $base;
        private array $connections = [];
        private array $closed_connections = [];
    
        public function __construct($host, $port) {
            $this->host = $host;
            $this->port = $port;
            $this->base = new EventBase();
    
        }
    
        public function run() {
            $http = new EventHttp($this->base);
            $http->bind($this->host, $this->port);
            $http->setDefaultCallback(function (EventHttpRequest $req) {
                $host = '127.0.0.1';
                $port = 8181;
                $conn = new EventHttpConnection($this->base, null, $host, $port);
                $conn_id = spl_object_id($conn);
                $this->connections[$conn_id] = $conn;
                $conn->setTimeout(5);
                $conn->setCloseCallback(function ($conn) use ($host, $port) {
                    echo "Connection to $host:$port closed\n";
                    $this->closed_connections[spl_object_id($conn)] = time();
                });
                $req2 = new EventHttpRequest(function ($req) {
                    echo "EventHttpRequest callback\n";
                    if ($req) {
                        $code = $req->getResponseCode();
                        $buf = $req->getInputBuffer();
                        $body = $buf->read($buf->length);
                        echo "{$code}:{$body}\n";
                    }
                });
                $req2->addHeader('Host', $host, EventHttpRequest::OUTPUT_HEADER);
                if ($conn->makeRequest($req2, EventHttpRequest::CMD_GET, '/test.txt')) {
                    $req->sendReply(200, 'OK');
                } else {
                    $req->sendReply(500, 'Internal Server Error');
                }
            });
    
            $timer = new Event($this->base, -1, Event::TIMEOUT, function($fd, $what, $timer) {
                $now = time();
                foreach ($this->closed_connections as $conn_id => $time_closed) {
                    if ($now - $time_closed > 1) {
                        echo "Cleaning up conneciton $conn_id\n";
                        unset($this->connections[$conn_id]);
                        unset($this->closed_connections[$conn_id]);
                    }
                }
                // This might depend on the logic of your app and maybe the length of $this->connections
                $timer->addTimer(1.1); 
            });
            $timer->data = $timer;
            $timer->addTimer(1.1);
    
            $this->base->loop();
        }
    }
    $server = new MyServer('0.0.0.0', 8888);
    $server->run();
    

  6. eechen reporter

    @Ruslan Osmanov

    Your example does not show how to return the EventHttpRequest's result to the EventHttp's reply, the browser can not get the result.
    In microservices, PHP needs to make some HTTP requests to difference API to get data non-blocking, then return the result to the browser/client after data process.
    If I use curl, it will block the PHP process until curl finishes request.
    Maybe Event can do it better, support it user-friendly, out of the box.
    Thanks for your reply.

  7. Ruslan Osmanov repo owner

    @eechen,

    Your example does not show how to return the EventHttpRequest's result to the EventHttp's reply, the browser can not get the result.

    Yes, I thought you figured out this part, so I skipped it for clarity. But here is how it can be done:

    if ($conn->makeRequest($req2, EventHttpRequest::CMD_GET, '/test.txt')) {
        $buf = new EventBuffer();
        $buf->add('<html>Hello World</html>');
        $req->addHeader('Content-Type', 'text/html; charset=utf-8', EventHttpRequest::OUTPUT_HEADER);
        $req->sendReply(200, 'OK', $buf);
    } else {
        $req->sendReply(500, 'Internal Server Error');
    }
    

    I’m not sure about the meaning of these words though: “the browser can not get the result”. Does the browser (or curl) receive an HTTP status at all? Is it 200 or something else? If it doesn’t, then the server probably fails for some reason.

    If I use curl, it will block the PHP process until curl finishes request.

    I guess you mean the PHP extension. Well, AFAIK, curl’s API assumes mainly blocking I/O. But there are means to make things asynchronous in PHP. Here are some of the ways to do this:

    • use sockets in non-blocking mode (a socket can be set to non-blocking, i.e. async, mode using socket_set_nonblock function)
    • use PHP streams in non-blocking mode (a stream can be made non-blocking with stream_set_blocking function)
    • you can even make curl (extension) to do its work asynchronously (see this old post for example)

    All the above solutions can be used in conjunction with pecl-event, pecl-ev or other extensions/frameworks providing async I/O API.

    Maybe Event can do it better, support it user-friendly, out of the box.

    Of course Event could do it better. It definitely could be more user friendly. But the fact is that pecl-event is quite a thin OO wrapper for libevent. Probably this is the reason why frameworks such as React PHP and Amp exist.

  8. eechen reporter

    @Ruslan Osmanov

    Sorry for my poor English.

    I mean return the EventHttpConnection’s result to the EventHttp’s reply, like the node.js example below:

    res.end(data); // data from http.get(url)
    

  9. Ruslan Osmanov repo owner

    @eechen,

    something like this:

                $req2 = new EventHttpRequest(function (EventHttpRequest $req2, EventHttpRequest $req) {
                    echo "EventHttpRequest callback\n";
                    if ($req2) {
                        $code = $req2->getResponseCode();
                        if ($code === 200) {
                            $buf = $req2->getInputBuffer();
                            $req->sendReply(200, 'OK', $buf);
                        } else {
                            $req->sendReply(500, 'Internal Server Error');
                        }
                    }
                }, $req);
                $req2->addHeader('Host', $host, EventHttpRequest::OUTPUT_HEADER);
                if (!$conn->makeRequest($req2, EventHttpRequest::CMD_GET, '/test.txt')) {
                    $req->sendReply(500, 'Internal Server Error');
                }
    

  10. Log in to comment