We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← The Expert Edge: Idiom, Review and Capstones step 5 of 14
Capstone: an ownership-only job scheduler
Make a small job scheduler compile, without changing what it does.
pub fn schedule(script: Vec<String>) -> Vec<String>
There is no algorithm here. The command grammar is trivial, the data
structures are a VecDeque and an Option, and the expected output is
written down for you. The only difficulty in this problem is satisfying the
borrow checker, which is exactly the point: knowing the three rules and
being able to write a program under them are different skills, and this is the
one that decides whether you can ship Rust.
The starter has four deliberate errors, one per command, and they are four different diagnostics. Fix them all.
The scheduler
Job derives nothing. Not Clone, not Copy, not Debug. It holds a
String id, a String payload, and an Rc<RefCell<Vec<String>>> handle to
the shared log, and it implements Drop to write drop <id> when it dies.
That Drop impl is the instrument: every move, every early free and every
forgotten value shows up in the log, in order.
| command | meaning |
|---|---|
submit <id> <payload> |
build a Job, move it to the back of pending. Logs submit <id>. |
start <id> |
move that job out of pending into the single running slot. If something was already running, it moves back to the front of pending first, logging preempt <oldid>. Logs start <id>, or start <id> missing. |
finish |
consume the running job by value, logging done <id>:<PAYLOAD-UPPERCASED> and then, as it dies, drop <id>. Logs finish idle if nothing is running. |
cancel <id> |
remove it from pending and drop it immediately: cancel <id> then drop <id>. Or cancel <id> missing. |
snapshot |
log pending a,b,c — without disturbing the queue. A read-only borrow. pending - when empty. |
Anything else logs bad <line>. At the end the log gets a shutdown line,
then running is dropped, then pending front to back — so the final drop
order is part of the expected output.
Every error path is a no-op that still logs. Cancelling a job that does not exist is not a panic and not silence; it is one line saying so. That is a design rule worth stealing: a command processor with a defined output for every input is testable, and one that panics on bad input is not.
The four errors, and why each one is the shape it is
start — E0382, borrow of moved value. running = Some(job); gives the
job away, and the next line reads job.id. Rust’s move checker is
flow-sensitive, so this is not “you cannot use a variable twice” — it is “the
value is not there any more”. Two fixes: read what you need before the move,
or read it back out of its new home. One of those is obviously better.
finish — E0509, cannot move out of a type which implements Drop. This
is the interesting one, and the one that stops people.
::: question Why does implementing Drop make let Job { id, payload, .. } = job;
illegal, when it would be fine on a struct without Drop?
Because Drop::drop takes &mut self — it is handed the whole value,
every field intact. If you were allowed to move payload out and then let the
rest of the job be destroyed, drop would run on a Job with a hole in it,
reading a field that has already been given away.
For a struct without Drop, that is fine: the compiler just drops each
remaining field individually and skips the moved one. Adding a Drop impl
makes the value indivisible — you can move the whole thing or nothing.
The two ways out, both idiomatic:
-
Consume the whole value in one place. Give
Joba method takingself, do the work inside it, and let the job die at the end of that method. This is the better answer here, and it has a bonus:doneis logged beforedropautomatically, because the destructor runs when the method returns. -
Leave a hole behind.
std::mem::take(&mut self.payload)swaps in an emptyStringand hands you the old one, so the job stays whole. Use this when you genuinely need the field before the value dies.
What you must not do is add #[derive(Clone)] and clone your way past it.
clippy::redundant_clone is denied in this file precisely so that shortcut
fails.
:::
cancel — E0502, cannot borrow as mutable because it is also borrowed as
immutable. pending.iter().find(..) hands back a reference into the
queue, and removing an element while that reference is alive is exactly the
bug the rule exists to prevent: the element could move in memory, or be the
one you are pointing at. Fix it by not holding the reference — find the
index, which is a usize and borrows nothing.
snapshot — E0507, cannot move out of a shared reference. .iter() gives
&Job, and j.id tries to take the String out of borrowed memory. .clone()
compiles and is the wrong instinct: you are building a comma-joined line and
never keep the strings, so j.id.as_str() gives you everything you need for
free. That is the difference between “make the error go away” and “understand
what the error was telling you”.
Reading the log as evidence
When you have it compiling, look at what the expected outputs prove.
In the case that ends with three jobs still pending, the log ends
shutdown, drop a, drop b, drop c — front to back, after the shutdown
marker. Nothing was freed early and nothing leaked. In the preempt case,
preempt a appears with no drop a after it, because the preempted job moved
back into the queue rather than being destroyed. In the finish case,
done x:HI is immediately followed by drop x, one line apart, because
consuming a value by reference-free means its destructor runs right there.
That is what an ownership discipline buys you: the lifecycle of every value
is visible in the source, and it is the same every run. No GC pause, no
finaliser queue, no “eventually”. The Drop log is just a way of printing
something the compiler already knew.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.