-
Notifications
You must be signed in to change notification settings - Fork 11
Interrupt #125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Interrupt #125
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 25 additions & 0 deletions
25
chasm/src/commonMain/kotlin/io/github/charlietap/chasm/embedding/Interrupt.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package io.github.charlietap.chasm.embedding | ||
|
|
||
| import io.github.charlietap.chasm.embedding.error.ChasmError | ||
| import io.github.charlietap.chasm.embedding.shapes.ChasmResult | ||
| import io.github.charlietap.chasm.embedding.shapes.Store | ||
|
|
||
| /** | ||
| * Stops the call running on a store created with `StoreConfig(interruptible = true)`. Safe to call | ||
| * from any thread: Wasm code in the call traps with `Interrupted` at its next function entry or loop | ||
| * iteration. A host function the call is running is not stopped itself; Wasm code it calls back into | ||
| * traps, as does its Wasm caller once it returns. | ||
| * | ||
| * Returns `true` if a call was running to receive the interrupt, and `false` if none was, in which | ||
| * case the interrupt is dropped and the next call runs normally. | ||
| */ | ||
| fun interrupt(store: Store): ChasmResult<Boolean, ChasmError.ExecutionError> { | ||
| val interrupt = store.store.interrupt | ||
| if (!interrupt.enabled) { | ||
| return ChasmResult.Error(ChasmError.ExecutionError("Interrupts are not enabled for this store")) | ||
| } | ||
| if (interrupt.depth == 0) return ChasmResult.Success(false) | ||
|
|
||
| interrupt.requested = true | ||
| return ChasmResult.Success(true) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
173 changes: 173 additions & 0 deletions
173
chasm/src/commonTest/kotlin/io/github/charlietap/chasm/integration/InterruptTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| package io.github.charlietap.chasm.integration | ||
|
|
||
| import com.goncalossilva.resources.Resource | ||
| import io.github.charlietap.chasm.config.StoreConfig | ||
| import io.github.charlietap.chasm.embedding.addFuel | ||
| import io.github.charlietap.chasm.embedding.error.ChasmError | ||
| import io.github.charlietap.chasm.embedding.fixture.publicImport | ||
| import io.github.charlietap.chasm.embedding.function | ||
| import io.github.charlietap.chasm.embedding.instance | ||
| import io.github.charlietap.chasm.embedding.interrupt | ||
| import io.github.charlietap.chasm.embedding.invoke | ||
| import io.github.charlietap.chasm.embedding.module | ||
| import io.github.charlietap.chasm.embedding.shapes.ChasmResult | ||
| import io.github.charlietap.chasm.embedding.shapes.Instance | ||
| import io.github.charlietap.chasm.embedding.shapes.Store | ||
| import io.github.charlietap.chasm.embedding.shapes.expect | ||
| import io.github.charlietap.chasm.embedding.store | ||
| import io.github.charlietap.chasm.fixture.type.functionType | ||
| import io.github.charlietap.chasm.fixture.type.i32ValueType | ||
| import io.github.charlietap.chasm.fixture.type.resultType | ||
| import io.github.charlietap.chasm.host.HostFunction | ||
| import io.github.charlietap.chasm.host.writeI32 | ||
| import io.github.charlietap.chasm.runtime.error.InvocationError | ||
| import io.github.charlietap.chasm.runtime.value.NumberValue | ||
| import kotlin.test.Test | ||
| import kotlin.test.assertEquals | ||
| import kotlin.test.assertIs | ||
|
|
||
| class InterruptTest { | ||
|
|
||
| @Test | ||
| fun `interrupting a store that is not interruptible returns an error`() { | ||
| val (store, _) = instantiate(StoreConfig()) | ||
|
|
||
| assertIs<ChasmResult.Error<*>>(interrupt(store)) | ||
| } | ||
|
|
||
| @Test | ||
| fun `an interrupt stops the running call at its next check`() { | ||
| val (store, instance) = instantiate(StoreConfig(interruptible = true)) | ||
|
|
||
| assertEquals(interrupted(), invoke(store, instance, "interrupt_then_spin")) | ||
| } | ||
|
|
||
| @Test | ||
| fun `a call after an interrupted one runs`() { | ||
| val (store, instance) = instantiate(StoreConfig(interruptible = true)) | ||
| assertEquals(interrupted(), invoke(store, instance, "interrupt_then_spin")) | ||
|
|
||
| assertEquals(ChasmResult.Success(listOf(NumberValue.I32(0))), count(store, instance, 3)) | ||
| } | ||
|
|
||
| @Test | ||
| fun `an interrupt made while no call runs reports it and has no effect`() { | ||
| val (store, instance) = instantiate(StoreConfig(interruptible = true)) | ||
|
|
||
| assertEquals(ChasmResult.Success(false), interrupt(store)) | ||
| assertEquals(ChasmResult.Success(listOf(NumberValue.I32(0))), count(store, instance, 3)) | ||
| } | ||
|
|
||
| @Test | ||
| fun `an interruptible store compiles a check per function and loop and a plain store compiles none`() { | ||
| val (interruptible, _) = instantiate(StoreConfig(interruptible = true)) | ||
| val (plain, _) = instantiate(StoreConfig()) | ||
|
|
||
| // Five functions and five loops. | ||
| assertEquals(10, interruptible.store.program.size - plain.store.program.size) | ||
| } | ||
|
|
||
| @Test | ||
| fun `a metered interruptible store compiles one combined check per function and loop`() { | ||
| val (both, _) = instantiate(StoreConfig(meterFuel = true, interruptible = true)) | ||
| val (plain, _) = instantiate(StoreConfig()) | ||
|
|
||
| assertEquals(10, both.store.program.size - plain.store.program.size) | ||
| } | ||
|
|
||
| @Test | ||
| fun `a metered interruptible store traps on an interrupt or on running out of fuel`() { | ||
| val (store, instance) = instantiate(StoreConfig(meterFuel = true, interruptible = true)) | ||
| addFuel(store, Long.MAX_VALUE) | ||
| assertEquals(interrupted(), invoke(store, instance, "interrupt_then_spin")) | ||
|
|
||
| val (metered, meteredInstance) = instantiate(StoreConfig(meterFuel = true, interruptible = true)) | ||
| addFuel(metered, 100) | ||
| assertEquals( | ||
| ChasmResult.Error(ChasmError.ExecutionError(InvocationError.FuelExhausted.toString())), | ||
| invoke(metered, meteredInstance, "spin"), | ||
| ) | ||
| } | ||
|
|
||
| @Test | ||
| fun `a metered interruptible store reports an interrupt over running out of fuel`() { | ||
| val (store, instance) = instantiate(StoreConfig(meterFuel = true, interruptible = true)) | ||
| // Enough for the interrupting function's entry check, none for its loop's. | ||
| addFuel(store, 1) | ||
|
|
||
| assertEquals(interrupted(), invoke(store, instance, "interrupt_then_spin")) | ||
| } | ||
|
|
||
| @Test | ||
| fun `an interrupt made during a call reports that it landed`() { | ||
| var interruptResult: ChasmResult<*, *>? = null | ||
| val (store, instance) = instantiate(StoreConfig(interruptible = true)) { store, _ -> | ||
| interruptResult = interrupt(store) | ||
| } | ||
|
|
||
| assertEquals(interrupted(), invoke(store, instance, "callback_then_spin")) | ||
| assertEquals(ChasmResult.Success(true), interruptResult) | ||
| } | ||
|
|
||
| @Test | ||
| fun `an interrupt raised in a call back into the store also stops its caller`() { | ||
| var callbackResult: ChasmResult<*, *>? = null | ||
| val (store, instance) = instantiate(StoreConfig(interruptible = true)) { store, instance -> | ||
| callbackResult = invoke(store, instance, "interrupt_then_spin") | ||
| } | ||
|
|
||
| assertEquals(interrupted(), invoke(store, instance, "callback_then_spin")) | ||
| assertEquals(interrupted(), callbackResult) | ||
| } | ||
|
|
||
| @Test | ||
| fun `an interrupt raised in a host function invoked directly stops its calls back into the store`() { | ||
| var callbackResult: ChasmResult<*, *>? = null | ||
| val (store, instance) = instantiate(StoreConfig(interruptible = true)) { store, instance -> | ||
| interrupt(store) | ||
| callbackResult = count(store, instance, 3) | ||
| } | ||
|
|
||
| assertEquals(ChasmResult.Success(emptyList()), invoke(store, instance, "callback")) | ||
| assertEquals(interrupted(), callbackResult) | ||
| } | ||
|
|
||
| private fun count(store: Store, instance: Instance, times: Int) = | ||
| invoke(store, instance, "count", listOf(NumberValue.I32(times))) | ||
|
|
||
| companion object { | ||
| private const val FIXTURE = "integration/interrupt.wasm" | ||
|
|
||
| internal fun interrupted() = | ||
| ChasmResult.Error(ChasmError.ExecutionError(InvocationError.Interrupted.toString())) | ||
|
|
||
| /** | ||
| * Instantiates the fixture. Its `env.interrupt` import interrupts the store it runs on, its | ||
| * `env.callback` import, which it also exports, runs [callback] with the store and the instance | ||
| * being created, and its `env.keep_running` import returns [keepRunning]. | ||
| */ | ||
| internal fun instantiate( | ||
| config: StoreConfig, | ||
| keepRunning: () -> Boolean = { false }, | ||
| callback: (Store, Instance) -> Unit = { _, _ -> }, | ||
| ): Pair<Store, Instance> { | ||
| val store = store(config) | ||
| lateinit var instance: Instance | ||
| val hostInterrupt = function(store, functionType(), HostFunction { _, _ -> interrupt(store) }) | ||
| val hostCallback = function(store, functionType(), HostFunction { _, _ -> callback(store, instance) }) | ||
| val hostKeepRunning = function( | ||
| store, | ||
| functionType(results = resultType(listOf(i32ValueType()))), | ||
| HostFunction { _, results -> results.writeI32(0, if (keepRunning()) 1 else 0) }, | ||
| ) | ||
| val module = module(Resource(FIXTURE).readBytes()).expect("module decodes") | ||
| val imports = listOf( | ||
| publicImport("env", "interrupt", hostInterrupt), | ||
| publicImport("env", "callback", hostCallback), | ||
| publicImport("env", "keep_running", hostKeepRunning), | ||
| ) | ||
| instance = instance(store, module, imports).expect("module instantiates") | ||
| return store to instance | ||
| } | ||
| } | ||
| } |
Binary file not shown.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| (module | ||
| (import "env" "interrupt" (func $interrupt)) | ||
| (import "env" "callback" (func $callback)) | ||
| (import "env" "keep_running" (func $keep_running (result i32))) | ||
| (export "callback" (func $callback)) | ||
|
|
||
| (func $spin (export "spin") | ||
| (loop $continue | ||
| (br $continue))) | ||
|
|
||
| (func $interrupt_then_spin (export "interrupt_then_spin") | ||
| (call $interrupt) | ||
| (loop $continue | ||
| (br $continue))) | ||
|
|
||
| (func $callback_then_spin (export "callback_then_spin") | ||
| (call $callback) | ||
| (loop $continue | ||
| (br $continue))) | ||
|
|
||
| (func $spin_while_host_allows (export "spin_while_host_allows") | ||
| (loop $continue | ||
| (br_if $continue (call $keep_running)))) | ||
|
|
||
| (func $count (export "count") (param $n i32) (result i32) | ||
| (loop $continue | ||
| (local.set $n (i32.sub (local.get $n) (i32.const 1))) | ||
| (br_if $continue (local.get $n))) | ||
| (local.get $n))) |
56 changes: 56 additions & 0 deletions
56
chasm/src/jvmTest/kotlin/io/github/charlietap/chasm/integration/InterruptThreadTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| package io.github.charlietap.chasm.integration | ||
|
|
||
| import io.github.charlietap.chasm.config.StoreConfig | ||
| import io.github.charlietap.chasm.embedding.interrupt | ||
| import io.github.charlietap.chasm.embedding.invoke | ||
| import io.github.charlietap.chasm.embedding.shapes.ChasmResult | ||
| import io.github.charlietap.chasm.integration.InterruptTest.Companion.interrupted | ||
| import io.github.charlietap.chasm.runtime.value.NumberValue | ||
| import java.util.concurrent.CountDownLatch | ||
| import java.util.concurrent.FutureTask | ||
| import java.util.concurrent.TimeUnit | ||
| import java.util.concurrent.atomic.AtomicBoolean | ||
| import kotlin.concurrent.thread | ||
| import kotlin.test.Test | ||
| import kotlin.test.assertEquals | ||
| import kotlin.test.assertTrue | ||
|
|
||
| class InterruptThreadTest { | ||
|
|
||
| @Test | ||
| fun `another thread stops a running loop, and the next call runs`() { | ||
| val started = CountDownLatch(1) | ||
| val abandoned = AtomicBoolean(false) | ||
| val (store, instance) = InterruptTest.instantiate( | ||
| StoreConfig(interruptible = true), | ||
| keepRunning = { | ||
| started.countDown() | ||
| !abandoned.get() | ||
| }, | ||
| ) | ||
|
|
||
| val invocation = FutureTask { | ||
| invoke(store, instance, "spin_while_host_allows") | ||
| } | ||
| // A daemon worker and timed waits let a broken interrupt fail the test rather than hang the JVM. | ||
| thread(isDaemon = true, name = "interrupt-test") { | ||
| invocation.run() | ||
| } | ||
|
|
||
| try { | ||
| assertTrue(started.await(5, TimeUnit.SECONDS), "Wasm execution did not reach the host callback") | ||
|
|
||
| assertEquals(ChasmResult.Success(true), interrupt(store)) | ||
|
|
||
| assertEquals(interrupted(), invocation.get(5, TimeUnit.SECONDS)) | ||
| } finally { | ||
| // Ends the loop if the interrupt did not, so a failure leaves no thread spinning. | ||
| abandoned.set(true) | ||
| } | ||
|
|
||
| assertEquals( | ||
| ChasmResult.Success(listOf(NumberValue.I32(0))), | ||
| invoke(store, instance, "count", listOf(NumberValue.I32(3))), | ||
| ) | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
note I'm using kotlin.concurrent.thread ^^^
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.