diff --git a/modules/openapi-generator-gradle-plugin/README.adoc b/modules/openapi-generator-gradle-plugin/README.adoc index 847428966cd8..73150eee039c 100644 --- a/modules/openapi-generator-gradle-plugin/README.adoc +++ b/modules/openapi-generator-gradle-plugin/README.adoc @@ -440,6 +440,27 @@ apply plugin: 'org.openapi.generator' |false |Defines whether the generator should run in dry-run mode. In dry-run mode no files are written and a summary about file states is output. + +|workerIsolation +|String / Provider +|`process` +a|Controls how the code-generation work action is isolated from the Gradle daemon. + +`classloader` (default):: Runs generation inside the Gradle daemon JVM using a separate `ClassLoader`. Avoids the per-task JVM +startup overhead, but generator classes accumulate in the daemon's Metaspace across tasks and builds. Suitable for +single-module projects or local developer loops where Metaspace pressure is not a concern. + +`process`:: Runs generation in a separate forked JVM. Generator classes are fully unloaded when the worker +exits, preventing Metaspace accumulation in the Gradle daemon. A small per-task JVM startup cost is incurred (~1–2 s +amortized across parallel builds). Recommended for CI/CD and multi-project builds to avoid the +https://docs.gradle.org/current/userguide/build_environment.html#sec:configuring_jvm_memory[metaspace exhaustion +warning]. + +|maxWorkerHeapSize +|String / Provider +|Gradle default (~512 MiB) +|Maximum heap size for the forked worker JVM when `workerIsolation` is `process` (e.g. `"512m"`, `"1g"`). +Has no effect when `workerIsolation` is `classloader`. |=== [NOTE] diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt index 6c2430a2c93b..901383ac6378 100644 --- a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt @@ -157,6 +157,8 @@ class OpenApiGeneratorPlugin : Plugin { engine.set(generate.engine) cleanupOutput.set(generate.cleanupOutput) dryRun.set(generate.dryRun) + workerIsolation.set(generate.workerIsolation) + maxWorkerHeapSize.set(generate.maxWorkerHeapSize) } } } diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGenerateExtension.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGenerateExtension.kt index ec87d07f56e5..556d6acb72a6 100644 --- a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGenerateExtension.kt +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGenerateExtension.kt @@ -409,6 +409,28 @@ open class OpenApiGeneratorGenerateExtension(private val project: Project) { */ val dryRun = project.objects.property() + /** + * Controls how the code generation worker is isolated from the Gradle daemon. + * + * - "classloader" (default): runs inside the Gradle daemon JVM with a separate ClassLoader. No process + * startup overhead, but generator classes accumulate in daemon Metaspace. Suitable for projects + * with very few generation tasks. + * + * - "process": runs in a separate JVM. Metaspace is isolated from the daemon and freed + * when the worker exits. Gradle reuses the worker process across tasks that share the same + * classpath, so the JVM startup cost is typically paid only once per parallel slot. + * Best for projects with many generation tasks. + */ + val workerIsolation = project.objects.property() + + /** + * Maximum heap size for the worker process when [workerIsolation] is "process" (e.g. "512m", "1g"). + * Has no effect when [workerIsolation] is "classloader". + * When not set, the JVM uses ergonomic defaults (typically based on available system memory). + * Only set this if you hit OutOfMemoryError during generation of unusually large specs. + */ + val maxWorkerHeapSize = project.objects.property() + init { applyDefaults() } diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt index 2b9fd47e151d..fa8be13d5cdd 100644 --- a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt @@ -282,6 +282,33 @@ abstract class GenerateTask : DefaultTask() { @get:Inject abstract val layout: ProjectLayout + /** + * Controls how the code generation worker is isolated from the Gradle daemon. + * + * - "process" (default): runs in a separate JVM process. Metaspace is fully isolated from the + * daemon and freed after the process exits. Gradle reuses the worker process across tasks that + * share the same classpath, so the JVM startup cost is paid at most once per parallel slot — + * not once per task. Best for projects with many generation tasks. + * + * - "classloader": runs inside the Gradle daemon JVM using a separate ClassLoader. No process + * startup overhead, but each task loads generator classes into the daemon's Metaspace. With + * many tasks this can exhaust Metaspace. Suitable for projects with very few tasks where the + * daemon memory budget is not a concern. + */ + @get:Optional + @get:Input + abstract val workerIsolation: Property + + /** + * Maximum heap size for the worker process when [workerIsolation] is "process" (e.g. "512m", "1g"). + * Has no effect when [workerIsolation] is "classloader". + * When not set, the JVM uses ergonomic defaults (typically based on available system memory). + * Only set this if you hit OutOfMemoryError during generation of unusually large specs. + */ + @get:Optional + @get:Input + abstract val maxWorkerHeapSize: Property + /** * The verbosity of generation */ @@ -863,8 +890,37 @@ abstract class GenerateTask : DefaultTask() { } } -// Submit generation logic to the isolated Worker API Queue - val workQueue = workerExecutor.classLoaderIsolation() +// Submit generation work using the configured isolation mode. +// "classloader" (default): worker runs inside the Gradle daemon JVM with a separate ClassLoader; no startup +// overhead but generator classes accumulate in daemon Metaspace across all tasks. +// "process": worker runs in a separate JVM; Metaspace is freed after each worker daemon +// exits, and Gradle reuses the same worker daemon across tasks that share the same classpath, +// so startup cost is amortized — typically paid only once per parallel slot. + val isolation = workerIsolation.getOrElse("classloader").lowercase() + val workQueue = when (isolation) { + "process" -> { + val heapMsg = maxWorkerHeapSize.orNull?.let { " (maxHeapSize=$it)" } ?: "" + logger.lifecycle( + "[openApiGenerate] Worker isolation: process$heapMsg " + + "(isolated JVM per task, no Metaspace leak - " + + "use workerIsolation = \"classloader\" to skip per-task JVM startup cost at the cost of increased Metaspace usage)" + ) + workerExecutor.processIsolation { + maxWorkerHeapSize.orNull?.let { forkOptions.maxHeapSize = it } + } + } + + "classloader" -> { + logger.lifecycle( + "[openApiGenerate] Worker isolation: classloader " + + "(fast startup, but generator classes accumulate in Gradle daemon Metaspace - " + + "consider workerIsolation = \"process\" if you hit metaspace pressure)" + ) + workerExecutor.classLoaderIsolation() + } + + else -> throw GradleException("Invalid workerIsolation mode: $isolation. Supported values are 'process' and 'classloader'.") + } workQueue.submit(OpenApiWorkAction::class.java, object : Action { override fun execute(parameters: OpenApiWorkParameters) {