123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328letsrc=Logs.Src.create"runtime"(* NOTE(dinosaure): the initial size of the bigstring into which we accumulate
what the peer sends us (it grows on demand, see [Buffer.put]). Almost every
caller overrides it with [H1.Config.read_buffer_size] /
[H2.Config.read_buffer_size] (0x1000 for [h1]); the default only applies
where no such configuration exists (the websocket connections).
Note that the effective read is [min read_buffer_size (free space)], so it
is really the [h1]/[h2] configuration which decides how much we read at a
time. Measured on a 64 MiB upload over loopback: 0x1000 gives 2.2 GB/s,
0x4000 gives 3.6 GB/s and it plateaus from there. *)letdefault_read_buffer_size=0x4000moduleLog=(valLogs.src_logsrc:Logs.LOG)moduleFlow=FlowmoduletypeS=sigtypetvalnext_read_operation:t->[`Read|`Yield|`Close|`Upgrade]valread:t->Bstr.t->off:int->len:int->intvalread_eof:t->Bstr.t->off:int->len:int->intvalyield_reader:t->(unit->unit)->unitvalnext_write_operation:t->[`WriteofBstr.tFaraday.ioveclist|`Closeofint|`Yield|`Upgrade]valreport_write_result:t->[`Okofint|`Closed]->unitvalyield_writer:t->(unit->unit)->unitvalreport_exn:t->exn->unitvalis_closed:t->boolendmoduleBuffer:sigtypetvalcreate:int->tvalget:t->fn:(Bstr.t->off:int->len:int->int)->intvalput:t->fn:(Bstr.t->off:int->len:int->int)->intend=structtypet={mutablebuffer:Bstr.t;mutableoff:int;mutablelen:int}letcreatesize=letbuffer=Bstr.createsizein{buffer;off=0;len=0}letcompresst=ift.len=0thenbegint.off<-0;t.len<-0endelseift.off>0thenbeginBstr.blitt.buffer~src_off:t.offt.buffer~dst_off:0~len:t.len;t.off<-0endletgett~fn=letn=fnt.buffer~off:t.off~len:t.lenint.off<-t.off+n;t.len<-t.len-n;ift.len=0thent.off<-0;nletputt~fn=compresst;letoff=t.off+t.leninletbuf=t.bufferinifBstr.lengthbuf=t.lenthenbegint.buffer<-Bstr.create(2*Bstr.lengthbuf);Bstr.blitbuf~src_off:t.offt.buffer~dst_off:0~len:t.lenend;letn=fnt.buffer~off~len:(Bstr.lengtht.buffer-off)int.len<-t.len+n;nendletempty_bt=Printexc.get_callstackmax_intletrecterminateorphans=matchMiou.careorphanswith|None->Miou.yield()|SomeNone->Miou.yield();terminateorphans|Some(Someprm)->(matchMiou.awaitprmwith|Ok()->terminateorphans|Errorexn->Log.err(funm->m"unexpected exception from an asynchronous task: %S"(Printexc.to_stringexn));terminateorphans)letreccleanorphans=matchMiou.careorphanswith|None|SomeNone->()|Some(Someprm)->beginmatchMiou.awaitprmwith|Ok()->cleanorphans|Errorexn->Log.err(funm->m"unexpected exception from an asynchronous task: %S"(Printexc.to_stringexn));cleanorphansendexceptionClosed_by_peer=Flow.Closed_by_peermoduletypeCONNECTION=sigtypeconntypeflowvalrun:conn->?tags:Logs.Tag.set->?read_buffer_size:int->?upgrade:(flow->unit)->flow->unitMiou.tendmoduleMake(Flow:Flow.S)(Runtime:S)=structtypeconn=Runtime.ttypeflow=Flow.tletshutdownflowcmd=tryFlow.shutdownflowcmdwithexn->Log.err(funm->m"error when we shutdown: %S"(Printexc.to_stringexn))(* TODO(dinosaure): It can happen that we try to shutdown a connection when it
is already closed (it all depends on the behavior of the peer). It seems
that the closing of a socket between two peers via HTTP is not as
standardized as all that. Thus, shutdown can raise an exception (saying
that the connection has already been closed by the peer).
We could check before attempting to shutdown the connection instead of
ignoring the exception that may have been raised. *)letrecvflowbuffer=letbytes_read=Buffer.putbuffer~fn:(Flow.readflow)inifbytes_read=0then`Eofelse`Okbytes_readletwritevflowbstrs=letlen=List.fold_left(funa{Faraday.len;_}->a+len)0bstrsintryFlow.writevflowbstrs;`Oklenwith|Closed_by_peer->`Closed|_exn->`Closedtypet={tags:Logs.Tag.set;conn:Runtime.t;flow:Flow.t;buffer:Buffer.t;stop:boolref;upgrade:unitMiou.Computation.t}letyield~name:_tregister=letwaker=Miou.Computation.create()inregistert.conn(fun()->ignore(Miou.Computation.try_returnwaker()));matchMiou.Computation.awaitwakerwith|Ok()->`Continue|Error(exn,bt)->Printexc.raise_with_backtraceexnbtletreadert=letrecprotected()=matchRuntime.next_read_operationt.connwith|`Read->letfn=Log.debug(funm->m"+read reader");matchrecvt.flowt.bufferwith|`Eof->Log.debug(funm->m"the flow was closed by peer");Runtime.read_eoft.conn|`Oklen->Log.debug(funm->m"got %d byte(s) from the given flow"len);Runtime.readt.conninlet_=Buffer.gett.buffer~fninprotected()|`Yield->let`Continue=yield~name:"reader"tRuntime.yield_readerinprotected()|`Close->Log.debug(funm->m"+close reader");shutdownt.flow`read;t.stop:=true|`Upgrade->ignore(Miou.Computation.try_returnt.upgrade())inprotectedletwritert=letrecprotected()=matchRuntime.next_write_operationt.connwith|`Writeiovecs->Log.debug(funm->m"+write writer");writevt.flowiovecs|>Runtime.report_write_resultt.conn;protected()|`Yield->let`Continue=yield~name:"writer"tRuntime.yield_writerinprotected()|`Close_->Log.debug(funm->m"+close writer");shutdownt.flow`write;t.stop:=true|`Upgrade->ignore(Miou.Computation.try_returnt.upgrade())inprotectedtypeg={tags:Logs.Tag.set;conn:Runtime.t;flow:Flow.t;buffer:Buffer.t;rd_stop:boolref;wr_stop:boolref;errored:boolref;rd_resolver:unitMiou.Computation.t;wr_resolver:unitMiou.Computation.t}(* NOTE(dinosaure): report exception only once. *)letreport_exngexn=Log.err(funm->m~tags:g.tags"user's exception: %s"(Printexc.to_stringexn));if!(g.errored)=falsethenbeginRuntime.report_exng.connexn;g.errored:=trueendletguardedgfn()=tryfn()withexn->report_exngexn;g.rd_stop:=true;g.wr_stop:=true;shutdowng.flow`read_write(* NOTE(dinosaure): a connection is three tasks under one "runner": a reader,
a writer, and one waiting for a possible protocol upgrade. The runner
creates them and awaits them.
NOTE(dinosaure): [Runtime.is_closed] does not mean that there are no more
tasks and that the connection can be "terminated" (via [Miou.await_exn] or
[Miou.cancel]); it merely indicates that our internal state is closed. The
only way to know whether we should indeed terminate the tasks is to trust
the [Runtime] state machine and expect that [`Close] is indeed issued by
the writer and the reader. It should be noted that an exception may be
thrown by the network layer, and this must be reported by the state machine
(via [report_exn]) to signal our main loop to stop everything (particularly
when a client connection is interrupted by a [^C]). *)letto_readerg={tags=g.tags;conn=g.conn;flow=g.flow;buffer=g.buffer;stop=g.rd_stop;upgrade=g.rd_resolver}letto_writerg={tags=g.tags;conn=g.conn;flow=g.flow;buffer=g.buffer;stop=g.wr_stop;upgrade=g.wr_resolver}letglobal~read_buffer_size~tagsconnflow=letbuffer=Buffer.createread_buffer_sizeinletrd_stop=reffalseinletwr_stop=reffalseinleterrored=reffalseinletrd_resolver=Miou.Computation.create()inletwr_resolver=Miou.Computation.create()in{tags;conn;flow;buffer;rd_stop;wr_stop;errored;rd_resolver;wr_resolver}letcancel=(Miou.Cancelled,empty_bt)letrunconn?(tags=Logs.Tag.empty)?(read_buffer_size=default_read_buffer_size)?upgradeflow=letg=global~read_buffer_size~tagsconnflowinletupgrade_task()=letrd=Miou.Computation.awaitg.rd_resolverinletwr=Miou.Computation.awaitg.wr_resolverinmatch(rd,wr,upgrade)with|Error_,_,_|_,Error_,_->()|_,_,None->Log.debug(funm->m~tags"No handler for websocket was given");Fmt.failwith"Upgrade unsupported"|Ok(),Ok(),Somefn->fnflow;Log.debug(funm->m~tags"Upgrade handler finished, shutdown the underlying flow");shutdownflow`read;shutdownflow`write;g.rd_stop:=true;g.wr_stop:=trueinletrunner()=letprm_rd=Miou.async(guardedg(reader(to_readerg)))inletprm_wr=Miou.async(guardedg(writer(to_writerg)))inletprm_up=Miou.async(guardedgupgrade_task)inlet_=Miou.await_all[prm_rd;prm_wr]inlet_=Miou.Computation.try_cancelg.rd_resolvercancelinlet_=Miou.Computation.try_cancelg.wr_resolvercancelinlet_=Miou.awaitprm_upinLog.debug(funm->m~tags"Connection closed")inMiou.asyncrunnerend