From 12ab7b0f1f39f174941b4fd8bd2661a8ce2f15b5 Mon Sep 17 00:00:00 2001 From: Helmut Michael Juskewycz Date: Wed, 7 Oct 2026 08:05:29 +0200 Subject: [PATCH 1/2] test(store-redis): retry failover topology startup on assertion failures The fixture retried only RuntimeExceptions, so an AssertionError from 'redis-cli --cluster create' (a node refusing connections) failed the test on the first attempt. Startup failures now retry on fresh ports and carry the tail of every node's redis.log. --- .../store/redis/RedisFailoverTopology.java | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) 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<>(); From ca6273ca07882b3508bbce3fe28178f80a40b2f8 Mon Sep 17 00:00:00 2001 From: Helmut Michael Juskewycz Date: Wed, 7 Oct 2026 08:05:29 +0200 Subject: [PATCH 2/2] chore(release): prepare 1.1.0 --- AGENTS.md | 2 +- CHANGELOG.md | 26 ++++++++++++++++++- README.md | 16 +++++++----- .../threadmill/gradle/ThreadmillVersion.kt | 2 +- docs/RELEASING.md | 4 +-- docs/compatibility.md | 26 +++++++++++++++++-- docs/quickstart.md | 12 ++++----- docs/release-checklist.md | 4 +-- threadmill-spring-boot/README.md | 9 +++---- threadmill-store-oracle/README.md | 2 +- 10 files changed, 75 insertions(+), 28 deletions(-) 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 ```