Skip to content

Commit d572554

Browse files
committed
Replace FIFOBuffer implementation with BufferStream/IOBuffer wrapper.
Add mark/reset support to Form <: IO Dont call eof() in body(::IO, ...) if content length is known (eof() can block) Work around JuliaLang/julia#24465 Use non-blocking readavailable() to read serverlog in test/server.jl Hack test/fifobuffer.jl to work with BufferStream/IOBuffer implementation. The BufferStream API does not expose the size of the buffer, so tests that passed a fixed size to FIFOBuffer(n) have been tweaked to pass with BufferStream's automatic buffer size managment behaviour. Add note re @test_broken and https://github.com/kennethreitz/httpbin/issues/340#issuecomment-330176449
1 parent ee7d868 commit d572554

File tree

13 files changed

+131
-349
lines changed

13 files changed

+131
-349
lines changed

src/HTTP.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,4 @@ try
5454
HTTP.parse(HTTP.Response, "HTTP/1.1 200 OK\r\n\r\n")
5555
HTTP.parse(HTTP.Request, "GET / HTTP/1.1\r\n\r\n")
5656
HTTP.get(HTTP.Client(nothing), "www.google.com")
57-
end
57+
end

src/client.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ function processresponse!(client, conn, response, host, method, maintask, stream
301301
return true, StatusError(status(response), response)
302302
elseif stream && headerscomplete
303303
@log "processing the rest of response asynchronously"
304-
response.body.task = @async processresponse!(client, conn, response, host, method, maintask, false, tm, canonicalizeheaders, false)
304+
@async processresponse!(client, conn, response, host, method, maintask, false, tm, canonicalizeheaders, false)
305305
return true, StatusError(status(response), response)
306306
end
307307
end
@@ -321,7 +321,7 @@ function request(client::Client, req::Request, opts::RequestOptions, stream::Boo
321321
p = port(u)
322322
conn = @retryif ClosedError 4 connectandsend(client, sch, host, ifelse(p == "", "80", p), req, opts, verbose)
323323

324-
response = Response(stream ? 2^24 : FIFOBuffers.DEFAULT_MAX, req)
324+
response = Response(req)
325325
reset!(client.parser)
326326
success, err = processresponse!(client, conn, response, host, HTTP.method(req), current_task(), stream, opts.readtimeout::Float64, opts.canonicalizeheaders::Bool, verbose)
327327
if !success
@@ -362,7 +362,7 @@ request(client::Client, req::Request;
362362
# build Request
363363
function request(client::Client, method, uri::URI;
364364
headers::Dict=Headers(),
365-
body=FIFOBuffers.EMPTYBODY,
365+
body=FIFOBuffer(),
366366
stream::Bool=false,
367367
verbose::Bool=false,
368368
args...)

src/fifobuffer.jl

Lines changed: 30 additions & 269 deletions
Original file line numberDiff line numberDiff line change
@@ -1,294 +1,55 @@
11
module FIFOBuffers
22

3-
import Base.==
4-
53
export FIFOBuffer
64

7-
"""
8-
FIFOBuffer([max::Integer])
9-
FIFOBuffer(string_or_bytes_vector)
10-
FIFOBuffer(io::IO)
11-
12-
A `FIFOBuffer` is a first-in, first-out, in-memory, async-friendly IO buffer type.
13-
14-
`FIFOBuffer([max])`: creates a "open" `FIFOBuffer` with a maximum size of `max`; this means that bytes can be written
15-
up until `max` number of bytes have been written (with none being read). At this point, the `FIFOBuffer` is full
16-
and will return 0 for all subsequent writes. If no `max` (`FIFOBuffer()`) argument is given, then a default size of `typemax(Int32)^2` is used;
17-
this essentially allows all writes every time. Note that providing a string or byte vector argument mirrors the behavior of `Base.IOBuffer`
18-
in that the `max` size of the `FIFOBuffer` is the length of the string/byte vector; it is also not writeable.
19-
20-
Reading is supported via `readavailable(f)` and `read(f, nb)`, which returns all or `nb` bytes, respectively, starting at the earliest bytes written.
21-
All read functions will return an empty byte vector, even if the buffer has been closed. Checking `eof` will correctly reflect when the buffer has
22-
been closed and no more bytes will be available for reading.
23-
24-
You may call `String(f::FIFOBuffer)` to view the current contents in the buffer without consuming them.
25-
26-
A `FIFOBuffer` is built to be used asynchronously to allow buffered reading and writing. In particular, a `FIFOBuffer`
27-
detects if it is being read from/written to the main task, or asynchronously, and will behave slightly differently depending on which.
28-
29-
Specifically, when reading from a `FIFOBuffer`, if accessed from the main task, it will not block if there are no bytes available to read, instead returning an empty `UInt8[]`.
30-
If being read from asynchronously, however, reading will block until additional bytes have been written. An example of this in action is:
31-
32-
```julia
33-
f = HTTP.FIFOBuffer(5) # create a FIFOBuffer that will hold at most 5 bytes, currently empty
34-
f2 = HTTP.FIFOBuffer(5) # a 2nd buffer that we'll write to asynchronously
35-
36-
# start an asynchronous writing task with the 2nd buffer
37-
tsk = @async begin
38-
while !eof(f)
39-
write(f2, readavailable(f))
40-
end
5+
struct FIFOBuffer{T <: Union{IOBuffer,BufferStream}} <: IO
6+
io::T
417
end
428

43-
# now write some bytes to the first buffer
44-
# writing triggers our async task to wake up and read the bytes we just wrote
45-
# leaving the first buffer empty again and blocking again until more bytes have been written
46-
write(f, [0x01, 0x02, 0x03, 0x04, 0x05])
47-
48-
# we can see that `f2` now holds the bytes we wrote to `f`
49-
String(readavailable(f2))
50-
51-
# our async task will continue until `f` is closed
52-
close(f)
53-
54-
istaskdone(tsk) # true
55-
```
56-
"""
57-
mutable struct FIFOBuffer <: IO
58-
len::Int64 # length of buffer in bytes
59-
max::Int64 # the max size buffer is allowed to grow to
60-
nb::Int64 # number of bytes available to read in buffer
61-
f::Int64 # buffer index that should be read next, unless nb == 0, then buffer is empty
62-
l::Int64 # buffer index that should be written to next, unless nb == len, then buffer is full
63-
buffer::Vector{UInt8}
64-
cond::Condition
65-
task::Task
66-
eof::Bool
67-
end
9+
FIFOBuffer() = FIFOBuffer{BufferStream}(BufferStream())
10+
FIFOBuffer(bytes::Vector{UInt8}) = FIFOBuffer{IOBuffer}(IOBuffer(bytes))
11+
FIFOBuffer(str::String) = FIFOBuffer{IOBuffer}(IOBuffer(str))
6812

69-
const DEFAULT_MAX = Int64(typemax(Int32))^Int64(2)
13+
FIFOBuffer(io::IOStream) = FIFOBuffer(read(io))
14+
FIFOBuffer(io::IO) = FIFOBuffer(readavailable(io))
7015

7116
FIFOBuffer(f::FIFOBuffer) = f
72-
FIFOBuffer(max) = FIFOBuffer(0, max, 0, 1, 1, UInt8[], Condition(), current_task(), false)
73-
FIFOBuffer() = FIFOBuffer(DEFAULT_MAX)
7417

75-
const EMPTYBODY = FIFOBuffer()
18+
Base.String(f::FIFOBuffer{IOBuffer}) = String(f.io.data[f.io.ptr:f.io.size])
19+
Base.String(f::FIFOBuffer{BufferStream}) = String(FIFOBuffer(f.io.buffer))
7620

77-
FIFOBuffer(str::String) = FIFOBuffer(Vector{UInt8}(str))
78-
function FIFOBuffer(bytes::Vector{UInt8})
79-
len = length(bytes)
80-
return FIFOBuffer(len, len, len, 1, 1, bytes, Condition(), current_task(), true)
21+
import Base.==
22+
function ==(a::FIFOBuffer, b::FIFOBuffer)
23+
(nb_available(a) == 0 && nb_available(b) == 0) || String(a) == String(b)
8124
end
82-
FIFOBuffer(io::IOStream) = FIFOBuffer(read(io))
83-
FIFOBuffer(io::IO) = FIFOBuffer(readavailable(io))
8425

85-
==(a::FIFOBuffer, b::FIFOBuffer) = String(a) == String(b)
86-
Base.length(f::FIFOBuffer) = f.nb
87-
Base.nb_available(f::FIFOBuffer) = f.nb
88-
Base.wait(f::FIFOBuffer) = wait(f.cond)
89-
Base.read(f::FIFOBuffer) = readavailable(f)
90-
Base.flush(f::FIFOBuffer) = nothing
91-
Base.position(f::FIFOBuffer) = f.f, f.l, f.nb
92-
function Base.seek(f::FIFOBuffer, pos::Tuple{Int64, Int64, Int64})
93-
f.f = pos[1]
94-
f.l = pos[2]
95-
f.nb = pos[3]
96-
return
97-
end
9826

99-
Base.eof(f::FIFOBuffer) = f.eof && f.nb == 0
100-
Base.isopen(f::FIFOBuffer) = !f.eof
101-
function Base.close(f::FIFOBuffer)
102-
f.eof = true
103-
notify(f.cond)
104-
return
105-
end
27+
Base.readavailable(f::FIFOBuffer) = readavailable(f.io)
10628

107-
# 0 | 1 | 2 | 3 | 4 | 5 |
108-
#---|---|---|---|---|---|
109-
# |f/l| _ | _ | _ | _ | empty, f == l, nb = 0, can't read, can write from l to l-1, don't need to change f, l = l, nb = len
110-
# | _ | _ |f/l| _ | _ | empty, f == l, nb = 0, can't read, can write from l:end, 1:l-1, don't need to change f, l = l, nb = len
111-
# | _ | f | x | l | _ | where f < l, can read f:l-1, then set f = l, can write l:end, 1:f-1, then set l = f, nb = len
112-
# | l | _ | _ | f | x | where l < f, can read f:end, 1:l-1, can write l:f-1, then set l = f
113-
# |f/l| x | x | x | x | full l == f, nb = len, can read f:l-1, can't write
114-
# | x | x |f/l| x | x | full l == f, nb = len, can read f:end, 1:l-1, can't write
115-
function Base.readavailable(f::FIFOBuffer)
116-
# no data to read
117-
if f.nb == 0
118-
if current_task() == f.task || f.eof
119-
return UInt8[]
120-
else # async + still open: block till there's data to read
121-
wait(f.cond)
122-
f.nb == 0 && return UInt8[]
123-
end
124-
end
125-
if f.f < f.l
126-
@inbounds bytes = f.buffer[f.f:f.l-1]
127-
else
128-
# we've wrapped around
129-
@inbounds bytes = f.buffer[f.f:end]
130-
@inbounds append!(bytes, view(f.buffer, 1:f.l-1))
131-
end
132-
f.f = f.l
133-
f.nb = 0
134-
notify(f.cond)
135-
return bytes
136-
end
29+
# See issue #24465: "mark/reset broken for BufferStream"
30+
# https://github.com/JuliaLang/julia/issues/24465
31+
# So, need to reach down into IOBuffer for readavailable():
32+
Base.readavailable(f::FIFOBuffer{BufferStream}) = readavailable(f.io.buffer)
13733

138-
# read at most `nb` bytes
139-
function Base.read(f::FIFOBuffer, nb::Int)
140-
# no data to read
141-
if f.nb == 0
142-
if current_task() == f.task || f.eof
143-
return UInt8[]
144-
else # async: block till there's data to read
145-
wait(f.cond)
146-
f.nb == 0 && return UInt8[]
147-
end
148-
end
149-
if f.f < f.l
150-
l = (f.l - f.f) <= nb ? (f.l - 1) : (f.f + nb - 1)
151-
@inbounds bytes = f.buffer[f.f:l]
152-
f.f = mod1(l + 1, f.max)
153-
else
154-
# we've wrapped around
155-
if nb <= (f.len - f.f + 1)
156-
# we can read all we need between f.f and f.len
157-
@inbounds bytes = f.buffer[f.f:(f.f + nb - 1)]
158-
f.f = mod1(f.f + nb, f.max)
159-
else
160-
@inbounds bytes = f.buffer[f.f:f.len]
161-
l = min(f.l - 1, nb - length(bytes))
162-
@inbounds append!(bytes, view(f.buffer, 1:l))
163-
f.f = mod1(l + 1, f.max)
164-
end
165-
end
166-
f.nb -= length(bytes)
167-
notify(f.cond)
168-
return bytes
169-
end
34+
Base.read(f::FIFOBuffer, a...) = read(f.io, a...)
35+
Base.read(f::FIFOBuffer, ::Type{UInt8}) = read(f.io, UInt8)
36+
Base.write(f::FIFOBuffer, bytes::Vector{UInt8}) = write(f.io, bytes)
17037

171-
function Base.read(f::FIFOBuffer, ::Type{Tuple{UInt8,Bool}})
172-
# no data to read
173-
if f.nb == 0
174-
if current_task() == f.task || f.eof
175-
return 0x00, false
176-
else # async: block till there's data to read
177-
f.eof && return 0x00, false
178-
wait(f.cond)
179-
f.nb == 0 && return 0x00, false
180-
end
181-
end
182-
# data to read
183-
@inbounds b = f.buffer[f.f]
184-
f.f = mod1(f.f + 1, f.max)
185-
f.nb -= 1
186-
notify(f.cond)
187-
return b, true
188-
end
38+
map(eval, :(Base.$f(f::FIFOBuffer) = $f(f.io))
39+
for f in [:nb_available, :flush, :mark, :reset, :eof, :isopen, :close])
18940

190-
function Base.read(f::FIFOBuffer, ::Type{UInt8})
191-
byte, valid = read(f, Tuple{UInt8,Bool})
192-
valid || throw(EOFError())
193-
return byte
194-
end
41+
Base.length(f::FIFOBuffer) = nb_available(f)
19542

196-
function Base.String(f::FIFOBuffer)
197-
f.nb == 0 && return ""
198-
if f.f < f.l
199-
return String(f.buffer[f.f:f.l-1])
200-
else
201-
bytes = f.buffer[f.f:end]
202-
append!(bytes, view(f.buffer, 1:f.l-1))
203-
return String(bytes)
43+
function Base.read(f::FIFOBuffer, ::Type{Tuple{UInt8,Bool}})
44+
if nb_available(f.io) == 0
45+
return 0x00, false
20446
end
47+
return read(f.io, UInt8), true
20548
end
20649

207-
function Base.write(f::FIFOBuffer, b::UInt8)
208-
# buffer full, check if we can grow it
209-
if f.nb == f.len || f.len < f.l
210-
if f.len < f.max
211-
push!(f.buffer, 0x00)
212-
f.len += 1
213-
else
214-
if current_task() == f.task || f.eof
215-
return 0
216-
else # async: block until there's room to write
217-
wait(f.cond)
218-
f.nb == f.len && return 0
219-
end
220-
end
221-
end
222-
# write our byte
223-
@inbounds f.buffer[f.l] = b
224-
f.l = mod1(f.l + 1, f.max)
225-
f.nb += 1
226-
notify(f.cond)
227-
return 1
228-
end
50+
Base.write(f::FIFOBuffer{BufferStream}, x::UInt8) = write(f.io, [x])
22951

230-
function Base.write(f::FIFOBuffer, bytes::Vector{UInt8}, i, j)
231-
len = j - i + 1
232-
if f.nb == f.len || f.len < f.l
233-
# buffer full, check if we can grow it
234-
if f.len < f.max
235-
append!(f.buffer, zeros(UInt8, min(len, f.max - f.len)))
236-
f.len = length(f.buffer)
237-
else
238-
if current_task() == f.task || f.eof
239-
return 0
240-
else # async: block until there's room to write
241-
wait(f.cond)
242-
f.nb == f.len && return 0
243-
end
244-
end
245-
end
246-
if f.f <= f.l
247-
# non-wraparound
248-
avail = f.len - f.l + 1
249-
if len > avail
250-
# need to wrap around, and check if there's enough room to write full bytes
251-
# write `avail` # of bytes to end of buffer
252-
unsafe_copy!(f.buffer, f.l, bytes, i, avail)
253-
if len - avail < f.f
254-
# there's enough room to write the rest of bytes
255-
unsafe_copy!(f.buffer, 1, bytes, avail + 1, len - avail)
256-
f.l = len - avail + 1
257-
else
258-
# not able to write all of bytes
259-
unsafe_copy!(f.buffer, 1, bytes, avail + 1, f.f - 1)
260-
f.l = f.f
261-
f.nb += avail + f.f - 1
262-
notify(f.cond)
263-
return avail + f.f - 1
264-
end
265-
else
266-
# there's enough room to write bytes through the end of the buffer
267-
unsafe_copy!(f.buffer, f.l, bytes, i, len)
268-
f.l = mod1(f.l + len, f.max)
269-
end
270-
else
271-
# already in wrap-around state
272-
if len > mod1(f.f - f.l, f.max)
273-
# not able to write all of bytes
274-
nb = f.f - f.l
275-
unsafe_copy!(f.buffer, f.l, bytes, i, nb)
276-
f.l = f.f
277-
f.nb += nb
278-
notify(f.cond)
279-
return nb
280-
else
281-
# there's enough room to write bytes
282-
unsafe_copy!(f.buffer, f.l, bytes, i, len)
283-
f.l = mod1(f.l + len, f.max)
284-
end
285-
end
286-
f.nb += len
287-
notify(f.cond)
288-
return len
289-
end
52+
Base.wait_readnb(f::FIFOBuffer{BufferStream}, nb::Int) = Base.wait_readnb(f.io, nb)
29053

291-
Base.write(f::FIFOBuffer, bytes::Vector{UInt8}) = write(f, bytes, 1, length(bytes))
292-
Base.write(f::FIFOBuffer, str::String) = write(f, Vector{UInt8}(str))
29354

294-
end # module
55+
end # module

src/multipart.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ mutable struct Form <: IO
1212
data::Vector{IO}
1313
index::Int
1414
boundary::String
15+
mark::Int
1516
end
1617

1718
Form(f::Form) = f
@@ -74,7 +75,7 @@ function Form(d::Dict)
7475
end
7576
seekstart(io)
7677
push!(data, io)
77-
return Form(data, 1, boundary)
78+
return Form(data, 1, boundary, 0)
7879
end
7980

8081
function writemultipartheader(io::IOBuffer, i::IOStream)

src/parser.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,15 +110,15 @@ function onbody(r, maintask, bytes, i, j)
110110
@debug(PARSING_DEBUG, String(bytes[i:j]))
111111
len = j - i + 1
112112
#TODO: avoid copying the bytes here? can we somehow write the bytes to a FIFOBuffer more efficiently?
113-
nb = write(r.body, bytes, i, j)
113+
nb = write(r.body, bytes[i:j])
114114
if nb < len # didn't write all available bytes
115115
if current_task() == maintask
116116
# main request function hasn't returned yet, so not safe to wait
117117
r.body.max += len - nb
118-
write(r.body, bytes, i + nb, j)
118+
write(r.body, bytes[i + nb:j])
119119
else
120120
while nb < len
121-
nb += write(r.body, bytes, i + nb, j)
121+
nb += write(r.body, bytes[i + nb:j])
122122
end
123123
end
124124
end

0 commit comments

Comments
 (0)