Skip to content

v0.9.6

Choose a tag to compare

@sarsko sarsko released this 10 Oct 01:53
776744e
  • Fix a stack overflow when a global tracing subscriber at TRACE level calls into Shuttle while it handles an event, for example to call current::clock(). Every access to the execution state emits a TRACE event, so the subscriber's own access emitted another event, which called the subscriber again, and so on until the stack overflowed, and the failure report and persisted schedule were lost with the process. Accesses made while that event is being handled no longer emit it. (#382)

  • Execution teardown no longer panics or aborts when destructors use Shuttle. When an execution ends, Shuttle drops the tasks that have not finished, so the destructors of everything they own run then. Almost every Shuttle operation panicked in those destructors, because it looked up the running task and there was none; the panic came from a destructor, so the process aborted, and the failure report was lost with it. Among them were Mutex::lock and try_lock (and the tokio wrapper's), RwLock, BatchSemaphore::try_acquire, atomics, channel sends, spawning, thread::current and block_on. (0.9.5 made current::clock() safe, but the operations that use the clock still looked up the running task right after it.) (#381)

    Teardown now drops each unfinished task as if the execution had cancelled it as it ended, and while it does, that task is the current task: destructors can use Shuttle as the task could, and what they do is attributed to it. A task that never ran drops its function, and a future task that is parked between polls drops its future, the way an async runtime drops its tasks' futures when it shuts down, each on the task's own stack. So a destructor that blocks, on a lock that another unfinished task holds say, waits until teardown has dropped that task, and one that yields lets the others run. A future task that stopped in the middle of poll has its stack unwound once no other task can make progress. Its destructors can use Shuttle too, but cannot wait: one that blocks or panics while the stack unwinds aborts the process, as any panic during unwinding does. A task that catches the unwind is unwound again at its next scheduling point. Teardown runs the tasks in a fixed order, without the scheduler, and doesn't extend the schedule, so it is the same when the schedule is replayed. A destructor that panics fails the test like any other panic, with the schedule persisted. So do destructors that block with nothing left to wake them, which is reported like a deadlock, and a destructor that exceeds the step bound, which counts their scheduling points too. Task-local values, tags and the tasks' spans are dropped as their task, and the execution's statics last, as if by the main thread, once no task is left that could still use them. A lock that a task holds when teardown unwinds its stack is released and not poisoned, and condition variables, barriers and channels forget a task that teardown unwinds while it waits on them. A task that teardown drops is finished, and awaiting its JoinHandle returns JoinError::Cancelled.

    A parked future is dropped rather than unwound now, so std::thread::panicking() is false in its destructors, and checks that are skipped while panicking now run. A tokio-test Mock with data left to read, say, now fails the test.

    A failed execution is now torn down too, before its failure is raised; until now, its task-local values and statics were dropped while the failure unwound, when Shuttle was no longer there to call into. It still leaks its tasks' stacks, like a stopped execution, and with them the futures of its parked future tasks, and the functions of its tasks that never ran, unless UngracefulShutdownConfig::continuation_function_behavior is Drop. Its task-local values and statics are dropped. While a stopped or failed execution is torn down, ExecutionState::should_stop() is true, so Shuttle's own destructors skip their bookkeeping, and panics are ignored: a stopped execution doesn't fail, and a failed one reports its original failure.

  • Fix a lost panic: a detached task that panicked and switched out while unwinding, to release a lock say, could be dropped at the end of the execution before it finished unwinding. The panic was swallowed, and the thread stayed in a panicking state, which changed how later executions behaved. The execution now waits for a task that is unwinding a panic, even if it is detached, also if it resumed the panic with std::panic::resume_unwind, as the tokio wrapper's watch::Sender::send_modify does. A stopped execution lets such a task finish unwinding as it is torn down, but no further than catching the panic, so that the panic fails the test. A task whose unwind blocks then, on a lock that a task that no longer runs holds say, fails the test too, and its stack is leaked, which leaves the thread panicking. Tasks share the OS thread, so a task that panics while another task is unwinding a panic can't tell its panic from the other one: if it is still unwinding when the execution ends, that fails the test, and its stack is leaked too. (#381)

  • Fix a use after free when a stopped execution's scoped threads never ran: their functions, which borrow from their parent's stack, were dropped after that stack was freed. (#381)

  • A panic out of the executor, such as a scheduler's when a replayed schedule doesn't fit the test, no longer skips execution teardown, which aborted the process if a destructor then used Shuttle. (#381)

  • BatchSemaphore: a task that takes a permit of an unfair semaphore while an Acquire of its own is still queued on it is no longer blocked by that Acquire. Waking a waker of a task of another execution no longer panics. (#381)

  • Add BatchSemaphore::acquire_reserving, for unfair semaphores. The request holds nothing and waits like an acquire until min_permits permits are available, and then reserves the semaphore in the same step: from then on no other request can take a permit, and the request takes its num_permits as soon as that many are available. The reservation ends when the request is granted or its future is dropped, and while it lasts, available_permits is zero. It models a lock that holds back new requests before it holds everything it asked for, such as a parking_lot RwLock writer, which sets WRITER_BIT and then waits for the readers to leave. BatchSemaphore::upgrade on an unfair semaphore now reserves the semaphore too, as soon as no other request holds the reservation, so that nothing can overtake it while it waits for the tasks that hold permits. (#374)

  • Publish shuttle-engine 0.1.4 and shuttle-std 0.1.3, which between them have all of the changes above. shuttle-std now requires shuttle-engine 0.1.4, whose teardown support it uses. shuttle-schedulers is unchanged at 0.1.1; it takes shuttle-engine as ^0.1.1, so it builds against 0.1.4 as it stands. shuttle's own source is unchanged too; it now requires shuttle-engine 0.1.4 and shuttle-std 0.1.3, so that upgrading to 0.9.6 brings the fixes with it. No wrapper is republished; they get these fixes through shuttle.