Interrupt - #125
Interrupt#125Wavesonics wants to merge 1 commit into
Conversation
| data object InterruptCheck : AdminInstruction | ||
|
|
||
| /** A [FuelCheck] and an [InterruptCheck] in one dispatch, for stores that are both metered and interruptible. */ | ||
| data object FuelAndInterruptCheck : AdminInstruction |
There was a problem hiding this comment.
Good use of super instructions, this is the way to go
There was a problem hiding this comment.
Thanks! Glad that fit.
| vstack.setFrameSlot(index, value.toLongFromBoxed()) | ||
| } | ||
| initializeLocals(vstack, callStrategy, ROOT_FP) | ||
| val interrupt = store.interrupt |
There was a problem hiding this comment.
I would move all of this into FunctionInvoker, so it covers invocation of a HostFunction also, its the same thing just a higher entry point
There was a problem hiding this comment.
Good call, moved it into FunctionInvoker so directly invoked host functions are covered too. Added a test for a host function that interrupts and then calls back into the store.
| import kotlin.test.Test | ||
| import kotlin.test.assertEquals | ||
|
|
||
| class InterruptThreadTest { |
There was a problem hiding this comment.
Could we use the host callback to signal that execution has started, then send a single interrupt and wait for completion with a timeout? That avoids the startup race and verifies that one interrupt is sufficient.
The current test can hang indefinitely if interruption breaks, a timed wait and daemon worker let it fail without keeping the test JVM alive. We can retain the assertion that the next invocation succeeds.
Roughly:
val started = CountDownLatch(1)
val (store, instance) = InterruptTest.instantiate(
StoreConfig(interruptible = true),
) { _, _ ->
started.countDown()
}
val invocation = FutureTask {
invoke(store, instance, "callback_then_spin")
}
thread(isDaemon = true, name = "interrupt-test") {
invocation.run()
}
assertTrue(
started.await(5, TimeUnit.SECONDS),
"Wasm execution did not reach the host callback",
)
assertEquals(ChasmResult.Success(Unit), interrupt(store))
assertEquals(
ChasmResult.Error(
ChasmError.ExecutionError(InvocationError.Interrupted.toString()),
),
invocation.get(5, TimeUnit.SECONDS),
)
assertEquals(
ChasmResult.Success(listOf(NumberValue.I32(0))),
invoke(store, instance, "count", listOf(NumberValue.I32(3))),
)note I'm using kotlin.concurrent.thread ^^^
There was a problem hiding this comment.
Done, thanks for the sketch. I went with it almost as written. The one addition is that the loop now polls a host import, so if the interrupt ever fails the test can release the worker instead of leaving it spinning.
An embedder had no way to stop a call from outside it. Fuel bounds how long a call runs, but a watchdog thread or a host-side cancel cannot end a call early without it. StoreConfig(interruptible = true) compiles a check at each function entry and loop branch target, the same sites fuel uses. interrupt() sets a volatile flag from any thread, and the running call traps with Interrupted at its next check. A store that is also metered emits one combined check per site rather than two, which reports an interrupt ahead of running out of fuel. A store with neither setting emits none. FunctionInvoker tracks the calls running on the store, so host functions invoked directly are covered as well as Wasm functions. The flag is cleared only when an outermost call starts, and before that call is published as running. interrupt() returns whether a call was running to receive it, so an interrupt made between calls is reported as dropped rather than lost silently. Calls made back into the store from a host function never clear it, so an interrupt raised during one also stops its caller. CoreMark on the JVM (Ryzen 9 8945HS), 5 alternating single runs per store configuration, median score with range: plain 993.5 (955.9 to 1033.5) fuel 947.9 (922.4 to 972.7) -4.6% interrupt 953.9 (943.7 to 981.8) -4.0% fuel+interrupt 926.9 (919.2 to 962.4) -6.7% The ranges overlap, so treat these as approximate. The coremark task takes the store configuration as an argument to reproduce them.
a0187e1 to
e1263bb
Compare
|
Heads up on one change beyond the review: |
Follow-up to #124, split out as requested in review. Built on that branch, so until it merges this PR also shows its commit; only the top commit (
runtime: interrupt running calls in interruptible stores) is new here.Adds a way to stop a running call from another thread, independent of fuel metering.
API
StoreConfig(interruptible = true)opts a store in.interrupt(store)can be called from any thread. The running call traps withInvocationError.Interruptedat its next function entry or loop iteration. It returns an error for a store that isn't interruptible.Implementation
emitFuelCheck()becomesemitCheckpoint(), which picks the dispatcher at compile time: fuel only, interrupt only, or a combined fuel-and-interrupt check, so a store with both settings pays for one dispatch per site. A store with neither emits nothing.Cost
CoreMark on the JVM, 5 alternating runs per configuration, median score:
The runs are noisy (the ranges overlap), so treat these as approximate.
./gradlew :benchmark:coremark --args=<plain|fuel|interrupt|fuel+interrupt>reproduces them.Tests
Fixture-based tests (
interrupt.wat) cover:A JVM test also interrupts a running loop from another thread.