We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← The Expert Edge: Idiom, Review and Capstones step 7 of 14
Capstone: an async executor with a logical-time reactor
Finish a single-threaded async runtime — a timer future, a Join2 and a
Select2 — and return the complete event log it produces.
pub fn runtime(spec: Vec<String>) -> Vec<String>
Async Rust is famous for being hard to learn, and the reason is that people
meet it as a framework rather than as a mechanism. It is a mechanism, and it
is small: a Future is a state machine with one method, an executor is a loop
that calls that method, and a reactor is the thing that decides when calling
it again could possibly help. async/.await is syntax that writes the state
machine for you. There is no magic left over once you have built one.
There is no real time here and no threads. The reactor holds a now: u64
which only moves when every task is blocked, and then jumps straight to the
earliest deadline anyone registered. That makes the whole run deterministic:
the log is the same, line for line, every time.
What is given
Reactor, the executor loop run, the spec parser and the task bodies. Read
run before you write anything — it is fifteen lines and it is the entire
concept:
loop {
// poll every live task once, in spawn order
// if none is still alive, stop
// otherwise advance the clock to the earliest deadline and go again
}
The Context is built from Waker::noop(), which is honest for this design:
because the executor polls every task on every round, no task ever needs to
be told it became ready. A real executor uses the waker to keep a run-queue of
exactly the tasks worth polling — that is the only difference, and it is a
performance difference, not a semantic one.
What you must fix — there are four things
1. Timer::poll is not written. On the first poll ever, log
t={now} poll {name}. If the clock has reached at, log
t={now} ready {name} and return Poll::Ready(name). Otherwise register at
with the reactor and return Poll::Pending.
::: question What breaks if poll returns Pending without registering the
deadline?
The executor asks the reactor for the earliest deadline, gets None because
nobody registered anything, and logs stalled. That is a deliberately visible
version of the most common bug in real async code: returning Pending
without arranging to be woken. In a real runtime there is no stalled
message — the task simply never runs again, and you get a hang with no
diagnostic at all.
The rule is absolute: before you return Pending, you must have handed
someone a way to make progress happen — a registered deadline here, a Waker
clone in a real reactor. This is the async equivalent of remembering to
release a lock.
:::
2. Timer‘s Drop does nothing. A timer that is destroyed before it ever
fired must log cancel {name}. A timer that completed must not. This is what
makes the Select2 case observable, and it is not a testing gimmick:
in Rust, cancelling a future is dropping it, which is a genuinely
unusual design. There is no cancel() method and no cancellation token in the
language; a future that is dropped simply stops existing, and any cleanup it
needs has to be in its destructor. That is why “cancellation safety” is a
phrase you will meet in every async library’s documentation.
3. Join2::poll short-circuits. It returns Pending the moment the first
child is not ready, so the second child is never even polled until the first
finishes — which turns concurrency into sequencing. A join must poll both
children on every round. The test for this is the one whose expected log opens
with t=0 poll a and t=0 poll b back to back: proof that both were started
before either completed.
4. Select2::poll asks the wrong child first. With two timers due at the
same instant, the winner is decided by polling order, and the expected output
says the first-listed child wins ties. Real select! implementations differ
here — some are strictly biased, some randomise to avoid starvation — but they
all document it, because a race whose winner is unspecified is a race you
cannot test.
Pin, and why Unpin shows up everywhere
Future::poll takes self: Pin<&mut Self>, because an async block that
holds a reference across an .await is a struct containing a pointer into
itself, and moving it would invalidate that pointer. Pin is the promise not
to move.
Your Timer has no self-references, so it is Unpin — the promise costs
nothing and Pin::new(&mut timer) is a safe function. That is why every
combinator here carries A: Future + Unpin. Delete those bounds and you get
E0277: A cannot be unpinned, pointing at self.get_mut(), with the
compiler suggesting pin! or Box::pin. Real combinators avoid the bound
with pin-projection (the pin-project crate, or a careful unsafe block);
requiring Unpin is the honest simplification for a teaching runtime, and
Box::pin is what the executor uses to hold the async blocks, which are
emphatically not Unpin.
Two lints worth carrying away
clippy::await_holding_refcell_ref. Holding a RefCell guard across an
.await is a latent panic: the task suspends with the borrow flag still set,
another task borrows the same cell, and borrow_mut panics at runtime. Notice
how the given task bodies read the clock — reactor.borrow().now inside a
format! argument, where the temporary guard dies at the end of the
statement. Never let g = cell.borrow(); something().await;.
clippy::async_yields_async. An async block whose final expression is
itself a future almost always means a missing .await. The block resolves to
a future that nobody is going to poll, so the work never happens and nothing
fails loudly.
The spec language
sleep <name> <t> spawns a timer and logs t={t} done {name} after it fires.
join <a> <ta> <b> <tb> joins two timers and logs join a+b.
race <a> <ta> <b> <tb> races them and logs race <winner>. Anything
unparsable logs bad <line>. The last line is always end t={now}.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.