(*---------------------------------------------------------------------------- * Copyright (c) 2019 António Nuno Monteiro * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * 3. Neither the name of the copyright holder nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. *---------------------------------------------------------------------------*) module StreamsTbl = struct include Hashtbl.MakeSeeded (struct type t = Stream_identifier.t let equal = Stream_identifier.( === ) let hash i k = Hashtbl.seeded_hash i k (* Required for OCaml >= 5.0.0, but causes errors for older compilers because it is an unused value declaration. *) let[@warning "-32"] seeded_hash = hash end) let[@inline] find_opt h key = try Some (find h key) with Not_found -> None end module type StreamDescriptor = sig type t val id : t -> Stream_identifier.t val requires_output : t -> bool val flush_write_body : t -> max_bytes:int -> int val finish_stream : t -> Stream.closed_reason -> unit val is_idle : t -> bool end module Make (Streamd : StreamDescriptor) = struct module rec PriorityTreeNode : sig type root = Root type nonroot = NonRoot type stream = nonroot node and parent = Parent : _ node -> parent and _ node = (* From RFC7540§5.3.1: * A stream that is not dependent on any other stream is given a stream * dependency of 0x0. In other words, the non-existent stream 0 forms * the root of the tree. * * Note: * We use a GADT because the root of the tree doesn't have an * associated request descriptor. It has the added advantage of * allowing us to enforce that all (other) streams in the tree are * associated with a request descriptor. *) | Connection : { all_streams : stream StreamsTbl.t ; mutable t_last : int ; mutable children : PriorityQueue.t ; (* Connection-level flow control window. * outbound flow control, what we're allowed to send. * * From RFC7540§6.9.1: * Two flow-control windows are applicable: the stream * flow-control window and the connection flow-control window. *) mutable flow : Settings.WindowSize.t ; (* inbound flow control, what the client is allowed to send. *) mutable inflow : Settings.WindowSize.t ; mutable marked_for_removal : Stream.closed StreamsTbl.t } -> root node | Stream : { descriptor : Streamd.t ; mutable t_last : int ; mutable t : int ; mutable priority : Priority.t ; mutable parent : parent ; mutable children : PriorityQueue.t ; (* Stream-level flow control window. See connection-level above. * * From RFC7540§6.9.1: * Two flow-control windows are applicable: the stream * flow-control window and the connection flow-control window. *) mutable flow : Settings.WindowSize.t ; mutable inflow : Settings.WindowSize.t } -> nonroot node end = PriorityTreeNode and PriorityQueue : (Psq.S with type k = Int32.t and type p = PriorityTreeNode.stream) = Psq.Make (Int32) (struct include PriorityTreeNode type t = stream let compare (Stream { t = t1; _ }) (Stream { t = t2; _ }) = compare t1 t2 end) include PriorityTreeNode type t = root node (* TODO(anmonteiro): change according to SETTINGS_MAX_CONCURRENT_STREAMS? *) let make_root ?(capacity = 65536) () = Connection { t_last = 0 ; children = PriorityQueue.empty ; all_streams = StreamsTbl.create ~random:true capacity ; flow = Settings.WindowSize.default_initial_window_size ; inflow = Settings.WindowSize.default_initial_window_size ; marked_for_removal = StreamsTbl.create ~random:true 256 } let create ~parent ~initial_send_window_size ~initial_recv_window_size descriptor = Stream { descriptor ; t_last = 0 ; t = 0 ; (* From RFC7540§5.3.5: * All streams are initially assigned a non-exclusive dependency on * stream 0x0. Pushed streams (Section 8.2) initially depend on their * associated stream. In both cases, streams are assigned a default * weight of 16. *) priority = Priority.default_priority ; parent ; children = PriorityQueue.empty ; flow = initial_send_window_size ; inflow = initial_recv_window_size } let stream_id : type a. a node -> int32 = function | Connection _ -> Stream_identifier.connection | Stream { descriptor; _ } -> Streamd.id descriptor let children : type a. a node -> PriorityQueue.t = function | Stream { children; _ } -> children | Connection { children; _ } -> children let remove_child : type a. a node -> int32 -> unit = fun parent id -> match parent with | Connection ({ children; _ } as node) -> (* From RFC7540§5.3.1: * A stream that is not dependent on any other stream is given a stream * dependency of 0x0. In other words, the non-existent stream 0 forms * the root of the tree. *) node.children <- PriorityQueue.remove id children | Stream ({ children; _ } as node) -> node.children <- PriorityQueue.remove id children let update_children : type a. a node -> PriorityQueue.t -> unit = fun parent updated_children -> match parent with | Connection s -> s.children <- updated_children | Stream s -> s.children <- updated_children let set_parent stream_node ~exclusive (Parent new_parent_node as new_parent) = let (Stream ({ descriptor; parent = Parent old_parent_node; _ } as stream)) = stream_node in let stream_id = Streamd.id descriptor in remove_child old_parent_node stream_id; stream.parent <- new_parent; let new_children = let new_children = children new_parent_node in if exclusive then ( (* From RFC7540§5.3.3: * Dependent streams move with their parent stream if the parent is * reprioritized. Setting a dependency with the exclusive flag for a * reprioritized stream causes all the dependencies of the new parent * stream to become dependent on the reprioritized stream. *) stream.children <- PriorityQueue.fold (fun k (Stream p as p_node) pq -> p.parent <- Parent stream_node; PriorityQueue.add k p_node pq) stream.children new_children; (* From RFC7540§5.3.1: * An exclusive flag allows for the insertion of a new level of * dependencies. The exclusive flag causes the stream to become the * sole dependency of its parent stream, causing other dependencies * to become dependent on the exclusive stream. *) PriorityQueue.sg stream_id stream_node) else PriorityQueue.add stream_id stream_node new_children in update_children new_parent_node new_children let would_create_cycle ~new_parent (Stream { descriptor; _ }) = let rec inner : type a. a node -> bool = function | Connection _ -> false | Stream { parent = Parent parent; _ } when Stream_identifier.(stream_id parent === Streamd.id descriptor) -> true | Stream { parent = Parent parent; _ } -> inner parent in let (Parent parent_node) = new_parent in inner parent_node let reprioritize_stream (Connection root as t) ~priority stream_node = let (Stream stream) = stream_node in let new_parent, new_priority = if Stream_identifier.is_connection priority.Priority.stream_dependency then Parent t, priority else match StreamsTbl.find_opt root.all_streams priority.stream_dependency with | Some parent_stream -> (match StreamsTbl.mem root.marked_for_removal priority.stream_dependency with | true -> (* A stream that is marked for removal is also not present in the tree *) Parent t, Priority.default_priority | false -> Parent parent_stream, priority) | None -> (* From RFC7540§5.3.1: * A dependency on a stream that is not currently in the tree — * such as a stream in the "idle" state — results in that stream * being given a default priority (Section 5.3.5). *) Parent t, Priority.default_priority in (* bail early if trying to set the same priority *) if not (Priority.equal stream.priority new_priority) then ( let { Priority.stream_dependency; exclusive; _ } = new_priority in let (Parent current_parent_node) = stream.parent in let current_parent_id = stream_id current_parent_node in (* only need to set a different parent if the parent or exclusive status * changed *) if (not Stream_identifier.(stream_dependency === current_parent_id)) || exclusive <> stream.priority.exclusive then ( let (Parent new_parent_node) = new_parent in (match new_parent_node with | Stream new_parent_stream -> if would_create_cycle ~new_parent stream_node then ( (* From RFC7540§5.3.3: * If a stream is made dependent on one of its own dependencies, * the formerly dependent stream is first moved to be dependent * on the reprioritized stream's previous parent. The moved * dependency retains its weight. *) set_parent new_parent_node ~exclusive:false stream.parent; new_parent_stream.priority <- { new_parent_stream.priority with stream_dependency = current_parent_id }) | Connection _ -> (* The root node cannot be dependent on any other streams, so we * don't need to worry about it creating cycles. *) ()); (* From RFC7540§5.3.1: * When assigning a dependency on another stream, the stream is added * as a new dependency of the parent stream. *) set_parent stream_node ~exclusive new_parent); stream.priority <- new_priority) let update_t node n = let (Stream ({ parent = Parent parent; descriptor; _ } as stream)) = node in let tlast_p = match parent with | Connection { t_last; _ } -> t_last | Stream { t_last; _ } -> t_last in stream.t <- tlast_p + (n * 256 / stream.priority.weight); let id = Streamd.id descriptor in remove_child parent id; let updated_children = PriorityQueue.add id node (children parent) in update_children parent updated_children let update_t_last : type a. a node -> int -> unit = fun p_node t_last -> match p_node with | Connection p -> p.t_last <- t_last | Stream p -> p.t_last <- t_last let add (Connection root as t) ~priority ~initial_send_window_size ~initial_recv_window_size descriptor = let stream = create ~parent:(Parent t) ~initial_send_window_size ~initial_recv_window_size descriptor in let stream_id = Streamd.id descriptor in StreamsTbl.add root.all_streams stream_id stream; root.children <- PriorityQueue.add stream_id stream root.children; if priority != Priority.default_priority then reprioritize_stream t ~priority stream; update_t stream 0; stream let get_node (Connection root) stream_id = StreamsTbl.find_opt root.all_streams stream_id let find t stream_id = match get_node t stream_id with | Some (Stream { descriptor; _ }) -> Some descriptor | None -> None let iter (Connection { all_streams; _ }) ~f = StreamsTbl.iter (fun _id stream -> f stream) all_streams let allowed_to_transmit (Connection root) (Stream stream) = Int32.compare root.flow 0l > 0 && Int32.compare stream.flow 0l > 0 let allowed_to_receive (Connection root) (Stream stream) size = size <= root.inflow && size <= stream.inflow let write (Connection root as t) stream_node = let (Stream ({ descriptor; _ } as stream)) = stream_node in (* From RFC7540§6.9.1: * Two flow-control windows are applicable: the stream flow-control * window and the connection flow-control window. The sender MUST NOT * send a flow-controlled frame with a length that exceeds the space * available in either of the flow-control windows advertised by the * receiver. *) let allowed_bytes = if allowed_to_transmit t stream_node then min root.flow stream.flow else (* There might be a zero-length DATA frame (with the end stream flag set) waiting to be sent. *) 0l in let written = Streamd.flush_write_body ~max_bytes:(Int32.to_int allowed_bytes) descriptor in let written32 = Int32.of_int written in (* From RFC7540§6.9.1: * After sending a flow-controlled frame, the sender reduces the space * available in both windows by the length of the transmitted frame. *) root.flow <- Int32.sub root.flow written32; stream.flow <- Int32.sub stream.flow written32; written let mark_for_removal (Connection root) id closed = StreamsTbl.replace root.marked_for_removal id closed let implicitly_close_idle_stream descriptor max_seen_ids = let implicitly_close_stream descriptor = if Streamd.is_idle descriptor then (* From RFC7540§5.1.1: * The first use of a new stream identifier implicitly closes all * streams in the "idle" state that might have been initiated by * that peer with a lower-valued stream identifier. *) Streamd.finish_stream descriptor Finished in let max_client_stream_id, max_pushed_stream_id = max_seen_ids in let stream_id = Streamd.id descriptor in if Stream_identifier.is_request stream_id then ( if stream_id < max_client_stream_id then implicitly_close_stream descriptor) else if stream_id < max_pushed_stream_id then implicitly_close_stream descriptor (* Scheduling algorithm from https://goo.gl/3sSHXJ (based on nghttp2): * * 1 def schedule(p): * 2 if stream #p has data to send: * 3 send data for #p, update nsent[p] * 4 return * 5 if #p's queue is empty: * 6 return * 7 pop #i from queue * 8 update t_last[p] = t[i] * 9 schedule(i) * 10 if #i or its descendant is "active": * 11 update t[i] and push it into queue again * 12 * 13 schedule(0) *) let flush t max_seen_ids = let rec schedule : type a. a node -> int * bool = function | Connection _ as p_node -> (* The root can never send data. *) traverse p_node | Stream ({ descriptor; _ } as stream) as p_node -> let written = if Streamd.requires_output descriptor then (* In this branch, flow-control has no bearing on activity, otherwise * a flow-controlled stream would be considered inactive (because it * can't make progress at the moment) and removed from the priority * tree altogether. *) write t p_node else 0 in if written > 0 then (* We check for activity again, because the stream may have gone * inactive after the call to `write` above. *) let subtree_is_active = Streamd.requires_output descriptor || not (PriorityQueue.is_empty stream.children) in written, subtree_is_active else (* If we haven't written anything, check if any of our children have. *) let written, subtree_is_active' = traverse p_node in let subtree_is_active = Streamd.requires_output descriptor || subtree_is_active' in (match written with | 0 -> written, subtree_is_active | written -> (* If there's still more to write, put the node back in the tree. *) if subtree_is_active then update_t p_node written; written, subtree_is_active) and traverse : type a. a node -> int * bool = fun p_node -> let rec loop remaining_children = match PriorityQueue.pop remaining_children with | Some ((id, (Stream i as i_node)), remaining_children') -> update_t_last p_node i.t; let written, subtree_is_active = schedule i_node in if not subtree_is_active then ( implicitly_close_idle_stream i.descriptor max_seen_ids; (* XXX(anmonteiro): we may not want to remove from the tree right * away. *) remove_child p_node id); (match written with | 0 -> (* If this subtree didn't write anything, check the other children in the priority queue. *) loop remaining_children' | written -> (* If there's still more to write, put the node back in the tree. *) if subtree_is_active then update_t i_node written; written, subtree_is_active) | None -> (* No data written, but queue was not originally empty. * Therefore, we can't determine the subtree is inactive. *) 0, true in let children = children p_node in match PriorityQueue.is_empty children with | true -> (* Queue is empty, see line 6 above. *) 0, false | false -> loop children in let (Connection root) = t in ignore (schedule t); StreamsTbl.iter (fun id closed -> (* When a stream completes, i.e. doesn't require more output and * enters the `Closed` state, we set a TTL value which represents the * number of writer yields that the stream has before it is removed * from the connection Hash Table. By doing this we avoid losing some * potentially useful information regarding the stream's state at the * cost of keeping it around for a little while longer. *) if closed.Stream.ttl = 0 then ( StreamsTbl.remove root.marked_for_removal id; StreamsTbl.remove root.all_streams id) else closed.ttl <- closed.ttl - 1) root.marked_for_removal (* XXX(anmonteiro): Consider using `optint` for this? * https://github.com/mirage/optint *) let check_flow flow growth flow' = (* Check for overflow on 32-bit systems. *) Int32.compare flow' growth > 0 = (Int32.compare flow 0l > 0) && Int32.compare flow' Settings.WindowSize.max_window_size <= 0 let add_flow : type a. a node -> int32 -> bool = fun t growth -> match t with | Connection ({ flow; _ } as root) -> let flow' = Int32.add flow growth in let valid_flow = check_flow flow growth flow' in if valid_flow then root.flow <- flow'; valid_flow | Stream ({ flow; _ } as stream) -> let flow' = Int32.add flow growth in let valid_flow = check_flow flow growth flow' in if valid_flow then stream.flow <- flow'; valid_flow let add_inflow : type a. a node -> int32 -> bool = fun t growth -> match t with | Connection ({ inflow; _ } as root) -> let inflow' = Int32.add inflow growth in let valid_inflow = check_flow inflow growth inflow' in if valid_inflow then root.inflow <- inflow'; valid_inflow | Stream ({ inflow; _ } as stream) -> let inflow' = Int32.add inflow growth in let valid_inflow = check_flow inflow growth inflow' in if valid_inflow then stream.inflow <- inflow'; valid_inflow let deduct_inflow : type a. a node -> int32 -> unit = fun t size -> match t with | Connection ({ inflow; _ } as root) -> (* no need to check, we verify that the peer is allowed to send. *) root.inflow <- Int32.sub inflow size | Stream ({ inflow; _ } as stream) -> stream.inflow <- Int32.sub inflow size let pp_hum fmt t = let rec pp_hum_inner level fmt t = let pp_binding fmt (id, Stream { children; t; _ }) = Format.fprintf fmt "\n%s%ld, %d -> [%a]" (String.make (level * 2) ' ') id t (pp_hum_inner (level + 1)) children in PriorityQueue.pp pp_binding fmt t in pp_hum_inner 0 fmt t let pp_hum fmt (Connection { children; _ }) = pp_hum fmt children end