pkl-doc: Fix/improve Executor handling in DocGenerator (#1590)

run() now creates and closes a default Executor per call. This is fine
because there is no good reason to call this method multiple times.

run(Executor) now lets callers provide their own Executor, which is
customary for a well-behaved library.

Also: Fix IntelliJ warning by calling toSet()

Closes #1583
This commit is contained in:
odenix
2026-05-14 20:02:23 +02:00
committed by GitHub
parent 6171dbde28
commit 1b6e89c971
2 changed files with 48 additions and 37 deletions
@@ -22,7 +22,9 @@ import java.net.URI
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.util.concurrent.Executor import java.util.concurrent.Executor
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import kotlin.io.path.* import kotlin.io.path.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.pkl.commons.copyRecursively import org.pkl.commons.copyRecursively
@@ -107,18 +109,10 @@ class DocGenerator(
.toList() .toList()
} }
/** internal fun createDefaultExecutor(): ExecutorService {
* The default executor when running the doc generator.
*
* Uses [Executors.newVirtualThreadPerTaskExecutor] if available (JDK 21). Otherwise, uses
* [Executors.newFixedThreadPool] with 64 threads, or the number of available processors
* available to the JVM (whichever is higher).
*/
internal val executor: Executor
get() {
try { try {
val method = Executors::class.java.getMethod("newVirtualThreadPerTaskExecutor") val method = Executors::class.java.getMethod("newVirtualThreadPerTaskExecutor")
return method.invoke(null) as Executor return method.invoke(null) as ExecutorService
} catch (e: Throwable) { } catch (e: Throwable) {
when (e) { when (e) {
is NoSuchMethodException, is NoSuchMethodException,
@@ -127,6 +121,7 @@ class DocGenerator(
return Executors.newFixedThreadPool( return Executors.newFixedThreadPool(
64.coerceAtLeast(Runtime.getRuntime().availableProcessors()) 64.coerceAtLeast(Runtime.getRuntime().availableProcessors())
) )
else -> throw e else -> throw e
} }
} }
@@ -163,8 +158,31 @@ class DocGenerator(
siteSearchIndex.map { async { tryLoadPackageData(it) } }.awaitAll().filterNotNull() siteSearchIndex.map { async { tryLoadPackageData(it) } }.awaitAll().filterNotNull()
} }
/** Runs this documentation generator. */ /**
fun run() = * Runs this documentation generator with a default executor created and closed during the run.
*
* On JDK 21+, the default executor is [Executors.newVirtualThreadPerTaskExecutor]. On earlier JDK
* versions, it is [Executors.newFixedThreadPool] with either 64 threads or the number of
* processors available to the JVM, whichever is greater.
*/
fun run() {
val executor = createDefaultExecutor()
try {
run(executor)
} finally {
// can't use close() because we compile with --release 17
executor.shutdown()
try {
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS)
} catch (e: InterruptedException) {
executor.shutdownNow()
throw e
}
}
}
/** Runs this documentation generator with the given executor. */
fun run(executor: Executor) =
runBlocking(executor.asCoroutineDispatcher()) { runBlocking(executor.asCoroutineDispatcher()) {
try { try {
if (!docMigrator.isUpToDate) { if (!docMigrator.isUpToDate) {
@@ -230,7 +248,7 @@ class DocGenerator(
currentPackages: List<PackageData>, currentPackages: List<PackageData>,
existingCurrentPackages: List<PackageData>, existingCurrentPackages: List<PackageData>,
) { ) {
val packagesToCreate = currentPackages - existingCurrentPackages val packagesToCreate = currentPackages - existingCurrentPackages.toSet()
for (packageData in packagesToCreate) { for (packageData in packagesToCreate) {
val basePath = outputDir.resolve(packageData.ref.pkg.pathEncoded) val basePath = outputDir.resolve(packageData.ref.pkg.pathEncoded)
val src = basePath.resolve(packageData.ref.version) val src = basePath.resolve(packageData.ref.version)
@@ -17,22 +17,15 @@ package org.pkl.doc
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.EnabledIf import org.junit.jupiter.api.condition.EnabledForJreRange
import org.junit.jupiter.api.condition.JRE
class DocGeneratorTest { class DocGeneratorTest {
companion object {
@JvmStatic
fun isJdk21OrLater(): Boolean {
return Runtime.version().feature() >= 21
}
}
@Test @Test
@EnabledIf("isJdk21OrLater") @EnabledForJreRange(min = JRE.JAVA_21)
fun `uses virtual thread executor on JDK 21`() { fun `uses virtual thread executor on JDK 21+`() {
// On older JDKs, we get a ThreadPoolExecutor. // On older JDKs, we get a ThreadPoolExecutor.
// not sure if there's a better assertion to make here. assertThat(DocGenerator.createDefaultExecutor().javaClass.canonicalName)
assertThat(DocGenerator.executor.javaClass.canonicalName)
.isEqualTo("java.util.concurrent.ThreadPerTaskExecutor") .isEqualTo("java.util.concurrent.ThreadPerTaskExecutor")
} }
} }