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
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,39 @@ grouped by date and milestone rather than semantic version. The content tracks
the [readingbat-core](https://github.com/readingbat/readingbat-core) platform, so
many entries reflect dependency and toolchain upgrades.

## [1.2.0] - 2026-10-13

Dependency upkeep, and the memory fix that the new Kotlin scripting runtime made
necessary. No challenge content changed.

### Changed
- Bumped `version` in `gradle.properties` to `1.2.0`.

- Upgraded dependencies: readingbat-core `3.4.0` → `3.5.0`, Kotlin `2.4.10` → `2.4.21`,
common-utils `3.2.2` → `5.1.1`, and the Gradle versions plugin `0.64.0` → `0.66.0`.
Ktor (`3.6.0`), Kotest (`6.2.5`), kotlinter (`5.7.0`), detekt (`2.0.0-alpha.6`), and the
JVM toolchain (**25**) are unchanged.

- Upgraded the Gradle wrapper `9.7.1` → `9.8.1`, which also regenerates `gradlew.bat`.

- `gradle.properties` turns the Gradle configuration cache on, and gives the Gradle and
Kotlin daemons a 6 GB heap with `-XX:+HeapDumpOnOutOfMemoryError`.

- `make versions` now prints only the dependencies with newer versions, any that
could not be checked, and a pending Gradle upgrade, via a custom `outputFormatter`
on `dependencyUpdates` in `build.gradle.kts`. The new `make versions-all` target
passes `--output-formatter=plain` for the plugin's full report, which also lists
every dependency already up to date.

- Held the runtime Kotlin scripting jars (`kotlin-scripting-*`, `kotlin-compiler-embeddable`,
`kotlin-daemon-embeddable`, `kotlin-script-runtime`) at `2.4.10` through a new
`kotlin-scripting` catalog version, applied by `configureScripting()` to the runtime and
test classpaths only; the project itself builds with Kotlin `2.4.21`. From Kotlin
`2.4.20` on, the JSR-223 engine never disposes a script state's compiler environment, so
each state keeps about 27 MB reachable. readingbat-core `3.5.0`, which brings in the
`2.4.21` scripting jars, resets its pooled engine after every Kotlin challenge, so
`ContentTests` ran out of heap partway through the Kotlin sweep, even with a 2 GB heap.

## [1.1.0] - 2026-09-21

Test-reliability and dependency upkeep. No challenge content changed.
Expand Down
19 changes: 18 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This is a **ReadingBat content repository** — it defines Java and Kotlin progr
## Conventions

Record notable changes in `CHANGELOG.md` under an `[Unreleased]` heading; it becomes the
next version's section at release time. Releases are tagged without a `v` prefix (`1.1.0`),
next version's section at release time. Releases are tagged without a `v` prefix (`1.2.0`),
and the version lives in `gradle.properties`.

`.gitattributes` governs line endings: stored blobs are LF, `gradlew` stays LF, and
Expand All @@ -21,6 +21,23 @@ and the version lives in `gradle.properties`.
./gradlew test -Dkotest.filter.tests="<name>" # Filter Kotest cases by name
```

`gradle.properties` turns the configuration cache on, so build logic has to be
configuration-cache safe. `dependencyUpdates` is marked incompatible, and the `make versions`
targets pass `--no-configuration-cache` for it.

## Dependencies

Every version lives in `gradle/libs.versions.toml`. One is deliberately out of step:
`kotlin-scripting` holds the runtime Kotlin scripting jars (`kotlin-scripting-*`,
`kotlin-compiler-embeddable`, `kotlin-daemon-embeddable`, `kotlin-script-runtime`) at 2.4.10
while `kotlin` moves on, and `configureScripting()` in `build.gradle.kts` applies it to
`runtimeClasspath` and `testRuntimeClasspath` only. From Kotlin 2.4.20 the JSR-223 engine
never disposes a script state's compiler environment (about 27 MB each), and readingbat-core
starts a state for every Kotlin challenge, so without the pin `ContentTests` runs out of heap
in the Kotlin sweep. Leave it in place when upgrading Kotlin. Remove it only after a Kotlin
release fixes the engine or readingbat-core pins the jars itself, and confirm with
`make clean tests`.

## Testing

`src/test/kotlin/ContentTests.kt` sweeps every challenge through a Ktor test host, one
Expand Down
12 changes: 10 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.PHONY: default help clean build lint format detekt detekt-baseline tests uberjar uber \
cc run versions upgrade-wrapper
cc run versions versions-all upgrade-wrapper

GRADLE_VERSION := $(shell sed -n 's/^gradle-wrapper = "\(.*\)"/\1/p' gradle/libs.versions.toml)

Expand Down Expand Up @@ -42,9 +42,17 @@ cc: ## Continuous compilation (watches for changes)
run: ## Start the content server locally (port 8080)
./gradlew run

versions: ## Check for dependency updates
# dependencyUpdates is marked incompatible with the configuration cache (build.gradle.kts),
# which gradle.properties turns on, so --no-configuration-cache skips the cache for these
# runs rather than building an entry only to discard it. build.gradle.kts gives the task a
# formatter that lists only what is behind; versions-all names the plugin's own plain
# report, which also lists everything up to date.
versions: ## List the dependencies with newer versions, and any that could not be checked
./gradlew dependencyUpdates --no-configuration-cache --no-parallel

versions-all: ## The plugin's full dependency report, up-to-date dependencies included
./gradlew dependencyUpdates --no-configuration-cache --no-parallel --output-formatter=plain

upgrade-wrapper: _require-gradle-version ## Upgrade the Gradle wrapper to the catalog version
# Gradle's documented upgrade procedure: the first run rewrites
# gradle-wrapper.properties using the *old* wrapper jar; the second run
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ make cc # Continuous compilation (watches for changes)
make run # Start the content server locally (port 8080)
make uberjar # Build fat jar at build/libs/server.jar
make uber # Build the uberjar and run it
make versions # Check for dependency updates
make versions # List dependencies with newer versions
make versions-all # Full dependency report, up-to-date dependencies included
```

When running locally, challenge files are loaded directly from disk (via
Expand Down Expand Up @@ -102,7 +103,11 @@ gradle/libs.versions.toml # Centralized dependency & toolchain versions
## Tech Stack

- Kotlin / JVM, Gradle (Kotlin DSL) with a version catalog
- [readingbat-core](https://github.com/readingbat/readingbat-core) — the platform library (from JitPack)
- [readingbat-core](https://github.com/readingbat/readingbat-core) — the platform library (from Maven Central)
- Kotlin's JSR-223 scripting runtime, which computes the Kotlin challenges' answers, is held at
2.4.10 by the `kotlin-scripting` catalog version while the project builds with a newer Kotlin.
From 2.4.20 the engine never releases a script's compiler environment (about 27 MB each), and
the challenge sweep runs out of heap; see `configureScripting()` in `build.gradle.kts`.
- Ktor test host + Kotest — challenge validation
- detekt + kotlinter — static analysis and formatting

Expand Down
34 changes: 34 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,40 @@ changes, see [CHANGELOG.md](CHANGELOG.md).

---

## v1.2.0 (October 2026)

A dependency release that turned into a memory investigation. Moving to readingbat-core
3.5.0 and Kotlin 2.4.21 made `ContentTests` fail partway through the Kotlin sweep with
`OutOfMemoryError`, and a larger test heap did not help: with 2 GB, full collections
freed almost nothing, and the heap stayed full.

The cause is a regression in Kotlin's JSR-223 script engine, which readingbat-core uses
to compute each Kotlin challenge's answers. From Kotlin 2.4.20 the engine is built on the
K2 REPL, and every new script state builds a complete compiler environment (a project,
FIR sessions, and an index of the whole classpath) that is never disposed, so each one
stays reachable for the life of the JVM, about 27 MB apiece here. readingbat-core resets
its pooled engine after every challenge, which starts a new state, so the sweep piled up
an environment per Kotlin challenge. The 2.4.10 engine released the same states, and
creating a fresh engine per script leaks just as badly on 2.4.21, so this is Kotlin's,
not the pooling's.

The fix keeps the runtime scripting jars on 2.4.10 while everything else moves forward.
A new `kotlin-scripting` catalog version is applied by `configureScripting()` to the
runtime and test classpaths only, so the project still compiles with Kotlin 2.4.21 and
challenge code is compiled by the 2.4.10 engine. With the pin in place the full suite
passes in about 30 seconds. The pin is temporary: it can go once a Kotlin release disposes
the state, or once readingbat-core carries the same pin itself.

Two build changes ship alongside. `make versions` now lists only what is behind (plus
anything it could not check and a pending Gradle upgrade), and `make versions-all` keeps
the versions plugin's full report. `gradle.properties` turns the Gradle configuration
cache on and gives the Gradle and Kotlin daemons a 6 GB heap with heap dumps on
`OutOfMemoryError`.

Dependencies: readingbat-core 3.5.0, Kotlin 2.4.21 (scripting runtime held at 2.4.10),
common-utils 5.1.1, the versions plugin 0.66.0, and the Gradle wrapper 9.8.1. No
challenge content changed in this release.

## v1.1.0 (September 2026)

A test-reliability release, prompted by a CI failure on a pull request that contained no
Expand Down
41 changes: 41 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ tasks.named("build") {
}

configureKotlin()
configureScripting()
configureDetekt()
configureKotlinter()
configureKtor()
Expand All @@ -61,6 +62,28 @@ fun Project.configureKotlin() {
}
}

fun Project.configureScripting() {
// Kotlin 2.4.20 and later leave each JSR-223 script state's compiler environment
// reachable (about 27 MB apiece) once the engine moves to a new one. readingbat-core
// resets its pooled engines after every Kotlin challenge it evaluates, so the
// ContentTests sweep exhausts any test heap. readingbat-core brings in the scripting
// jars at its own Kotlin version; this holds them at one that releases the state. Only
// the runtime classpaths are pinned: the compiler that builds this project is untouched.
val scriptingVersion = libs.versions.kotlin.scripting.get()
val compilerModules = setOf("kotlin-compiler-embeddable", "kotlin-daemon-embeddable", "kotlin-script-runtime")

configurations.matching { it.name in setOf("runtimeClasspath", "testRuntimeClasspath") }.configureEach {
resolutionStrategy.eachDependency {
if (requested.group == "org.jetbrains.kotlin" &&
(requested.name.startsWith("kotlin-scripting-") || requested.name in compilerModules)
) {
useVersion(scriptingVersion)
because("Kotlin 2.4.20+ scripting retains every script state's compiler environment")
}
}
}
}

fun Project.configureDetekt() {
detekt {
source.setFrom("src/main/kotlin", "src/test/kotlin")
Expand Down Expand Up @@ -124,5 +147,23 @@ fun Project.configureVersions() {
rejectVersionIf {
isNonStable(candidate.version) && !isNonStable(currentVersion)
}

// Prints only what is behind, and what could not be checked. The plugin's own
// report, which `make versions-all` asks for with --output-formatter=plain, first
// lists every dependency that is already up to date.
outputFormatter {
val behind = outdated.dependencies.sortedBy { "${it.group}:${it.name}" }
if (behind.isEmpty()) println("All dependencies are up to date")
behind.forEach { d ->
val latest = with(d.available) { release ?: milestone ?: integration }
println(" - ${d.group}:${d.name} ${d.version} -> $latest")
}
unresolved.dependencies.sortedBy { "${it.group}:${it.name}" }.forEach {
println(" ? ${it.group}:${it.name}:${it.version} could not be checked")
}
if (gradle.current.isUpdateAvailable) {
println(" - Gradle ${gradle.running.version} -> ${gradle.current.version}")
}
}
}
}
7 changes: 4 additions & 3 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
group=com.readingbat
version=1.1.0
version=1.2.0

kotlin.code.style=official
org.gradle.caching=true
org.gradle.configuration-cache=false
org.gradle.daemon=true
org.gradle.caching=true
org.gradle.configuration-cache=true
org.gradle.jvmargs=-Xmx6g -Dkotlin.daemon.jvm.options=-Xmx6g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
12 changes: 7 additions & 5 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
[versions]
detekt = "2.0.0-alpha.6"
gradle-wrapper = "9.7.1"
gradle-wrapper = "9.8.1"
jvm = "25"
kotest = "6.2.5"
kotlin = "2.4.10"
kotlin = "2.4.21"
# The Kotlin scripting jars challenges run on; held back from kotlin (see configureScripting)
kotlin-scripting = "2.4.10"
kotlinter = "5.7.0"
ktor = "3.6.0"
logging = "8.0.4"
readingbat = "3.4.0"
utils = "3.2.2"
versions = "0.64.0"
readingbat = "3.5.0"
utils = "5.1.1"
versions = "0.66.0"

[libraries]
core-utils = { module = "com.pambrose.common-utils:core-utils", version.ref = "utils" }
Expand Down
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
Expand Down
52 changes: 41 additions & 11 deletions gradlew.bat

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ Each challenge is a standalone source file. The `main()` function prints expecte
## Tech Stack

- Kotlin / JVM, Gradle (Kotlin DSL) with a version catalog. The JVM toolchain version is centralized in `gradle/libs.versions.toml` under the `jvm` key.
- readingbat-core (platform library, from JitPack)
- readingbat-core (platform library, from Maven Central)
- Kotlin's JSR-223 scripting runtime is pinned to 2.4.10 (`kotlin-scripting` in the version catalog, applied by `configureScripting()` to the runtime and test classpaths only) while the project builds with a newer Kotlin. From 2.4.20 the engine never disposes a script state's compiler environment (~27 MB each), and readingbat-core starts a state per Kotlin challenge, so the test sweep runs out of heap without the pin.
- The Gradle configuration cache is on (`gradle.properties`); `dependencyUpdates` opts out, so the `make versions` targets pass `--no-configuration-cache`.
- Kotest + Ktor test host (testing)
- detekt + kotlinter (static analysis and formatting, enforced in CI)

Expand All @@ -35,7 +37,8 @@ Run `make help` for the authoritative target list.
- `make run` — start the content server on http://localhost:8080
- `make uberjar` — build fat jar at `build/libs/server.jar`
- `make uber` — build uberjar and run it
- `make versions` — check for dependency updates
- `make versions` — list only the dependencies (and Gradle) with newer versions, plus any that could not be checked
- `make versions-all` — the versions plugin's full plain report, up-to-date dependencies included
- `./gradlew test -Dkotest.filter.tests="<name>"` — filter Kotest cases by name

## Testing
Expand Down
Loading