-
Notifications
You must be signed in to change notification settings - Fork 87
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
fix channel resource leak on close of underlying flow; add exceptions to channel #119
Changes from 7 commits
e2363fb
c9ed5fc
700677f
41a5af3
82a6499
87a8314
2174b0c
0c8fb0e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,6 +25,10 @@ module Make(Flow:V1_LWT.FLOW) = struct | |
type +'a io = 'a Lwt.t | ||
type 'a io_stream = 'a Lwt_stream.t | ||
|
||
exception End_of_file (* at least one user understands this exception *) | ||
exception Write_error of string | ||
exception Read_error of string | ||
|
||
type t = { | ||
flow: flow; | ||
mutable ibuf: Cstruct.t option; (* Queue of incoming buf *) | ||
|
@@ -35,81 +39,95 @@ module Make(Flow:V1_LWT.FLOW) = struct | |
abort_u: unit Lwt.u; | ||
} | ||
|
||
exception Closed | ||
|
||
let create flow = | ||
let ibuf = None in | ||
let obufq = [] in | ||
let obuf = None in | ||
let opos = 0 in | ||
let abort_t, abort_u = Lwt.task () in | ||
let abort_t, abort_u = MProf.Trace.named_task "Channel.t.abort" in | ||
{ ibuf; obuf; flow; obufq; opos; abort_t; abort_u } | ||
|
||
let to_flow { flow; _ } = flow | ||
|
||
let ibuf_refill t = | ||
Flow.read t.flow >>= function | ||
| `Ok buf -> | ||
(* users of get_ibuf (and therefore ibuf_refill) expect the buffer | ||
returned here to have length >0; if Flow.read ever gives us empty | ||
buffers, this will be violated causing Channel users to see Cstruct | ||
exceptions *) | ||
t.ibuf <- Some buf; | ||
return_unit | ||
| `Error _ | `Eof -> | ||
fail Closed | ||
| `Error e -> | ||
fail (Read_error (Flow.error_message e)) | ||
| `Eof -> | ||
(* close the flow before throwing exception; otherwise it will never be | ||
GC'd *) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This sounds like it might be another bug. Why doesn't the flow get GC'd eventually? Can we close forgotten flows automatically using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The flow provided by There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm a bit afraid that adding finalisers on every flow will put a lot of pressure on the GC. |
||
Flow.close t.flow >>= fun () -> | ||
fail End_of_file | ||
|
||
let rec get_ibuf t = | ||
match t.ibuf with | ||
|None -> ibuf_refill t >>= fun () -> get_ibuf t | ||
|Some buf when Cstruct.len buf = 0 -> ibuf_refill t >>= fun () -> get_ibuf t | ||
|Some buf -> return buf | ||
| None -> ibuf_refill t >>= fun () -> get_ibuf t | ||
| Some buf when Cstruct.len buf = 0 -> ibuf_refill t >>= fun () -> get_ibuf t | ||
| Some buf -> return buf | ||
|
||
(* Read one character from the input channel *) | ||
let read_char t = | ||
get_ibuf t >>= fun buf -> | ||
get_ibuf t (* the fact that we returned means we have at least 1 char *) | ||
>>= fun buf -> | ||
let c = Cstruct.get_char buf 0 in | ||
t.ibuf <- Some (Cstruct.shift buf 1); | ||
t.ibuf <- Some (Cstruct.shift buf 1); (* advance read buffer, possibly to | ||
EOF *) | ||
return c | ||
|
||
(* Read up to len characters from the input channel | ||
and at most a full view. If not specified, read all *) | ||
let read_some ?len t = | ||
(* get_ibuf potentially throws EOF-related exceptions *) | ||
get_ibuf t >>= fun buf -> | ||
let avail = Cstruct.len buf in | ||
let len = match len with |Some len -> len |None -> avail in | ||
if len < avail then begin | ||
let hd,tl = Cstruct.split buf len in | ||
t.ibuf <- Some tl; | ||
t.ibuf <- Some tl; (* leave some in the buffer; next time, we won't do a | ||
blocking read *) | ||
return hd | ||
end else begin | ||
t.ibuf <- None; | ||
return buf | ||
end | ||
|
||
(* Read up to len characters from the input channel as a | ||
stream (and read all available if no length specified *) | ||
(* Read up to len characters from the input channel as a | ||
stream (and read all available if no length specified *) | ||
let read_stream ?len t = | ||
Lwt_stream.from (fun () -> | ||
Lwt.catch | ||
(fun () -> read_some ?len t >>= fun v -> return (Some v)) | ||
(function Closed -> return_none | e -> fail e) | ||
(function End_of_file -> return_none | e -> fail e) | ||
) | ||
|
||
(* Read until a character is found *) | ||
let read_until t ch = | ||
get_ibuf t >>= fun buf -> | ||
let len = Cstruct.len buf in | ||
let rec scan off = | ||
if off = len then None else begin | ||
if Cstruct.get_char buf off = ch then | ||
Some off else scan (off+1) | ||
end | ||
in | ||
match scan 0 with | ||
|None -> (* not found, return what we have until EOF *) | ||
t.ibuf <- None; | ||
return (false, buf) | ||
|Some off -> (* found, so split the buffer *) | ||
let hd = Cstruct.sub buf 0 off in | ||
t.ibuf <- Some (Cstruct.shift buf (off+1)); | ||
return (true, hd) | ||
Lwt.catch | ||
(fun () -> get_ibuf t >>= fun buf -> | ||
let len = Cstruct.len buf in | ||
let rec scan off = | ||
if off = len then None else begin | ||
if Cstruct.get_char buf off = ch then | ||
Some off else scan (off+1) | ||
end | ||
in | ||
match scan 0 with | ||
|None -> (* not found, return what we have until EOF *) | ||
t.ibuf <- None; (* basically guaranteeing that next read is EOF *) | ||
return (false, buf) | ||
|Some off -> (* found, so split the buffer *) | ||
let hd = Cstruct.sub buf 0 off in | ||
t.ibuf <- Some (Cstruct.shift buf (off+1)); | ||
return (true, hd) | ||
) | ||
(function End_of_file -> return (false, Cstruct.create 0) | e -> fail e) | ||
|
||
(* This reads a line of input, which is terminated either by a CRLF | ||
sequence, or the end of the channel (which counts as a line). | ||
|
@@ -195,12 +213,15 @@ module Make(Flow:V1_LWT.FLOW) = struct | |
queue_obuf t; | ||
let l = List.rev t.obufq in | ||
t.obufq <- []; | ||
Flow.writev t.flow l | ||
>>= fun _ -> return_unit | ||
Flow.writev t.flow l >>= function | ||
| `Ok () -> Lwt.return_unit | ||
| `Error (e : Flow.error) -> fail (Write_error (Flow.error_message e)) | ||
| `Eof -> fail (End_of_file) | ||
|
||
let close t = | ||
flush t | ||
>>= fun () -> | ||
Flow.close t.flow | ||
try_lwt | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could use Lwt.finalize (fun () -> flush t) (fun () -> Flow.close t.flow) to avoid adding the |
||
flush t | ||
finally | ||
Flow.close t.flow | ||
|
||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
open Lwt | ||
|
||
(* this is a very small set of tests for the channel interface, intended to | ||
ensure that EOF conditions on the underlying flow are handled properly *) | ||
module Channel = Channel.Make(Fflow) | ||
|
||
let cmp a b = | ||
match (String.compare a b) with | 0 -> true | _ -> false | ||
|
||
let printer = function | ||
| `Success -> "success" | ||
| `Failure s -> s | ||
|
||
let test_read_char_eof () = | ||
let f = Fflow.make () in | ||
let c = Channel.create f in | ||
let try_char_read () = | ||
Channel.read_char c >>= fun ch -> | ||
OUnit.assert_failure (Printf.sprintf "character %c was returned from | ||
Channel.read_char on an empty flow" ch) | ||
in | ||
Lwt.try_bind | ||
(try_char_read) | ||
(fun () -> Lwt.return (`Failure "no exception" )) (* "success" case (no exceptions) *) | ||
(function | ||
| Channel.End_of_file -> Lwt.return (`Success) | ||
| e -> Lwt.return (`Failure (Printf.sprintf "wrong exception: %s" | ||
(Printexc.to_string e))) | ||
) | ||
|
||
let test_read_until_eof () = | ||
let check a b = OUnit.assert_equal ~printer:(fun a -> a) ~cmp a | ||
(Cstruct.to_string b) in | ||
let input = Fflow.input_string "I am the very model of a modern major general" | ||
in | ||
let f = Fflow.make ~input () in | ||
let c = Channel.create f in | ||
Channel.read_until c 'v' >>= function | ||
| true, buf -> | ||
check "I am the " buf; | ||
Channel.read_until c '\xff' >>= fun (found, buf) -> | ||
OUnit.assert_equal ~msg:"claimed we found a char that couldn't have been | ||
there in read_until" false found; | ||
check "ery model of a modern major general" buf; | ||
Channel.read_until c '\n' >>= fun (found, buf) -> | ||
OUnit.assert_equal ~msg:"claimed we found a char after EOF in read_until" | ||
false found; | ||
OUnit.assert_equal ~printer:string_of_int 0 (Cstruct.len buf); | ||
Lwt.return_unit | ||
| false, _ -> | ||
OUnit.assert_failure "thought we couldn't find a 'v' in input test" | ||
|
||
let _ = | ||
Lwt_main.run ( | ||
test_read_char_eof () >>= fun res -> | ||
OUnit.assert_equal ~printer `Success res; | ||
test_read_until_eof () >>= fun () -> | ||
Lwt.return_unit | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,6 +31,8 @@ depends: [ | |
"mirage-net-unix" {>= "1.1.0"} | ||
"ipaddr" {>= "2.2.0"} | ||
"mirage-profile" | ||
"mirage-flow" {test} | ||
"ounit2" {test} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be |
||
] | ||
depopts: [ | ||
"mirage-xen" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what this comment means -- do you mean one external user is already expecting this exception to be thrown?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep. https://github.com/mirage/mirage-http/blob/master/lib/HTTP_IO.ml#L40
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aha, good catch! I think that might be
Pervasives.End_of_file
, so it still wouldn't be caught by defining anotherEnd_of_file
here in Channel (exceptions are not like polymorphic variants -- if you define them twice in two different modules, they are two different exceptions for the purposes of catching them). I suspect thatEnd_of_file
leaked from the socket backend (still needs to be confirmed). This basically reinforces the "exceptions need to disappear from these interfaces" argument, but this fixup en route is just fine.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That code is doing two horrible things: using an exception to represent the (non-exceptional, should-be-handled) case of end of file, and then converting it to the magic value
""
to mean the same thing.The cohttp docs say:
Would be good to standardise on something a bit saner everywhere...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed ... I think keeping the same (crazy) semantics in that PR is useful though,