Description
I've recently been helping @quinnj to debug his new HTTP.jl package, in particular a new FIFOBuffer
type that is intended to behave like the IO buffers in Base (#87, #86, #76, #75, #74). The process of trying to be consistent with Base has highlighted a number of IO behaviour inconsistencies. I've hacked up a script that runs through a sequence of IO operations for various types and produces a MD table of the results (see below).
The two main issues are with the blocking behaviour of read()
and eof()
.
The spec for eof()
says: "If the stream is not yet exhausted, this function will block to wait for more data if necessary, and then return false."
-
eof()
works per spec forBufferStream
andTCPSocket
. - For
Filesystem.File
,IOStream
, andPipeBuffer
,eof()
does not block to wait for more data. Instead it returnstrue
without blocking if there is not currently data available to be read. - For
IOBuffer
it seems thateof()
just always returns true.
The spec for read(::IO, ::Int)
says: "[By default] this function will block repeatedly trying to read all requested bytes, until an error or end-of-file occurs."
- For
BufferStream
andTCPSocket
,read()
behaves as specified. - However, for the other types
read()
seems to just return however many bytes are available at the time and does not block. - For
IOBuffer
,read()
always returns an empty array.
Other issues:
- For
BufferStream
andIOStream
,isreadable()
returns true afterclose()
is called. - For
BufferStream
,iswriteable()
returns true afterclose()
. - For
BufferStream
andIOStream
,read()
afterclose()
returns empty data, whereas the other types throw an error. - For
BufferStream
andTCPSocket
,read(io, String)
blocks until the stream is closed (this seems consistent with the blocking behaviour ofread(io, nb)
, however the other types return immediately with a string containing however many bytes are available at the time. -
mark
/reset
don't work forBufferStream
mark/reset broken for BufferStream #24465 - There is no API for sending TCP FIN, e.g.
shutdown(fd, SHUT_WR)
oruv_shutdown()
. If a TCP server waits for a request to be sent before responding, a Julia client would have to callclose()
to signal end of request, but would then be unable to read the response. - Perhaps the missing
shutdown
is related to the inconsistencies withisreadable()
andiswriteable()
. It seem like maybe there should be acloseread()
andclosewrite()
that respectively causeisreadable()
andiswriteable()
to return false. - Calling
close
onBufferStream
causeseof()
= true andisopen()
= false, butiswriteable()
andisreadable()
are both still true, and in fact reads and writes continue to work with the closed stream. It seems thatBufferStream
would benefit from a seperateclosewrite()
for signallingeof()
to the reader.
type | IOBuffer | PipeBuffer | BufferStream | File (IOStream) | Filesystem |
---|---|---|---|---|---|
init | io = IOBuffer() | io = PipeBuffer() | io = BufferStream() | echo Hello > file | echo Hello > file |
write(io, "Hello") | write(io, "Hello") | write(io, "Hello") | open("file") | Filesystem. open("file") | |
isreadable() | true | true | true | true | MethodError |
isopen() | true | true | true | true | true |
eof() | true❗️ | false | false | false | false |
position | 5 | 0 | MethodError | 0 | 0 |
read(io, 5) | ""❗️ | "Hello" | "Hello" | "Hello" | "Hello" |
eof() | true | true❗️ | blocked ✅ | true | true |
"Again" | write(io, "Again") | write(io, "Again") | write(io, "Again") | echo Again >> file | echo Again >> file |
position(io) | 10 | 0 | MethodError | 5 | 5 |
eof(io) | true❗️ | false | false | false | false |
read(io, 5) | ""❗️ | "Again" | "Again" | "Again" | "Again" |
eof(io) | true | true❗️ | blocked ✅ | true | true |
"Again" | write(io, "Again") | write(io, "Again") | write(io, "Again") | echo Again >> file | echo Again >> file |
eof(io) | true❗️ | false | false | false | false |
read(io, String) | ""❗️ | "Again" | blocked ✅ | "Again" | "Again" |
iswritable(io) | true | true | true | false | MethodError |
isreadable(io) | true | true | true | true | MethodError |
close(io); isopen(io) | false | false | false | false | false |
iswritable(io) | false | false | true❗️ | false | MethodError |
isreadable(io) | false | false | true❗️ | true❗️ | MethodError |
eof(io) | true | true | true | true | Base.UVError |
read(io, 5) | Argument Error | Argument Error | ""❗️ | ""❗️ | Base.UVError |
Note that TCPSocket
behaves almost the same as BufferStream
but without the isopen()
and isreadable()
issues:
type | BufferStream | TCPSocket |
---|---|---|
init | io = BufferStream() | srv = accept(); io = connect() |
write(io, "Hello") | write(srv, "Hello") | |
isreadable() | true | true |
isopen() | true | true |
eof() | false | false |
position | MethodError | MethodError |
read(io, 5) | "Hello" | "Hello" |
eof(io) | blocked | blocked |
close(io); isopen(io) | true ❗️ | false |
eof(io) | true | true |
isreadable(io) | true❗️ | false |
read(io, 5) | "" | "" |