Skip to content

Commit 9faa37f

Browse files
authored
[GRADLE-WRAPPER] feat: add configurable worker isolation and max heap size for code generation (#23648)
* feat: add configurable worker isolation and max heap size for code generation * implement feedback from CR
1 parent 5acb7cc commit 9faa37f

4 files changed

Lines changed: 103 additions & 2 deletions

File tree

modules/openapi-generator-gradle-plugin/README.adoc

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,27 @@ apply plugin: 'org.openapi.generator'
440440
|false
441441
|Defines whether the generator should run in dry-run mode. In dry-run mode no files are written and a summary about
442442
file states is output.
443+
444+
|workerIsolation
445+
|String / Provider<String>
446+
|`process`
447+
a|Controls how the code-generation work action is isolated from the Gradle daemon.
448+
449+
`classloader` (default):: Runs generation inside the Gradle daemon JVM using a separate `ClassLoader`. Avoids the per-task JVM
450+
startup overhead, but generator classes accumulate in the daemon's Metaspace across tasks and builds. Suitable for
451+
single-module projects or local developer loops where Metaspace pressure is not a concern.
452+
453+
`process`:: Runs generation in a separate forked JVM. Generator classes are fully unloaded when the worker
454+
exits, preventing Metaspace accumulation in the Gradle daemon. A small per-task JVM startup cost is incurred (~1–2 s
455+
amortized across parallel builds). Recommended for CI/CD and multi-project builds to avoid the
456+
https://docs.gradle.org/current/userguide/build_environment.html#sec:configuring_jvm_memory[metaspace exhaustion
457+
warning].
458+
459+
|maxWorkerHeapSize
460+
|String / Provider<String>
461+
|Gradle default (~512 MiB)
462+
|Maximum heap size for the forked worker JVM when `workerIsolation` is `process` (e.g. `"512m"`, `"1g"`).
463+
Has no effect when `workerIsolation` is `classloader`.
443464
|===
444465

445466
[NOTE]

modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ class OpenApiGeneratorPlugin : Plugin<Project> {
157157
engine.set(generate.engine)
158158
cleanupOutput.set(generate.cleanupOutput)
159159
dryRun.set(generate.dryRun)
160+
workerIsolation.set(generate.workerIsolation)
161+
maxWorkerHeapSize.set(generate.maxWorkerHeapSize)
160162
}
161163
}
162164
}

modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGenerateExtension.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,28 @@ open class OpenApiGeneratorGenerateExtension(private val project: Project) {
409409
*/
410410
val dryRun = project.objects.property<Boolean>()
411411

412+
/**
413+
* Controls how the code generation worker is isolated from the Gradle daemon.
414+
*
415+
* - "classloader" (default): runs inside the Gradle daemon JVM with a separate ClassLoader. No process
416+
* startup overhead, but generator classes accumulate in daemon Metaspace. Suitable for projects
417+
* with very few generation tasks.
418+
*
419+
* - "process": runs in a separate JVM. Metaspace is isolated from the daemon and freed
420+
* when the worker exits. Gradle reuses the worker process across tasks that share the same
421+
* classpath, so the JVM startup cost is typically paid only once per parallel slot.
422+
* Best for projects with many generation tasks.
423+
*/
424+
val workerIsolation = project.objects.property<String>()
425+
426+
/**
427+
* Maximum heap size for the worker process when [workerIsolation] is "process" (e.g. "512m", "1g").
428+
* Has no effect when [workerIsolation] is "classloader".
429+
* When not set, the JVM uses ergonomic defaults (typically based on available system memory).
430+
* Only set this if you hit OutOfMemoryError during generation of unusually large specs.
431+
*/
432+
val maxWorkerHeapSize = project.objects.property<String>()
433+
412434
init {
413435
applyDefaults()
414436
}

modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,33 @@ abstract class GenerateTask : DefaultTask() {
282282
@get:Inject
283283
abstract val layout: ProjectLayout
284284

285+
/**
286+
* Controls how the code generation worker is isolated from the Gradle daemon.
287+
*
288+
* - "process" (default): runs in a separate JVM process. Metaspace is fully isolated from the
289+
* daemon and freed after the process exits. Gradle reuses the worker process across tasks that
290+
* share the same classpath, so the JVM startup cost is paid at most once per parallel slot —
291+
* not once per task. Best for projects with many generation tasks.
292+
*
293+
* - "classloader": runs inside the Gradle daemon JVM using a separate ClassLoader. No process
294+
* startup overhead, but each task loads generator classes into the daemon's Metaspace. With
295+
* many tasks this can exhaust Metaspace. Suitable for projects with very few tasks where the
296+
* daemon memory budget is not a concern.
297+
*/
298+
@get:Optional
299+
@get:Input
300+
abstract val workerIsolation: Property<String>
301+
302+
/**
303+
* Maximum heap size for the worker process when [workerIsolation] is "process" (e.g. "512m", "1g").
304+
* Has no effect when [workerIsolation] is "classloader".
305+
* When not set, the JVM uses ergonomic defaults (typically based on available system memory).
306+
* Only set this if you hit OutOfMemoryError during generation of unusually large specs.
307+
*/
308+
@get:Optional
309+
@get:Input
310+
abstract val maxWorkerHeapSize: Property<String>
311+
285312
/**
286313
* The verbosity of generation
287314
*/
@@ -863,8 +890,37 @@ abstract class GenerateTask : DefaultTask() {
863890
}
864891
}
865892

866-
// Submit generation logic to the isolated Worker API Queue
867-
val workQueue = workerExecutor.classLoaderIsolation()
893+
// Submit generation work using the configured isolation mode.
894+
// "classloader" (default): worker runs inside the Gradle daemon JVM with a separate ClassLoader; no startup
895+
// overhead but generator classes accumulate in daemon Metaspace across all tasks.
896+
// "process": worker runs in a separate JVM; Metaspace is freed after each worker daemon
897+
// exits, and Gradle reuses the same worker daemon across tasks that share the same classpath,
898+
// so startup cost is amortized — typically paid only once per parallel slot.
899+
val isolation = workerIsolation.getOrElse("classloader").lowercase()
900+
val workQueue = when (isolation) {
901+
"process" -> {
902+
val heapMsg = maxWorkerHeapSize.orNull?.let { " (maxHeapSize=$it)" } ?: ""
903+
logger.lifecycle(
904+
"[openApiGenerate] Worker isolation: process$heapMsg " +
905+
"(isolated JVM per task, no Metaspace leak - " +
906+
"use workerIsolation = \"classloader\" to skip per-task JVM startup cost at the cost of increased Metaspace usage)"
907+
)
908+
workerExecutor.processIsolation {
909+
maxWorkerHeapSize.orNull?.let { forkOptions.maxHeapSize = it }
910+
}
911+
}
912+
913+
"classloader" -> {
914+
logger.lifecycle(
915+
"[openApiGenerate] Worker isolation: classloader " +
916+
"(fast startup, but generator classes accumulate in Gradle daemon Metaspace - " +
917+
"consider workerIsolation = \"process\" if you hit metaspace pressure)"
918+
)
919+
workerExecutor.classLoaderIsolation()
920+
}
921+
922+
else -> throw GradleException("Invalid workerIsolation mode: $isolation. Supported values are 'process' and 'classloader'.")
923+
}
868924

869925
workQueue.submit(OpenApiWorkAction::class.java, object : Action<OpenApiWorkParameters> {
870926
override fun execute(parameters: OpenApiWorkParameters) {

0 commit comments

Comments
 (0)