Skip to content

Commit ab16391

Browse files
committed
feat: add configurable worker isolation and max heap size for code generation
1 parent 878d821 commit ab16391

4 files changed

Lines changed: 101 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+
`process` (default):: Runs generation in a separate forked JVM. Generator classes are fully unloaded when the worker
450+
exits, preventing Metaspace accumulation in the Gradle daemon. A small per-task JVM startup cost is incurred (~1–2 s
451+
amortized across parallel builds). Recommended for CI/CD and multi-project builds to avoid the
452+
https://docs.gradle.org/current/userguide/build_environment.html#sec:configuring_jvm_memory[metaspace exhaustion
453+
warning].
454+
455+
`classloader`:: Runs generation inside the Gradle daemon JVM using a separate `ClassLoader`. Avoids the per-task JVM
456+
startup overhead, but generator classes accumulate in the daemon's Metaspace across tasks and builds. Suitable for
457+
single-module projects or local developer loops where Metaspace pressure is not a concern.
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+
* - "process" (default): runs in a separate JVM. Metaspace is isolated from the daemon and freed
416+
* when the worker exits. Gradle reuses the worker process across tasks that share the same
417+
* classpath, so the JVM startup cost is typically paid only once per parallel slot.
418+
* Best for projects with many generation tasks.
419+
*
420+
* - "classloader": runs inside the Gradle daemon JVM with a separate ClassLoader. No process
421+
* startup overhead, but generator classes accumulate in daemon Metaspace. Suitable for projects
422+
* with very few 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: 56 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,35 @@ 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+
// "process" (default): worker runs in a separate JVM; Metaspace is freed after each worker daemon
895+
// exits, and Gradle reuses the same worker daemon across tasks that share the same classpath,
896+
// so startup cost is amortized — typically paid only once per parallel slot.
897+
// "classloader": worker runs inside the Gradle daemon JVM with a separate ClassLoader; no startup
898+
// overhead but generator classes accumulate in daemon Metaspace across all tasks.
899+
val isolation = workerIsolation.getOrElse("process").lowercase()
900+
val workQueue = when (isolation) {
901+
"classloader" -> {
902+
logger.lifecycle(
903+
"[openApiGenerate] Worker isolation: classloader " +
904+
"(fast startup, but generator classes accumulate in Gradle daemon Metaspace - " +
905+
"consider workerIsolation = \"process\" if you hit metaspace pressure)"
906+
)
907+
workerExecutor.classLoaderIsolation()
908+
}
909+
910+
else -> {
911+
val heapMsg = maxWorkerHeapSize.orNull?.let { " (maxHeapSize=$it)" } ?: ""
912+
logger.lifecycle(
913+
"[openApiGenerate] Worker isolation: process$heapMsg " +
914+
"(isolated JVM per task, no Metaspace leak - " +
915+
"use workerIsolation = \"classloader\" to skip per-task JVM startup cost at the cost of increased Metaspace usage)"
916+
)
917+
workerExecutor.processIsolation {
918+
maxWorkerHeapSize.orNull?.let { forkOptions.maxHeapSize = it }
919+
}
920+
}
921+
}
868922

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

0 commit comments

Comments
 (0)