amphp / byte-stream

A non-blocking stream abstraction for PHP based on Amp.

Home Page:https://amphp.org/byte-stream

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

feof may hang

brstgt opened this issue · comments

I encountered hanging feof resulting in StreamException("The stream was closed by the peer").
This happens when doing requests in Kubernetes to Facebook.

Omitting the feof check "fixes" it.

A workaround (and better code) would be to not check on @feof before the write but setting an error handler for the fwrite and cathing warnings and converting them to stream exceptions.

Example:

                    $eh = set_error_handler([$this, 'onSocketWriteError']);
                    
                    if ($chunkSize) {
                        $written = \fwrite($stream, $data, $chunkSize);
                    } else {
                        $written = \fwrite($stream, $data);
                    }

                    set_error_handler($eh);

fwrite emits a warning when attempting a write and the buffer is full. I'm not aware of any way to avoid that problem.

Do we need the feof check at all? It seems unreliable at best, which is why we started counting empty writes in the first place.

It seems like this works pretty well:

            set_error_handler(function ($errno, $msg, $file, $line) {
                // Skip EAGAIN errors
                if (strpos($msg, "errno=11") !== false) {
                    return;
                }
                throw new StreamException("Error writing to stream: $msg ($errno)", $errno);
            });

            try {
                // Use conditional, because PHP doesn't like getting null passed
                if ($this->chunkSize) {
                    $written = \fwrite($this->resource, $data, $this->chunkSize);
                } else {
                    $written = \fwrite($this->resource, $data);
                }
            }
            finally {
                restore_error_handler();
            }

What happens if you only delete the feof and don't bother with the error handler? Does it just keep waiting to write then?

There was no error, so it was fine but i guess the proper way should be to handle fwrite errors, shouldn't it?

I'm just trying to understand why feof hangs, but fwrite succeeds. The custom error handler is probably fine. I think EAGAIN writes are uncommon enough that the performance difference will be negligible.

On macOS EAGAIN seems to equal 35 rather than 11. PHP has some extension specific constants that expose this number (PCNTL_EAGAIN, SOCKET_EAGAIN, and MSG_EAGAIN). Will have to look if there's another way to find this value.

Using strpos("Resource temporarily unavailable") also works for me. Can I depend on that error message though?

Edit: Looks like no, that is also an OS-dependent message.

Removing the feof check from the initial write in ResourceOutputStream::send() causes the test testClosedRemoteSocketWithFork to fail, so I'm not sure we can remove that entirely.

Does it fail also with the error handler?

I get temporarily unavailable on Linux but i first thought errno=11 would be safer. It shouldnt be a big deal to have a platform dependent check, right?

Platform dependent checks are always ugly. Error number will be better than message, because these messages can be localized.

@brstgt The test still fails with the error handler.

Maybe we should be looking at why feof is hanging on a non-blocking stream.

I encountered hanging feof resulting in StreamException("The stream was closed by the peer").

If it results in a StreamException, it doesn't hang. Could you clarify what hang means?

The only thing I recall that might result in hanging are fragmented TLS records (packets), but we have never verified that.

The latest PHP releases are fixed now.