pub trait Reentry {
// Required methods
fn call(
&mut self,
callee: &Value,
args: Vec<Value>,
) -> Result<Value, RuntimeError>;
fn call_until(
&mut self,
callee: &Value,
args: Vec<Value>,
stop: &Cancellation,
) -> Result<Value, RuntimeError>;
fn is_cancelled(&self) -> bool;
fn time_left(&self) -> Option<Duration>;
fn task(&self) -> u64;
}Expand description
How a host runs a Cove callback it was handed.
A Host API call is a value in and a value out, which is enough until an operation is given work to do rather than data to act on. A route’s handler, a repeating timer’s body, and the block a timeout bounds are all Cove closures the host holds and has to run; without this they could be stored and never called.
The callback runs on the task that made the host call, on that task’s own stack, charged to that task’s budget. There is no second thread and no scheduler: a host that wants concurrency spawns nothing, because concurrency in Cove belongs to a task scope the program wrote.
It is also how a host asks about the run it is inside.
Reentry::is_cancelled and Reentry::time_left answer the two
questions an operation that waits has to keep asking, and they are the
only way to ask them: a host holds no budget and no interpreter of its
own. HostApi says what a host owes them.
And it is what the boundary itself asks one question of.
Reentry::task says which task is calling, which nothing else at the
boundary knows: a HostRegistry is shared by every thread of a run,
while the way back borrows the interpreter of exactly one task. The answer
goes on the call’s trace event, so a trace of a run with concurrent tasks
can be grouped by whose I/O each call was.
§For the current call, and no longer
A &mut dyn Reentry arrives with a lifetime of its own, shorter than the
&self beside it, and dyn Reentry is neither Send nor Sync. So a
host cannot put one in a field: a HostApi is Send + Sync and shared
across the tasks of a run, and neither the lifetime nor the auto traits
will let this through. Both refusals are deliberate rather than an
accident of how the signature was written, and neither is worth working
around with an Arc or a raw pointer. Behind the reference is a borrow of
the interpreter running the task that made this call. It is valid while
that call is on the stack, and once the call returns the interpreter has
moved on: a retained one would name a stack frame the task has left, and
would run Cove code on a task that is doing something else.
A host that wants work done later does not keep the way back. It keeps
the callback — a Value is an ordinary owned value — and asks for
another call, which is what http.Server.handle is and why the loop
around it is written in Cove.
§As many times as the operation means
A host may call its callback none, once, or many times. clock.every
calls it once a period until the timer’s task is cancelled;
http.Server.handle calls it once, and not at all when no request
arrived. Nothing here counts invocations and nothing makes the second one
cheaper than the first.
Each one is a call the run pays for in full. Fuel is charged at the
callback’s own safepoints, its calls count against the run’s call-depth
limit while it is on the stack, and the run’s deadline and cancellation
stop it wherever they would stop any other Cove code. A host that loops
therefore does not have to police the run: a body that would overrun the
budget stops of its own accord and the error comes back out of
Reentry::call. What the host owes is to stop looping when it is told
to — Reentry::is_cancelled between rounds — rather than to keep
starting rounds that will all fail.
§Nested, up to a bound
A callback is Cove code, so it may call any host the run granted, including this one, and that host may in turn be handed work. The nesting is real: the second host call builds a second way back further down the same native stack, and it reenters the same interpreter, so the inner callback sees the same task, the same heap, and the same budget as the outer one.
How deep it may go is a runtime control, like recursion depth and for the
same reason. Two bound it. The Cove frames a callback makes count against
the run’s call-depth limit, since they are ordinary calls. And the number
of host calls running a callback that may be stacked on one thread is
bounded separately and much lower, because between one callback’s frame
and the next sits however much native stack the host chose to use, which
nothing can measure. Past that bound the next reentry is refused with a
RuntimeError; the run stops, rather than the process. It is a bound
and not a proof: a host that uses an enormous amount of stack before it
reenters can still exhaust it at the first level, and that is the host’s
responsibility, not the boundary’s.
§One at a time, on the calling thread
A host may not call back from a thread of its own, and cannot: there is
exactly one &mut dyn Reentry per host call and it is neither Send nor
Sync, so two threads cannot hold it and it cannot be moved to one. This
is the design’s answer and not a limitation waiting to be lifted.
Concurrency in Cove belongs to a task scope the program wrote; a host that
ran a program’s code on threads the program never asked for would be
deciding how much of that program runs at once, and would be doing it
outside every control the run was given.
§No lock may be held across it
A host must not hold a resource mutex, or any other non-reentrant lock,
while it calls a callback. The callback is Cove code and Cove code may
call this same host again; a std::sync::Mutex is not reentrant, so the
second call would deadlock the task on a lock the first call is holding
three frames up its own stack. Nothing detects this, because from the
lock’s point of view nothing is wrong.
The shape that works is to take what the callback’s work needs while the
lock is held, release it, and then reenter. http’s Server.handle is
the worked example: it takes the next request, or a clone of the listening
socket, out from under the table of open listeners, drops the guard, and
only then runs the route’s handler — which is free to call http.listen
again, or to serve on the same handle.
§Reentry is not task transfer
Nothing crosses a task boundary here, so the rules that govern one do not
apply. A callback’s arguments are handed from a host to the interpreter of
the task that called it, and its result comes back the same way, both on
one thread, both belonging to one task throughout. crate::task::Transfer
is not consulted and cannot be: an argument that may not cross a task
boundary — a Vector, a resource handle whose schema says
task_safe: false — is a perfectly ordinary argument to a callback, and a
callback may answer with one.
Two things nearby do belong to tasks, and it is worth saying which.
ResourceSchema::task_safe still decides whether the handle an
operation returns may later be captured by a spawn; that is a question
about the value, asked at the boundary the value eventually crosses, and
reentry is not that boundary. And a callback that is an async fn answers
with a task, which the implementation settles before handing the value
back — the host was given a callback rather than a task, so this is what
await would have done at the call site the host is standing in for. That
settle is a join, and a value coming back out of a spawned task does
cross a boundary and is checked; a settled async fn body ran on this
thread and crosses nothing.
Required Methods§
Sourcefn call(
&mut self,
callee: &Value,
args: Vec<Value>,
) -> Result<Value, RuntimeError>
fn call( &mut self, callee: &Value, args: Vec<Value>, ) -> Result<Value, RuntimeError>
Calls callee with args and answers what it produced.
The call runs to completion before this returns, on this thread. An
Err is what the callback failed with, or what stopped the run while
it was running — exhausted fuel, an expired deadline, a raised
cancellation, a depth limit — and a host that receives one has nothing
useful to add: pass it on, so the reason the run stopped reaches the
caller as the runtime wrote it.
No lock the host owns may be held across this call. See the trait’s documentation for why, and for what a host may and may not do with the way back it was handed.
Sourcefn call_until(
&mut self,
callee: &Value,
args: Vec<Value>,
stop: &Cancellation,
) -> Result<Value, RuntimeError>
fn call_until( &mut self, callee: &Value, args: Vec<Value>, stop: &Cancellation, ) -> Result<Value, RuntimeError>
Calls callee with args, stopping it at its next safepoint if
stop is raised while it runs.
This is how a timeout is a timeout rather than a measurement taken afterwards: the body observes the flag exactly where it observes its own task’s cancellation, and stops there.
stop bounds this call and everything inside it, including a further
host call the body makes and any callback that host runs in turn — a
bound that a nested call escaped would not be a bound. It adds to the
reasons the body may stop and replaces none of them: the run’s own
cancellation and deadline still apply, and a body stopped by one of
those is not stopped by this. A caller that needs to tell the two
apart reads stop afterwards, which is what clock.timeout does to
decide whether to report its bound or the error it was given.
Sourcefn is_cancelled(&self) -> bool
fn is_cancelled(&self) -> bool
Whether the work that made this host call has been asked to stop.
This is everything a safepoint in Cove code would answer to: the run’s
own cancellation, the task’s, and the flag of any bounded call this
one is nested inside — a blocking call made from the body of a
clock.timeout is inside that bound as much as any Cove statement is.
A host that loops or waits reads this between rounds and gives up when
it is raised: clock.every ends the timer rather than leaving it
running with nobody waiting, and http.Server.handle stops waiting
for a connection nobody is going to make. Giving up means answering
whatever the operation’s own “nothing happened” is. The stop belongs
to the runtime, which reports it at the next safepoint with the limit
that was configured; a host that raised an error of its own would be
answering a question it was not asked.
Sourcefn time_left(&self) -> Option<Duration>
fn time_left(&self) -> Option<Duration>
How long the run that made this host call has before its deadline expires.
None means the run has no deadline and nothing here bounds it.
Some(Duration::ZERO) means the deadline has passed, and is as much a
reason to stop as Reentry::is_cancelled answering true.
A host that waits reads this for two things. It stops when the answer reaches zero, the same way it stops when the run is cancelled. And it clamps its own timeouts by it, so an operation willing to wait thirty seconds for a peer does not sit there for thirty seconds on behalf of a run that had two hundred milliseconds left: the shorter of the two allowances is the one that is honest.
The answer is a duration rather than an instant because that is what a host does with it — pass it to a socket timeout, or compare it against zero — and because a run’s deadline is measured from when the run started, which is the budget’s business and not the host’s.
Sourcefn task(&self) -> u64
fn task(&self) -> u64
Which task made this host call: the innermost spawned task’s id, or
crate::runtime::ENTRY_TASK when the call came from the entry.
Nothing else can answer it. A HostRegistry is shared by every
thread of a run and knows nothing about who is calling; the way back is
the one thing at the boundary that belongs to one task, because it
borrows that task’s interpreter. So the boundary asks it, and writes
the answer on the call’s trace event — which is what lets a trace of a
run with concurrent tasks be grouped by whose I/O each call was.
A host is not expected to do anything with this. It is asked once per call, before the operation is dispatched, and a host that reads it is reading an identity rather than a capability: knowing which task is calling grants nothing, and two calls from one task are as unrelated as any other two.
Dyn Compatibility§
This trait is dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".