Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions modules/openapi-generator-gradle-plugin/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>
|`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<String>
|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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ class OpenApiGeneratorPlugin : Plugin<Project> {
engine.set(generate.engine)
cleanupOutput.set(generate.cleanupOutput)
dryRun.set(generate.dryRun)
workerIsolation.set(generate.workerIsolation)
maxWorkerHeapSize.set(generate.maxWorkerHeapSize)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,28 @@ open class OpenApiGeneratorGenerateExtension(private val project: Project) {
*/
val dryRun = project.objects.property<Boolean>()

/**
* 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<String>()

/**
* 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<String>()

init {
applyDefaults()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>

/**
* 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<String>

/**
* The verbosity of generation
*/
Expand Down Expand Up @@ -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<OpenApiWorkParameters> {
override fun execute(parameters: OpenApiWorkParameters) {
Expand Down
Loading