diff --git a/zipline-testing/src/jsMain/kotlin/app/cash/zipline/testing/recursing.kt b/zipline-testing/src/jsMain/kotlin/app/cash/zipline/testing/recursing.kt new file mode 100644 index 0000000000..a342a2f77d --- /dev/null +++ b/zipline-testing/src/jsMain/kotlin/app/cash/zipline/testing/recursing.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package app.cash.zipline.testing + +import app.cash.zipline.Zipline + +private val zipline by lazy { Zipline.get() } + +class RecursingEchoService : EchoService { + override fun echo(request: EchoRequest): EchoResponse { + val recurseCount = request.message.toInt() + if (recurseCount > 0) echo(EchoRequest((recurseCount - 1).toString())) + return EchoResponse("recursed $recurseCount times!") + } +} + +@JsExport +fun prepareRecursingService() { + zipline.bind("recursingService", RecursingEchoService()) +} diff --git a/zipline/src/hostMain/kotlin/app/cash/zipline/Zipline.kt b/zipline/src/hostMain/kotlin/app/cash/zipline/Zipline.kt index 88b8fb1df1..513e66dcf9 100644 --- a/zipline/src/hostMain/kotlin/app/cash/zipline/Zipline.kt +++ b/zipline/src/hostMain/kotlin/app/cash/zipline/Zipline.kt @@ -165,8 +165,10 @@ actual class Zipline private constructor( eventListener: EventListener = EventListener.NONE, ): Zipline { val quickJs = QuickJs.create() - // TODO(jwilson): figure out a 512 KiB limit caused intermittent stack overflow failures. - quickJs.maxStackSize = 0L + // The default stack size is 256 KiB. QuickJS is not graceful when the stack size is exceeded + // so we set a high limit so it only fails on definitely buggy code, not just recursive code. + // Expect callers to use 8 MiB stack sizes for their calling threads. + quickJs.maxStackSize = 6 * 1024 * 1024L initModuleLoader(quickJs) val scope = CoroutineScope(dispatcher) diff --git a/zipline/src/jniTest/kotlin/app/cash/zipline/ZiplineStackSizeTest.kt b/zipline/src/jniTest/kotlin/app/cash/zipline/ZiplineStackSizeTest.kt new file mode 100644 index 0000000000..00e8e6b27b --- /dev/null +++ b/zipline/src/jniTest/kotlin/app/cash/zipline/ZiplineStackSizeTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package app.cash.zipline + +import app.cash.zipline.testing.EchoRequest +import app.cash.zipline.testing.EchoResponse +import app.cash.zipline.testing.EchoService +import app.cash.zipline.testing.loadTestingJs +import assertk.assertThat +import assertk.assertions.isEqualTo +import java.util.concurrent.Executors +import kotlin.test.assertFailsWith +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Before +import org.junit.Ignore +import org.junit.Test + +class ZiplineStackSizeTest { + /** An executor service that uses 8 MiB thread stacks. */ + private val executorService = Executors.newSingleThreadExecutor { runnable -> + Thread(null, runnable, "Treehouse", 8 * 1024 * 1024) + } + private val dispatcher = executorService.asCoroutineDispatcher() + private lateinit var zipline: Zipline + + @Before + fun setUp() = runBlocking(dispatcher) { + zipline = Zipline.create(dispatcher) + zipline.loadTestingJs() + } + + @After + fun tearDown() = runBlocking(dispatcher) { + zipline.close() + } + + @Test + fun deepRecursionDoesntCrash() { + runBlocking(dispatcher) { + zipline.quickJs.evaluate("testing.app.cash.zipline.testing.prepareRecursingService()") + + val recurseCount = 500 + val service = zipline.take("recursingService") + val echoResponse = service.echo(EchoRequest("$recurseCount")) + assertThat(echoResponse).isEqualTo(EchoResponse("recursed $recurseCount times!")) + } + } + + @Test + @Ignore("https://github.com/cashapp/zipline/issues/1130") + fun veryDeepRecursionFailsGracefully() { + runBlocking(dispatcher) { + zipline.quickJs.evaluate("testing.app.cash.zipline.testing.prepareRecursingService()") + + val recurseCount = 2000 + val service = zipline.take("recursingService") + val e = assertFailsWith { + service.echo(EchoRequest("$recurseCount")) + } + assertThat(e.message).isEqualTo("stack overflow") + } + } +}