Fix external reader process leak in pkl-server (#1885)

This commit is contained in:
Jen Basch authored and GitHub committed 2026-10-02 14:16:25 -07:00
1 parent 82fe176c69
commit adaafc4045
7 files changed
+107 -38

No files matched your search

+10
View File
@@ -48,6 +48,8 @@ val nativeTestRuntimeOnly: Configuration =
extendsFrom(configurations.testRuntimeOnly.get()) extendsFrom(configurations.testRuntimeOnly.get())
} }
val externalReaderFixtureConfiguration = configurations.create("externalReaderFixture")
dependencies { dependencies {
compileOnly(libs.graalSdk) compileOnly(libs.graalSdk)
@@ -62,6 +64,8 @@ dependencies {
nativeTestImplementation(libs.jna) nativeTestImplementation(libs.jna)
nativeTestImplementation(libs.jnaPlatform) nativeTestImplementation(libs.jnaPlatform)
nativeTestRuntimeOnly("org.junit.platform:junit-platform-launcher") nativeTestRuntimeOnly("org.junit.platform:junit-platform-launcher")
externalReaderFixtureConfiguration(project(":pkl-core", "externalReaderFixture"))
} }
tasks.withType(CCompile::class) { tasks.withType(CCompile::class) {
@@ -344,6 +348,7 @@ val processFiles =
val testNativeJava = val testNativeJava =
tasks.register<Test>("testNativeJava") { tasks.register<Test>("testNativeJava") {
dependsOn(tasks.assembleNative) dependsOn(tasks.assembleNative)
dependsOn(externalReaderFixtureConfiguration)
description = "Test native libraries from Java" description = "Test native libraries from Java"
group = "verification" group = "verification"
@@ -366,6 +371,11 @@ val testNativeJava =
environment("LD_LIBRARY_PATH", buildInfo.targetMachine.libraryDir.get().asFile.absolutePath) environment("LD_LIBRARY_PATH", buildInfo.targetMachine.libraryDir.get().asFile.absolutePath)
useJUnitPlatform() useJUnitPlatform()
systemProperty(
"org.pkl.core.testExternalReaderPath",
externalReaderFixtureConfiguration.files.single().absolutePath,
)
} }
// link to static library // link to static library
@@ -19,6 +19,7 @@ import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.exists import kotlin.io.path.exists
import org.pkl.commons.test.FileTestUtils.rootProjectDir import org.pkl.commons.test.FileTestUtils.rootProjectDir
import org.pkl.core.util.IoUtils
sealed class ExecutablePaths(protected val gradleProject: String) { sealed class ExecutablePaths(protected val gradleProject: String) {
abstract val allNative: List<Path> abstract val allNative: List<Path>
@@ -80,4 +81,20 @@ object Executables {
override val allNative: List<Path> = override val allNative: List<Path> =
listOf(macAarch64, linuxAarch64, linuxAmd64, alpineAmd64, windowsAmd64) listOf(macAarch64, linuxAarch64, linuxAmd64, alpineAmd64, windowsAmd64)
} }
val externalReaderFixture by lazy {
val readerPath =
System.getProperty("org.pkl.core.testExternalReaderPath")
?: "pkl-core/build/fixtures/externalreader"
.let { if (IoUtils.isWindows()) "$it.bat" else it }
rootProjectDir.resolve(readerPath).also { path ->
if (!Files.exists(path)) {
throw AssertionError(
"Fixture `${readerPath.substringAfterLast('/')}` not found. To fix this problem, first run" +
" `./gradlew pkl-core:externalReaderFixture`."
)
}
}
}
} }
@@ -35,6 +35,7 @@ import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir import org.junit.jupiter.api.io.TempDir
import org.msgpack.core.MessagePack import org.msgpack.core.MessagePack
import org.pkl.commons.test.Executables
import org.pkl.commons.test.PackageServer import org.pkl.commons.test.PackageServer
import org.pkl.commons.test.debugRendering import org.pkl.commons.test.debugRendering
import org.pkl.core.messaging.Messages.* import org.pkl.core.messaging.Messages.*
@@ -238,9 +239,6 @@ abstract class AbstractServerTest {
assertThat(value.asStringValue().asString()).isEqualTo("my bahumbug") assertThat(value.asStringValue().asString()).isEqualTo("my bahumbug")
} }
@Disabled(
"Unable to construct ReadResourceResponse with null contents due to Kotlin compiler bug"
)
@Test @Test
fun `read resource -- null contents and null error`() { fun `read resource -- null contents and null error`() {
val reader = ResourceReaderSpec("bahumbug", true, false) val reader = ResourceReaderSpec("bahumbug", true, false)
@@ -260,18 +258,10 @@ abstract class AbstractServerTest {
assertThat(readResourceMsg.uri.toString()).isEqualTo("bahumbug:/foo.pkl") assertThat(readResourceMsg.uri.toString()).isEqualTo("bahumbug:/foo.pkl")
assertThat(readResourceMsg.evaluatorId).isEqualTo(evaluatorId) assertThat(readResourceMsg.evaluatorId).isEqualTo(evaluatorId)
client.send(ReadResourceResponse(readResourceMsg.requestId, evaluatorId, byteArrayOf(), null)) client.send(ReadResourceResponse(readResourceMsg.requestId, evaluatorId, null, null))
// for this test to be correct this should actually be:
// client.send(ReadResourceResponse(readResourceMsg.requestId, evaluatorId, null, null))
// this should be evaluated again once https://github.com/apple/pkl/issues/698 is addressed
// see conversation here https://github.com/apple/pkl/pull/660#discussion_r1819545811
val evaluateResponse = client.receive<EvaluateResponse>() val evaluateResponse = client.receive<EvaluateResponse>()
assertThat(evaluateResponse.error).isNull() assertThat(evaluateResponse.error).contains("Cannot find resource `bahumbug:/foo.pkl`.")
val unpacker = MessagePack.newDefaultUnpacker(evaluateResponse.result)
val value = unpacker.unpackValue()
assertThat(value.asStringValue().asString()).isEqualTo("")
} }
@Test @Test
@@ -1133,6 +1123,40 @@ abstract class AbstractServerTest {
.contains("Rewrite rule must end with '/', but was 'https://example.com'") .contains("Rewrite rule must end with '/', but was 'https://example.com'")
} }
@Test
fun `test external reader`() {
val evaluatorId =
client.sendCreateEvaluatorRequest(
externalResourceReaders =
mapOf(
"foo" to ExternalReader(Executables.externalReaderFixture.toString(), emptyList(), null)
)
)
val requestId = 234L
client.send(
EvaluateRequest(
requestId,
evaluatorId,
URI("repl:text"),
"""
res = read("foo:bar")
"""
.trimIndent(),
"res.text",
)
)
val response = client.receive<EvaluateResponse>()
assertThat(response.error).isNull()
assertThat(response.result).isNotNull
assertThat(response.requestId()).isEqualTo(requestId)
val unpacker = MessagePack.newDefaultUnpacker(response.result)
val value = unpacker.unpackValue()
assertThat(value.asStringValue().asString()).isEqualTo("hello")
}
protected fun TestTransport.sendCreateEvaluatorRequest( protected fun TestTransport.sendCreateEvaluatorRequest(
requestId: Long = 123, requestId: Long = 123,
resourceReaders: List<ResourceReaderSpec> = listOf(), resourceReaders: List<ResourceReaderSpec> = listOf(),
@@ -1141,6 +1165,8 @@ abstract class AbstractServerTest {
project: Project? = null, project: Project? = null,
cacheDir: Path? = null, cacheDir: Path? = null,
http: Http? = null, http: Http? = null,
externalResourceReaders: Map<String, ExternalReader>? = null,
externalModuleReaders: Map<String, ExternalReader>? = null,
): Long { ): Long {
val message = val message =
CreateEvaluatorRequest( CreateEvaluatorRequest(
@@ -1158,8 +1184,8 @@ abstract class AbstractServerTest {
null, null,
project, project,
http, http,
null, externalModuleReaders,
null, externalResourceReaders,
null, null,
) )
+10 -1
View File
@@ -40,6 +40,8 @@ val externalReaderFixtureImplementation: Configuration =
extendsFrom(configurations.testImplementation.get()) extendsFrom(configurations.testImplementation.get())
} }
val externalReaderFixtureConfiguration = configurations.create("externalReaderFixture")
idea { idea {
module { module {
// mark generated/truffle as generated source dir // mark generated/truffle as generated source dir
@@ -170,6 +172,13 @@ val externalReaderFixture =
} }
} }
artifacts {
add(
externalReaderFixtureConfiguration.name,
externalReaderFixture.map { it.outputs.files.singleFile },
)
}
tasks.test { tasks.test {
configureTest() configureTest()
dependsOn(externalReaderFixture) dependsOn(externalReaderFixture)
@@ -191,7 +200,7 @@ tasks.test {
systemProperty( systemProperty(
"org.pkl.core.testExternalReaderPath", "org.pkl.core.testExternalReaderPath",
externalReaderFixture.map { it.outputs.files.singleFile.absolutePath }, externalReaderFixture.map { it.outputs.files.singleFile.absolutePath }.get(),
) )
} }
@@ -17,7 +17,6 @@ package org.pkl.core.module
import java.io.File import java.io.File
import java.net.URI import java.net.URI
import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.io.path.createDirectories import kotlin.io.path.createDirectories
@@ -27,7 +26,7 @@ import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assumptions.assumeTrue import org.junit.jupiter.api.Assumptions.assumeTrue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir import org.junit.jupiter.api.io.TempDir
import org.pkl.commons.test.FileTestUtils import org.pkl.commons.test.Executables
import org.pkl.commons.toPath import org.pkl.commons.toPath
import org.pkl.commons.writeString import org.pkl.commons.writeString
import org.pkl.core.EvaluatorBuilder import org.pkl.core.EvaluatorBuilder
@@ -38,29 +37,14 @@ import org.pkl.core.externalreader.ExternalReaderProcess
import org.pkl.core.externalreader.TestExternalModuleReader import org.pkl.core.externalreader.TestExternalModuleReader
import org.pkl.core.externalreader.TestExternalReaderProcess import org.pkl.core.externalreader.TestExternalReaderProcess
import org.pkl.core.resource.ResourceReaders import org.pkl.core.resource.ResourceReaders
import org.pkl.core.util.IoUtils
class ModuleKeyFactoriesTest { class ModuleKeyFactoriesTest {
companion object { companion object {
private val externalReaderFixture by lazy {
val readerPath =
"pkl-core/build/fixtures/externalreader".let { if (IoUtils.isWindows()) "$it.bat" else it }
FileTestUtils.rootProjectDir.resolve(readerPath).also { path ->
if (!Files.exists(path)) {
throw AssertionError(
"Fixture `externalreader` not found. To fix this problem, first run" +
" `./gradlew pkl-core:externalReaderFixture`."
)
}
}
}
@JvmStatic @JvmStatic
private fun pathEnvIsSet(): Boolean { private fun pathEnvIsSet(): Boolean {
return System.getenv("PATH") return System.getenv("PATH")
?.split(File.pathSeparator) ?.split(File.pathSeparator)
?.contains(externalReaderFixture.toAbsolutePath().toString()) ?: false ?.contains(Executables.externalReaderFixture.toAbsolutePath().toString()) ?: false
} }
} }
@@ -184,7 +168,7 @@ class ModuleKeyFactoriesTest {
@Test @Test
fun `external process -- spawning an executable using a path`() { fun `external process -- spawning an executable using a path`() {
testExternalReader(externalReaderFixture.toAbsolutePath().toString()) testExternalReader(Executables.externalReaderFixture.toAbsolutePath().toString())
} }
@Test @Test
+23 -1
View File
@@ -19,6 +19,8 @@ plugins {
id("pklNativeLifecycle") id("pklNativeLifecycle")
} }
val externalReaderFixtureConfiguration = configurations.create("externalReaderFixture")
dependencies { dependencies {
implementation(projects.pklCore) implementation(projects.pklCore)
implementation(libs.msgpack) implementation(libs.msgpack)
@@ -26,14 +28,34 @@ dependencies {
testImplementation(projects.pklCommonsTest) testImplementation(projects.pklCommonsTest)
testImplementation(libs.wiremock) testImplementation(libs.wiremock)
externalReaderFixtureConfiguration(project(":pkl-core", "externalReaderFixture"))
} }
tasks.test { exclude("**/NativeServerTest.*") } tasks.test {
configureTest()
exclude("**/NativeServerTest.*")
}
private fun Test.configureTest() {
dependsOn(externalReaderFixtureConfiguration)
systemProperty(
"org.pkl.core.testExternalReaderPath",
externalReaderFixtureConfiguration.files.single().absolutePath,
)
}
private fun Test.configureNativeTest() { private fun Test.configureNativeTest() {
configureTest()
testClassesDirs = files(tasks.test.get().testClassesDirs) testClassesDirs = files(tasks.test.get().testClassesDirs)
classpath = tasks.test.get().classpath classpath = tasks.test.get().classpath
include("**/NativeServerTest.*") include("**/NativeServerTest.*")
systemProperty(
"org.pkl.core.testExternalReaderPath",
externalReaderFixtureConfiguration.files.single().absolutePath,
)
} }
val testMacExecutableAarch64 = val testMacExecutableAarch64 =
@@ -89,9 +89,10 @@ class Server(private val transport: MessageTransport) : AutoCloseable {
*/ */
override fun close() { override fun close() {
transport.closeQuietly() transport.closeQuietly()
for ((_, evaluator) in evaluators) { for ((evaluatorId, evaluator) in evaluators) {
// if currently in use, blocks until cancellation complete // if currently in use, blocks until cancellation complete
evaluator.closeQuietly() evaluator.closeQuietly()
externalReaderProcesses[evaluatorId]?.values?.forEach { it.closeQuietly() }
} }
executor.shutdown() executor.shutdown()
} }
@@ -145,7 +146,7 @@ class Server(private val transport: MessageTransport) : AutoCloseable {
evaluator.close() evaluator.close()
// close any running ExternalProcess instances for the closed evaluator // close any running ExternalProcess instances for the closed evaluator
externalReaderProcesses[message.evaluatorId]?.values?.forEach { it.close() } externalReaderProcesses.remove(message.evaluatorId)?.values?.forEach { it.close() }
} }
private fun buildDeclaredDependencies( private fun buildDeclaredDependencies(