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
Complete four parts of a small logical-time runtime: Timer::poll, Timer::drop, the polling behavior of Join2, and the polling order of Select2. Keep the supplied reactor, executor, parser, and task bodies unchanged.
pub fn runtime(spec: Vec<String>) -> Vec<String>
Return the complete event log in order. The program runs on one thread without sleeping or reading wall-clock time. Its clock starts at zero and jumps between registered deadlines.
Input commands
Each input string is split on whitespace. Names are single tokens and may repeat. Commands are case-sensitive and must have exactly the indicated number of tokens:
| Command | Task |
|---|---|
sleep <name> <at> |
Wait for one timer, then log t={now} done {name} |
join <a> <ta> <b> <tb> |
Wait for both timers, then log t={now} join {a}+{b} |
race <a> <ta> <b> <tb> |
Wait for the first ready timer, then log t={now} race {winner} |
Times are absolute deadlines, parsed with Rust's u64::from_str behavior. Decimal digits, leading zeros, and a leading + are accepted; negative, overflowing, or otherwise invalid times are rejected. A malformed line logs bad {line}, preserving the original line after bad , and creates no task.
Parsing all lines happens before execution. Therefore all bad entries appear before any timer polls, even when a malformed line follows valid commands. The final entry is always end t={now}. An empty specification returns ["end t=0"].
How the supplied executor advances
In each round, poll every unfinished task once, in valid-command order. Remove tasks that return Ready. If none remain, stop without advancing the clock again.
Otherwise, the reactor selects the smallest deadline registered since the previous advance, clears its registration list, moves now to that deadline, and logs t={now} tick. Pending timers register again on subsequent polls. If tasks remain but there is no registered deadline, the executor logs stalled and stops. Dropping the remaining tasks may then log timer cancellations, before the final end entry.
Registrations are not removed when a timer is cancelled. For example, with race late 9 now 0 followed by sleep tail 20, the first child registers 9 before the second wins immediately. The cancelled registration causes a tick at 9 while the other task is still pending. Preserve this behavior of the given reactor.
Waker::noop() is sufficient for this closed simulation because the executor explicitly polls every remaining task after each clock advance. It is not a general executor for arbitrary futures. A future waiting for external I/O needs a functioning wakeup path; ignoring it can prevent progress, not merely reduce performance.
1. Complete Timer::poll
Read the clock without retaining the RefCell guard. On the timer's first poll only, log t={now} poll {name} and mark it polled.
If now >= at, mark the timer done, log t={now} ready {name}, and return its name in Poll::Ready. Otherwise, register its deadline and return Poll::Pending.
For sleep a 3, the complete log is:
t=0 poll a
t=3 tick
t=3 ready a
t=3 done a
end t=3
Check your understanding What happens if a pending timer never registers its deadline? Show answer Hide answer
If no other timer supplies a deadline, the executor cannot advance and logs stalled. If another timer does register one, the missing registration might initially be masked by later polls.
In a wake-driven executor, a pending future must arrange for its task to be notified when progress is possible, often by delegating to another future or registering the current waker with an event source. This simulation substitutes deadline registration for that mechanism.
2. Log cancellation in Timer::drop
If the timer has not returned Ready, log cancel {name} when it is dropped. This includes a timer that was never polled. A timer marked done logs no cancellation.
Dropping an owned future stops further polling of that future. It does not universally undo work already performed or stop an independently spawned operation. Here cancellation is deliberately simple: the timer owns no external operation and only records a log entry.
3. Poll both unfinished Join2 children
Poll child a, then child b, if each is still present. Do not return early merely because a is pending. Save each ready output and remove its completed child, so that child is never polled again.
Return Ready((a_output, b_output)) when both outputs are available. The output order remains a, b regardless of which child finishes first. With join a 5 b 2, both first-poll entries occur at zero, b is ready at 2, and the join completes at 5.
“Poll both” means both unfinished children. A future that has returned Ready should not be polled again.
4. Make Select2 prefer the first child
Poll a first. Only if it is pending should you poll b. If either is ready, drop both stored children before returning the winner's output. The completed timer is silent on drop; the loser logs cancellation before the task logs its race result.
For race a 0 b 0, a wins immediately. The log contains poll a, ready a, cancel b, and race a at time zero where applicable. There is no poll b entry and no tick. When both deadlines are the same positive value, both children are initially polled, but a still wins the later tie.
This is a specified bias for this exercise, not a rule for every select combinator. Other APIs may choose different fairness policies; tests should reflect the guarantees their APIs actually provide.
Why the Unpin bounds are present
Future::poll receives Pin<&mut Self>. Pinning supports futures whose state depends on a stable memory address, such as a generated state machine with internal references. It is not a guarantee that every future contains such references.
These combinators require their child futures to be Unpin, allowing safe Pin::new calls on mutable child references. Join2 also stores outputs and requires those output types to be Unpin, so it can use self.get_mut(). The timer's fields satisfy these bounds. The outer async task blocks are stored in Pin<Box<_>>, which keeps their underlying values pinned while the box handles move through the executor.
Keep these bounds for the exercise. Supporting arbitrary pinned children requires a different representation or pin projection, not just deleting the constraints.
Keep borrows and nested futures explicit
Holding a RefCell guard across .await can cause a runtime panic if another task requests a conflicting borrow of the same cell. Copy the clock value into a local and release the guard before awaiting. The relevant Clippy warning highlights that risk; suspension alone does not guarantee a panic.
An async block can also return another future as its output. That is legal, but awaiting the outer block does not automatically await the returned future. Check for a missing .await when Clippy flags this pattern.
Further reading: Future::poll and wakeups and pinning and Unpin.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.