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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down
26 changes: 25 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,24 @@ 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
<!-- Maven -->
<dependency>
<groupId>com.hemju.threadmill</groupId>
<artifactId>threadmill-core</artifactId>
<version>1.0.0</version>
<version>1.1.0</version>
</dependency>
```

Expand Down Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
4 changes: 2 additions & 2 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand Down
26 changes: 24 additions & 2 deletions docs/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/release-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<version> -PdependencyScanRequired=true
```

`productionCheck` owns the clean-all-projects boundary, every subproject check,
Expand Down
9 changes: 4 additions & 5 deletions threadmill-spring-boot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
```

Expand Down
2 changes: 1 addition & 1 deletion threadmill-store-oracle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ private RedisFailoverTopology(ProcessContainer container, List<Integer> 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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<Integer> availablePorts(int count) throws IOException {
var ports = new ArrayList<Integer>();
Set<Integer> reserved = new HashSet<>();
Expand Down
Loading