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
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,40 @@ jobs:
if-no-files-found: ignore
retention-days: 7

oracle-xe:
name: Oracle store on 21c XE
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Set up JDK 25
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4
with:
distribution: temurin
java-version: "25"

- name: Set up Gradle
uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3

# `check` runs the Oracle suite on 23ai Free. Oracle publishes no free 19c
# image, so 21c XE — the closest free release, without 23ai-only syntax —
# guards the supported 19c floor here; real 19c is a release-checklist run.
- name: Run the Oracle store suite on 21c XE
run: >-
./gradlew :threadmill-store-oracle:test
-PoracleImage=gvenzl/oracle-xe:21-slim-faststart --stacktrace

- name: Upload test reports
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: oracle-xe-test-reports
path: threadmill-store-oracle/build/reports/tests/**
if-no-files-found: ignore
retention-days: 7

dashboard-browser:
name: Dashboard browser smoke
runs-on: ubuntu-latest
Expand Down
48 changes: 44 additions & 4 deletions AGENTS.md

Large diffs are not rendered by default.

32 changes: 23 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
A modern, lightweight background-job-processing library for Java 25.

Threadmill runs your idempotent background work durably, on a cluster of
application nodes, against a real datastore (PostgreSQL or Redis) — or an
in-memory store for local development. The design uses well-understood
application nodes, against a real datastore (PostgreSQL, Oracle Database, or
Redis) — or an in-memory store for local development. The design uses well-understood
techniques: a durable job store, optimistic concurrency control, polling
worker loops, an explicit state machine, and a small, deliberate public
API.
Expand All @@ -20,8 +20,8 @@ and **[JobRunr](https://www.jobrunr.io/)** on Java 25. It gives you both what
Quartz gives you (durable cron and interval scheduling, clustering, misfire
handling) and what JobRunr gives you (fire-and-forget background jobs, a
dashboard, retries) behind one small, idempotency-first API — built on virtual
threads and scoped values, with first-class PostgreSQL **and** Redis backends
held to a single shared contract test suite. If you are choosing between the
threads and scoped values, with first-class PostgreSQL, Oracle, **and** Redis
backends held to a single shared contract test suite. If you are choosing between the
two, or want to consolidate both onto one library, Threadmill is the
replacement.

Expand Down Expand Up @@ -50,7 +50,7 @@ These examples use **1.0.0**. Upgrade existing installations using the
```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-redis / -store-memory
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
```

Expand Down Expand Up @@ -116,6 +116,7 @@ See [docs/quickstart.md](docs/quickstart.md) for a complete Spring walkthrough,
| `threadmill-core` | Job model, state machine, `JobStore` SPI, serialization, engine (`ProcessingNode`, `Dispatcher`, `MaintenanceCycle`), interceptors, scheduling API. **No storage, no framework code.** |
| `threadmill-store-memory` | In-memory `JobStore` for tests and local dev. Held to the same contract as the real backends. |
| `threadmill-store-postgres` | PostgreSQL backend (auto-migrations, `SELECT … FOR UPDATE SKIP LOCKED` claim, per-state counter table with trigger). |
| `threadmill-store-oracle` | Oracle Database 19c+ backend (standard JDBC, indexed virtual columns as partial indexes, `FOR UPDATE SKIP LOCKED` claim, resumable migrations). |
| `threadmill-store-redis` | Redis backend (reliable-fetch via Lua, per-job HASH + per-queue ZSET + counts HASH). |
| `threadmill-spring-boot` | Spring Boot 4.x auto-configuration. |
| `threadmill-metrics` | Micrometer integration: jobs-per-state gauges, processed/failed counters, processing-time timer. |
Expand All @@ -135,6 +136,16 @@ path). Migrations are applied automatically on startup; an
`emitPendingSql()` method produces pending SQL for teams that prefer
Flyway/Liquibase, and `emitCleanInstallSql()` emits the full clean-install DDL.

**Oracle Database 19c+** is a first-class backend with the same model: the
body is a `CLOB`, scalar columns are indexed through virtual columns that emulate
partial indexes, and the claim uses `FOR UPDATE SKIP LOCKED`. It needs an
`AL32UTF8` database and only `CREATE TABLE`/`CREATE TRIGGER` privileges, uses
standard JDBC (bring your own driver), and resumes interrupted migrations
because Oracle commits DDL statement by statement. It has no cross-node wake
channel; other nodes pick up new work within `poll-interval`. See the
[module README](threadmill-store-oracle/README.md) and
[Oracle schema](docs/oracle-schema.md).

**Redis 7.4+** is a first-class backend and requires `noeviction`. Every multi-key state
transition is a single atomic Lua script. Standalone, Sentinel, and Cluster
topologies are configured through one factory path. Redis Cluster uses a
Expand Down Expand Up @@ -230,8 +241,8 @@ process.
- Job model with append-only state history, optimistic-lock versioning,
relationship and result fields, and bounded size.
- Centralised state machine; illegal transitions throw.
- `JobStore` SPI plus three backends (in-memory, PostgreSQL, Redis), each
held to one shared abstract contract test.
- `JobStore` SPI plus four backends (in-memory, PostgreSQL, Oracle, Redis),
each held to one shared abstract contract test.
- Processing engine: virtual-thread workers, scoped-value context
propagation, per-job timeout, single failure code path, circuit-breaker
decay, store-outage tolerance, orphan-reclaim, poison-job quarantine.
Expand Down Expand Up @@ -301,8 +312,11 @@ and the engine resumes automatically once the store is reachable again.
./gradlew productionCheck # release-candidate validation gauntlet
```

The PostgreSQL and Redis tests use Testcontainers and require a running
container runtime (Docker / Podman / Colima / OrbStack).
The PostgreSQL, Oracle, and Redis tests use Testcontainers and require a
running container runtime (Docker / Podman / Colima / OrbStack). The Oracle
suite runs on Oracle 23ai Free by default; see the
[Oracle store README](threadmill-store-oracle/README.md#tests) for 21c XE and
real 19c runs.

## Examples

Expand Down
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ val publishedProjects =
project(":threadmill-core"),
project(":threadmill-store-memory"),
project(":threadmill-store-postgres"),
project(":threadmill-store-oracle"),
project(":threadmill-store-redis"),
project(":threadmill-spring-boot"),
project(":threadmill-test-support"),
Expand Down Expand Up @@ -337,6 +338,7 @@ val productionCheck by
dependsOn(subprojects.map { it.tasks.matching { task -> task.name == "javadoc" } })
dependsOn(
":threadmill-store-postgres:test",
":threadmill-store-oracle:test",
":threadmill-store-redis:test",
":threadmill-dashboard-spring:browserTest",
":threadmill-soak:soakRegression",
Expand Down
13 changes: 13 additions & 0 deletions docs/backend-execution-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,19 @@ database, connection pool, worker count, or queue topology can drain. A growing
queue with passing concurrency invariants is a capacity signal, not by itself a
same-key correctness failure.

### Oracle Path

Oracle follows the Postgres path — the same tables, row locks, group rows, and
workflow holds in one transaction — with three mechanical differences. Oracle
rejects a row limit on a locking query, so locking cursors are index-ordered and
fetched only as far as the page budget; Oracle locks `SKIP LOCKED` rows as they
are fetched. Keyed candidates are first read through one top-n branch per key
(at most 128 keys per unproductive poll, rotated like Postgres) and then locked
by primary key with `SKIP LOCKED`, dropping rows another claimer holds. The
leapfrog rule is decided by `EXISTS` range probes bounded at each candidate's own
`current_state_at`. See the
[Oracle store README](../threadmill-store-oracle/README.md#how-the-claim-works).

## Redis Path

Redis uses server-side Lua scripts as the atomic boundary. Java may inspect
Expand Down
4 changes: 3 additions & 1 deletion docs/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ targets 1.0.0; it is not yet a published release.

## Supported platform and storage

Java 25 is required. PostgreSQL requires 18 or later. Redis data nodes require
Java 25 is required. PostgreSQL requires 18 or later. Oracle Database requires
19c or later with the `AL32UTF8` database character set and the default
`NLS_COMP=BINARY`; the application supplies the JDBC driver. Redis data nodes require
7.4 or later and `noeviction`; validate every node that may become a primary.
The exact tested dependency versions are in Gradle/npm locks and the release
validation artifacts. A supported major/minimum is not evidence that every
Expand Down
20 changes: 18 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,22 @@ its own `JobStore` bean.
See [postgres-schema.md](postgres-schema.md) for manual DDL, validation, and
reset guidance.

## Oracle Store Properties

Spring auto-creates an Oracle store when a `DataSource` bean exists and
`threadmill-store-oracle` is on the classpath, unless the application defines
its own `JobStore` bean or Redis is configured. The application supplies the
Oracle JDBC driver.

| Setting | Default | Notes |
|---|---:|---|
| `threadmill.store.oracle.schema-mode` | `migrate` | `migrate`, `validate`, `none`, or `drop-and-migrate`. Auto-configured Oracle stores run this action before `OracleJobStore` is created. |
| `threadmill.store.oracle.allow-destructive-schema-reset` | `false` | Must be `true` for `drop-and-migrate`. This drops only Threadmill-owned tables and destroys stored jobs. |
| `threadmill.store.jdbc-type` | | `postgres` or `oracle`. Needed only when both JDBC store modules are on the classpath and `spring.datasource.url` does not name the database. Without it, a `jdbc:oracle:` URL selects Oracle, a `jdbc:postgresql:` URL selects PostgreSQL, and otherwise the one module present wins (PostgreSQL when both are). |

See [oracle-schema.md](oracle-schema.md) for privileges, manual DDL, and how
interrupted migrations resume.

## Redis Store Properties

Spring can auto-create a Redis store when `threadmill.store.redis.*` is set.
Expand Down Expand Up @@ -111,14 +127,14 @@ correctness fallback.

| Setting | Default | Notes |
|---|---:|---|
| `threadmill.remote-wake.enabled` | `true` | Auto-create Postgres `LISTEN`/`NOTIFY` or Redis Pub/Sub wake hints for Spring auto-configured durable stores. In-memory and custom stores stay local-wake only unless the application provides a `RemoteWakeChannel` bean. |
| `threadmill.remote-wake.enabled` | `true` | Auto-create Postgres `LISTEN`/`NOTIFY` or Redis Pub/Sub wake hints for Spring auto-configured durable stores. Oracle, in-memory, and custom stores stay local-wake only unless the application provides a `RemoteWakeChannel` bean; their other nodes pick up work within `poll-interval`. |
| `threadmill.remote-wake.channel` | backend default | Optional channel override. Defaults to `threadmill_wake` for Postgres and `{threadmill}:wake` for Redis. Set this when multiple isolated Threadmill deployments share one Postgres database or Redis instance. |

## Spring Recurring Properties

| Setting | Default | Notes |
|---|---:|---|
| `threadmill.spring.enqueue-mode` | `after_commit` | `after_commit`, `join_transaction`, or `immediate`. `join_transaction` is Spring + Postgres only. |
| `threadmill.spring.enqueue-mode` | `after_commit` | `after_commit`, `join_transaction`, or `immediate`. `join_transaction` is Spring + PostgreSQL or Oracle only. |
| `threadmill.spring.recurring-namespace` | `spring.application.name` | Namespace whose annotation-driven recurring tasks are reconciled at startup. If neither value is set, Threadmill only upserts discovered tasks and does not delete stale ones. |

### Initial and lifecycle size budgets
Expand Down
5 changes: 5 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
- [Configuration](configuration.md) — every `threadmill.*` property.
- [PostgreSQL schema](postgres-schema.md) — schema modes, manual DDL, and
reset guidance.
- [Oracle schema](oracle-schema.md) — privileges, schema modes, resumable
migrations, and SQL*Plus-ready manual DDL.
- [Redis topologies](redis-topologies.md) — standalone, Sentinel, Cluster.
- [Operations](operations.md) — production runbook, pause / resume, monitoring.
- [Troubleshooting](troubleshooting.md) — symptom → cause → fix.
Expand Down Expand Up @@ -69,6 +71,9 @@ operational notes:
- [`threadmill-store-postgres`](../threadmill-store-postgres/README.md) — the
full schema, PG18-only enforcement, claim semantics, deadlock retry,
connection-pool sizing.
- [`threadmill-store-oracle`](../threadmill-store-oracle/README.md) — Oracle
19c+ requirements, virtual-column indexes, the claim path, driver and locale
notes, and how to test against 21c XE or a real 19c database.
- [`threadmill-store-redis`](../threadmill-store-redis/README.md) — the full
key layout, the Lua script inventory, the reliable-fetch claim, AOF
durability.
Expand Down
7 changes: 6 additions & 1 deletion docs/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,12 @@ stores also publish cross-node wake hints after local wake timing has already
decided that work is visible. Postgres uses `LISTEN`/`NOTIFY` on
`threadmill_wake` by default. Redis uses Pub/Sub on `{threadmill}:wake` by
default. Set `threadmill.remote-wake.channel` when multiple isolated
Threadmill deployments share one datastore.
Threadmill deployments share one datastore. Oracle has no lightweight
notification primitive (`DBMS_ALERT` would serialize every enqueuing
transaction), so an Oracle deployment relies on polling across nodes: work
enqueued on another node starts within `threadmill.pollInterval`, while same-JVM
enqueues still wake their dispatcher immediately. Lower the poll interval if that
latency matters more than the extra idle queries.

The Postgres listener holds one JDBC connection for as long as remote wake is
enabled. If it uses the same application pool, size that pool with one
Expand Down
Loading
Loading