diff --git a/AGENTS.md b/AGENTS.md
index b346f591..1882bd72 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -2,7 +2,7 @@
This is the standing brief for any AI agent (or human) working on Threadmill. Read it fully before touching the repository.
-**Project status:** v1.0.0 is the release version and the v1 feature set is complete. Follow-on improvements continue through the public issue tracker. Four storage backends (in-memory, PostgreSQL 18+, Oracle Database 19c+, Redis with standalone/Sentinel/Cluster), the processing engine, scheduling and recurring APIs, the per-queue pause primitive, the bulk-enqueue path, claim-time per-key concurrency with workflow inheritance, queue-family lanes, Spring Boot integration with explicit enqueue transaction modes (`after_commit`, `join_transaction`, `immediate`), Micrometer metrics, optional OpenTelemetry tracing, the data-first dashboard model, Spring dashboard API, static React dashboard UI, docs, examples, and a soak/load module are all in place. Store-backed maintenance leadership, crash-safe Redis claim, producer-side deduplication, long-running job check-ins, idle-worker wake signal, cross-node remote wake hints, and failure-detail truncation are part of the production-readiness baseline. The soak harness verifies its invariants live (bounded streaming checks, fail-fast, `progress.json`), supports node churn and external datastores, and ships a dual-backend `soakEndurance` orchestrator for hours-scale Postgres+Redis sign-off runs.
+**Project status:** v1.1.0 is the release version and the v1 feature set is complete. Follow-on improvements continue through the public issue tracker. Four storage backends (in-memory, PostgreSQL 18+, Oracle Database 19c+, Redis with standalone/Sentinel/Cluster), the processing engine, scheduling and recurring APIs, the per-queue pause primitive, the bulk-enqueue path, claim-time per-key concurrency with workflow inheritance, queue-family lanes, Spring Boot integration with explicit enqueue transaction modes (`after_commit`, `join_transaction`, `immediate`), Micrometer metrics, optional OpenTelemetry tracing, the data-first dashboard model, Spring dashboard API, static React dashboard UI, docs, examples, and a soak/load module are all in place. Store-backed maintenance leadership, crash-safe Redis claim, producer-side deduplication, long-running job check-ins, idle-worker wake signal, cross-node remote wake hints, and failure-detail truncation are part of the production-readiness baseline. The soak harness verifies its invariants live (bounded streaming checks, fail-fast, `progress.json`), supports node churn and external datastores, and ships a dual-backend `soakEndurance` orchestrator for hours-scale Postgres+Redis sign-off runs.
---
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c7b74939..1327b091 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,31 @@
# Changelog
-## Unreleased
+## 1.1.0 — 2026-10-07
+Threadmill 1.1.0 adds an Oracle Database backend. It is a compatible minor
+release: core, the PostgreSQL, Redis and in-memory stores and their storage
+formats are unchanged, so upgrading from 1.0.x is a version bump. See
+[Upgrade from 1.0.0](docs/compatibility.md#upgrade-from-100).
+
+- Added `threadmill-store-oracle`, an `OracleJobStore` for Oracle Database 19c
+ and later (issue #137). It passes the same shared contract suite as the other
+ backends and requires the `AL32UTF8` database character set and the default
+ `NLS_COMP=BINARY`, both checked at startup. It uses standard JDBC only;
+ applications supply the Oracle driver. The schema user needs `CREATE TABLE`,
+ `CREATE TRIGGER` and a tablespace quota. See
+ [Oracle schema](docs/oracle-schema.md).
+- Added `OracleMigrationRunner`. Oracle commits each DDL statement, so the runner
+ records progress per statement and resumes an interrupted migration where it
+ stopped. Concurrent migrators are serialized.
+- Added Spring Boot auto-configuration for the Oracle store, including
+ `join_transaction` enqueue. With both JDBC stores on the classpath, Spring
+ picks one from `threadmill.store.jdbc-type`, then the `spring.datasource.url`
+ scheme, and otherwise keeps PostgreSQL. Oracle schema handling is set with
+ `threadmill.store.oracle.schema-mode`.
+- Oracle has no cross-node wake channel: a job enqueued on one node is picked up
+ by another within `pollInterval` (default 500 ms).
+- The shared store contract now requires that a `null` mutex release holder
+ never frees another holder's mutex.
- Upgraded Jackson 2 to 2.21.7 and constrained Spring Boot's test-scope
Jackson 3 to 3.1.7, resolving GHSA-7hhh-6rmp-j9qf, GHSA-p6pp-m3f8-5c89,
GHSA-cxp5-3px4-pw24, GHSA-wv8q-qhhj-9h54, GHSA-gx83-3vf8-gh7j,
diff --git a/README.md b/README.md
index 1fc7fb0b..3e68abff 100644
--- a/README.md
+++ b/README.md
@@ -44,14 +44,16 @@ Threadmill is published to Maven Central under the `com.hemju.threadmill`
group. Pick the core plus the store you run against (and the Spring Boot
starter if you use Spring):
-These examples use **1.0.0**. Upgrade existing installations using the
-[0.3.0-to-1.0 migration guide](docs/compatibility.md#upgrade-from-v030).
+These examples use **1.1.0**. 1.0.x installations upgrade by bumping the
+version (see the [1.0-to-1.1 notes](docs/compatibility.md#upgrade-from-100));
+0.3.0 installations follow the
+[0.3.0-to-1.0 migration guide](docs/compatibility.md#upgrade-from-v030) first.
```kotlin
// build.gradle.kts
-implementation("com.hemju.threadmill:threadmill-core:1.0.0")
-implementation("com.hemju.threadmill:threadmill-store-postgres:1.0.0") // or -store-oracle / -store-redis / -store-memory
-implementation("com.hemju.threadmill:threadmill-spring-boot:1.0.0") // optional Spring Boot integration
+implementation("com.hemju.threadmill:threadmill-core:1.1.0")
+implementation("com.hemju.threadmill:threadmill-store-postgres:1.1.0") // or -store-oracle / -store-redis / -store-memory
+implementation("com.hemju.threadmill:threadmill-spring-boot:1.1.0") // optional Spring Boot integration
```
```xml
@@ -59,7 +61,7 @@ implementation("com.hemju.threadmill:threadmill-spring-boot:1.0.0") // option
com.hemju.threadmill
threadmill-core
- 1.0.0
+ 1.1.0
```
@@ -234,7 +236,7 @@ stable API.
## Status
-**Threadmill 1.0.0** includes the features below. See the
+**Threadmill 1.1.0** includes the features below. See the
[release checklist](docs/release-checklist.md) for the validation and publication
process.
diff --git a/buildSrc/src/main/kotlin/com/hemju/threadmill/gradle/ThreadmillVersion.kt b/buildSrc/src/main/kotlin/com/hemju/threadmill/gradle/ThreadmillVersion.kt
index 746d3c67..60b3868d 100644
--- a/buildSrc/src/main/kotlin/com/hemju/threadmill/gradle/ThreadmillVersion.kt
+++ b/buildSrc/src/main/kotlin/com/hemju/threadmill/gradle/ThreadmillVersion.kt
@@ -2,5 +2,5 @@ package com.hemju.threadmill.gradle
/** The single source of truth for the version of every published Threadmill module. */
object ThreadmillVersion {
- const val CURRENT = "1.0.0"
+ const val CURRENT = "1.1.0"
}
diff --git a/docs/RELEASING.md b/docs/RELEASING.md
index 6daad948..39e45c8e 100644
--- a/docs/RELEASING.md
+++ b/docs/RELEASING.md
@@ -65,7 +65,7 @@ signing are configured by `threadmill.publish` in `buildSrc`.
Review the reports for failures or skipped real-store tests. Confirm the
example, browser tests, simulations, dependency scans, Javadoc and artifact
- inspection passed. Check all eleven published modules, their POMs and
+ inspection passed. Check all twelve published modules, their POMs and
intra-Threadmill dependency versions. Binary JARs must contain
`META-INF/LICENSE` and `META-INF/NOTICE`, with no test or private local files.
4. Commit any remaining release preparation using a Conventional Commit such
@@ -92,7 +92,7 @@ and publishes automatically because `publishingType` is `AUTOMATIC`.
Watch the workflow to completion and verify the deployment in
[Central Portal](https://central.sonatype.com/publishing/deployments). Confirm all
-eleven modules at version 1.0.0 are retrievable from Maven Central, including
+twelve modules at the release version are retrievable from Maven Central, including
POMs, binary/source/Javadoc JARs and signatures. Resolve the README's installation
coordinates from a fresh consumer project on Java 25.
diff --git a/docs/compatibility.md b/docs/compatibility.md
index a3548fc2..8b813d40 100644
--- a/docs/compatibility.md
+++ b/docs/compatibility.md
@@ -5,8 +5,9 @@ execute again; handlers and external side effects must be idempotent. The change
in issue [#135](https://github.com/hemju/threadmill/issues/135) harden that contract.
They do not make execution exactly once. This document defines the 1.0
compatibility boundary; a release still requires the recorded validation and
-endurance gates in the [release checklist](release-checklist.md). The branch
-targets 1.0.0; it is not yet a published release.
+endurance gates in the [release checklist](release-checklist.md). It applies
+to every 1.x release; [Upgrade from 1.0.0](#upgrade-from-100) lists what 1.1
+adds.
## Supported platform and storage
@@ -133,6 +134,27 @@ through both standalone and Cluster clients, including an acquired exclusive
workflow hold. These tests cover the shipped upgrade path; they are not a promise
of arbitrary payload-shape migration.
+## Upgrade from 1.0.0
+
+1.1.0 is a compatible minor release. It adds the Oracle Database store
+(`threadmill-store-oracle`) and its Spring Boot auto-configuration; core, the
+PostgreSQL, Redis and in-memory stores, and their storage formats are unchanged.
+There are no PostgreSQL migrations and no Redis format change, so upgrading is a
+version bump. Keep every Threadmill module on the same version.
+
+- **Spring with both JDBC stores on the classpath.** When `threadmill-store-oracle`
+ and `threadmill-store-postgres` are both present, Spring chooses one from
+ `threadmill.store.jdbc-type`, then the `spring.datasource.url` scheme, and falls
+ back to PostgreSQL. Applications with only the PostgreSQL module behave exactly
+ as before.
+- **Custom `JobStore` implementations.** The shared contract suite adds
+ `releaseWithANullHolderNeverFreesAnotherHoldersMutex`; a store must either
+ reject a `null` release holder or leave the mutex held.
+- **Dashboard UI.** The packaged console is built with Tailwind CSS 4 and targets
+ Safari 16.4+, Chrome 111+ and Firefox 128+.
+- **Jackson.** Threadmill now depends on Jackson 2.21.7. Applications that manage
+ Jackson through their own BOM should move to the same patched release.
+
## Changes after 1.0
Compatible minor releases may add optional fields/operations with explicit
diff --git a/docs/quickstart.md b/docs/quickstart.md
index c9dc0817..1e9dbb27 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -10,14 +10,14 @@ run the same logical job more than once.
## Dependencies
-Use Java 25 and add the Spring module plus one store. These examples target
-the unreleased 1.0.0 candidate; see the [release status](../README.md#status).
+Use Java 25 and add the Spring module plus one store. These examples use
+the 1.1.0 release.
```kotlin
-implementation("com.hemju.threadmill:threadmill-spring-boot:1.0.0")
-implementation("com.hemju.threadmill:threadmill-store-postgres:1.0.0")
-// or: implementation("com.hemju.threadmill:threadmill-store-oracle:1.0.0") + your Oracle JDBC driver
-// or: implementation("com.hemju.threadmill:threadmill-store-redis:1.0.0")
+implementation("com.hemju.threadmill:threadmill-spring-boot:1.1.0")
+implementation("com.hemju.threadmill:threadmill-store-postgres:1.1.0")
+// or: implementation("com.hemju.threadmill:threadmill-store-oracle:1.1.0") + your Oracle JDBC driver
+// or: implementation("com.hemju.threadmill:threadmill-store-redis:1.1.0")
```
The default Spring enqueue mode is `after_commit`: returned ids are reserved
diff --git a/docs/release-checklist.md b/docs/release-checklist.md
index b74fd245..e42246ab 100644
--- a/docs/release-checklist.md
+++ b/docs/release-checklist.md
@@ -18,14 +18,14 @@ Before merging a 1.0 release candidate:
- Record which datastore versions and topologies were qualified and resolve
every release-blocking finding. Requalify affected behavior after fixes.
- Align `ThreadmillVersion.CURRENT`, installation examples, the changelog,
- compatibility/migration guide and release notes at 1.0.0. Historical 0.3.0
+ compatibility/migration guide and release notes at the release version. Historical 0.3.0
references and immutable migration fixtures retain their original versions.
- Check the final PR head, required CI, review disposition and merge result.
Run the final candidate gate:
```bash
-./gradlew productionCheck verifyReleaseTag -PreleaseTag=v1.0.0 -PdependencyScanRequired=true
+./gradlew productionCheck verifyReleaseTag -PreleaseTag=v -PdependencyScanRequired=true
```
`productionCheck` owns the clean-all-projects boundary, every subproject check,
diff --git a/threadmill-spring-boot/README.md b/threadmill-spring-boot/README.md
index 3f9fe0e4..e013349a 100644
--- a/threadmill-spring-boot/README.md
+++ b/threadmill-spring-boot/README.md
@@ -22,14 +22,13 @@ single place to confirm "which engine, which store, which lanes" at boot time.
## Quick start
-These dependencies target the unreleased 1.0.0 candidate; see the
-[release status](../README.md#status). Add the store your application uses.
+These dependencies use the 1.1.0 release. Add the store your application uses.
```kotlin
dependencies {
- implementation("com.hemju.threadmill:threadmill-spring-boot:1.0.0")
- implementation("com.hemju.threadmill:threadmill-store-postgres:1.0.0")
- // or: implementation("com.hemju.threadmill:threadmill-store-redis:1.0.0")
+ implementation("com.hemju.threadmill:threadmill-spring-boot:1.1.0")
+ implementation("com.hemju.threadmill:threadmill-store-postgres:1.1.0")
+ // or: implementation("com.hemju.threadmill:threadmill-store-redis:1.1.0")
}
```
diff --git a/threadmill-store-oracle/README.md b/threadmill-store-oracle/README.md
index 7b1c8b28..c913281d 100644
--- a/threadmill-store-oracle/README.md
+++ b/threadmill-store-oracle/README.md
@@ -5,7 +5,7 @@ Oracle Database 19c+ backend for the `JobStore` SPI. It passes the same shared
and uses standard JDBC only: applications bring their own Oracle driver.
```kotlin
-implementation("com.hemju.threadmill:threadmill-store-oracle:1.0.0")
+implementation("com.hemju.threadmill:threadmill-store-oracle:1.1.0")
runtimeOnly("com.oracle.database.jdbc:ojdbc11:23.26.3.0.0") // or the driver your platform mandates
```
diff --git a/threadmill-store-redis/src/test/java/com/hemju/threadmill/store/redis/RedisFailoverTopology.java b/threadmill-store-redis/src/test/java/com/hemju/threadmill/store/redis/RedisFailoverTopology.java
index 3c3fe3b1..c8c26d82 100644
--- a/threadmill-store-redis/src/test/java/com/hemju/threadmill/store/redis/RedisFailoverTopology.java
+++ b/threadmill-store-redis/src/test/java/com/hemju/threadmill/store/redis/RedisFailoverTopology.java
@@ -35,7 +35,7 @@ private RedisFailoverTopology(ProcessContainer container, List ports, b
}
static RedisFailoverTopology start(String image, boolean cluster) throws Exception {
- RuntimeException lastFailure = null;
+ AssertionError lastFailure = null;
for (int attempt = 0; attempt < 3; attempt++) {
var ports = availablePorts(cluster ? 6 : 5);
var container = new ProcessContainer(image, ports);
@@ -135,8 +135,15 @@ static RedisFailoverTopology start(String image, boolean cluster) throws Excepti
});
}
return result;
- } catch (RuntimeException failure) {
- lastFailure = failure;
+ } catch (RuntimeException | AssertionError failure) {
+ // A node that is not yet serving, or a cluster that has not converged, is a
+ // startup failure: retry on fresh ports and keep the node logs for diagnosis.
+ var startup = new AssertionError(
+ "Redis " + (cluster ? "Cluster" : "Sentinel") + " topology on " + image
+ + " failed to start (attempt " + (attempt + 1) + ")" + nodeLogs(container),
+ failure);
+ if (lastFailure != null) startup.addSuppressed(lastFailure);
+ lastFailure = startup;
container.close();
} catch (Throwable failure) {
container.close();
@@ -249,6 +256,21 @@ public void close() {
}
}
+ private static String nodeLogs(ProcessContainer container) {
+ try {
+ var logs = container.execInContainer(
+ "sh",
+ "-c",
+ "for f in /data/node-*/redis.log; do echo \"== $f\"; tail -n 20 \"$f\"; done");
+ return "\n" + logs.getStdout() + logs.getStderr();
+ } catch (InterruptedException interrupted) {
+ Thread.currentThread().interrupt();
+ return " (node logs unavailable: interrupted)";
+ } catch (Exception unavailable) {
+ return " (node logs unavailable: " + unavailable + ")";
+ }
+ }
+
private static List availablePorts(int count) throws IOException {
var ports = new ArrayList();
Set reserved = new HashSet<>();