From 956b9eef22160325936194119dec1c58d60212c6 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 09:29:46 +0900
Subject: [PATCH 01/92] docs: design lease-fenced durable job worker
---
...6-08-05-durable-job-lease-worker-design.md | 109 ++++++++++++++++++
1 file changed, 109 insertions(+)
create mode 100644 docs/superpowers/specs/2026-08-05-durable-job-lease-worker-design.md
diff --git a/docs/superpowers/specs/2026-08-05-durable-job-lease-worker-design.md b/docs/superpowers/specs/2026-08-05-durable-job-lease-worker-design.md
new file mode 100644
index 00000000..2ead4c57
--- /dev/null
+++ b/docs/superpowers/specs/2026-08-05-durable-job-lease-worker-design.md
@@ -0,0 +1,109 @@
+# Durable ETL Job Lease Worker Design
+
+## Status
+
+Accepted implementation design for issue #120. This is a bounded follow-on to the durable asynchronous intake merged in PR #119 and is stacked on PR #121 until that workflow-security prerequisite reaches `develop`.
+
+## Product outcome
+
+Accepted asynchronous ETL jobs must progress from `PENDING` to a terminal state without depending on the client connection or on one service replica. The worker must distribute work across replicas through PostgreSQL row locking, fence stale owners, bound retry attempts, atomically couple target effects with terminal success, clear retained payloads at terminal state, and expose only stable non-sensitive status metadata through the existing owner-scoped API.
+
+## Scope
+
+This slice adds:
+
+- a PostgreSQL-owned claim operation using deterministic ordering and `FOR UPDATE SKIP LOCKED`;
+- process-lifetime `lease_owner_id` and per-claim `lease_claim_id` fencing;
+- lease expiry and reclaim;
+- bounded attempts with deterministic terminal failure codes;
+- fixed-delay polling that is disabled by default;
+- atomic ETL target writes plus conditional `SUCCEEDED` transition;
+- retry and failure transitions that require the exact live lease;
+- finite-cardinality execution metrics;
+- migration, rollback, privacy, operations, and failure-recovery documentation.
+
+Cancellation, priorities, recurring schedules, manual replay, result-body persistence, and a dead-letter user interface remain out of scope.
+
+## Data model
+
+Flyway migration `V3__add_etl_job_lease_fencing.sql` adds the following descriptive `snake_case` columns to `etl_job_records`:
+
+- `lease_claim_id UUID` — unique token generated for every claim or reclaim;
+- `lease_owner_id VARCHAR(128)` — stable non-sensitive identifier for one worker process;
+- `lease_expires_at TIMESTAMPTZ` — database-time expiry boundary.
+
+A lifecycle constraint requires all three lease columns for `RUNNING` rows and requires all three to be null for every other state. A failure lifecycle constraint requires `failure_code` only for `FAILED` rows. The existing terminal-payload constraint remains authoritative. An eligibility index covers `job_status`, `lease_expires_at`, `created_at`, and `job_record_id`.
+
+## Claim protocol
+
+`EtlJobLeaseRepository.claimNext` runs in one transaction:
+
+1. Terminalize eligible rows whose `attempt_count` has reached the configured maximum. Clear `request_payload` and all lease columns and assign `etl_worker_attempts_exhausted`.
+2. Select one `PENDING` row or one expired `RUNNING` row with `attempt_count < max_attempts`, ordered by `created_at, job_record_id`, using `FETCH FIRST 1 ROW ONLY FOR UPDATE SKIP LOCKED`.
+3. Read `CURRENT_TIMESTAMP` from the database in the same statement and derive the next expiry from that database time.
+4. Generate a new `lease_claim_id`, increment `attempt_count`, set `RUNNING`, set the owner and expiry, clear any prior failure code, and commit.
+
+The scheduler does not provide uniqueness. The database row lock and state predicate are the cross-replica authority. PostgreSQL documents `SKIP LOCKED` as suitable for avoiding contention among multiple consumers of a queue-like table, while warning that it is not a general-purpose consistent view; that limitation is appropriate here because each worker needs one exclusive claim rather than a complete snapshot.
+
+## Execution and fencing
+
+`EtlJobExecutionService.execute` starts a new transaction, calls the existing `EtlService.processData` through a separate Spring bean, then conditionally transitions the job to `SUCCEEDED` only when all of the following still match:
+
+- `job_record_id`;
+- `job_status = 'RUNNING'`;
+- exact `lease_claim_id`;
+- exact `lease_owner_id`;
+- `lease_expires_at > CURRENT_TIMESTAMP`.
+
+If the conditional update affects no row, `StaleEtlJobLeaseException` is thrown. The exception rolls back the same transaction, including all target writes, so an expired or superseded worker cannot commit target effects.
+
+## Failure policy
+
+The polling coordinator catches execution failures after the execution transaction rolls back and performs a separate exact-lease transition:
+
+- `TransientDataAccessException`: return to `PENDING` when attempts remain; otherwise terminal `FAILED` with `etl_target_unavailable`;
+- `EtlRequestException`: terminal `FAILED` with the existing stable request `errorCode`;
+- other `DataAccessException`: terminal `FAILED` with `etl_target_failure`;
+- other `RuntimeException`: terminal `FAILED` with `etl_internal_error`;
+- `StaleEtlJobLeaseException`: make no state change because another owner or expiry boundary is authoritative.
+
+Every retry or failure update repeats the exact-live-lease predicate. A zero-row update is treated as stale evidence, not as success.
+
+## Scheduling and activation
+
+Spring fixed-delay scheduling is used because the next delay is measured after completion of the previous invocation. `xtrmetl.etl.jobs.worker.enabled` defaults to `false`; operators must explicitly enable both intake and worker execution. Configurable values are bounded and validated:
+
+- `fixed-delay-milliseconds` > 0;
+- `initial-delay-milliseconds` >= 0;
+- `lease-duration-seconds` > 0;
+- `max-attempts` between 1 and 100;
+- `lease-owner-id` is 8–128 safe ASCII characters and defaults to a process-lifetime generated identifier.
+
+One polling invocation claims at most one job. Horizontal throughput is achieved by replicas and repeated fixed-delay invocations rather than unbounded in-process fan-out.
+
+## Observability and privacy
+
+The worker emits a duration timer and a finite outcome counter for `claimed`, `succeeded`, `retried`, `failed`, and `stale`. Metric tags never include payloads, principals, idempotency keys, job identifiers, SQL, lease identifiers, or exception messages. Logs follow the same rule. Database client instrumentation should retain the stable OpenTelemetry SQL semantic conventions and avoid opting raw query text into telemetry unless the deployment has separately assessed that exposure.
+
+## Testing strategy
+
+- Migration tests enforce descriptive names, lifecycle constraints, index shape, and rollback instructions.
+- Repository integration tests use H2's supported `FOR UPDATE SKIP LOCKED` syntax to prove one live claim, deterministic ordering, expiry reclaim, attempt increment, and exhaustion terminalization.
+- Execution integration tests prove target rows and `SUCCEEDED` commit together and prove a stale claim rolls target writes back.
+- Coordinator tests cover every failure classification, retry bound, zero-work poll, metrics outcome, and stale transition.
+- Property tests cover every validation boundary and generated owner identifier.
+- Documentation and coverage policy tests require complete public Javadoc and zero missed instruction, line, method, and branch coverage for the durable-job package.
+
+## Rollback
+
+Before application rollback, stop all workers and disable intake. Allow active leases to expire, confirm no `RUNNING` rows remain, and decide whether pending payloads will be drained or explicitly failed. Roll back the application first. The three lease columns and eligibility index may be removed only after all rows are non-running and no deployed binary reads them. Flyway versioned migrations are not edited or deleted after publication; a forward compensating migration must perform any production schema reversal.
+
+## Standards and primary documentation
+
+Fielding, R., Nottingham, M., & Reschke, J. (2022). *HTTP semantics* (RFC 9110). Internet Engineering Task Force. https://www.rfc-editor.org/rfc/rfc9110.html
+
+OpenTelemetry Authors. (2026). *Semantic conventions for database calls and systems*. Cloud Native Computing Foundation. https://opentelemetry.io/docs/specs/semconv/db/
+
+PostgreSQL Global Development Group. (2026). *PostgreSQL 18 documentation: SELECT*. https://www.postgresql.org/docs/18/sql-select.html
+
+Spring Authors. (2026). *Task execution and scheduling*. Broadcom. https://docs.spring.io/spring-framework/reference/integration/scheduling.html
From 4f170ecc8a4f40a6ac3f4fcca21e7c0d6810d8eb Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 09:30:17 +0900
Subject: [PATCH 02/92] docs: plan lease-fenced durable job worker
---
.../2026-08-05-durable-job-lease-worker.md | 108 ++++++++++++++++++
1 file changed, 108 insertions(+)
create mode 100644 docs/superpowers/plans/2026-08-05-durable-job-lease-worker.md
diff --git a/docs/superpowers/plans/2026-08-05-durable-job-lease-worker.md b/docs/superpowers/plans/2026-08-05-durable-job-lease-worker.md
new file mode 100644
index 00000000..cde8d2b0
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-05-durable-job-lease-worker.md
@@ -0,0 +1,108 @@
+# Durable ETL Job Lease Worker Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Execute accepted asynchronous ETL jobs safely across replicas with PostgreSQL claim locking, exact lease fencing, bounded retries, atomic success, terminal payload clearing, and operator-safe evidence.
+
+**Architecture:** A transaction-scoped repository owns claim and state transitions; a separate transactional execution service couples existing ETL target writes with an exact-live-lease success update; a fixed-delay coordinator classifies failures and performs retry or terminal transitions in a new transaction. PostgreSQL row state is the distribution and fencing authority, while scheduling only supplies repeated polling.
+
+**Tech Stack:** Java 25, Spring Boot, Spring JDBC transactions, Spring scheduling, PostgreSQL 18 SQL, Flyway, Micrometer, JUnit 5, Mockito, H2 compatibility tests, Maven/Jacoco.
+
+## Global Constraints
+
+- Preserve standalone operation and modular MSA compatibility with ContextualWisdomLab/.github, naruon, and other CWL services.
+- Database objects contain at least two descriptive words and use `snake_case`.
+- Worker activation is fail-closed and disabled by default.
+- Every public production type and method has beginner-readable Javadoc.
+- Added durable-job production code must have zero missed instruction, line, method, or branch coverage.
+- Payloads, principals, idempotency keys, job identifiers, lease identifiers, SQL, and exception messages never enter metrics or logs.
+- A stale or expired lease cannot commit target effects or state transitions.
+- Versioned Flyway migrations are immutable after publication; rollback uses a forward compensating migration.
+
+---
+
+### Task 1: Lock the schema and configuration contracts
+
+**Files:**
+- Create: `etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobLeaseMigrationTest.java`
+- Create: `etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobWorkerPropertiesTest.java`
+- Create: `etl-service/src/main/resources/db/migration/V3__add_etl_job_lease_fencing.sql`
+- Create: `etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobWorkerProperties.java`
+- Modify: `etl-service/src/main/java/com/xtrmetl/etl/EtlApplication.java`
+- Modify: `etl-service/src/main/resources/application.yml`
+
+**Interfaces:**
+- Produces: `EtlJobWorkerProperties` with `enabled`, `fixedDelayMilliseconds`, `initialDelayMilliseconds`, `leaseDurationSeconds`, `maxAttempts`, and `leaseOwnerId`.
+
+- [ ] Write migration and property tests first. Require the three lease columns, lifecycle constraints, claim index, fail-closed defaults, safe owner profile, and all numeric boundaries.
+- [ ] Run `./mvnw -B -pl etl-service -Dtest=EtlJobLeaseMigrationTest,EtlJobWorkerPropertiesTest test` and record the expected missing-file/type failure.
+- [ ] Add the migration, properties, application registration, and environment-backed defaults.
+- [ ] Re-run the focused tests and commit.
+
+### Task 2: Add exclusive claim and exact transition persistence
+
+**Files:**
+- Create: `etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobLeaseRepositoryIntegrationTest.java`
+- Create: `etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobLease.java`
+- Create: `etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobLeaseRepository.java`
+- Create: `etl-service/src/main/java/com/xtrmetl/etl/job/StaleEtlJobLeaseException.java`
+
+**Interfaces:**
+- Produces: `Optional claimNext(String leaseOwnerId, Duration leaseDuration, int maxAttempts)`.
+- Produces: `markSucceeded`, `releaseForRetry`, and `markFailed`, each returning only after an exact, unexpired lease transition or throwing `StaleEtlJobLeaseException`.
+
+- [ ] Write H2 integration tests for deterministic order, simultaneous single claim, expired reclaim, exhausted terminalization, success, retry, failure, and stale update refusal.
+- [ ] Run the focused test and record the missing-type failure.
+- [ ] Implement the two-statement lock-and-update claim transaction using `FOR UPDATE SKIP LOCKED` and database `CURRENT_TIMESTAMP`.
+- [ ] Implement exact-live-lease transition predicates and stable failure validation.
+- [ ] Re-run the focused test and commit.
+
+### Task 3: Couple ETL target effects to terminal success
+
+**Files:**
+- Create: `etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobExecutionServiceIntegrationTest.java`
+- Create: `etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobExecutionService.java`
+
+**Interfaces:**
+- Consumes: `EtlJobLease`, `EtlService.processData`, `EtlJobLeaseRepository.markSucceeded`.
+- Produces: `void execute(EtlJobLease lease)` in one Spring transaction.
+
+- [ ] Write integration tests proving target rows and `SUCCEEDED` commit together.
+- [ ] Add a stale-lease test that changes the claim before execution and asserts both the exception and zero committed target rows.
+- [ ] Run the focused test and record the missing-type failure.
+- [ ] Implement the minimal transactional service and re-run the tests.
+- [ ] Commit.
+
+### Task 4: Add bounded fixed-delay coordination and evidence
+
+**Files:**
+- Create: `etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobWorkerTest.java`
+- Create: `etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobWorker.java`
+
+**Interfaces:**
+- Consumes: repository claim/transitions, execution service, worker properties, `MeterRegistry`.
+- Produces: one `pollOnce()` invocation that claims at most one job and records finite outcomes.
+
+- [ ] Write tests for no work, success, transient retry, exhausted transient failure, deterministic request failure, non-transient target failure, unexpected failure, and stale evidence.
+- [ ] Run the focused test and record the missing-type failure.
+- [ ] Implement the conditional worker bean, fixed-delay method, failure classification, retry bound, duration timer, and finite-cardinality outcome counter.
+- [ ] Re-run the tests and commit.
+
+### Task 5: Complete operations, privacy, compatibility, and release evidence
+
+**Files:**
+- Modify: `docs/etl/durable-job-intake.md`
+- Create: `docs/operations/durable-job-worker.md`
+- Modify: `CHANGELOG.md`
+- Modify: `etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobMigrationDocumentationTest.java`
+- Modify: `etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobCoveragePolicyTest.java`
+
+**Interfaces:**
+- Produces: authoritative activation, SLO, metrics, failure-code, recovery, retention, and rollback guidance.
+
+- [ ] Add documentation-first tests requiring activation pairs, privacy boundaries, exact failure codes, rollback ordering, and standards references.
+- [ ] Update the authoritative docs and changelog.
+- [ ] Run `./mvnw -B -pl etl-service test`.
+- [ ] Run `./mvnw -B test` across the full reactor.
+- [ ] Inspect Jacoco for zero missed durable-job instructions, lines, methods, and branches.
+- [ ] Open a stacked draft PR against `ci/hourly-opencode-nvidia-nim`, inspect every review and exact-head check, and mark ready only after all gates pass.
From e4960fe9cc67d62bff0d3d96ff0eea6e0901dd0e Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 09:30:37 +0900
Subject: [PATCH 03/92] test: specify durable job lease schema
---
.../etl/job/EtlJobLeaseMigrationTest.java | 87 +++++++++++++++++++
1 file changed, 87 insertions(+)
create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobLeaseMigrationTest.java
diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobLeaseMigrationTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobLeaseMigrationTest.java
new file mode 100644
index 00000000..8d73c761
--- /dev/null
+++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobLeaseMigrationTest.java
@@ -0,0 +1,87 @@
+package com.xtrmetl.etl.job;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Specifies the additive Flyway contract for exact durable-job lease fencing.
+ */
+class EtlJobLeaseMigrationTest {
+
+ @Test
+ void addsDescriptiveLeaseColumnsAndEligibilityIndex() throws IOException {
+ String migration = readMigration();
+
+ assertTrue(migration.contains("ADD COLUMN lease_claim_id UUID"));
+ assertTrue(migration.contains("ADD COLUMN lease_owner_id VARCHAR(128)"));
+ assertTrue(migration.contains("ADD COLUMN lease_expires_at TIMESTAMPTZ"));
+ assertTrue(migration.contains("CREATE INDEX etl_job_claim_eligibility_index"));
+ assertTrue(migration.contains(
+ "(job_status, lease_expires_at, created_at, job_record_id)"
+ ));
+ assertFalse(migration.contains(" ADD COLUMN owner "));
+ assertFalse(migration.contains(" ADD COLUMN lease "));
+ }
+
+ @Test
+ void requiresLeaseFieldsOnlyForRunningRows() throws IOException {
+ String migration = normalize(readMigration());
+
+ assertTrue(migration.contains("CONSTRAINT etl_job_lease_lifecycle_check"));
+ assertTrue(migration.contains(
+ "job_status = 'RUNNING' AND lease_claim_id IS NOT NULL AND lease_owner_id IS NOT NULL AND lease_expires_at IS NOT NULL"
+ ));
+ assertTrue(migration.contains(
+ "job_status <> 'RUNNING' AND lease_claim_id IS NULL AND lease_owner_id IS NULL AND lease_expires_at IS NULL"
+ ));
+ }
+
+ @Test
+ void requiresFailureCodesOnlyForFailedRows() throws IOException {
+ String migration = normalize(readMigration());
+
+ assertTrue(migration.contains("CONSTRAINT etl_job_failure_lifecycle_check"));
+ assertTrue(migration.contains("job_status = 'FAILED' AND failure_code IS NOT NULL"));
+ assertTrue(migration.contains("job_status <> 'FAILED' AND failure_code IS NULL"));
+ }
+
+ private static String readMigration() throws IOException {
+ return Files.readString(
+ projectRoot().resolve(
+ "etl-service/src/main/resources/db/migration/"
+ + "V3__add_etl_job_lease_fencing.sql"
+ ),
+ StandardCharsets.UTF_8
+ );
+ }
+
+ private static String normalize(String value) {
+ return value.replaceAll("\\s+", " ").trim();
+ }
+
+ private static Path projectRoot() {
+ Path current = Paths.get(System.getProperty("user.dir")).toAbsolutePath();
+ Path lastPomParent = null;
+ while (current != null) {
+ if (Files.exists(current.resolve(".git"))) {
+ return current;
+ }
+ if (Files.exists(current.resolve("pom.xml"))) {
+ lastPomParent = current;
+ }
+ current = current.getParent();
+ }
+ if (lastPomParent != null) {
+ return lastPomParent;
+ }
+ throw new IllegalStateException("Could not find project root");
+ }
+}
From 8f526301d1e6555a87a2c3c5601fb1be72c68a77 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 09:30:57 +0900
Subject: [PATCH 04/92] test: specify worker configuration bounds
---
.../etl/job/EtlJobWorkerPropertiesTest.java | 94 +++++++++++++++++++
1 file changed, 94 insertions(+)
create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobWorkerPropertiesTest.java
diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobWorkerPropertiesTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobWorkerPropertiesTest.java
new file mode 100644
index 00000000..603eef51
--- /dev/null
+++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobWorkerPropertiesTest.java
@@ -0,0 +1,94 @@
+package com.xtrmetl.etl.job;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Specifies fail-closed activation and bounded durable-job worker configuration.
+ */
+class EtlJobWorkerPropertiesTest {
+
+ @Test
+ void defaultsToDisabledBoundedPollingWithGeneratedSafeOwner() {
+ EtlJobWorkerProperties properties = new EtlJobWorkerProperties();
+
+ assertFalse(properties.isEnabled());
+ assertEquals(5_000L, properties.getFixedDelayMilliseconds());
+ assertEquals(5_000L, properties.getInitialDelayMilliseconds());
+ assertEquals(300L, properties.getLeaseDurationSeconds());
+ assertEquals(3, properties.getMaxAttempts());
+ assertTrue(properties.getLeaseOwnerId().matches("[A-Za-z0-9._:-]{8,128}"));
+
+ EtlJobWorkerProperties another = new EtlJobWorkerProperties();
+ assertNotEquals(properties.getLeaseOwnerId(), another.getLeaseOwnerId());
+ }
+
+ @Test
+ void acceptsEverySupportedBoundary() {
+ EtlJobWorkerProperties properties = new EtlJobWorkerProperties();
+
+ properties.setEnabled(true);
+ properties.setFixedDelayMilliseconds(1L);
+ properties.setInitialDelayMilliseconds(0L);
+ properties.setLeaseDurationSeconds(1L);
+ properties.setMaxAttempts(1);
+ properties.setLeaseOwnerId("worker-01");
+
+ assertTrue(properties.isEnabled());
+ assertEquals(1L, properties.getFixedDelayMilliseconds());
+ assertEquals(0L, properties.getInitialDelayMilliseconds());
+ assertEquals(1L, properties.getLeaseDurationSeconds());
+ assertEquals(1, properties.getMaxAttempts());
+ assertEquals("worker-01", properties.getLeaseOwnerId());
+
+ properties.setMaxAttempts(100);
+ properties.setLeaseOwnerId("w".repeat(128));
+ assertEquals(100, properties.getMaxAttempts());
+ assertEquals(128, properties.getLeaseOwnerId().length());
+ }
+
+ @Test
+ void rejectsUnsafeNumericConfiguration() {
+ EtlJobWorkerProperties properties = new EtlJobWorkerProperties();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> properties.setFixedDelayMilliseconds(0L)
+ );
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> properties.setInitialDelayMilliseconds(-1L)
+ );
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> properties.setLeaseDurationSeconds(0L)
+ );
+ assertThrows(IllegalArgumentException.class, () -> properties.setMaxAttempts(0));
+ assertThrows(IllegalArgumentException.class, () -> properties.setMaxAttempts(101));
+ }
+
+ @Test
+ void rejectsMissingShortLongOrUnsafeOwnerIdentifiers() {
+ EtlJobWorkerProperties properties = new EtlJobWorkerProperties();
+
+ assertThrows(NullPointerException.class, () -> properties.setLeaseOwnerId(null));
+ assertThrows(IllegalArgumentException.class, () -> properties.setLeaseOwnerId("short"));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> properties.setLeaseOwnerId("w".repeat(129))
+ );
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> properties.setLeaseOwnerId("worker identifier")
+ );
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> properties.setLeaseOwnerId("worker/identifier")
+ );
+ }
+}
From 6155eb807743ac0f1cf8fd2e72c56dd156d49aae Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 09:35:31 +0900
Subject: [PATCH 05/92] feat(etl): add durable job lease schema
---
.../V3__add_etl_job_lease_fencing.sql | 51 +++++++++++++++++++
1 file changed, 51 insertions(+)
create mode 100644 etl-service/src/main/resources/db/migration/V3__add_etl_job_lease_fencing.sql
diff --git a/etl-service/src/main/resources/db/migration/V3__add_etl_job_lease_fencing.sql b/etl-service/src/main/resources/db/migration/V3__add_etl_job_lease_fencing.sql
new file mode 100644
index 00000000..d54bf6df
--- /dev/null
+++ b/etl-service/src/main/resources/db/migration/V3__add_etl_job_lease_fencing.sql
@@ -0,0 +1,51 @@
+ALTER TABLE etl_job_records
+ ADD COLUMN lease_claim_id UUID,
+ ADD COLUMN lease_owner_id VARCHAR(128),
+ ADD COLUMN lease_expires_at TIMESTAMPTZ;
+
+-- Repair legacy rows before enforcing the stronger failure lifecycle invariant.
+UPDATE etl_job_records
+SET failure_code = 'etl_legacy_failure'
+WHERE job_status = 'FAILED'
+ AND failure_code IS NULL;
+
+UPDATE etl_job_records
+SET failure_code = NULL
+WHERE job_status <> 'FAILED'
+ AND failure_code IS NOT NULL;
+
+ALTER TABLE etl_job_records
+ ADD CONSTRAINT etl_job_lease_lifecycle_check CHECK (
+ (
+ job_status = 'RUNNING'
+ AND lease_claim_id IS NOT NULL
+ AND lease_owner_id IS NOT NULL
+ AND lease_expires_at IS NOT NULL
+ )
+ OR
+ (
+ job_status <> 'RUNNING'
+ AND lease_claim_id IS NULL
+ AND lease_owner_id IS NULL
+ AND lease_expires_at IS NULL
+ )
+ ),
+ ADD CONSTRAINT etl_job_failure_lifecycle_check CHECK (
+ (
+ job_status = 'FAILED'
+ AND failure_code IS NOT NULL
+ )
+ OR
+ (
+ job_status <> 'FAILED'
+ AND failure_code IS NULL
+ )
+ );
+
+CREATE INDEX etl_job_claim_eligibility_index
+ ON etl_job_records (
+ job_status,
+ lease_expires_at,
+ created_at,
+ job_record_id
+ );
From 0325b863140867517d1bd91465c090b3b667171b Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 09:35:54 +0900
Subject: [PATCH 06/92] feat(etl): define fail-closed worker properties
---
.../etl/job/EtlJobWorkerProperties.java | 172 ++++++++++++++++++
1 file changed, 172 insertions(+)
create mode 100644 etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobWorkerProperties.java
diff --git a/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobWorkerProperties.java b/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobWorkerProperties.java
new file mode 100644
index 00000000..64ff386b
--- /dev/null
+++ b/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobWorkerProperties.java
@@ -0,0 +1,172 @@
+package com.xtrmetl.etl.job;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.Objects;
+import java.util.UUID;
+import java.util.regex.Pattern;
+
+/**
+ * Holds bounded, fail-closed configuration for durable ETL job execution.
+ *
+ *
The worker is disabled unless an operator explicitly enables it. A process-lifetime lease
+ * owner identifier is generated when no external value is supplied. The identifier is deliberately
+ * restricted to a short safe ASCII profile because it is persisted as operational metadata and
+ * must never become a free-form log or database injection surface.
+ */
+@ConfigurationProperties(prefix = "xtrmetl.etl.jobs.worker")
+public class EtlJobWorkerProperties {
+
+ private static final Pattern SAFE_LEASE_OWNER_PATTERN = Pattern.compile(
+ "[A-Za-z0-9._:-]{8,128}"
+ );
+
+ private boolean enabled;
+ private long fixedDelayMilliseconds = 5_000L;
+ private long initialDelayMilliseconds = 5_000L;
+ private long leaseDurationSeconds = 300L;
+ private int maxAttempts = 3;
+ private String leaseOwnerId = "worker-" + UUID.randomUUID();
+
+ /**
+ * Creates disabled worker configuration with bounded production-safe defaults.
+ */
+ public EtlJobWorkerProperties() {
+ // Spring Boot binds through the public setters while preserving generated defaults.
+ }
+
+ /**
+ * Reports whether scheduled durable-job execution is explicitly enabled.
+ *
+ * @return {@code true} only when an operator enabled the worker
+ */
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ /**
+ * Enables or disables scheduled durable-job execution.
+ *
+ * @param enabled whether the worker should run
+ */
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ /**
+ * Returns the delay measured after one polling invocation completes.
+ *
+ * @return positive fixed delay in milliseconds
+ */
+ public long getFixedDelayMilliseconds() {
+ return fixedDelayMilliseconds;
+ }
+
+ /**
+ * Sets the delay measured after one polling invocation completes.
+ *
+ * @param fixedDelayMilliseconds positive fixed delay in milliseconds
+ * @throws IllegalArgumentException when the delay is zero or negative
+ */
+ public void setFixedDelayMilliseconds(long fixedDelayMilliseconds) {
+ if (fixedDelayMilliseconds <= 0L) {
+ throw new IllegalArgumentException("fixedDelayMilliseconds must be positive");
+ }
+ this.fixedDelayMilliseconds = fixedDelayMilliseconds;
+ }
+
+ /**
+ * Returns the delay before the first polling invocation after application startup.
+ *
+ * @return non-negative initial delay in milliseconds
+ */
+ public long getInitialDelayMilliseconds() {
+ return initialDelayMilliseconds;
+ }
+
+ /**
+ * Sets the delay before the first polling invocation after application startup.
+ *
+ * @param initialDelayMilliseconds non-negative initial delay in milliseconds
+ * @throws IllegalArgumentException when the delay is negative
+ */
+ public void setInitialDelayMilliseconds(long initialDelayMilliseconds) {
+ if (initialDelayMilliseconds < 0L) {
+ throw new IllegalArgumentException("initialDelayMilliseconds must not be negative");
+ }
+ this.initialDelayMilliseconds = initialDelayMilliseconds;
+ }
+
+ /**
+ * Returns how long one database claim remains valid without renewal.
+ *
+ * @return positive lease duration in seconds
+ */
+ public long getLeaseDurationSeconds() {
+ return leaseDurationSeconds;
+ }
+
+ /**
+ * Sets how long one database claim remains valid without renewal.
+ *
+ * @param leaseDurationSeconds positive lease duration in seconds
+ * @throws IllegalArgumentException when the duration is zero or negative
+ */
+ public void setLeaseDurationSeconds(long leaseDurationSeconds) {
+ if (leaseDurationSeconds <= 0L) {
+ throw new IllegalArgumentException("leaseDurationSeconds must be positive");
+ }
+ this.leaseDurationSeconds = leaseDurationSeconds;
+ }
+
+ /**
+ * Returns the maximum number of claims permitted before terminal failure.
+ *
+ * @return maximum attempt count from 1 through 100
+ */
+ public int getMaxAttempts() {
+ return maxAttempts;
+ }
+
+ /**
+ * Sets the maximum number of claims permitted before terminal failure.
+ *
+ * @param maxAttempts maximum attempt count from 1 through 100
+ * @throws IllegalArgumentException when the value is outside the supported range
+ */
+ public void setMaxAttempts(int maxAttempts) {
+ if (maxAttempts < 1 || maxAttempts > 100) {
+ throw new IllegalArgumentException("maxAttempts must be between 1 and 100");
+ }
+ this.maxAttempts = maxAttempts;
+ }
+
+ /**
+ * Returns the non-sensitive process identifier persisted on active leases.
+ *
+ * @return safe process-lifetime lease owner identifier
+ */
+ public String getLeaseOwnerId() {
+ return leaseOwnerId;
+ }
+
+ /**
+ * Sets the non-sensitive process identifier persisted on active leases.
+ *
+ * @param leaseOwnerId 8-to-128-character safe ASCII process identifier
+ * @throws NullPointerException when the identifier is {@code null}
+ * @throws IllegalArgumentException when the identifier is too short, too long, or unsafe
+ */
+ public void setLeaseOwnerId(String leaseOwnerId) {
+ String requiredOwnerId = Objects.requireNonNull(
+ leaseOwnerId,
+ "leaseOwnerId must not be null"
+ );
+ if (!SAFE_LEASE_OWNER_PATTERN.matcher(requiredOwnerId).matches()) {
+ throw new IllegalArgumentException(
+ "leaseOwnerId must match [A-Za-z0-9._:-]{8,128}"
+ );
+ }
+ this.leaseOwnerId = requiredOwnerId;
+ }
+}
From a12933356e943af57bbd59aeebd97328d919cf34 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 09:36:05 +0900
Subject: [PATCH 07/92] feat(etl): register worker configuration
---
.../src/main/java/com/xtrmetl/etl/EtlApplication.java | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/etl-service/src/main/java/com/xtrmetl/etl/EtlApplication.java b/etl-service/src/main/java/com/xtrmetl/etl/EtlApplication.java
index c6796c2b..17213106 100644
--- a/etl-service/src/main/java/com/xtrmetl/etl/EtlApplication.java
+++ b/etl-service/src/main/java/com/xtrmetl/etl/EtlApplication.java
@@ -1,6 +1,7 @@
package com.xtrmetl.etl;
import com.xtrmetl.etl.connector.ConnectorProperties;
+import com.xtrmetl.etl.job.EtlJobWorkerProperties;
import com.xtrmetl.etl.service.EtlBatchProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@@ -16,7 +17,11 @@
@EnableDiscoveryClient
@EnableAspectJAutoProxy(proxyTargetClass = true)
@EnableRetry
-@EnableConfigurationProperties({ConnectorProperties.class, EtlBatchProperties.class})
+@EnableConfigurationProperties({
+ ConnectorProperties.class,
+ EtlBatchProperties.class,
+ EtlJobWorkerProperties.class
+})
public class EtlApplication {
/**
From aeeeb380a1900e63738a2f2e4d5508d16914b026 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 09:36:22 +0900
Subject: [PATCH 08/92] feat(etl): add fail-closed worker defaults
---
etl-service/src/main/resources/application.yml | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/etl-service/src/main/resources/application.yml b/etl-service/src/main/resources/application.yml
index dbb4ebcb..642675d3 100644
--- a/etl-service/src/main/resources/application.yml
+++ b/etl-service/src/main/resources/application.yml
@@ -27,9 +27,15 @@ xtrmetl:
max-payload-bytes: ${ETL_MAX_PAYLOAD_BYTES:1048576}
max-batch-records: ${ETL_MAX_BATCH_RECORDS:1000}
jobs:
- # Intake persists validated payloads but does not execute them in this bounded slice.
- # Keep disabled until an operator explicitly accepts the temporary retention boundary.
+ # Intake persists validated payloads for durable worker execution.
intake-enabled: ${ETL_JOB_INTAKE_ENABLED:false}
+ worker:
+ # Execution remains fail-closed until an operator enables the worker explicitly.
+ enabled: ${ETL_JOB_WORKER_ENABLED:false}
+ fixed-delay-milliseconds: ${ETL_JOB_WORKER_FIXED_DELAY_MILLISECONDS:5000}
+ initial-delay-milliseconds: ${ETL_JOB_WORKER_INITIAL_DELAY_MILLISECONDS:5000}
+ lease-duration-seconds: ${ETL_JOB_WORKER_LEASE_DURATION_SECONDS:300}
+ max-attempts: ${ETL_JOB_WORKER_MAX_ATTEMPTS:3}
# Warehouse/BI targets: SPI + config binding + validation + catalog; writes remain SCAFFOLD.
connectors:
databricks:
From faab9debd45724fe59b080de98e8e5ffe1e19595 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 09:36:49 +0900
Subject: [PATCH 09/92] test(etl): align claim index contract
---
.../db/migration/V3__add_etl_job_lease_fencing.sql | 7 +------
1 file changed, 1 insertion(+), 6 deletions(-)
diff --git a/etl-service/src/main/resources/db/migration/V3__add_etl_job_lease_fencing.sql b/etl-service/src/main/resources/db/migration/V3__add_etl_job_lease_fencing.sql
index d54bf6df..0b5a5555 100644
--- a/etl-service/src/main/resources/db/migration/V3__add_etl_job_lease_fencing.sql
+++ b/etl-service/src/main/resources/db/migration/V3__add_etl_job_lease_fencing.sql
@@ -43,9 +43,4 @@ ALTER TABLE etl_job_records
);
CREATE INDEX etl_job_claim_eligibility_index
- ON etl_job_records (
- job_status,
- lease_expires_at,
- created_at,
- job_record_id
- );
+ ON etl_job_records (job_status, lease_expires_at, created_at, job_record_id);
From f1eac6e2263408015ecdd1f75a0adedc77359167 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 09:38:37 +0900
Subject: [PATCH 10/92] test(etl): specify lease-fenced job persistence
---
.../EtlJobLeaseRepositoryIntegrationTest.java | 430 ++++++++++++++++++
1 file changed, 430 insertions(+)
create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobLeaseRepositoryIntegrationTest.java
diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobLeaseRepositoryIntegrationTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobLeaseRepositoryIntegrationTest.java
new file mode 100644
index 00000000..773c5936
--- /dev/null
+++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobLeaseRepositoryIntegrationTest.java
@@ -0,0 +1,430 @@
+package com.xtrmetl.etl.job;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import javax.sql.DataSource;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Proves exclusive durable-job claims and exact-live-lease state transitions against SQL.
+ */
+@SpringJUnitConfig(EtlJobLeaseRepositoryIntegrationTest.TestConfiguration.class)
+class EtlJobLeaseRepositoryIntegrationTest {
+
+ private static final Duration LEASE_DURATION = Duration.ofMinutes(5);
+ private static final String OWNER_ALPHA = "worker-alpha";
+ private static final String OWNER_BETA = "worker-beta";
+ private static final String PAYLOAD = "[{\"id\":\"record_alpha\"}]";
+
+ private final EtlJobLeaseRepository repository;
+ private final JdbcTemplate jdbcTemplate;
+ private ExecutorService executorService;
+
+ @Autowired
+ EtlJobLeaseRepositoryIntegrationTest(
+ EtlJobLeaseRepository repository,
+ JdbcTemplate jdbcTemplate
+ ) {
+ this.repository = repository;
+ this.jdbcTemplate = jdbcTemplate;
+ }
+
+ @BeforeEach
+ void createJobTable() {
+ executorService = Executors.newFixedThreadPool(2);
+ jdbcTemplate.execute("DROP TABLE IF EXISTS etl_job_records");
+ jdbcTemplate.execute("""
+ CREATE TABLE etl_job_records (
+ job_record_id UUID PRIMARY KEY,
+ principal_scope_hash CHAR(64) NOT NULL,
+ submission_key_hash CHAR(64) NOT NULL,
+ request_digest CHAR(64) NOT NULL,
+ request_payload CLOB,
+ job_status VARCHAR(32) NOT NULL,
+ attempt_count INTEGER NOT NULL DEFAULT 0,
+ failure_code VARCHAR(128),
+ lease_claim_id UUID,
+ lease_owner_id VARCHAR(128),
+ lease_expires_at TIMESTAMP WITH TIME ZONE,
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE NOT NULL
+ )
+ """);
+ }
+
+ @AfterEach
+ void closeExecutor() {
+ executorService.close();
+ }
+
+ @Test
+ void claimsTheOldestEligibleJobAndIncrementsItsAttempt() {
+ UUID newerJobId = insertPending(Instant.parse("2026-08-05T00:01:00Z"), 0);
+ UUID olderJobId = insertPending(Instant.parse("2026-08-05T00:00:00Z"), 1);
+
+ EtlJobLease lease = repository.claimNext(OWNER_ALPHA, LEASE_DURATION, 3).orElseThrow();
+
+ assertEquals(olderJobId, lease.jobRecordId());
+ assertEquals(OWNER_ALPHA, lease.leaseOwnerId());
+ assertEquals(PAYLOAD, lease.requestPayload());
+ assertEquals(2, lease.attemptCount());
+ assertNotNull(lease.leaseClaimId());
+ assertTrue(lease.leaseExpiresAt().isAfter(Instant.now()));
+ assertEquals("RUNNING", textColumn(olderJobId, "job_status"));
+ assertEquals(2, integerColumn(olderJobId, "attempt_count"));
+ assertEquals("PENDING", textColumn(newerJobId, "job_status"));
+ }
+
+ @Test
+ void skipsLiveClaimsAndReclaimsExpiredClaimsWithFreshFencing() {
+ UUID liveJobId = insertRunning(
+ Instant.parse("2026-08-05T00:00:00Z"),
+ 1,
+ OWNER_ALPHA,
+ UUID.randomUUID(),
+ Instant.now().plusSeconds(300)
+ );
+ UUID priorClaimId = UUID.randomUUID();
+ UUID expiredJobId = insertRunning(
+ Instant.parse("2026-08-05T00:01:00Z"),
+ 1,
+ OWNER_ALPHA,
+ priorClaimId,
+ Instant.now().minusSeconds(60)
+ );
+
+ EtlJobLease reclaimed = repository.claimNext(OWNER_BETA, LEASE_DURATION, 3).orElseThrow();
+
+ assertEquals(expiredJobId, reclaimed.jobRecordId());
+ assertEquals(OWNER_BETA, reclaimed.leaseOwnerId());
+ assertNotEquals(priorClaimId, reclaimed.leaseClaimId());
+ assertEquals(2, reclaimed.attemptCount());
+ assertEquals("RUNNING", textColumn(liveJobId, "job_status"));
+ assertEquals(OWNER_ALPHA, textColumn(liveJobId, "lease_owner_id"));
+ }
+
+ @Test
+ void returnsEmptyWhenEveryClaimIsLive() {
+ insertRunning(
+ Instant.now(),
+ 1,
+ OWNER_ALPHA,
+ UUID.randomUUID(),
+ Instant.now().plusSeconds(300)
+ );
+
+ Optional lease = repository.claimNext(OWNER_BETA, LEASE_DURATION, 3);
+
+ assertTrue(lease.isEmpty());
+ }
+
+ @Test
+ void onlyOneConcurrentWorkerCanClaimOnePendingJob() throws Exception {
+ UUID jobRecordId = insertPending(Instant.now(), 0);
+ CountDownLatch start = new CountDownLatch(1);
+
+ Future> alpha = executorService.submit(() -> {
+ start.await();
+ return repository.claimNext(OWNER_ALPHA, LEASE_DURATION, 3);
+ });
+ Future> beta = executorService.submit(() -> {
+ start.await();
+ return repository.claimNext(OWNER_BETA, LEASE_DURATION, 3);
+ });
+ start.countDown();
+
+ List claims = List.of(alpha.get(), beta.get()).stream()
+ .flatMap(Optional::stream)
+ .toList();
+
+ assertEquals(1, claims.size());
+ assertEquals(jobRecordId, claims.getFirst().jobRecordId());
+ assertEquals(1, integerColumn(jobRecordId, "attempt_count"));
+ }
+
+ @Test
+ void terminalizesExhaustedEligibleRowsBeforeLookingForWork() {
+ UUID pendingJobId = insertPending(Instant.now(), 3);
+ UUID expiredJobId = insertRunning(
+ Instant.now().plusSeconds(1),
+ 3,
+ OWNER_ALPHA,
+ UUID.randomUUID(),
+ Instant.now().minusSeconds(60)
+ );
+
+ Optional lease = repository.claimNext(OWNER_BETA, LEASE_DURATION, 3);
+
+ assertTrue(lease.isEmpty());
+ assertTerminalExhaustion(pendingJobId);
+ assertTerminalExhaustion(expiredJobId);
+ }
+
+ @Test
+ void exactLiveLeaseCanSucceedRetryOrFailAndClearsTheRightFields() {
+ UUID successJobId = insertPending(Instant.parse("2026-08-05T00:00:00Z"), 0);
+ EtlJobLease successLease = repository.claimNext(OWNER_ALPHA, LEASE_DURATION, 3)
+ .orElseThrow();
+ repository.markSucceeded(successLease);
+ assertEquals(successJobId, successLease.jobRecordId());
+ assertEquals("SUCCEEDED", textColumn(successJobId, "job_status"));
+ assertNull(textColumn(successJobId, "request_payload"));
+ assertNull(textColumn(successJobId, "failure_code"));
+ assertNull(textColumn(successJobId, "lease_owner_id"));
+
+ UUID retryJobId = insertPending(Instant.parse("2026-08-05T00:01:00Z"), 0);
+ EtlJobLease retryLease = repository.claimNext(OWNER_ALPHA, LEASE_DURATION, 3)
+ .orElseThrow();
+ repository.releaseForRetry(retryLease, 3);
+ assertEquals(retryJobId, retryLease.jobRecordId());
+ assertEquals("PENDING", textColumn(retryJobId, "job_status"));
+ assertEquals(PAYLOAD, textColumn(retryJobId, "request_payload"));
+ assertNull(textColumn(retryJobId, "failure_code"));
+ assertNull(textColumn(retryJobId, "lease_owner_id"));
+
+ UUID failedJobId = insertPending(Instant.parse("2026-08-05T00:02:00Z"), 2);
+ EtlJobLease failedLease = repository.claimNext(OWNER_ALPHA, LEASE_DURATION, 3)
+ .orElseThrow();
+ repository.markFailed(failedLease, "etl_target_failure");
+ assertEquals(failedJobId, failedLease.jobRecordId());
+ assertEquals("FAILED", textColumn(failedJobId, "job_status"));
+ assertNull(textColumn(failedJobId, "request_payload"));
+ assertEquals("etl_target_failure", textColumn(failedJobId, "failure_code"));
+ assertNull(textColumn(failedJobId, "lease_owner_id"));
+ }
+
+ @Test
+ void rejectsExpiredSupersededOrExhaustedTransitions() {
+ UUID jobRecordId = insertPending(Instant.now(), 0);
+ EtlJobLease lease = repository.claimNext(OWNER_ALPHA, LEASE_DURATION, 1).orElseThrow();
+ jdbcTemplate.update(
+ "UPDATE etl_job_records SET lease_expires_at = ? WHERE job_record_id = ?",
+ Instant.now().minusSeconds(1),
+ jobRecordId
+ );
+
+ assertThrows(StaleEtlJobLeaseException.class, () -> repository.markSucceeded(lease));
+ assertThrows(
+ StaleEtlJobLeaseException.class,
+ () -> repository.releaseForRetry(lease, 1)
+ );
+ assertThrows(
+ StaleEtlJobLeaseException.class,
+ () -> repository.markFailed(lease, "etl_target_failure")
+ );
+
+ jdbcTemplate.update(
+ """
+ UPDATE etl_job_records
+ SET lease_claim_id = ?, lease_expires_at = ?
+ WHERE job_record_id = ?
+ """,
+ UUID.randomUUID(),
+ Instant.now().plusSeconds(300),
+ jobRecordId
+ );
+ assertThrows(StaleEtlJobLeaseException.class, () -> repository.markSucceeded(lease));
+ }
+
+ @Test
+ void rejectsInvalidPublicArgumentsBeforeSqlExecution() {
+ assertThrows(
+ NullPointerException.class,
+ () -> repository.claimNext(null, LEASE_DURATION, 3)
+ );
+ assertThrows(
+ NullPointerException.class,
+ () -> repository.claimNext(OWNER_ALPHA, null, 3)
+ );
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> repository.claimNext("short", LEASE_DURATION, 3)
+ );
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> repository.claimNext(OWNER_ALPHA, Duration.ZERO, 3)
+ );
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> repository.claimNext(OWNER_ALPHA, LEASE_DURATION, 0)
+ );
+ assertThrows(
+ NullPointerException.class,
+ () -> repository.markSucceeded(null)
+ );
+ assertThrows(
+ NullPointerException.class,
+ () -> repository.releaseForRetry(null, 3)
+ );
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> repository.releaseForRetry(sampleLease(), 0)
+ );
+ assertThrows(
+ NullPointerException.class,
+ () -> repository.markFailed(null, "etl_target_failure")
+ );
+ assertThrows(
+ NullPointerException.class,
+ () -> repository.markFailed(sampleLease(), null)
+ );
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> repository.markFailed(sampleLease(), "UNSAFE FAILURE")
+ );
+ }
+
+ private UUID insertPending(Instant createdAt, int attemptCount) {
+ UUID jobRecordId = UUID.randomUUID();
+ jdbcTemplate.update(
+ """
+ INSERT INTO etl_job_records (
+ job_record_id, principal_scope_hash, submission_key_hash,
+ request_digest, request_payload, job_status, attempt_count,
+ created_at, updated_at
+ ) VALUES (?, ?, ?, ?, ?, 'PENDING', ?, ?, ?)
+ """,
+ jobRecordId,
+ "a".repeat(64),
+ UUID.randomUUID().toString().replace("-", "").repeat(2),
+ "b".repeat(64),
+ PAYLOAD,
+ attemptCount,
+ createdAt,
+ createdAt
+ );
+ return jobRecordId;
+ }
+
+ private UUID insertRunning(
+ Instant createdAt,
+ int attemptCount,
+ String ownerId,
+ UUID claimId,
+ Instant expiresAt
+ ) {
+ UUID jobRecordId = UUID.randomUUID();
+ jdbcTemplate.update(
+ """
+ INSERT INTO etl_job_records (
+ job_record_id, principal_scope_hash, submission_key_hash,
+ request_digest, request_payload, job_status, attempt_count,
+ lease_claim_id, lease_owner_id, lease_expires_at,
+ created_at, updated_at
+ ) VALUES (?, ?, ?, ?, ?, 'RUNNING', ?, ?, ?, ?, ?, ?)
+ """,
+ jobRecordId,
+ "c".repeat(64),
+ UUID.randomUUID().toString().replace("-", "").repeat(2),
+ "d".repeat(64),
+ PAYLOAD,
+ attemptCount,
+ claimId,
+ ownerId,
+ expiresAt,
+ createdAt,
+ createdAt
+ );
+ return jobRecordId;
+ }
+
+ private void assertTerminalExhaustion(UUID jobRecordId) {
+ assertEquals("FAILED", textColumn(jobRecordId, "job_status"));
+ assertEquals(
+ "etl_worker_attempts_exhausted",
+ textColumn(jobRecordId, "failure_code")
+ );
+ assertNull(textColumn(jobRecordId, "request_payload"));
+ assertNull(textColumn(jobRecordId, "lease_owner_id"));
+ }
+
+ private String textColumn(UUID jobRecordId, String columnName) {
+ return jdbcTemplate.queryForObject(
+ "SELECT " + columnName + " FROM etl_job_records WHERE job_record_id = ?",
+ String.class,
+ jobRecordId
+ );
+ }
+
+ private int integerColumn(UUID jobRecordId, String columnName) {
+ Integer value = jdbcTemplate.queryForObject(
+ "SELECT " + columnName + " FROM etl_job_records WHERE job_record_id = ?",
+ Integer.class,
+ jobRecordId
+ );
+ return value == null ? -1 : value;
+ }
+
+ private static EtlJobLease sampleLease() {
+ return new EtlJobLease(
+ UUID.randomUUID(),
+ UUID.randomUUID(),
+ OWNER_ALPHA,
+ PAYLOAD,
+ 1,
+ Instant.now().plusSeconds(300)
+ );
+ }
+
+ /**
+ * Minimal transaction-enabled SQL context for durable lease persistence tests.
+ */
+ @Configuration
+ @EnableTransactionManagement
+ static class TestConfiguration {
+
+ @Bean
+ DataSource dataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .generateUniqueName(true)
+ .setType(EmbeddedDatabaseType.H2)
+ .build();
+ }
+
+ @Bean
+ JdbcTemplate jdbcTemplate(DataSource dataSource) {
+ return new JdbcTemplate(dataSource);
+ }
+
+ @Bean
+ PlatformTransactionManager transactionManager(DataSource dataSource) {
+ return new DataSourceTransactionManager(dataSource);
+ }
+
+ @Bean
+ EtlJobLeaseRepository etlJobLeaseRepository(
+ JdbcTemplate jdbcTemplate,
+ PlatformTransactionManager transactionManager
+ ) {
+ return new EtlJobLeaseRepository(jdbcTemplate, transactionManager);
+ }
+ }
+}
From d037bf1c4a4ece20f85fca92b9445897de89ef98 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 09:38:59 +0900
Subject: [PATCH 11/92] test(etl): specify lease value invariants
---
.../xtrmetl/etl/job/EtlJobLeaseModelTest.java | 79 +++++++++++++++++++
1 file changed, 79 insertions(+)
create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobLeaseModelTest.java
diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobLeaseModelTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobLeaseModelTest.java
new file mode 100644
index 00000000..bf677eb1
--- /dev/null
+++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobLeaseModelTest.java
@@ -0,0 +1,79 @@
+package com.xtrmetl.etl.job;
+
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Specifies the immutable value contract carried from a database claim into execution.
+ */
+class EtlJobLeaseModelTest {
+
+ private static final UUID JOB_RECORD_ID = UUID.randomUUID();
+ private static final UUID LEASE_CLAIM_ID = UUID.randomUUID();
+ private static final String OWNER_ID = "worker-alpha";
+ private static final String PAYLOAD = "[{\"id\":\"record_alpha\"}]";
+ private static final Instant EXPIRY = Instant.parse("2026-08-05T01:00:00Z");
+
+ @Test
+ void retainsEveryValidatedClaimField() {
+ EtlJobLease lease = new EtlJobLease(
+ JOB_RECORD_ID,
+ LEASE_CLAIM_ID,
+ OWNER_ID,
+ PAYLOAD,
+ 2,
+ EXPIRY
+ );
+
+ assertEquals(JOB_RECORD_ID, lease.jobRecordId());
+ assertEquals(LEASE_CLAIM_ID, lease.leaseClaimId());
+ assertEquals(OWNER_ID, lease.leaseOwnerId());
+ assertEquals(PAYLOAD, lease.requestPayload());
+ assertEquals(2, lease.attemptCount());
+ assertEquals(EXPIRY, lease.leaseExpiresAt());
+ }
+
+ @Test
+ void rejectsMissingUnsafeOrImpossibleFields() {
+ assertThrows(
+ NullPointerException.class,
+ () -> new EtlJobLease(null, LEASE_CLAIM_ID, OWNER_ID, PAYLOAD, 1, EXPIRY)
+ );
+ assertThrows(
+ NullPointerException.class,
+ () -> new EtlJobLease(JOB_RECORD_ID, null, OWNER_ID, PAYLOAD, 1, EXPIRY)
+ );
+ assertThrows(
+ NullPointerException.class,
+ () -> new EtlJobLease(JOB_RECORD_ID, LEASE_CLAIM_ID, null, PAYLOAD, 1, EXPIRY)
+ );
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new EtlJobLease(
+ JOB_RECORD_ID,
+ LEASE_CLAIM_ID,
+ "unsafe owner",
+ PAYLOAD,
+ 1,
+ EXPIRY
+ )
+ );
+ assertThrows(
+ NullPointerException.class,
+ () -> new EtlJobLease(JOB_RECORD_ID, LEASE_CLAIM_ID, OWNER_ID, null, 1, EXPIRY)
+ );
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new EtlJobLease(JOB_RECORD_ID, LEASE_CLAIM_ID, OWNER_ID, PAYLOAD, 0, EXPIRY)
+ );
+ assertThrows(
+ NullPointerException.class,
+ () -> new EtlJobLease(JOB_RECORD_ID, LEASE_CLAIM_ID, OWNER_ID, PAYLOAD, 1, null)
+ );
+ }
+}
From f1d066f2c01fb3678fea8a998bcd46db129a7655 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 09:39:12 +0900
Subject: [PATCH 12/92] feat(etl): add immutable lease value
---
.../java/com/xtrmetl/etl/job/EtlJobLease.java | 52 +++++++++++++++++++
1 file changed, 52 insertions(+)
create mode 100644 etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobLease.java
diff --git a/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobLease.java b/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobLease.java
new file mode 100644
index 00000000..23ca99b1
--- /dev/null
+++ b/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobLease.java
@@ -0,0 +1,52 @@
+package com.xtrmetl.etl.job;
+
+import java.time.Instant;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.regex.Pattern;
+
+/**
+ * Carries one immutable, fenced database claim into ETL execution.
+ *
+ * @param jobRecordId durable job identifier
+ * @param leaseClaimId unique token generated for this exact claim or reclaim
+ * @param leaseOwnerId non-sensitive process-lifetime worker identifier
+ * @param requestPayload validated JSON payload retained while the job is non-terminal
+ * @param attemptCount one-based claim attempt count after this claim was persisted
+ * @param leaseExpiresAt database-derived instant after which this claim is stale
+ */
+public record EtlJobLease(
+ UUID jobRecordId,
+ UUID leaseClaimId,
+ String leaseOwnerId,
+ String requestPayload,
+ int attemptCount,
+ Instant leaseExpiresAt
+) {
+
+ private static final Pattern SAFE_LEASE_OWNER_PATTERN = Pattern.compile(
+ "[A-Za-z0-9._:-]{8,128}"
+ );
+
+ /**
+ * Validates every field needed for exact lease fencing and deterministic execution.
+ */
+ public EtlJobLease {
+ Objects.requireNonNull(jobRecordId, "jobRecordId must not be null");
+ Objects.requireNonNull(leaseClaimId, "leaseClaimId must not be null");
+ String requiredOwnerId = Objects.requireNonNull(
+ leaseOwnerId,
+ "leaseOwnerId must not be null"
+ );
+ if (!SAFE_LEASE_OWNER_PATTERN.matcher(requiredOwnerId).matches()) {
+ throw new IllegalArgumentException(
+ "leaseOwnerId must match [A-Za-z0-9._:-]{8,128}"
+ );
+ }
+ Objects.requireNonNull(requestPayload, "requestPayload must not be null");
+ if (attemptCount < 1) {
+ throw new IllegalArgumentException("attemptCount must be positive");
+ }
+ Objects.requireNonNull(leaseExpiresAt, "leaseExpiresAt must not be null");
+ }
+}
From b04a10549ec73ed7b698651ce14f6fc81edfb609 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 09:39:39 +0900
Subject: [PATCH 13/92] feat(etl): define stale lease failure
---
.../etl/job/StaleEtlJobLeaseException.java | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
create mode 100644 etl-service/src/main/java/com/xtrmetl/etl/job/StaleEtlJobLeaseException.java
diff --git a/etl-service/src/main/java/com/xtrmetl/etl/job/StaleEtlJobLeaseException.java b/etl-service/src/main/java/com/xtrmetl/etl/job/StaleEtlJobLeaseException.java
new file mode 100644
index 00000000..d7320870
--- /dev/null
+++ b/etl-service/src/main/java/com/xtrmetl/etl/job/StaleEtlJobLeaseException.java
@@ -0,0 +1,17 @@
+package com.xtrmetl.etl.job;
+
+/**
+ * Signals that a worker no longer owns the exact live lease required for a state transition.
+ *
+ *
The exception intentionally carries no job, claim, owner, payload, SQL, or timestamp values so
+ * accidental logging cannot disclose operational identifiers or retained customer data.
Every claim transaction first terminalizes eligible exhausted rows, then locks at most one
+ * oldest eligible row with {@code FOR UPDATE SKIP LOCKED}, and finally writes a fresh claim token,
+ * owner, expiry, and incremented attempt count before commit. State transitions repeat the exact
+ * claim token, owner, running status, and database-time expiry predicates so stale workers cannot
+ * mutate lifecycle state.
+ */
+@Repository
+public class EtlJobLeaseRepository {
+
+ /** Stable terminal code assigned when no additional claim is permitted. */
+ public static final String ATTEMPTS_EXHAUSTED_FAILURE_CODE =
+ "etl_worker_attempts_exhausted";
+
+ private static final Pattern SAFE_OWNER_PATTERN = Pattern.compile(
+ "[A-Za-z0-9._:-]{8,128}"
+ );
+ private static final Pattern SAFE_FAILURE_CODE_PATTERN = Pattern.compile(
+ "[a-z][a-z0-9_]{2,127}"
+ );
+
+ private static final String TERMINALIZE_EXHAUSTED_SQL = """
+ UPDATE etl_job_records
+ SET job_status = 'FAILED',
+ request_payload = NULL,
+ failure_code = ?,
+ lease_claim_id = NULL,
+ lease_owner_id = NULL,
+ lease_expires_at = NULL,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE attempt_count >= ?
+ AND (
+ job_status = 'PENDING'
+ OR (
+ job_status = 'RUNNING'
+ AND lease_expires_at <= CURRENT_TIMESTAMP
+ )
+ )
+ """;
+
+ private static final String SELECT_CANDIDATE_SQL = """
+ SELECT job_record_id,
+ request_payload,
+ attempt_count,
+ CURRENT_TIMESTAMP AS database_now
+ FROM etl_job_records
+ WHERE attempt_count < ?
+ AND (
+ job_status = 'PENDING'
+ OR (
+ job_status = 'RUNNING'
+ AND lease_expires_at <= CURRENT_TIMESTAMP
+ )
+ )
+ ORDER BY created_at, job_record_id
+ FETCH FIRST 1 ROW ONLY
+ FOR UPDATE SKIP LOCKED
+ """;
+
+ private static final String CLAIM_CANDIDATE_SQL = """
+ UPDATE etl_job_records
+ SET job_status = 'RUNNING',
+ attempt_count = attempt_count + 1,
+ failure_code = NULL,
+ lease_claim_id = ?,
+ lease_owner_id = ?,
+ lease_expires_at = ?,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE job_record_id = ?
+ AND attempt_count = ?
+ AND (
+ job_status = 'PENDING'
+ OR (
+ job_status = 'RUNNING'
+ AND lease_expires_at <= CURRENT_TIMESTAMP
+ )
+ )
+ """;
+
+ private static final String MARK_SUCCEEDED_SQL = """
+ UPDATE etl_job_records
+ SET job_status = 'SUCCEEDED',
+ request_payload = NULL,
+ failure_code = NULL,
+ lease_claim_id = NULL,
+ lease_owner_id = NULL,
+ lease_expires_at = NULL,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE job_record_id = ?
+ AND job_status = 'RUNNING'
+ AND lease_claim_id = ?
+ AND lease_owner_id = ?
+ AND lease_expires_at > CURRENT_TIMESTAMP
+ """;
+
+ private static final String RELEASE_FOR_RETRY_SQL = """
+ UPDATE etl_job_records
+ SET job_status = 'PENDING',
+ failure_code = NULL,
+ lease_claim_id = NULL,
+ lease_owner_id = NULL,
+ lease_expires_at = NULL,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE job_record_id = ?
+ AND job_status = 'RUNNING'
+ AND lease_claim_id = ?
+ AND lease_owner_id = ?
+ AND lease_expires_at > CURRENT_TIMESTAMP
+ AND attempt_count < ?
+ """;
+
+ private static final String MARK_FAILED_SQL = """
+ UPDATE etl_job_records
+ SET job_status = 'FAILED',
+ request_payload = NULL,
+ failure_code = ?,
+ lease_claim_id = NULL,
+ lease_owner_id = NULL,
+ lease_expires_at = NULL,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE job_record_id = ?
+ AND job_status = 'RUNNING'
+ AND lease_claim_id = ?
+ AND lease_owner_id = ?
+ AND lease_expires_at > CURRENT_TIMESTAMP
+ """;
+
+ private final JdbcTemplate jdbcTemplate;
+ private final TransactionTemplate transactionTemplate;
+
+ /**
+ * Creates lease persistence using one JDBC adapter and one transaction authority.
+ *
+ * @param jdbcTemplate JDBC operations for the durable-job table
+ * @param transactionManager transaction manager that owns row locks and claim commits
+ */
+ public EtlJobLeaseRepository(
+ JdbcTemplate jdbcTemplate,
+ PlatformTransactionManager transactionManager
+ ) {
+ this.jdbcTemplate = Objects.requireNonNull(
+ jdbcTemplate,
+ "jdbcTemplate must not be null"
+ );
+ this.transactionTemplate = new TransactionTemplate(Objects.requireNonNull(
+ transactionManager,
+ "transactionManager must not be null"
+ ));
+ }
+
+ /**
+ * Claims at most one oldest eligible job for one worker process.
+ *
+ * @param leaseOwnerId safe non-sensitive process identifier
+ * @param leaseDuration positive duration applied to database claim time
+ * @param maxAttempts maximum permitted claim count from 1 through 100
+ * @return a fresh claim, or an empty result when no row is eligible
+ * @throws NullPointerException when an argument is {@code null}
+ * @throws IllegalArgumentException when an argument violates its bounded contract
+ * @throws IllegalStateException when a locked candidate unexpectedly cannot be claimed
+ */
+ public Optional claimNext(
+ String leaseOwnerId,
+ Duration leaseDuration,
+ int maxAttempts
+ ) {
+ String validatedOwnerId = requireSafeOwnerId(leaseOwnerId);
+ Duration validatedDuration = requirePositiveDuration(leaseDuration);
+ int validatedMaxAttempts = requireMaxAttempts(maxAttempts);
+
+ return Objects.requireNonNull(transactionTemplate.execute(transactionStatus -> {
+ jdbcTemplate.update(
+ TERMINALIZE_EXHAUSTED_SQL,
+ ATTEMPTS_EXHAUSTED_FAILURE_CODE,
+ validatedMaxAttempts
+ );
+ List candidates = jdbcTemplate.query(
+ SELECT_CANDIDATE_SQL,
+ (resultSet, rowNumber) -> new ClaimCandidate(
+ resultSet.getObject("job_record_id", UUID.class),
+ resultSet.getString("request_payload"),
+ resultSet.getInt("attempt_count"),
+ resultSet.getObject("database_now", OffsetDateTime.class).toInstant()
+ ),
+ validatedMaxAttempts
+ );
+ if (candidates.isEmpty()) {
+ return Optional.empty();
+ }
+
+ ClaimCandidate candidate = candidates.getFirst();
+ UUID leaseClaimId = UUID.randomUUID();
+ Instant leaseExpiresAt = candidate.databaseNow().plus(validatedDuration);
+ int updatedRows = jdbcTemplate.update(
+ CLAIM_CANDIDATE_SQL,
+ leaseClaimId,
+ validatedOwnerId,
+ OffsetDateTime.ofInstant(leaseExpiresAt, ZoneOffset.UTC),
+ candidate.jobRecordId(),
+ candidate.attemptCount()
+ );
+ if (updatedRows != 1) {
+ throw new IllegalStateException("Locked ETL job candidate could not be claimed");
+ }
+ return Optional.of(new EtlJobLease(
+ candidate.jobRecordId(),
+ leaseClaimId,
+ validatedOwnerId,
+ candidate.requestPayload(),
+ candidate.attemptCount() + 1,
+ leaseExpiresAt
+ ));
+ }), "claim transaction must return a result");
+ }
+
+ /**
+ * Commits terminal success only for the exact live claim.
+ *
+ * @param lease exact claim whose target effects completed in the same transaction
+ * @throws NullPointerException when the lease is {@code null}
+ * @throws StaleEtlJobLeaseException when the claim is expired or no longer authoritative
+ */
+ public void markSucceeded(EtlJobLease lease) {
+ EtlJobLease requiredLease = Objects.requireNonNull(lease, "lease must not be null");
+ requireTransition(jdbcTemplate.update(
+ MARK_SUCCEEDED_SQL,
+ requiredLease.jobRecordId(),
+ requiredLease.leaseClaimId(),
+ requiredLease.leaseOwnerId()
+ ));
+ }
+
+ /**
+ * Returns a failed execution to pending only while attempts remain and the claim is exact.
+ *
+ * @param lease exact live claim to release
+ * @param maxAttempts maximum permitted claim count from 1 through 100
+ * @throws NullPointerException when the lease is {@code null}
+ * @throws IllegalArgumentException when the maximum is outside the supported range
+ * @throws StaleEtlJobLeaseException when the claim is stale or no retry remains
+ */
+ public void releaseForRetry(EtlJobLease lease, int maxAttempts) {
+ EtlJobLease requiredLease = Objects.requireNonNull(lease, "lease must not be null");
+ int validatedMaxAttempts = requireMaxAttempts(maxAttempts);
+ requireTransition(jdbcTemplate.update(
+ RELEASE_FOR_RETRY_SQL,
+ requiredLease.jobRecordId(),
+ requiredLease.leaseClaimId(),
+ requiredLease.leaseOwnerId(),
+ validatedMaxAttempts
+ ));
+ }
+
+ /**
+ * Commits terminal failure and clears the retained payload for the exact live claim.
+ *
+ * @param lease exact live claim to fail
+ * @param failureCode stable non-sensitive machine-readable failure classification
+ * @throws NullPointerException when an argument is {@code null}
+ * @throws IllegalArgumentException when the failure code is unsafe
+ * @throws StaleEtlJobLeaseException when the claim is expired or no longer authoritative
+ */
+ public void markFailed(EtlJobLease lease, String failureCode) {
+ EtlJobLease requiredLease = Objects.requireNonNull(lease, "lease must not be null");
+ String validatedFailureCode = requireSafeFailureCode(failureCode);
+ requireTransition(jdbcTemplate.update(
+ MARK_FAILED_SQL,
+ validatedFailureCode,
+ requiredLease.jobRecordId(),
+ requiredLease.leaseClaimId(),
+ requiredLease.leaseOwnerId()
+ ));
+ }
+
+ private static String requireSafeOwnerId(String leaseOwnerId) {
+ String requiredOwnerId = Objects.requireNonNull(
+ leaseOwnerId,
+ "leaseOwnerId must not be null"
+ );
+ if (!SAFE_OWNER_PATTERN.matcher(requiredOwnerId).matches()) {
+ throw new IllegalArgumentException(
+ "leaseOwnerId must match [A-Za-z0-9._:-]{8,128}"
+ );
+ }
+ return requiredOwnerId;
+ }
+
+ private static Duration requirePositiveDuration(Duration leaseDuration) {
+ Duration requiredDuration = Objects.requireNonNull(
+ leaseDuration,
+ "leaseDuration must not be null"
+ );
+ if (requiredDuration.isZero() || requiredDuration.isNegative()) {
+ throw new IllegalArgumentException("leaseDuration must be positive");
+ }
+ return requiredDuration;
+ }
+
+ private static int requireMaxAttempts(int maxAttempts) {
+ if (maxAttempts < 1 || maxAttempts > 100) {
+ throw new IllegalArgumentException("maxAttempts must be between 1 and 100");
+ }
+ return maxAttempts;
+ }
+
+ private static String requireSafeFailureCode(String failureCode) {
+ String requiredFailureCode = Objects.requireNonNull(
+ failureCode,
+ "failureCode must not be null"
+ );
+ if (!SAFE_FAILURE_CODE_PATTERN.matcher(requiredFailureCode).matches()) {
+ throw new IllegalArgumentException(
+ "failureCode must match [a-z][a-z0-9_]{2,127}"
+ );
+ }
+ return requiredFailureCode;
+ }
+
+ private static void requireTransition(int updatedRows) {
+ if (updatedRows != 1) {
+ throw new StaleEtlJobLeaseException();
+ }
+ }
+
+ private record ClaimCandidate(
+ UUID jobRecordId,
+ String requestPayload,
+ int attemptCount,
+ Instant databaseNow
+ ) {
+ }
+}
From c118dc51a26468829c38650ecc8243272a7061a2 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 09:42:32 +0900
Subject: [PATCH 15/92] test(etl): specify atomic leased execution
---
...EtlJobExecutionServiceIntegrationTest.java | 256 ++++++++++++++++++
1 file changed, 256 insertions(+)
create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobExecutionServiceIntegrationTest.java
diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobExecutionServiceIntegrationTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobExecutionServiceIntegrationTest.java
new file mode 100644
index 00000000..6b376423
--- /dev/null
+++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobExecutionServiceIntegrationTest.java
@@ -0,0 +1,256 @@
+package com.xtrmetl.etl.job;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.xtrmetl.etl.service.EtlBatchProperties;
+import com.xtrmetl.etl.service.EtlRequestLock;
+import com.xtrmetl.etl.service.EtlService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import javax.sql.DataSource;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Proves that target writes and exact-live-lease success commit or roll back together.
+ */
+@SpringJUnitConfig(EtlJobExecutionServiceIntegrationTest.TestConfiguration.class)
+class EtlJobExecutionServiceIntegrationTest {
+
+ private static final String OWNER_ID = "worker-alpha";
+ private static final String PAYLOAD = """
+ [{"id":"record_alpha","name":"accepted","email":"USER@EXAMPLE.COM"}]
+ """;
+
+ private final EtlJobExecutionService executionService;
+ private final EtlJobLeaseRepository leaseRepository;
+ private final JdbcTemplate jdbcTemplate;
+
+ @Autowired
+ EtlJobExecutionServiceIntegrationTest(
+ EtlJobExecutionService executionService,
+ EtlJobLeaseRepository leaseRepository,
+ JdbcTemplate jdbcTemplate
+ ) {
+ this.executionService = executionService;
+ this.leaseRepository = leaseRepository;
+ this.jdbcTemplate = jdbcTemplate;
+ }
+
+ @BeforeEach
+ void createTables() {
+ jdbcTemplate.execute("DROP TABLE IF EXISTS processed_data");
+ jdbcTemplate.execute("DROP TABLE IF EXISTS etl_job_records");
+ jdbcTemplate.execute("""
+ CREATE TABLE etl_job_records (
+ job_record_id UUID PRIMARY KEY,
+ principal_scope_hash CHAR(64) NOT NULL,
+ submission_key_hash CHAR(64) NOT NULL,
+ request_digest CHAR(64) NOT NULL,
+ request_payload CLOB,
+ job_status VARCHAR(32) NOT NULL,
+ attempt_count INTEGER NOT NULL DEFAULT 0,
+ failure_code VARCHAR(128),
+ lease_claim_id UUID,
+ lease_owner_id VARCHAR(128),
+ lease_expires_at TIMESTAMP WITH TIME ZONE,
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE NOT NULL
+ )
+ """);
+ jdbcTemplate.execute("""
+ CREATE TABLE processed_data (
+ processed_record_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ data VARCHAR(8192) NOT NULL
+ )
+ """);
+ }
+
+ @Test
+ void commitsTargetRowsAndTerminalSuccessInOneTransaction() {
+ UUID jobRecordId = insertPendingJob();
+ EtlJobLease lease = leaseRepository.claimNext(
+ OWNER_ID,
+ Duration.ofMinutes(5),
+ 3
+ ).orElseThrow();
+
+ executionService.execute(lease);
+
+ assertEquals(jobRecordId, lease.jobRecordId());
+ assertEquals(1, processedRowCount());
+ assertEquals(
+ "ID:record_alpha,NAME:ACCEPTED,EMAIL:user@example.com,",
+ jdbcTemplate.queryForObject("SELECT data FROM processed_data", String.class)
+ );
+ assertEquals("SUCCEEDED", jobStatus(jobRecordId));
+ assertEquals(0, retainedPayloadCount(jobRecordId));
+ }
+
+ @Test
+ void rollsBackTargetRowsWhenTheClaimWasSuperseded() {
+ UUID jobRecordId = insertPendingJob();
+ EtlJobLease lease = leaseRepository.claimNext(
+ OWNER_ID,
+ Duration.ofMinutes(5),
+ 3
+ ).orElseThrow();
+ jdbcTemplate.update(
+ "UPDATE etl_job_records SET lease_claim_id = ? WHERE job_record_id = ?",
+ UUID.randomUUID(),
+ jobRecordId
+ );
+
+ assertThrows(StaleEtlJobLeaseException.class, () -> executionService.execute(lease));
+
+ assertEquals(0, processedRowCount());
+ assertEquals("RUNNING", jobStatus(jobRecordId));
+ assertEquals(1, retainedPayloadCount(jobRecordId));
+ }
+
+ @Test
+ void rejectsMissingCollaboratorsOrLease() {
+ assertThrows(
+ NullPointerException.class,
+ () -> new EtlJobExecutionService(null, leaseRepository)
+ );
+ assertThrows(
+ NullPointerException.class,
+ () -> new EtlJobExecutionService(executionService.etlService(), null)
+ );
+ assertThrows(NullPointerException.class, () -> executionService.execute(null));
+ }
+
+ private UUID insertPendingJob() {
+ UUID jobRecordId = UUID.randomUUID();
+ Instant now = Instant.now();
+ jdbcTemplate.update(
+ """
+ INSERT INTO etl_job_records (
+ job_record_id, principal_scope_hash, submission_key_hash,
+ request_digest, request_payload, job_status, attempt_count,
+ created_at, updated_at
+ ) VALUES (?, ?, ?, ?, ?, 'PENDING', 0, ?, ?)
+ """,
+ jobRecordId,
+ "a".repeat(64),
+ "b".repeat(64),
+ "c".repeat(64),
+ PAYLOAD,
+ now,
+ now
+ );
+ return jobRecordId;
+ }
+
+ private int processedRowCount() {
+ Integer count = jdbcTemplate.queryForObject(
+ "SELECT COUNT(*) FROM processed_data",
+ Integer.class
+ );
+ return count == null ? 0 : count;
+ }
+
+ private String jobStatus(UUID jobRecordId) {
+ return jdbcTemplate.queryForObject(
+ "SELECT job_status FROM etl_job_records WHERE job_record_id = ?",
+ String.class,
+ jobRecordId
+ );
+ }
+
+ private int retainedPayloadCount(UUID jobRecordId) {
+ Integer count = jdbcTemplate.queryForObject(
+ """
+ SELECT COUNT(*)
+ FROM etl_job_records
+ WHERE job_record_id = ?
+ AND request_payload IS NOT NULL
+ """,
+ Integer.class,
+ jobRecordId
+ );
+ return count == null ? 0 : count;
+ }
+
+ /**
+ * Transaction-enabled execution context using one database for source state and target effects.
+ */
+ @Configuration
+ @EnableTransactionManagement
+ static class TestConfiguration {
+
+ @Bean
+ DataSource dataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .generateUniqueName(true)
+ .setType(EmbeddedDatabaseType.H2)
+ .build();
+ }
+
+ @Bean
+ JdbcTemplate jdbcTemplate(DataSource dataSource) {
+ return new JdbcTemplate(dataSource);
+ }
+
+ @Bean
+ PlatformTransactionManager transactionManager(DataSource dataSource) {
+ return new DataSourceTransactionManager(dataSource);
+ }
+
+ @Bean
+ EtlBatchProperties etlBatchProperties() {
+ return new EtlBatchProperties();
+ }
+
+ @Bean
+ ObjectMapper objectMapper() {
+ return new ObjectMapper();
+ }
+
+ @Bean
+ EtlRequestLock etlRequestLock() {
+ return idempotencyKeyHash -> true;
+ }
+
+ @Bean
+ EtlService etlService(
+ JdbcTemplate jdbcTemplate,
+ ObjectMapper objectMapper,
+ EtlBatchProperties properties,
+ EtlRequestLock requestLock
+ ) {
+ return new EtlService(jdbcTemplate, objectMapper, properties, requestLock);
+ }
+
+ @Bean
+ EtlJobLeaseRepository etlJobLeaseRepository(
+ JdbcTemplate jdbcTemplate,
+ PlatformTransactionManager transactionManager
+ ) {
+ return new EtlJobLeaseRepository(jdbcTemplate, transactionManager);
+ }
+
+ @Bean
+ EtlJobExecutionService etlJobExecutionService(
+ EtlService etlService,
+ EtlJobLeaseRepository leaseRepository
+ ) {
+ return new EtlJobExecutionService(etlService, leaseRepository);
+ }
+ }
+}
From 72fbed4f507a2998a32f7aafcb19fcf3d843bd7c Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 09:42:46 +0900
Subject: [PATCH 16/92] feat(etl): couple target writes to lease success
---
.../etl/job/EtlJobExecutionService.java | 55 +++++++++++++++++++
1 file changed, 55 insertions(+)
create mode 100644 etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobExecutionService.java
diff --git a/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobExecutionService.java b/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobExecutionService.java
new file mode 100644
index 00000000..7286dfc1
--- /dev/null
+++ b/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobExecutionService.java
@@ -0,0 +1,55 @@
+package com.xtrmetl.etl.job;
+
+import com.xtrmetl.etl.service.EtlService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Objects;
+
+/**
+ * Executes one claimed ETL payload and commits terminal success in the same transaction.
+ *
+ *
The existing {@link EtlService} performs validated target writes. The subsequent conditional
+ * success transition must match the exact unexpired claim. If that transition reports a stale
+ * lease, {@link StaleEtlJobLeaseException} escapes and Spring rolls back every target write made by
+ * the same transaction.
The database claim repository, not the scheduler, distributes work across replicas. The
+ * worker classifies failures into stable non-sensitive codes, retries only transient database
+ * failures while attempts remain, and treats every failed exact-lease transition as stale evidence.
+ * Metrics use a fixed outcome vocabulary and never tag payloads, principals, keys, job identifiers,
+ * lease identifiers, SQL, exception classes, or exception messages.
+ */
+@Component
+@ConditionalOnBooleanProperty(
+ prefix = "xtrmetl.etl.jobs.worker",
+ name = "enabled",
+ havingValue = true,
+ matchIfMissing = false
+)
+public class EtlJobWorker {
+
+ /** Stable target-unavailability code used after transient attempts are exhausted. */
+ public static final String TARGET_UNAVAILABLE_FAILURE_CODE = "etl_target_unavailable";
+
+ /** Stable non-transient database failure code. */
+ public static final String TARGET_FAILURE_CODE = "etl_target_failure";
+
+ /** Stable unexpected implementation failure code. */
+ public static final String INTERNAL_FAILURE_CODE = "etl_internal_error";
+
+ private static final String METRIC_OUTCOMES = "etl.jobs.worker.outcomes";
+ private static final String METRIC_DURATION = "etl.jobs.execution.duration";
+ private static final String IDLE_OUTCOME = "idle";
+ private static final String CLAIMED_OUTCOME = "claimed";
+ private static final String SUCCEEDED_OUTCOME = "succeeded";
+ private static final String RETRIED_OUTCOME = "retried";
+ private static final String FAILED_OUTCOME = "failed";
+ private static final String STALE_OUTCOME = "stale";
+ private static final List FINITE_OUTCOMES = List.of(
+ IDLE_OUTCOME,
+ CLAIMED_OUTCOME,
+ SUCCEEDED_OUTCOME,
+ RETRIED_OUTCOME,
+ FAILED_OUTCOME,
+ STALE_OUTCOME
+ );
+
+ private final EtlJobLeaseRepository leaseRepository;
+ private final EtlJobExecutionService executionService;
+ private final EtlJobWorkerProperties properties;
+ private final MeterRegistry meterRegistry;
+ private final Map outcomeCounters;
+ private final Map outcomeTimers;
+
+ /**
+ * Creates one fail-closed worker and pre-registers its finite metric vocabulary.
+ *
+ * @param leaseRepository database claim and transition authority
+ * @param executionService atomic target-write and success boundary
+ * @param properties bounded worker configuration
+ * @param meterRegistry metrics registry for finite-cardinality evidence
+ */
+ public EtlJobWorker(
+ EtlJobLeaseRepository leaseRepository,
+ EtlJobExecutionService executionService,
+ EtlJobWorkerProperties properties,
+ MeterRegistry meterRegistry
+ ) {
+ this.leaseRepository = Objects.requireNonNull(
+ leaseRepository,
+ "leaseRepository must not be null"
+ );
+ this.executionService = Objects.requireNonNull(
+ executionService,
+ "executionService must not be null"
+ );
+ this.properties = Objects.requireNonNull(properties, "properties must not be null");
+ this.meterRegistry = Objects.requireNonNull(
+ meterRegistry,
+ "meterRegistry must not be null"
+ );
+
+ Map counters = new LinkedHashMap<>();
+ Map timers = new LinkedHashMap<>();
+ for (String outcome : FINITE_OUTCOMES) {
+ counters.put(
+ outcome,
+ Counter.builder(METRIC_OUTCOMES)
+ .description("Durable ETL worker outcomes")
+ .tag("outcome", outcome)
+ .register(this.meterRegistry)
+ );
+ timers.put(
+ outcome,
+ Timer.builder(METRIC_DURATION)
+ .description("Duration of one durable ETL worker poll")
+ .tag("outcome", outcome)
+ .register(this.meterRegistry)
+ );
+ }
+ this.outcomeCounters = Map.copyOf(counters);
+ this.outcomeTimers = Map.copyOf(timers);
+ }
+
+ /**
+ * Claims and handles at most one eligible durable job.
+ *
+ *
Fixed delay is measured after this invocation completes. A database outage during claim is
+ * converted into a finite failed metric without copying diagnostic text into application logs or
+ * telemetry. Spring invokes the method only when worker activation is explicitly enabled.
The three lowercase SHA-256 values are non-reversible persistence identifiers copied from the
+ * accepted job row. They let the worker reuse the durable response ledger without retaining or
+ * reconstructing raw authenticated principals or raw client idempotency keys.
The exception exposes only one stable machine-readable failure code and a non-sensitive
+ * message. It deliberately omits payloads, hashes, identifiers, SQL, timestamps, and stored
+ * response bodies so accidental logging does not disclose customer or operational data.
SHA-256 is required by the Java platform. The defensive exception branch therefore indicates
+ * a broken runtime rather than invalid customer input.
+ */
+public final class Sha256Digest {
+
+ private Sha256Digest() {
+ // Utility class.
+ }
+
+ /**
+ * Hashes one UTF-8 string into lowercase 64-character SHA-256 hexadecimal text.
+ *
+ * @param value text to hash
+ * @return lowercase SHA-256 hexadecimal digest
+ * @throws NullPointerException when the value is {@code null}
+ * @throws IllegalStateException when the Java runtime lacks mandatory SHA-256 support
+ */
+ public static String digest(String value) {
+ String requiredValue = Objects.requireNonNull(value, "value must not be null");
+ try {
+ MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
+ byte[] digest = messageDigest.digest(requiredValue.getBytes(StandardCharsets.UTF_8));
+ return HexFormat.of().formatHex(digest);
+ } catch (NoSuchAlgorithmException exception) {
+ throw new IllegalStateException("SHA-256 is required by the Java platform", exception);
+ }
+ }
+}
From 262d53b43676fcbcec0e4c856b319ed519558640 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 10:03:40 +0900
Subject: [PATCH 33/92] feat(etl): reuse response ledger for durable jobs
---
.../etl/job/EtlJobIdempotencyService.java | 136 ++++++++++++++++++
1 file changed, 136 insertions(+)
create mode 100644 etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobIdempotencyService.java
diff --git a/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobIdempotencyService.java b/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobIdempotencyService.java
new file mode 100644
index 00000000..aa120a49
--- /dev/null
+++ b/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobIdempotencyService.java
@@ -0,0 +1,136 @@
+package com.xtrmetl.etl.job;
+
+import com.xtrmetl.etl.service.EtlRequestLock;
+import com.xtrmetl.etl.service.EtlService;
+import com.xtrmetl.etl.service.Sha256Digest;
+import org.springframework.dao.CannotAcquireLockException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Reuses the durable ETL response ledger with hashed job identity only.
+ *
+ *
The accepted job stores independent hashes of its authenticated principal and normalized
+ * submission key. This service domain-separates and hashes those values into one response-ledger
+ * key, verifies the exact retained payload digest, serializes execution with the existing
+ * transaction-level request lock, replays an existing matching response, or writes the target and
+ * response ledger in the surrounding transaction. Raw principals and client keys are neither
+ * required nor reconstructed.
+ */
+@Service
+public class EtlJobIdempotencyService {
+
+ private static final String LEDGER_KEY_DOMAIN = "mightyetl:durable-job:v1:";
+ private static final String SELECT_LEDGER_SQL = """
+ SELECT request_digest, response_body
+ FROM etl_idempotency_records
+ WHERE idempotency_key_hash = ?
+ """;
+ private static final String INSERT_LEDGER_SQL = """
+ INSERT INTO etl_idempotency_records (
+ idempotency_key_hash,
+ request_digest,
+ response_body
+ ) VALUES (?, ?, ?)
+ """;
+
+ private final JdbcTemplate jdbcTemplate;
+ private final EtlService etlService;
+ private final EtlRequestLock requestLock;
+
+ /**
+ * Creates the hashed durable-job response-ledger adapter.
+ *
+ * @param jdbcTemplate parameterized response-ledger database access
+ * @param etlService validated ETL target writer
+ * @param requestLock transaction-lifetime response-ledger lock
+ */
+ public EtlJobIdempotencyService(
+ JdbcTemplate jdbcTemplate,
+ EtlService etlService,
+ EtlRequestLock requestLock
+ ) {
+ this.jdbcTemplate = Objects.requireNonNull(
+ jdbcTemplate,
+ "jdbcTemplate must not be null"
+ );
+ this.etlService = Objects.requireNonNull(etlService, "etlService must not be null");
+ this.requestLock = Objects.requireNonNull(requestLock, "requestLock must not be null");
+ }
+
+ /**
+ * Executes or replays one durable job inside a real database transaction.
+ *
+ * @param lease exact live claim carrying hashed execution identity and retained payload
+ * @return newly generated or replayed stable response body
+ * @throws NullPointerException when the lease is {@code null}
+ * @throws IllegalStateException when invoked without an actual transaction
+ * @throws EtlJobIntegrityException when retained payload or ledger identity conflicts
+ * @throws CannotAcquireLockException when another transaction owns the execution ledger key
+ */
+ @Transactional
+ public String process(EtlJobLease lease) {
+ EtlJobLease requiredLease = Objects.requireNonNull(lease, "lease must not be null");
+ requireActiveTransaction();
+ if (!Sha256Digest.digest(requiredLease.requestPayload()).equals(
+ requiredLease.requestDigest()
+ )) {
+ throw new EtlJobIntegrityException();
+ }
+
+ String ledgerKeyHash = Sha256Digest.digest(
+ LEDGER_KEY_DOMAIN
+ + requiredLease.principalScopeHash()
+ + ':'
+ + requiredLease.submissionKeyHash()
+ );
+ if (!requestLock.tryLock(ledgerKeyHash)) {
+ throw new CannotAcquireLockException("Durable ETL execution ledger is busy");
+ }
+
+ List storedResponses = jdbcTemplate.query(
+ SELECT_LEDGER_SQL,
+ (resultSet, rowNumber) -> new StoredResponse(
+ resultSet.getString("request_digest"),
+ resultSet.getString("response_body")
+ ),
+ ledgerKeyHash
+ );
+ if (!storedResponses.isEmpty()) {
+ StoredResponse storedResponse = storedResponses.getFirst();
+ if (!storedResponse.requestDigest().equals(requiredLease.requestDigest())) {
+ throw new EtlJobIntegrityException();
+ }
+ return storedResponse.responseBody();
+ }
+
+ String responseBody = etlService.processData(requiredLease.requestPayload());
+ jdbcTemplate.update(
+ INSERT_LEDGER_SQL,
+ ledgerKeyHash,
+ requiredLease.requestDigest(),
+ responseBody
+ );
+ return responseBody;
+ }
+
+ private static void requireActiveTransaction() {
+ if (!TransactionSynchronizationManager.isActualTransactionActive()) {
+ throw new IllegalStateException(
+ "Durable ETL job execution requires an active transaction"
+ );
+ }
+ }
+
+ private record StoredResponse(String requestDigest, String responseBody) {
+ private StoredResponse {
+ Objects.requireNonNull(requestDigest, "requestDigest must not be null");
+ Objects.requireNonNull(responseBody, "responseBody must not be null");
+ }
+ }
+}
From b072dacbbb445a03e1a4dc06e0af6dd123f62b2d Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 10:03:59 +0900
Subject: [PATCH 34/92] feat(etl): commit ledger and target with lease success
---
.../etl/job/EtlJobExecutionService.java | 30 +++++++++++--------
1 file changed, 17 insertions(+), 13 deletions(-)
diff --git a/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobExecutionService.java b/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobExecutionService.java
index 7286dfc1..74b0192a 100644
--- a/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobExecutionService.java
+++ b/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobExecutionService.java
@@ -1,36 +1,39 @@
package com.xtrmetl.etl.job;
-import com.xtrmetl.etl.service.EtlService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Objects;
/**
- * Executes one claimed ETL payload and commits terminal success in the same transaction.
+ * Executes one claimed ETL payload and commits ledger, target, and terminal success atomically.
*
- *
The existing {@link EtlService} performs validated target writes. The subsequent conditional
- * success transition must match the exact unexpired claim. If that transition reports a stale
- * lease, {@link StaleEtlJobLeaseException} escapes and Spring rolls back every target write made by
- * the same transaction.
+ *
{@link EtlJobIdempotencyService} verifies the retained payload identity, acquires the durable
+ * execution-ledger lock, replays or writes the response ledger, and writes target rows. The
+ * subsequent conditional success transition must match the exact unexpired claim. If that
+ * transition reports a stale lease, {@link StaleEtlJobLeaseException} escapes and Spring rolls back
+ * every target and response-ledger write made by the same transaction.
*/
@Service
public class EtlJobExecutionService {
- private final EtlService etlService;
+ private final EtlJobIdempotencyService idempotencyService;
private final EtlJobLeaseRepository leaseRepository;
/**
* Creates the atomic durable-job execution boundary.
*
- * @param etlService validated ETL target writer
+ * @param idempotencyService hashed response-ledger and target execution service
* @param leaseRepository exact lease-fenced lifecycle persistence
*/
public EtlJobExecutionService(
- EtlService etlService,
+ EtlJobIdempotencyService idempotencyService,
EtlJobLeaseRepository leaseRepository
) {
- this.etlService = Objects.requireNonNull(etlService, "etlService must not be null");
+ this.idempotencyService = Objects.requireNonNull(
+ idempotencyService,
+ "idempotencyService must not be null"
+ );
this.leaseRepository = Objects.requireNonNull(
leaseRepository,
"leaseRepository must not be null"
@@ -38,18 +41,19 @@ public EtlJobExecutionService(
}
/**
- * Processes the retained payload and marks the exact live claim successful atomically.
+ * Processes or replays the retained job and marks the exact live claim successful atomically.
*
* @param lease exact database claim to execute
* @throws NullPointerException when the lease is {@code null}
+ * @throws EtlJobIntegrityException when persisted job or ledger identity conflicts
* @throws com.xtrmetl.etl.service.EtlRequestException when the retained request is invalid
- * @throws org.springframework.dao.DataAccessException when a target write fails
+ * @throws org.springframework.dao.DataAccessException when locking or a database write fails
* @throws StaleEtlJobLeaseException when the claim expires or is superseded before success
*/
@Transactional
public void execute(EtlJobLease lease) {
EtlJobLease requiredLease = Objects.requireNonNull(lease, "lease must not be null");
- etlService.processData(requiredLease.requestPayload());
+ idempotencyService.process(requiredLease);
leaseRepository.markSucceeded(requiredLease);
}
}
From 081093731d59ba40d3c2fefb1472672ca6568bd9 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 10:04:40 +0900
Subject: [PATCH 35/92] test(etl): prove atomic ledger target and success
---
...EtlJobExecutionServiceIntegrationTest.java | 51 +++++++++++++------
1 file changed, 36 insertions(+), 15 deletions(-)
diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobExecutionServiceIntegrationTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobExecutionServiceIntegrationTest.java
index 3f29c0a6..de034e77 100644
--- a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobExecutionServiceIntegrationTest.java
+++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobExecutionServiceIntegrationTest.java
@@ -4,6 +4,7 @@
import com.xtrmetl.etl.service.EtlBatchProperties;
import com.xtrmetl.etl.service.EtlRequestLock;
import com.xtrmetl.etl.service.EtlService;
+import com.xtrmetl.etl.service.Sha256Digest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -26,7 +27,7 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
/**
- * Proves that target writes and exact-live-lease success commit or roll back together.
+ * Proves that ledger, target writes, and exact-live-lease success commit or roll back together.
*/
@SpringJUnitConfig(EtlJobExecutionServiceIntegrationTest.TestConfiguration.class)
class EtlJobExecutionServiceIntegrationTest {
@@ -38,25 +39,26 @@ class EtlJobExecutionServiceIntegrationTest {
private final EtlJobExecutionService executionService;
private final EtlJobLeaseRepository leaseRepository;
- private final EtlService etlService;
+ private final EtlJobIdempotencyService idempotencyService;
private final JdbcTemplate jdbcTemplate;
@Autowired
EtlJobExecutionServiceIntegrationTest(
EtlJobExecutionService executionService,
EtlJobLeaseRepository leaseRepository,
- EtlService etlService,
+ EtlJobIdempotencyService idempotencyService,
JdbcTemplate jdbcTemplate
) {
this.executionService = executionService;
this.leaseRepository = leaseRepository;
- this.etlService = etlService;
+ this.idempotencyService = idempotencyService;
this.jdbcTemplate = jdbcTemplate;
}
@BeforeEach
void createTables() {
jdbcTemplate.execute("DROP TABLE IF EXISTS processed_data");
+ jdbcTemplate.execute("DROP TABLE IF EXISTS etl_idempotency_records");
jdbcTemplate.execute("DROP TABLE IF EXISTS etl_job_records");
jdbcTemplate.execute("""
CREATE TABLE etl_job_records (
@@ -81,10 +83,18 @@ CREATE TABLE processed_data (
data VARCHAR(8192) NOT NULL
)
""");
+ jdbcTemplate.execute("""
+ CREATE TABLE etl_idempotency_records (
+ idempotency_key_hash CHAR(64) PRIMARY KEY,
+ request_digest CHAR(64) NOT NULL,
+ response_body CLOB NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
+ )
+ """);
}
@Test
- void commitsTargetRowsAndTerminalSuccessInOneTransaction() {
+ void commitsLedgerTargetRowsAndTerminalSuccessInOneTransaction() {
UUID jobRecordId = insertPendingJob();
EtlJobLease lease = leaseRepository.claimNext(
OWNER_ID,
@@ -95,7 +105,8 @@ void commitsTargetRowsAndTerminalSuccessInOneTransaction() {
executionService.execute(lease);
assertEquals(jobRecordId, lease.jobRecordId());
- assertEquals(1, processedRowCount());
+ assertEquals(1, tableCount("processed_data"));
+ assertEquals(1, tableCount("etl_idempotency_records"));
assertEquals(
"ID:record_alpha,NAME:ACCEPTED,EMAIL:user@example.com,",
jdbcTemplate.queryForObject("SELECT data FROM processed_data", String.class)
@@ -105,7 +116,7 @@ void commitsTargetRowsAndTerminalSuccessInOneTransaction() {
}
@Test
- void rollsBackTargetRowsWhenTheClaimWasSuperseded() {
+ void rollsBackLedgerAndTargetRowsWhenTheClaimWasSuperseded() {
UUID jobRecordId = insertPendingJob();
EtlJobLease lease = leaseRepository.claimNext(
OWNER_ID,
@@ -120,7 +131,8 @@ void rollsBackTargetRowsWhenTheClaimWasSuperseded() {
assertThrows(StaleEtlJobLeaseException.class, () -> executionService.execute(lease));
- assertEquals(0, processedRowCount());
+ assertEquals(0, tableCount("processed_data"));
+ assertEquals(0, tableCount("etl_idempotency_records"));
assertEquals("RUNNING", jobStatus(jobRecordId));
assertEquals(1, retainedPayloadCount(jobRecordId));
}
@@ -133,7 +145,7 @@ void rejectsMissingCollaboratorsOrLease() {
);
assertThrows(
NullPointerException.class,
- () -> new EtlJobExecutionService(etlService, null)
+ () -> new EtlJobExecutionService(idempotencyService, null)
);
assertThrows(NullPointerException.class, () -> executionService.execute(null));
}
@@ -152,7 +164,7 @@ INSERT INTO etl_job_records (
jobRecordId,
"a".repeat(64),
"b".repeat(64),
- "c".repeat(64),
+ Sha256Digest.digest(PAYLOAD),
PAYLOAD,
now,
now
@@ -160,9 +172,9 @@ INSERT INTO etl_job_records (
return jobRecordId;
}
- private int processedRowCount() {
+ private int tableCount(String tableName) {
Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM processed_data",
+ "SELECT COUNT(*) FROM " + tableName,
Integer.class
);
return count == null ? 0 : count;
@@ -191,7 +203,7 @@ SELECT COUNT(*)
}
/**
- * Transaction-enabled execution context using one database for source state and target effects.
+ * Transaction-enabled execution context using one database for job, ledger, and target effects.
*/
@Configuration
@EnableTransactionManagement
@@ -240,6 +252,15 @@ EtlService etlService(
return new EtlService(jdbcTemplate, objectMapper, properties, requestLock);
}
+ @Bean
+ EtlJobIdempotencyService etlJobIdempotencyService(
+ JdbcTemplate jdbcTemplate,
+ EtlService etlService,
+ EtlRequestLock requestLock
+ ) {
+ return new EtlJobIdempotencyService(jdbcTemplate, etlService, requestLock);
+ }
+
@Bean
EtlJobLeaseRepository etlJobLeaseRepository(
JdbcTemplate jdbcTemplate,
@@ -250,10 +271,10 @@ EtlJobLeaseRepository etlJobLeaseRepository(
@Bean
EtlJobExecutionService etlJobExecutionService(
- EtlService etlService,
+ EtlJobIdempotencyService idempotencyService,
EtlJobLeaseRepository leaseRepository
) {
- return new EtlJobExecutionService(etlService, leaseRepository);
+ return new EtlJobExecutionService(idempotencyService, leaseRepository);
}
}
}
From de85194524d44b91f10720d168ad3b5f2e43e022 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 10:05:29 +0900
Subject: [PATCH 36/92] feat(etl): expose stable integrity failures
---
etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobWorker.java | 2 ++
1 file changed, 2 insertions(+)
diff --git a/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobWorker.java b/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobWorker.java
index d83f2537..ce8b625f 100644
--- a/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobWorker.java
+++ b/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobWorker.java
@@ -163,6 +163,8 @@ private String runOnePoll() {
return STALE_OUTCOME;
} catch (TransientDataAccessException exception) {
return handleTransientFailure(lease);
+ } catch (EtlJobIntegrityException exception) {
+ return markFailedOrStale(lease, exception.failureCode());
} catch (EtlRequestException exception) {
return markFailedOrStale(lease, exception.error().errorCode());
} catch (DataAccessException exception) {
From c3752dbacc524c4c08cd2043ab7fbc188751223c Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 10:06:10 +0900
Subject: [PATCH 37/92] test(etl): cover integrity failure classification
---
.../java/com/xtrmetl/etl/job/EtlJobWorkerTest.java | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobWorkerTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobWorkerTest.java
index 9381fd8e..4c7f0d3b 100644
--- a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobWorkerTest.java
+++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobWorkerTest.java
@@ -117,6 +117,19 @@ void terminalizesTransientFailureAtTheAttemptLimit() {
assertMetric("failed", 1.0, 1L);
}
+ @Test
+ void terminalizesIntegrityFailureWithItsStableCode() {
+ EtlJobLease lease = lease(1);
+ when(leaseRepository.claimNext(anyString(), any(), anyInt()))
+ .thenReturn(Optional.of(lease));
+ doThrow(new EtlJobIntegrityException()).when(executionService).execute(lease);
+
+ worker.pollOnce();
+
+ verify(leaseRepository).markFailed(lease, "etl_job_integrity_failure");
+ assertMetric("failed", 1.0, 1L);
+ }
+
@Test
void terminalizesDeterministicRequestFailureWithItsStableCode() {
EtlJobLease lease = lease(1);
From 8c31ed993bcea14238f878d5cbc3b542bd9503e9 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 10:07:01 +0900
Subject: [PATCH 38/92] test(etl): verify SHA-256 digest utility
---
.../xtrmetl/etl/service/Sha256DigestTest.java | 25 +++++++++++++++++++
1 file changed, 25 insertions(+)
create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/service/Sha256DigestTest.java
diff --git a/etl-service/src/test/java/com/xtrmetl/etl/service/Sha256DigestTest.java b/etl-service/src/test/java/com/xtrmetl/etl/service/Sha256DigestTest.java
new file mode 100644
index 00000000..907a3680
--- /dev/null
+++ b/etl-service/src/test/java/com/xtrmetl/etl/service/Sha256DigestTest.java
@@ -0,0 +1,25 @@
+package com.xtrmetl.etl.service;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Verifies deterministic lowercase SHA-256 text identities used by durable ETL persistence.
+ */
+class Sha256DigestTest {
+
+ @Test
+ void producesThePublishedSha256Vector() {
+ assertEquals(
+ "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ Sha256Digest.digest("abc")
+ );
+ }
+
+ @Test
+ void rejectsMissingInput() {
+ assertThrows(NullPointerException.class, () -> Sha256Digest.digest(null));
+ }
+}
From e536c9bc377dd06396b3564aa066422c405b631d Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 10:07:58 +0900
Subject: [PATCH 39/92] docs: add durable job worker runbook
---
docs/operations/durable-job-worker.md | 166 ++++++++++++++++++++++++++
1 file changed, 166 insertions(+)
create mode 100644 docs/operations/durable-job-worker.md
diff --git a/docs/operations/durable-job-worker.md b/docs/operations/durable-job-worker.md
new file mode 100644
index 00000000..04ab521a
--- /dev/null
+++ b/docs/operations/durable-job-worker.md
@@ -0,0 +1,166 @@
+# Durable ETL job worker operations
+
+## Purpose and safety boundary
+
+The durable worker moves accepted `etl_job_records` from `PENDING` through `RUNNING` to
+`SUCCEEDED` or `FAILED`. PostgreSQL row state is the distribution and fencing authority. Spring's
+fixed-delay scheduler only initiates polls; it does not establish exclusivity across replicas.
+
+The worker is fail-closed. Both of the following product switches must be reviewed deliberately:
+
+```text
+mightyetl.etl.jobs.intake-enabled=true
+mightyetl.etl.jobs.worker.enabled=true
+```
+
+The supported legacy aliases are `xtrmetl.etl.jobs.intake-enabled` and
+`xtrmetl.etl.jobs.worker.enabled`. Environment variables are `ETL_JOB_INTAKE_ENABLED` and
+`ETL_JOB_WORKER_ENABLED`. When both full namespaces are configured, `mightyetl.*` wins.
+
+Enable intake without the worker only for controlled maintenance windows where retained `PENDING`
+payloads are acceptable. Enable the worker without intake only to drain already accepted work.
+
+## Configuration
+
+| Preferred property | Environment variable | Default | Constraint |
+| --- | --- | ---: | --- |
+| `mightyetl.etl.jobs.worker.enabled` | `ETL_JOB_WORKER_ENABLED` | `false` | explicit opt-in |
+| `mightyetl.etl.jobs.worker.fixed-delay-milliseconds` | `ETL_JOB_WORKER_FIXED_DELAY_MILLISECONDS` | `5000` | greater than zero |
+| `mightyetl.etl.jobs.worker.initial-delay-milliseconds` | `ETL_JOB_WORKER_INITIAL_DELAY_MILLISECONDS` | `5000` | zero or greater |
+| `mightyetl.etl.jobs.worker.lease-duration-seconds` | `ETL_JOB_WORKER_LEASE_DURATION_SECONDS` | `300` | greater than zero |
+| `mightyetl.etl.jobs.worker.max-attempts` | `ETL_JOB_WORKER_MAX_ATTEMPTS` | `3` | 1 through 100 |
+| `mightyetl.etl.jobs.worker.lease-owner-id` | deployment-specific | generated | 8–128 safe ASCII characters |
+
+Set an explicit `lease-owner-id` only when the deployment platform can guarantee one stable,
+non-sensitive value per process. Never use a hostname containing customer data, a pod annotation
+containing credentials, an email address, a tenant identifier, or a raw infrastructure token.
+
+Choose a lease duration longer than the normal high-percentile execution time plus database and
+network variance. The current slice does not renew leases. A lease that expires during execution
+causes the final success transition to fail and rolls back target and response-ledger writes.
+
+## Claim, execution, and recovery
+
+Each poll handles at most one job:
+
+1. Eligible rows at or above `max-attempts` become terminal `FAILED`; their payload and lease fields
+ are cleared with `etl_worker_attempts_exhausted`.
+2. The worker selects the oldest `PENDING` row or expired `RUNNING` row below the attempt limit using
+ `FOR UPDATE SKIP LOCKED`.
+3. The claim writes a new `lease_claim_id`, the process `lease_owner_id`, database-derived expiry,
+ and incremented attempt count.
+4. The execution transaction verifies the retained payload digest, acquires the domain-separated
+ response-ledger lock, replays or writes `etl_idempotency_records`, writes target rows, and then
+ conditionally marks the exact live lease `SUCCEEDED`.
+5. A stale, superseded, or expired lease cannot commit target rows, response-ledger rows, or terminal
+ state. The whole execution transaction rolls back.
+
+An expired `RUNNING` job is reclaimed with a new claim identifier. The earlier worker may continue
+using CPU, but its target and lifecycle writes cannot commit after losing the exact live lease.
+
+## Stable failure codes
+
+| Failure code | Meaning | Operator response |
+| --- | --- | --- |
+| `etl_worker_attempts_exhausted` | an eligible row had no remaining claim attempt | inspect target availability and payload validity before any future replay feature |
+| `etl_target_unavailable` | transient database failures consumed the attempt limit | restore database service and retain evidence for incident review |
+| `etl_target_failure` | non-transient database write failure | inspect schema, constraints, permissions, and target compatibility |
+| `etl_job_integrity_failure` | retained payload or response-ledger identity conflicted | stop affected workers, preserve database evidence, investigate tampering or inconsistent migration |
+| `etl_internal_error` | unexpected non-database runtime failure | inspect sanitized application diagnostics and open a defect |
+| existing `etl_*` request codes | retained request failed deterministic ETL validation | correct the producer or migration source; do not blindly retry |
+
+Terminal states clear `request_payload` in the same state transition. The status API exposes only the
+stable failure code, attempt count, lifecycle state, and timestamps to the authenticated owner.
+
+## Observability and SLO evidence
+
+The worker publishes finite-cardinality metrics only:
+
+- `etl.jobs.worker.outcomes{outcome=idle|claimed|succeeded|retried|failed|stale}`;
+- `etl.jobs.execution.duration{outcome=idle|succeeded|retried|failed|stale}`.
+
+Do not add payloads, raw principals, raw idempotency keys, hashes, job identifiers, lease identifiers,
+SQL text, exception messages, or unbounded exception classes as metric tags or log fields.
+
+Recommended initial service-level indicators are:
+
+- accepted-to-terminal latency by status;
+- oldest eligible `PENDING` age;
+- expired `RUNNING` count;
+- terminal success ratio;
+- retry and stale outcome rates;
+- exhausted-attempt and integrity-failure counts;
+- database connection-pool saturation and transaction latency.
+
+A production SLO must be calibrated from representative load and recovery tests. Do not claim a
+numerical availability or latency SLO until monitoring, alert thresholds, and retained evidence have
+been validated in the buyer's deployment topology.
+
+For OpenTelemetry database telemetry, use the stable SQL/PostgreSQL semantic conventions where the
+instrumentation supports them. Prefer low-cardinality `db.query.summary`; treat raw `db.query.text`
+and query parameters as opt-in sensitive telemetry requiring a separate privacy assessment.
+
+## Incident procedures
+
+### Backlog growth
+
+1. Confirm intake and worker switches independently.
+2. Check database connectivity, pool saturation, lock waits, and worker failure outcomes.
+3. Compare oldest eligible `PENDING` age with execution duration.
+4. Add replicas only after confirming the database can support the additional claim and target-write
+ concurrency.
+5. Do not update lifecycle fields manually while workers are active.
+
+### Repeated stale outcomes
+
+1. Compare the configured lease duration with high-percentile transaction duration.
+2. Check clock-independent database latency and long-running statements; lease decisions use database
+ time.
+3. Verify every process has a safe, distinct lease owner identifier.
+4. Increase the lease duration only after confirming that crash recovery delay remains acceptable.
+
+### Integrity failure
+
+1. Disable the worker while preserving intake only if continued payload retention is acceptable.
+2. Snapshot the affected database under incident-response controls.
+3. Compare the job's stored request digest with a digest of the retained payload and compare the
+ domain-separated response-ledger row.
+4. Review migration, restore, replication, and unauthorized-write evidence.
+5. Do not disclose hashes or payloads in tickets, chat, dashboards, or ordinary logs.
+
+## Deployment and rollback
+
+Before enabling the worker:
+
+1. Apply and validate Flyway migration `V3__add_etl_job_lease_fencing.sql`.
+2. Confirm the application principal has only the required table and advisory-lock permissions.
+3. Run migration, claim-contention, stale-lease rollback, response-replay, and target compatibility
+ tests against a production-equivalent PostgreSQL environment.
+4. Deploy with the worker disabled, inspect health and schema evidence, then enable a canary replica.
+5. Verify target, response-ledger, and terminal state atomicity before widening rollout.
+
+Rollback order is fail-closed:
+
+1. Disable intake when new accepted work must stop.
+2. Disable all workers and wait for active transactions to complete or roll back.
+3. Confirm no `RUNNING` rows remain; allow leases to expire if necessary.
+4. Decide whether `PENDING` payloads will be drained by the current version or retained under an
+ approved data-retention exception.
+5. Roll back application binaries before any schema compensation.
+6. Never edit or delete an applied Flyway versioned migration. Use a new forward compensating
+ migration only after every deployed binary no longer reads the lease columns.
+
+## Standards and primary documentation
+
+Fielding, R., Nottingham, M., & Reschke, J. (2022). *HTTP semantics* (RFC 9110). RFC Editor.
+https://www.rfc-editor.org/rfc/rfc9110
+
+OpenTelemetry Authors. (2026). *OpenTelemetry semantic conventions 1.43.0: Semantic conventions for
+SQL databases client operations*. Cloud Native Computing Foundation.
+https://opentelemetry.io/docs/specs/semconv/db/sql/
+
+PostgreSQL Global Development Group. (2026). *PostgreSQL 18 documentation: SELECT*.
+https://www.postgresql.org/docs/18/sql-select.html
+
+Spring Authors. (2026). *Task execution and scheduling*. Broadcom.
+https://docs.spring.io/spring-framework/reference/integration/scheduling.html
From 1b7417fb4fc53387065dbb7d035e5d9d753d8273 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 10:08:33 +0900
Subject: [PATCH 40/92] docs: connect durable intake to lease worker
---
docs/etl/durable-job-intake.md | 193 ++++++++++++++++++---------------
1 file changed, 108 insertions(+), 85 deletions(-)
diff --git a/docs/etl/durable-job-intake.md b/docs/etl/durable-job-intake.md
index 7e29a2d2..dc54f365 100644
--- a/docs/etl/durable-job-intake.md
+++ b/docs/etl/durable-job-intake.md
@@ -1,24 +1,25 @@
-# Durable asynchronous ETL job intake
+# Durable asynchronous ETL jobs
-## Scope
+## Scope and activation
-`POST /api/etl/jobs` creates a durable, authenticated-principal-scoped ETL job resource. This
-bounded intake slice persists accepted work and exposes its status monitor; it does not execute jobs
-yet. Execution, PostgreSQL `FOR UPDATE SKIP LOCKED` claiming, lease fencing, bounded attempts,
-terminal payload clearing, and crash recovery belong to the following worker and lease-fencing slice.
+`POST /api/etl/jobs` creates a durable, authenticated-principal-scoped ETL job resource. A separate
+lease-fenced worker claims accepted jobs across replicas, replays or writes the durable response
+ledger, writes target rows, and commits terminal state atomically.
-The incomplete intake surface is disabled by default. It is absent unless an operator explicitly
-sets the preferred `mightyetl.etl.jobs.intake-enabled=true` property, its supported legacy alias
-`xtrmetl.etl.jobs.intake-enabled=true`, or `ETL_JOB_INTAKE_ENABLED=true`. When both full namespaces
-are supplied, `mightyetl.*` wins. Enabling intake accepts the temporary boundary that submitted
-payloads remain retained in `PENDING` jobs until the worker and terminal payload-clearing slice is
-implemented. Deployments that cannot accept that retention boundary must leave the setting false.
+Both capabilities are fail-closed:
-The existing synchronous `POST /api/etl/process` endpoint remains unchanged.
+```text
+mightyetl.etl.jobs.intake-enabled=false
+mightyetl.etl.jobs.worker.enabled=false
+```
-## Submit a job
+The supported legacy aliases use the `xtrmetl.*` namespace. Environment variables are
+`ETL_JOB_INTAKE_ENABLED` and `ETL_JOB_WORKER_ENABLED`. When both full namespaces are supplied,
+`mightyetl.*` wins. Intake may be enabled alone for a controlled retention window, and the worker may
+be enabled alone to drain accepted work. The existing synchronous `POST /api/etl/process` endpoint
+remains unchanged.
-A client sends:
+## Submit a job
```http
POST /api/etl/jobs HTTP/1.1
@@ -29,18 +30,13 @@ Idempotency-Key: "550e8400-e29b-41d4-a716-446655440000"
[{"id":"record_alpha","name":"accepted"}]
```
-The service requires:
-
-- the same authenticated principal for every retry;
-- the same semantic `Idempotency-Key`; and
-- byte-for-byte same JSON text for every retry of that key.
-
-The preferred header representation is an RFC 9651 quoted String. The legacy raw safe-ASCII profile
-remains accepted for compatibility and normalizes to the same semantic key.
+The service requires the same authenticated principal, the same semantic idempotency key, and
+byte-for-byte identical JSON text for every retry. The preferred header representation is an RFC
+9651 quoted String. The legacy raw safe-ASCII profile remains accepted and normalizes to the same
+semantic key.
-A new or replayed durable submission returns RFC 9110 `202 Accepted` because acceptance does not mean
-that processing has completed. The representation describes the current state and the `Location`
-header identifies the status monitor:
+A new or replayed submission returns RFC 9110 `202 Accepted`; acceptance does not mean processing is
+complete. The `Location` header identifies the owner-scoped status monitor:
```http
HTTP/1.1 202 Accepted
@@ -56,15 +52,10 @@ Content-Type: application/json
}
```
-All successful and covered problem responses for durable job resources include
-`Cache-Control: no-store`. These authenticated operational resources must not be retained by shared
-or private caches.
-
-A retry that resolves to the same durable resource returns the same job identifier and
-`Idempotency-Replayed: true`. Reusing the same principal-scoped key with different JSON text returns
-`422 etl_job_submission_key_reused`. A concurrent creation attempt that cannot acquire the
-transaction-level submission lock returns `409 etl_job_submission_in_progress` rather than waiting
-without a client-visible bound.
+All successful and covered problem responses include `Cache-Control: no-store`. A replay returns the
+same job identifier and `Idempotency-Replayed: true`. Reusing one principal-scoped key with different
+JSON returns `422 etl_job_submission_key_reused`. A concurrent creation attempt that cannot acquire
+the transaction-level submission lock returns `409 etl_job_submission_in_progress`.
## Read job status
@@ -73,68 +64,100 @@ GET /api/etl/jobs/{job_record_id} HTTP/1.1
Authorization: Basic
```
-The service hashes the current authenticated principal and queries by both principal scope and job
-identifier. A malformed or missing identifier and an identifier owned by another principal all
-return `404 etl_job_not_found`; callers cannot use this endpoint to probe another tenant's job
-existence.
+The query binds the current principal hash and job identifier. A malformed, missing, or foreign-owned
+identifier returns the same `404 etl_job_not_found`, preventing tenant-existence probing.
+
+The representation exposes only the opaque job identifier, stable lifecycle state, bounded attempt
+count, stable failure code where applicable, status URL, and timestamps. It excludes request payload,
+raw principal, raw submission key, internal hashes, lease identifiers, SQL, and response-ledger data.
+
+## Lifecycle and distribution
+
+Flyway migrations create descriptive multi-word `snake_case` objects:
-The response excludes the request payload, raw principal, raw submission key, and all internal
-hashes. Timestamps are explicit ISO-8601 strings. Before worker execution is implemented, newly
-accepted jobs remain `PENDING` with an `attemptCount` of zero.
+- `V2__create_etl_job_records.sql` creates `etl_job_records` and the submission uniqueness contract;
+- `V3__add_etl_job_lease_fencing.sql` adds `lease_claim_id`, `lease_owner_id`,
+ `lease_expires_at`, lifecycle constraints, and `etl_job_claim_eligibility_index`.
-## Validation and persistence
+The stable lifecycle is `PENDING`, `RUNNING`, `SUCCEEDED`, and `FAILED`.
-Before lock or table access, mightyETL enforces the same configured UTF-8 payload and record-count
-bounds used by synchronous ETL admission. The complete body must be a JSON array, duplicate JSON
-fields are rejected, every element must be an object with a safe textual `id`, and normalized field
-names must remain unique.
+Each fixed-delay poll handles at most one job. PostgreSQL, not scheduler uniqueness, distributes work:
-Flyway migration `V2__create_etl_job_records.sql` creates `etl_job_records`. All schema objects use
-descriptive multi-word `snake_case` names. The database stores:
+1. rows at the attempt limit are terminalized and their payloads are cleared;
+2. one oldest eligible `PENDING` row or expired `RUNNING` row is selected with
+ `FOR UPDATE SKIP LOCKED`;
+3. a fresh claim identifier, process owner identifier, database-derived expiry, and incremented
+ attempt count are persisted;
+4. execution verifies the retained payload digest and acquires a domain-separated response-ledger
+ lock derived only from stored hashes;
+5. an existing matching response is replayed, or target rows and `etl_idempotency_records` are written;
+6. `SUCCEEDED` is committed only for the exact unexpired claim in the same transaction.
-- an opaque UUID job identifier;
-- SHA-256 hashes of the principal scope, semantic submission key, and exact JSON text;
-- the request payload needed by the future worker while status is `PENDING` or `RUNNING`;
-- status, attempt, failure, and timestamp fields.
+If the lease is expired or superseded, the final transition fails and rolls back target and ledger
+writes. An expired row can be reclaimed with a new claim identifier. A stale worker therefore cannot
+commit duplicate target effects or terminalize a newer owner's work.
-The schema reserves the stable lifecycle vocabulary `PENDING`, `RUNNING`, `SUCCEEDED`, and `FAILED`.
-A database check requires a non-null request payload only for the two nonterminal states and requires
-that payload to be null for both terminal states. This makes terminal payload clearing an enforced
-persistence invariant rather than a documentation-only convention.
+## Retry and failure behavior
-Raw authenticated principal names and raw idempotency keys are never persisted. The request payload
-is sensitive operational data and must inherit the classification of its source records. Until the
-worker slice reaches a terminal state and clears it, operators must apply database access control,
-encryption, backup, and retention policy accordingly.
+Transient database failures return the job to `PENDING` while attempts remain. At the configured
+limit they become `FAILED` with `etl_target_unavailable`. Non-transient database failures use
+`etl_target_failure`. Persisted payload or ledger identity conflicts use
+`etl_job_integrity_failure`. Unexpected runtime failures use `etl_internal_error`. Deterministic ETL
+validation retains its existing stable `etl_*` request code. Eligible rows already at the attempt
+limit use `etl_worker_attempts_exhausted`.
-## Operational boundary
+Every retry or terminal transition repeats the exact live lease predicate. A zero-row transition is
+stale evidence and does not overwrite the authoritative owner.
-This slice deliberately does not advertise job completion or background execution. The controller is
-disabled by default; setting an activation property to `true` is an explicit operator opt-in to
-durable intake without execution. Deployments that need completed asynchronous processing must wait
-for the worker and lease-fencing slice. The next slice must claim jobs safely across replicas, fence
-stale lease owners, commit target effects and terminal success atomically, reclaim expired leases,
-bound attempts, publish stable failure codes, and clear the stored request payload at terminal state.
+## Validation, privacy, and retention
+
+Before submission lock or table access, mightyETL enforces configured UTF-8 payload and record-count
+bounds. The complete body must be a JSON array, duplicate JSON fields are rejected, every element
+must be an object with a safe textual `id`, and normalized field names must remain unique.
+
+The database stores an opaque UUID, SHA-256 hashes of principal scope, semantic submission key, and
+exact JSON text, the retained request payload while nonterminal, lifecycle and attempt fields, and
+lease metadata while running. Raw principal names and raw idempotency keys are never persisted.
+
+The request payload inherits the source records' data classification. Database constraints require a
+payload for nonterminal rows and require it to be null for terminal rows. Success, deterministic
+failure, attempts exhaustion, and non-retryable failure clear the payload in their terminal
+transition. Apply least privilege, encryption, backup, restore, and retention controls while data is
+retained.
+
+Metrics and ordinary logs must not include payloads, principals, client keys, hashes, job or lease
+identifiers, SQL, exception messages, or unbounded error classes. Operational procedures and metric
+contracts are authoritative in `docs/operations/durable-job-worker.md`.
## Standards basis
-- RFC 9110 Section 15.3.3 defines `202 Accepted` as noncommittal and recommends that the response
- describe current status and point to a status monitor.
-- RFC 9457 supplies the problem-details representation used by deterministic submission and lookup
- failures.
-- RFC 9651 defines the current Structured Fields String syntax accepted for `Idempotency-Key`.
-- The expired IETF HTTPAPI `Idempotency-Key` draft-07 is used only as work-in-progress design
- evidence for unique client keys, request fingerprints, `422` payload conflicts, and tenant-isolation
- security concerns. It expired on April 18, 2026 and is not represented as a published RFC.
+- RFC 9110 Section 15.3.3 defines `202 Accepted` as noncommittal and recommends a current-status
+ representation and status monitor.
+- RFC 9457 supplies deterministic problem-details representations.
+- RFC 9651 defines the accepted Structured Fields String syntax.
+- PostgreSQL 18 documents `SKIP LOCKED` as unsuitable for a general consistent view but useful for
+ avoiding contention among multiple consumers of a queue-like table.
+- Spring fixed-delay scheduling measures each delay from completion of the preceding invocation.
+- OpenTelemetry SQL/PostgreSQL semantic conventions define stable database telemetry fields; raw
+ query text and parameters remain privacy-sensitive opt-in data.
### References
-- Fielding, R., Nottingham, M., & Reschke, J. (2022). *HTTP semantics* (RFC 9110). RFC Editor.
- https://www.rfc-editor.org/rfc/rfc9110
-- Jena, J., & Dalal, S. (2025). *The Idempotency-Key HTTP header field*
- (draft-ietf-httpapi-idempotency-key-header-07, expired April 18, 2026). Internet Engineering Task
- Force. https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/
-- Nottingham, M., & Wilde, E. (2023). *Problem details for HTTP APIs* (RFC 9457). RFC Editor.
- https://www.rfc-editor.org/rfc/rfc9457
-- Nottingham, M., & Kamp, P. (2024). *Structured field values for HTTP* (RFC 9651). RFC Editor.
- https://www.rfc-editor.org/rfc/rfc9651
+Fielding, R., Nottingham, M., & Reschke, J. (2022). *HTTP semantics* (RFC 9110). RFC Editor.
+https://www.rfc-editor.org/rfc/rfc9110
+
+Nottingham, M., & Wilde, E. (2023). *Problem details for HTTP APIs* (RFC 9457). RFC Editor.
+https://www.rfc-editor.org/rfc/rfc9457
+
+Nottingham, M., & Kamp, P. (2024). *Structured field values for HTTP* (RFC 9651). RFC Editor.
+https://www.rfc-editor.org/rfc/rfc9651
+
+OpenTelemetry Authors. (2026). *OpenTelemetry semantic conventions 1.43.0: Semantic conventions for
+SQL databases client operations*. Cloud Native Computing Foundation.
+https://opentelemetry.io/docs/specs/semconv/db/sql/
+
+PostgreSQL Global Development Group. (2026). *PostgreSQL 18 documentation: SELECT*.
+https://www.postgresql.org/docs/18/sql-select.html
+
+Spring Authors. (2026). *Task execution and scheduling*. Broadcom.
+https://docs.spring.io/spring-framework/reference/integration/scheduling.html
From fcefbf428a805dad6c165d7a053b0516375f07d3 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 10:09:34 +0900
Subject: [PATCH 41/92] docs: align lease worker design with response ledger
---
...6-08-05-durable-job-lease-worker-design.md | 139 ++++++++++++++----
1 file changed, 107 insertions(+), 32 deletions(-)
diff --git a/docs/superpowers/specs/2026-08-05-durable-job-lease-worker-design.md b/docs/superpowers/specs/2026-08-05-durable-job-lease-worker-design.md
index 2ead4c57..ee347f50 100644
--- a/docs/superpowers/specs/2026-08-05-durable-job-lease-worker-design.md
+++ b/docs/superpowers/specs/2026-08-05-durable-job-lease-worker-design.md
@@ -2,11 +2,17 @@
## Status
-Accepted implementation design for issue #120. This is a bounded follow-on to the durable asynchronous intake merged in PR #119 and is stacked on PR #121 until that workflow-security prerequisite reaches `develop`.
+Accepted implementation design for issue #120. This is a bounded follow-on to the durable
+asynchronous intake merged in PR #119 and is stacked on PR #121 until that workflow-security
+prerequisite reaches `develop`.
## Product outcome
-Accepted asynchronous ETL jobs must progress from `PENDING` to a terminal state without depending on the client connection or on one service replica. The worker must distribute work across replicas through PostgreSQL row locking, fence stale owners, bound retry attempts, atomically couple target effects with terminal success, clear retained payloads at terminal state, and expose only stable non-sensitive status metadata through the existing owner-scoped API.
+Accepted asynchronous ETL jobs must progress from `PENDING` to a terminal state without depending on
+the client connection or one service replica. The worker must distribute work across replicas through
+PostgreSQL row locking, fence stale owners, bound retry attempts, atomically couple response-ledger
+and target effects with terminal success, clear retained payloads at terminal state, and expose only
+stable non-sensitive status metadata through the existing owner-scoped API.
## Scope
@@ -17,37 +23,70 @@ This slice adds:
- lease expiry and reclaim;
- bounded attempts with deterministic terminal failure codes;
- fixed-delay polling that is disabled by default;
-- atomic ETL target writes plus conditional `SUCCEEDED` transition;
+- hashed durable execution identity copied from the accepted job;
+- response-ledger replay or creation, target writes, and conditional `SUCCEEDED` in one transaction;
- retry and failure transitions that require the exact live lease;
- finite-cardinality execution metrics;
- migration, rollback, privacy, operations, and failure-recovery documentation.
-Cancellation, priorities, recurring schedules, manual replay, result-body persistence, and a dead-letter user interface remain out of scope.
+Cancellation, priorities, recurring schedules, manual replay, result-body exposure, and a dead-letter
+user interface remain out of scope.
## Data model
-Flyway migration `V3__add_etl_job_lease_fencing.sql` adds the following descriptive `snake_case` columns to `etl_job_records`:
+Flyway migration `V3__add_etl_job_lease_fencing.sql` adds the following descriptive `snake_case`
+columns to `etl_job_records`:
- `lease_claim_id UUID` — unique token generated for every claim or reclaim;
- `lease_owner_id VARCHAR(128)` — stable non-sensitive identifier for one worker process;
- `lease_expires_at TIMESTAMPTZ` — database-time expiry boundary.
-A lifecycle constraint requires all three lease columns for `RUNNING` rows and requires all three to be null for every other state. A failure lifecycle constraint requires `failure_code` only for `FAILED` rows. The existing terminal-payload constraint remains authoritative. An eligibility index covers `job_status`, `lease_expires_at`, `created_at`, and `job_record_id`.
+A lifecycle constraint requires all three lease columns for `RUNNING` rows and requires all three to
+be null for every other state. A failure lifecycle constraint requires `failure_code` only for
+`FAILED` rows. The existing terminal-payload constraint remains authoritative. An eligibility index
+covers `job_status`, `lease_expires_at`, `created_at`, and `job_record_id`.
+
+The claim also carries the accepted row's `principal_scope_hash`, `submission_key_hash`, and
+`request_digest`. These independent lowercase SHA-256 values support durable execution without
+persisting or reconstructing raw authenticated principals or raw client idempotency keys.
## Claim protocol
`EtlJobLeaseRepository.claimNext` runs in one transaction:
-1. Terminalize eligible rows whose `attempt_count` has reached the configured maximum. Clear `request_payload` and all lease columns and assign `etl_worker_attempts_exhausted`.
-2. Select one `PENDING` row or one expired `RUNNING` row with `attempt_count < max_attempts`, ordered by `created_at, job_record_id`, using `FETCH FIRST 1 ROW ONLY FOR UPDATE SKIP LOCKED`.
-3. Read `CURRENT_TIMESTAMP` from the database in the same statement and derive the next expiry from that database time.
-4. Generate a new `lease_claim_id`, increment `attempt_count`, set `RUNNING`, set the owner and expiry, clear any prior failure code, and commit.
+1. Terminalize eligible rows whose `attempt_count` has reached the configured maximum. Clear
+ `request_payload` and all lease columns and assign `etl_worker_attempts_exhausted`.
+2. Select one `PENDING` row or one expired `RUNNING` row with `attempt_count < max_attempts`, ordered
+ by `created_at, job_record_id`, using `FETCH FIRST 1 ROW ONLY FOR UPDATE SKIP LOCKED`.
+3. Read `CURRENT_TIMESTAMP` from the database in the same statement and derive the next expiry from
+ that database time.
+4. Generate a new `lease_claim_id`, increment `attempt_count`, set `RUNNING`, set the owner and
+ expiry, clear any prior failure code, and commit.
+
+The scheduler does not provide uniqueness. The database row lock and state predicate are the
+cross-replica authority. PostgreSQL documents `SKIP LOCKED` as suitable for avoiding contention among
+multiple consumers of a queue-like table while warning that it is not a general-purpose consistent
+view. That limitation is appropriate because each worker needs one exclusive claim rather than a
+complete snapshot.
+
+## Durable idempotent execution and fencing
+
+`EtlJobExecutionService.execute` starts one transaction and delegates to
+`EtlJobIdempotencyService` before attempting terminal success.
-The scheduler does not provide uniqueness. The database row lock and state predicate are the cross-replica authority. PostgreSQL documents `SKIP LOCKED` as suitable for avoiding contention among multiple consumers of a queue-like table, while warning that it is not a general-purpose consistent view; that limitation is appropriate here because each worker needs one exclusive claim rather than a complete snapshot.
+The idempotency service:
-## Execution and fencing
+1. requires an actual Spring transaction;
+2. recomputes the SHA-256 digest of `request_payload` and compares it with the stored
+ `request_digest` before lock or table access;
+3. domain-separates and hashes `principal_scope_hash` plus `submission_key_hash` into a response
+ ledger key without recovering raw identity values;
+4. acquires the existing transaction-lifetime `EtlRequestLock` for that key;
+5. replays a matching `etl_idempotency_records` response or calls the existing validated
+ `EtlService.processData` target writer and inserts the response ledger row.
-`EtlJobExecutionService.execute` starts a new transaction, calls the existing `EtlService.processData` through a separate Spring bean, then conditionally transitions the job to `SUCCEEDED` only when all of the following still match:
+The execution service then conditionally transitions the job to `SUCCEEDED` only when all of the
+following still match:
- `job_record_id`;
- `job_status = 'RUNNING'`;
@@ -55,55 +94,91 @@ The scheduler does not provide uniqueness. The database row lock and state predi
- exact `lease_owner_id`;
- `lease_expires_at > CURRENT_TIMESTAMP`.
-If the conditional update affects no row, `StaleEtlJobLeaseException` is thrown. The exception rolls back the same transaction, including all target writes, so an expired or superseded worker cannot commit target effects.
+If the conditional update affects no row, `StaleEtlJobLeaseException` is thrown. The exception rolls
+back the same transaction, including target and response-ledger writes. An expired or superseded
+worker therefore cannot commit duplicate target effects, create a misleading response ledger, or
+terminalize a newer owner's job.
## Failure policy
-The polling coordinator catches execution failures after the execution transaction rolls back and performs a separate exact-lease transition:
+The polling coordinator catches execution failures after the execution transaction rolls back and
+performs a separate exact-lease transition:
-- `TransientDataAccessException`: return to `PENDING` when attempts remain; otherwise terminal `FAILED` with `etl_target_unavailable`;
+- `TransientDataAccessException`: return to `PENDING` when attempts remain; otherwise terminal
+ `FAILED` with `etl_target_unavailable`;
+- `EtlJobIntegrityException`: terminal `FAILED` with `etl_job_integrity_failure`;
- `EtlRequestException`: terminal `FAILED` with the existing stable request `errorCode`;
- other `DataAccessException`: terminal `FAILED` with `etl_target_failure`;
- other `RuntimeException`: terminal `FAILED` with `etl_internal_error`;
-- `StaleEtlJobLeaseException`: make no state change because another owner or expiry boundary is authoritative.
+- `StaleEtlJobLeaseException`: make no state change because another owner or expiry boundary is
+ authoritative.
-Every retry or failure update repeats the exact-live-lease predicate. A zero-row update is treated as stale evidence, not as success.
+Every retry or failure update repeats the exact-live-lease predicate. A zero-row update is treated as
+stale evidence, not as success.
## Scheduling and activation
-Spring fixed-delay scheduling is used because the next delay is measured after completion of the previous invocation. `xtrmetl.etl.jobs.worker.enabled` defaults to `false`; operators must explicitly enable both intake and worker execution. Configurable values are bounded and validated:
+Spring fixed-delay scheduling is used because the next delay is measured after completion of the
+previous invocation. `mightyetl.etl.jobs.worker.enabled` and its supported `xtrmetl.*` alias default
+to `false`. Configurable values are bounded and validated:
- `fixed-delay-milliseconds` > 0;
- `initial-delay-milliseconds` >= 0;
- `lease-duration-seconds` > 0;
- `max-attempts` between 1 and 100;
-- `lease-owner-id` is 8–128 safe ASCII characters and defaults to a process-lifetime generated identifier.
+- `lease-owner-id` is 8–128 safe ASCII characters and defaults to a process-lifetime generated
+ identifier.
-One polling invocation claims at most one job. Horizontal throughput is achieved by replicas and repeated fixed-delay invocations rather than unbounded in-process fan-out.
+One polling invocation claims at most one job. Horizontal throughput is achieved by replicas and
+repeated fixed-delay invocations rather than unbounded in-process fan-out.
## Observability and privacy
-The worker emits a duration timer and a finite outcome counter for `claimed`, `succeeded`, `retried`, `failed`, and `stale`. Metric tags never include payloads, principals, idempotency keys, job identifiers, SQL, lease identifiers, or exception messages. Logs follow the same rule. Database client instrumentation should retain the stable OpenTelemetry SQL semantic conventions and avoid opting raw query text into telemetry unless the deployment has separately assessed that exposure.
+The worker emits a duration timer and a finite outcome counter for `idle`, `claimed`, `succeeded`,
+`retried`, `failed`, and `stale`. Metric tags never include payloads, principals, idempotency keys,
+hashes, job identifiers, SQL, lease identifiers, exception classes, or exception messages. Logs
+follow the same rule. Database client instrumentation should retain stable OpenTelemetry
+SQL/PostgreSQL semantic conventions and avoid opting raw query text or parameters into telemetry
+unless the deployment has separately assessed that exposure.
## Testing strategy
-- Migration tests enforce descriptive names, lifecycle constraints, index shape, and rollback instructions.
-- Repository integration tests use H2's supported `FOR UPDATE SKIP LOCKED` syntax to prove one live claim, deterministic ordering, expiry reclaim, attempt increment, and exhaustion terminalization.
-- Execution integration tests prove target rows and `SUCCEEDED` commit together and prove a stale claim rolls target writes back.
-- Coordinator tests cover every failure classification, retry bound, zero-work poll, metrics outcome, and stale transition.
+- Migration tests enforce descriptive names, lifecycle constraints, index shape, and rollback
+ instructions.
+- Repository integration tests use H2's supported `FOR UPDATE SKIP LOCKED` syntax to prove one live
+ claim, deterministic ordering, expiry reclaim, attempt increment, execution identity, and
+ exhaustion terminalization.
+- Idempotency integration tests prove first execution, response replay without duplicate target
+ writes, payload digest rejection before locking, ledger conflict rejection, transient lock
+ contention, and fail-closed transaction requirements.
+- Execution integration tests prove target rows, response ledger, and `SUCCEEDED` commit together and
+ prove a stale claim rolls all three effects back.
+- Coordinator tests cover every failure classification, retry bound, zero-work poll, metrics outcome,
+ and stale transition.
- Property tests cover every validation boundary and generated owner identifier.
-- Documentation and coverage policy tests require complete public Javadoc and zero missed instruction, line, method, and branch coverage for the durable-job package.
+- Documentation and coverage policy tests require complete public Javadoc and zero missed
+ instruction, line, method, and branch coverage for the durable-job package.
## Rollback
-Before application rollback, stop all workers and disable intake. Allow active leases to expire, confirm no `RUNNING` rows remain, and decide whether pending payloads will be drained or explicitly failed. Roll back the application first. The three lease columns and eligibility index may be removed only after all rows are non-running and no deployed binary reads them. Flyway versioned migrations are not edited or deleted after publication; a forward compensating migration must perform any production schema reversal.
+Before application rollback, stop all workers and disable intake. Allow active leases to expire,
+confirm no `RUNNING` rows remain, and decide whether pending payloads will be drained or retained
+under an approved exception. Roll back the application first. The three lease columns and eligibility
+index may be removed only after all rows are non-running and no deployed binary reads them. Flyway
+versioned migrations are not edited or deleted after publication; a forward compensating migration
+must perform any production schema reversal.
## Standards and primary documentation
-Fielding, R., Nottingham, M., & Reschke, J. (2022). *HTTP semantics* (RFC 9110). Internet Engineering Task Force. https://www.rfc-editor.org/rfc/rfc9110.html
+Fielding, R., Nottingham, M., & Reschke, J. (2022). *HTTP semantics* (RFC 9110). RFC Editor.
+https://www.rfc-editor.org/rfc/rfc9110.html
-OpenTelemetry Authors. (2026). *Semantic conventions for database calls and systems*. Cloud Native Computing Foundation. https://opentelemetry.io/docs/specs/semconv/db/
+OpenTelemetry Authors. (2026). *OpenTelemetry semantic conventions 1.43.0: Semantic conventions for
+SQL databases client operations*. Cloud Native Computing Foundation.
+https://opentelemetry.io/docs/specs/semconv/db/sql/
-PostgreSQL Global Development Group. (2026). *PostgreSQL 18 documentation: SELECT*. https://www.postgresql.org/docs/18/sql-select.html
+PostgreSQL Global Development Group. (2026). *PostgreSQL 18 documentation: SELECT*.
+https://www.postgresql.org/docs/18/sql-select.html
-Spring Authors. (2026). *Task execution and scheduling*. Broadcom. https://docs.spring.io/spring-framework/reference/integration/scheduling.html
+Spring Authors. (2026). *Task execution and scheduling*. Broadcom.
+https://docs.spring.io/spring-framework/reference/integration/scheduling.html
From a727b87e1400a8131f42f0992f7213f88a48679d Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 10:10:13 +0900
Subject: [PATCH 42/92] docs: record lease-fenced durable execution
---
CHANGELOG.md | 316 ++++++++++++---------------------------------------
1 file changed, 75 insertions(+), 241 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 86c4082b..2666f769 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,257 +1,91 @@
# Changelog
-All notable changes to this project will be documented in this file.
+All notable changes to mightyETL are documented in this file.
-The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
-and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
+[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
-### Changed
-
-- The hourly pull-request disposition loop now requires at least one non-author approval anchored to the exact current head SHA; stale approvals, comment-only reviews, and the mere absence of requested changes cannot authorize unattended merge.
-- The hourly OpenCode workflow now scopes repository write permissions to its sole maintenance job, replaces the npm installation command with the immutable OpenCode 1.18.13 Linux release archive plus pinned SHA-256 validation, and uses a removable repository-local GitHub CLI credential helper instead of storing an encoded authorization header while retaining `persist-credentials: false`.
-- The managed Jackson component set now uses the patched 2.21.5 BOM, closing CVE-2026-54515, CVE-2026-59889, and GHSA-mhm7-754m-9p8w while keeping core, annotations, datatype, and module artifacts aligned.
-- Durable `POST /api/etl/jobs` submissions now return RFC 9110 `202 Accepted`, a stable pending-job representation, `Location` status-monitor metadata, and explicit replay metadata without changing the synchronous `/api/etl/process` contract. The incomplete intake controller is fail-closed and requires explicit `xtrmetl.etl.jobs.intake-enabled=true` operator opt-in until worker execution and terminal payload clearing are implemented.
-- Concurrent requests using the same authenticated-principal-scoped semantic idempotency key now return immediate RFC 9457 `409 etl_idempotency_request_in_progress` responses through PostgreSQL `pg_try_advisory_xact_lock`; retries after completion still replay the committed response.
-- `POST /api/etl/process` now supports optional authenticated-principal-scoped `Idempotency-Key` retries with atomic target writes, durable response replay, payload-conflict rejection, and explicit replay response metadata.
-- `Idempotency-Key` now prefers the quoted RFC 9651 Structured Field String representation while retaining and normalizing the legacy raw representation to the same durable ledger key.
-- ETL request errors now use RFC 9457 `application/problem+json` responses with a stable `errorCode`, fixed type URI, explicit 400/401/404/409/413/422/503/500 taxonomy, and no internal exception text in client responses.
-- ETL requests now enforce bounded UTF-8 payload and record-count limits, prevalidate and transform the complete batch before the first JDBC call, and commit accepted records inside one Spring transaction.
-- ETL transformations now preserve comma/colon-bearing values, use locale-independent text conversion and deterministic `BigDecimal` amount formatting, and retry only transient Spring data-access failures.
-- Product branding: user-facing docs and suggested image tags use **mightyETL** (formerly xtrmETL).
- - Legacy Java packages (`com.xtrmetl.*`), Maven `artifactId` `xtrmETL`, and some env/topic defaults remain for compatibility.
- - See `docs/rebrand-name-matrix.md`.
-
### Added
-- A separate fail-closed hourly OpenCode maintenance workflow pinned to OpenCode 1.18.13 and `nvidia/qwen/qwen3-coder-480b-a35b-instruct`, using only the existing `NVIDIA_NIM_API_KEY` through OpenCode's `NVIDIA_API_KEY` provider variable while preserving the independent review agent and deterministic merge-disposition workflow.
-- Principal-scoped durable asynchronous ETL job intake and owner-scoped status resources, Flyway `etl_job_records` migration, deterministic replay/conflict coverage, and the explicit worker boundary in `docs/etl/durable-job-intake.md`.
-- Durable idempotency ledger migration, PostgreSQL transaction advisory-lock adapter, deterministic concurrency/rollback coverage, and the operator/client contract `docs/etl/idempotent-retries.md`.
-- ETL problem-details client and operator contract: `docs/api/problem-details.md`.
-- Operator-configurable ETL admission limits under `mightyetl.etl.*` / `xtrmetl.etl.*`, backed by `ETL_MAX_PAYLOAD_BYTES` and `ETL_MAX_BATCH_RECORDS` environment variables with hard safety ceilings.
-- ETL transaction rollback integration coverage and the operator runbook `docs/etl/bounded-atomic-batches.md`.
-- Connector scaffolds (contracts + docs only): Qlik Sense, Databricks, Snowflake under `docs/connectors/` and `etl-service` SPI stubs.
-- Any-to-any CDC design notes and source SPI scaffold: `docs/cdc/any-to-any-cdc.md`, `cdc-service` SPI stubs.
-- CDC operations notes: `docs/cdc/ops-and-reliability.md`.
-- Product upgrade progress tracker: `docs/mightyETL-product-upgrade-progress.md`.
-- CDC status/sources API: `GET /api/cdc/status`, `GET /api/cdc/sources` (no secrets).
-- `DebeziumChangeRecordMapper` + `CanonicalChangeRecord` (mapper unit-tested; not on live publish path).
-- CDC target SPI registry (`kafka`, `jdbc-replica`) for any-to-any routing scaffold.
-- `etl-service` `xtrmetl.connectors.*` disabled config keys for Databricks/Snowflake/Qlik.
-- Dual-read config aliases: `mightyetl.*` preferred → `xtrmetl.*` (`MightyEtlConfigAliasEnvironmentPostProcessor`).
-- Configurable replica tables (`xtrmetl.replica.tables`) for `(id,data)`-shaped tables.
-- Optional CDC canonical-map counters (`xtrmetl.cdc.canonical-map-enabled`).
-- ETL connector catalog API `GET /api/etl/connectors` + scaffold enable guard.
-- CDC replication slot lag probe on `GET /api/cdc/status` (`ReplicationSlotProbe`).
-- CDC multi-source config list + `CdcSourceFactory` (declarative; single live engine).
-- `GET /api/cdc/targets` for target SPI discovery.
-- Actuator `cdcEngine` health indicator (engine running + slot details).
-- SPI lifecycle: `PostgresDebeziumCdcSource.start/stop` delegates to `CdcService`.
-- Scaffold CDC sources: `mysql-debezium`, `sqlserver-debezium` (discovery only).
-- Root POM `mightyETL` (artifactId remains `xtrmETL`).
-- README honest “Supported today” matrix; compose file product-name header.
+- Lease-fenced durable ETL execution across replicas with deterministic PostgreSQL
+ `FOR UPDATE SKIP LOCKED` claiming, process and per-claim fencing, expiry reclaim, bounded attempts,
+ exact-live-lease transitions, and terminal payload clearing.
+- Hashed durable execution identity and domain-separated reuse of `etl_idempotency_records`, so
+ response replay, target writes, and `SUCCEEDED` commit atomically without retaining or
+ reconstructing raw principals or raw client idempotency keys.
+- Stable durable-worker failure classifications, finite-cardinality outcome and duration metrics,
+ migration/rollback evidence, contention and stale-lease rollback tests, and the operator runbook
+ `docs/operations/durable-job-worker.md`.
+- A separate fail-closed hourly OpenCode maintenance workflow pinned to OpenCode 1.18.13 and
+ `nvidia/qwen/qwen3-coder-480b-a35b-instruct`, using the existing `NVIDIA_NIM_API_KEY` through
+ OpenCode's `NVIDIA_API_KEY` provider variable while preserving the independent review agent and
+ deterministic merge-disposition workflow.
+- Principal-scoped durable asynchronous ETL job intake and owner-scoped status resources, Flyway
+ `etl_job_records` migration, deterministic replay/conflict coverage, and the authoritative
+ contract `docs/etl/durable-job-intake.md`.
+- Durable synchronous idempotency ledger migration, PostgreSQL transaction advisory-lock adapter,
+ deterministic concurrency/rollback coverage, and `docs/etl/idempotent-retries.md`.
+- RFC 9457 ETL problem-details contract in `docs/api/problem-details.md`.
+- Operator-configurable ETL admission limits under `mightyetl.etl.*` and supported `xtrmetl.etl.*`
+ aliases, backed by bounded environment variables.
+- ETL transaction rollback integration coverage and `docs/etl/bounded-atomic-batches.md`.
+- Connector contract and documentation scaffolds for Qlik Sense, Databricks, and Snowflake.
+- Any-to-any CDC design notes, source and target SPI scaffolds, status/source/target APIs, replication
+ slot lag evidence, health indicators, and operations documentation.
+- Product upgrade progress tracking and the preferred `mightyetl.*` configuration namespace with
+ supported compatibility aliases.
-### Added (historical)
+### Changed
-- Comprehensive documentation suite (2026-01-08)
- - `README.md`: Quick start guide and project overview
- - `PRD.md`: Product Requirements Document with detailed specifications
- - `ARCHITECTURE.md`: System architecture and technical diagrams
- - `SUMMARY_KR.md`: Korean language summary
- - `CHANGELOG.md`: This file
+- The hourly pull-request disposition loop now requires at least one non-author approval anchored to
+ the exact current head SHA; stale approvals, comment-only reviews, and absence of requested changes
+ cannot authorize unattended merge.
+- The hourly OpenCode workflow now scopes write permissions to its maintenance job, installs an
+ immutable checksum-pinned release archive, validates the exact archive shape, and uses a removable
+ repository-local GitHub CLI credential helper while retaining `persist-credentials: false`.
+- The managed Jackson component set now uses the patched 2.21.5 BOM, closing CVE-2026-54515,
+ CVE-2026-59889, and GHSA-mhm7-754m-9p8w while keeping managed artifacts aligned.
+- Durable `POST /api/etl/jobs` submissions return RFC 9110 `202 Accepted`, `Location` status-monitor
+ metadata, stable replay metadata, and fail-closed intake activation without changing synchronous
+ `/api/etl/process` behavior.
+- The durable job worker and all worker configuration aliases remain disabled by default. Operators
+ may independently enable intake or execution for controlled drain and maintenance procedures.
+- Concurrent synchronous idempotency requests use PostgreSQL transaction advisory locks and return
+ deterministic RFC 9457 conflict responses instead of waiting without a client-visible bound.
+- `POST /api/etl/process` supports authenticated-principal-scoped idempotency keys, atomic target and
+ response-ledger writes, response replay, and payload-conflict rejection.
+- `Idempotency-Key` prefers the RFC 9651 quoted Structured Field String representation while retaining
+ the normalized legacy safe-ASCII representation.
+- ETL request errors use non-sensitive RFC 9457 `application/problem+json` responses with stable
+ error codes and explicit HTTP taxonomy.
+- ETL admission validates the complete bounded UTF-8 batch before the first JDBC write, preserves
+ punctuation-bearing values, uses locale-independent conversion and deterministic decimal
+ formatting, and retries only transient data-access failures.
+- User-facing documentation and recommended image tags use **mightyETL**. Legacy Java packages,
+ Maven artifact identifiers, and selected environment/topic defaults remain compatibility surfaces
+ documented in `docs/rebrand-name-matrix.md`.
+
+### Security
+
+- Durable-worker telemetry excludes payloads, raw principals, raw idempotency keys, internal hashes,
+ job and lease identifiers, SQL, exception messages, and unbounded exception labels.
+- Exact payload-digest and response-ledger conflicts fail closed with
+ `etl_job_integrity_failure`; stale workers cannot commit target, ledger, or terminal-state effects.
## [1.0.0] - 2026-01-08
-### Project Documentation Initiative
-
-This release focuses on reverse-engineering and documenting the
-existing xtrmETL platform.
-
-#### Added Documentation
-
-1. **README.md** (478 lines)
- - Project overview and value proposition
- - Quick start guide with prerequisites
- - Service descriptions for all microservices
- - Authentication flow and API examples
- - Database setup scripts
- - Testing instructions
- - Monitoring setup with Zipkin
- - Technology stack reference
- - Development guidelines
-
-2. **PRD.md** (608 lines)
- - Executive summary and product vision
- - Problem statement analysis
- - Solution overview with core capabilities
- - Functional requirements (FR-CDC-1 through FR-GATE-1)
- - Non-functional requirements (Performance, Reliability, Security, etc.)
- - Complete data model specifications
- - API specifications with examples
- - Deployment architecture
- - Use cases and scenarios
- - Future enhancements roadmap
- - Success metrics and KPIs
- - Risk assessment and mitigation strategies
- - Comprehensive glossary
-
-3. **ARCHITECTURE.md** (633 lines)
- - High-level system architecture diagrams
- - Service communication patterns (synchronous/asynchronous)
- - Detailed data flow diagrams for:
- - ETL processing
- - CDC event capture
- - Authentication flow
- - Service discovery and registration
- - Security architecture
- - Monitoring and observability stack
- - Deployment architectures (single-node and multi-node)
- - Debezium integration details
- - Spring Retry mechanism
- - Network and port configuration
- - Scalability considerations
-
-4. **SUMMARY_KR.md** (206 lines)
- - Korean language summary for stakeholders
- - Project purpose and goals
- - Key features overview
- - System architecture summary
- - Technology stack
- - Use cases
- - API specifications
- - Quick start guide
- - Future improvements
- - Technical debt assessment
-
-#### Project Understanding
-
-Through code analysis, identified the platform as:
-
-- **Enterprise ETL and CDC Platform**
-- Microservices-based architecture using Spring Cloud
-- Real-time Change Data Capture using Debezium
-- Data transformation pipelines with parallel processing
-- JWT-based security with role-based access control
-- Event streaming via Apache Kafka
-- Service discovery with Netflix Eureka
-- Distributed tracing with Zipkin
-
-#### Key Components Documented
-
-1. **CDC Service** (Port 8001)
- - PostgreSQL change data capture
- - Debezium embedded engine
- - Kafka event publishing
- - Real-time monitoring capabilities
-
-2. **ETL Service** (Port 8000)
- - JSON data processing
- - Parallel record processing
- - Configurable transformations
- - Automatic retry mechanism
- - Target database loading
-
-3. **Zuul Gateway** (Port 8080)
- - API Gateway with routing
- - JWT authentication filter
- - Load balancing
- - Request routing to services
-
-4. **Eureka Server** (Port 8761)
- - Service discovery
- - Service registration
- - Health monitoring
-
-5. **Config Server** (Port 8888)
- - Centralized configuration (planned)
-
-6. **Zipkin** (Port 9412)
- - Distributed tracing
- - Performance monitoring
-
-#### Technology Stack Documented
-
-- Java 25
-- Spring Boot 2.7.14
-- Spring Cloud 2021.0.8
-- Debezium 2.3.x - 2.5.x
-- PostgreSQL 12+
-- Apache Kafka
-- Netflix Zuul
-- Netflix Eureka
-- Maven
-
-#### Identified Technical Debt
-
-- Common module referenced but not implemented
-- MyBatis dependencies present but unused
-- Redis integration configured but not utilized
-- Config Server implemented but not actively used
-- Missing Spring Boot Actuator health checks
-
-#### Future Enhancements Documented
-
-- Multi-database CDC support (MySQL, Oracle, SQL Server)
-- Custom transformation functions
-- Data quality validation
-- Web UI for configuration and monitoring
-- Schema registry integration
-- Dead Letter Queue for failed messages
-- Enhanced metrics dashboard
-
-### Files Changed
-
-- `CHANGELOG.md` (new)
-- `README.md` (new)
-- `PRD.md` (new)
-- `ARCHITECTURE.md` (new)
-- `SUMMARY_KR.md` (new)
-
-### Issue Resolved
-
-This release addresses the GitHub issue requesting reverse-engineering of the program's purpose and PRD creation. The issue noted: "이 프로그램이 무엇을 하고 싶었던 프로그램인지 역추적하고 PRD 작성. 아마도 데이터베이스 CDC 프로그램이었던 것 같음."
-
-**Confirmation**: Yes, this is a database CDC (Change Data Capture) program, specifically an enterprise-grade ETL and CDC platform for real-time data integration.
-
-### Documentation Statistics
-
-- Total lines of documentation: 1,925
-- Total files created: 4
-- Total size: ~75 KB
-- Languages: English (primary), Korean (summary)
-
-### Related Documents
-
-For more information, see:
-
-- [README.md](README.md) - Quick start guide
-- [PRD.md](PRD.md) - Product Requirements Document
-- [ARCHITECTURE.md](ARCHITECTURE.md) - Technical architecture
-- [SUMMARY_KR.md](SUMMARY_KR.md) - Korean summary
-- Original design notes (Korean) in project files
-
----
-
-## Notes on Versioning
-
-Since this is documentation work on an existing codebase:
-
-- Version 1.0.0 represents the first documented release
-- The actual codebase existed before this documentation
-- Future versions will track both code and documentation changes
-
-## Changelog Maintenance
-
-This changelog will be updated:
+### Added
-- When new features are added
-- When bugs are fixed
-- When documentation is significantly updated
-- For each release or milestone
+- Initial reverse-engineered product documentation: `README.md`, `PRD.md`, `ARCHITECTURE.md`, and
+ `SUMMARY_KR.md`.
+- Baseline documentation for the Java/Spring microservice architecture, PostgreSQL ETL path,
+ Debezium-based CDC path, Kafka publication, service discovery, gateway routing, and Zipkin tracing.
----
+### Known baseline limitations
-**Changelog Version**: 1.0
-**Last Updated**: 2026-08-04
-**Maintained By**: Development Team
\ No newline at end of file
+- Several connector and multi-source capabilities were documented or scaffolded rather than live.
+- Config Server, Redis, and selected dependencies were present without complete production usage.
+- Operational health, security, idempotency, bounded admission, and durable asynchronous execution
+ required the later unreleased hardening documented above.
From 1c44790fc18640617a7efc6757bfb89d3115462e Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 10:14:28 +0900
Subject: [PATCH 43/92] docs: preserve stacked changelog history
---
CHANGELOG.md | 324 +++++++++++++++++++++++++++++++++++++++------------
1 file changed, 251 insertions(+), 73 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2666f769..1b8f46be 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,91 +1,269 @@
# Changelog
-All notable changes to mightyETL are documented in this file.
+All notable changes to this project will be documented in this file.
-The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
-[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
-### Added
+### Changed
-- Lease-fenced durable ETL execution across replicas with deterministic PostgreSQL
- `FOR UPDATE SKIP LOCKED` claiming, process and per-claim fencing, expiry reclaim, bounded attempts,
- exact-live-lease transitions, and terminal payload clearing.
-- Hashed durable execution identity and domain-separated reuse of `etl_idempotency_records`, so
- response replay, target writes, and `SUCCEEDED` commit atomically without retaining or
- reconstructing raw principals or raw client idempotency keys.
-- Stable durable-worker failure classifications, finite-cardinality outcome and duration metrics,
- migration/rollback evidence, contention and stale-lease rollback tests, and the operator runbook
- `docs/operations/durable-job-worker.md`.
-- A separate fail-closed hourly OpenCode maintenance workflow pinned to OpenCode 1.18.13 and
- `nvidia/qwen/qwen3-coder-480b-a35b-instruct`, using the existing `NVIDIA_NIM_API_KEY` through
- OpenCode's `NVIDIA_API_KEY` provider variable while preserving the independent review agent and
- deterministic merge-disposition workflow.
-- Principal-scoped durable asynchronous ETL job intake and owner-scoped status resources, Flyway
- `etl_job_records` migration, deterministic replay/conflict coverage, and the authoritative
- contract `docs/etl/durable-job-intake.md`.
-- Durable synchronous idempotency ledger migration, PostgreSQL transaction advisory-lock adapter,
- deterministic concurrency/rollback coverage, and `docs/etl/idempotent-retries.md`.
-- RFC 9457 ETL problem-details contract in `docs/api/problem-details.md`.
-- Operator-configurable ETL admission limits under `mightyetl.etl.*` and supported `xtrmetl.etl.*`
- aliases, backed by bounded environment variables.
-- ETL transaction rollback integration coverage and `docs/etl/bounded-atomic-batches.md`.
-- Connector contract and documentation scaffolds for Qlik Sense, Databricks, and Snowflake.
-- Any-to-any CDC design notes, source and target SPI scaffolds, status/source/target APIs, replication
- slot lag evidence, health indicators, and operations documentation.
-- Product upgrade progress tracking and the preferred `mightyetl.*` configuration namespace with
- supported compatibility aliases.
+- Durable asynchronous ETL jobs now progress from `PENDING` through lease-fenced execution to `SUCCEEDED` or `FAILED`; PostgreSQL owns cross-replica claiming, stale workers cannot commit target or lifecycle effects, and intake and execution remain independently fail-closed.
+- The hourly pull-request disposition loop now requires at least one non-author approval anchored to the exact current head SHA; stale approvals, comment-only reviews, and the mere absence of requested changes cannot authorize unattended merge.
+- The hourly OpenCode workflow now scopes repository write permissions to its sole maintenance job, replaces the npm installation command with the immutable OpenCode 1.18.13 Linux release archive plus pinned SHA-256 validation, requires exactly one regular-file archive member before private-directory extraction, rejects non-regular or symbolic-link output, and uses a removable repository-local GitHub CLI credential helper instead of storing an encoded authorization header while retaining `persist-credentials: false`.
+- The hourly OpenCode workflow now uses the current free NVIDIA `deepseek-ai/deepseek-v4-pro` endpoint for long-context coding and agentic tool use instead of the deprecated Qwen3 Coder free endpoint; model or endpoint rejection fails visibly without a non-NVIDIA, partner-only, or automatic fallback.
+- The managed Jackson component set now uses the patched 2.21.5 BOM, closing CVE-2026-54515, CVE-2026-59889, and GHSA-mhm7-754m-9p8w while keeping core, annotations, datatype, and module artifacts aligned.
+- Durable `POST /api/etl/jobs` submissions now return RFC 9110 `202 Accepted`, a stable job representation, `Location` status-monitor metadata, and explicit replay metadata without changing the synchronous `/api/etl/process` contract.
+- Concurrent requests using the same authenticated-principal-scoped semantic idempotency key now return immediate RFC 9457 `409 etl_idempotency_request_in_progress` responses through PostgreSQL `pg_try_advisory_xact_lock`; retries after completion still replay the committed response.
+- `POST /api/etl/process` now supports optional authenticated-principal-scoped `Idempotency-Key` retries with atomic target writes, durable response replay, payload-conflict rejection, and explicit replay response metadata.
+- `Idempotency-Key` now prefers the quoted RFC 9651 Structured Field String representation while retaining and normalizing the legacy raw representation to the same durable ledger key.
+- ETL request errors now use RFC 9457 `application/problem+json` responses with a stable `errorCode`, fixed type URI, explicit 400/401/404/409/413/422/503/500 taxonomy, and no internal exception text in client responses.
+- ETL requests now enforce bounded UTF-8 payload and record-count limits, prevalidate and transform the complete batch before the first JDBC call, and commit accepted records inside one Spring transaction.
+- ETL transformations now preserve comma/colon-bearing values, use locale-independent text conversion and deterministic `BigDecimal` amount formatting, and retry only transient Spring data-access failures.
+- Product branding: user-facing docs and suggested image tags use **mightyETL** (formerly xtrmETL).
+ - Legacy Java packages (`com.xtrmetl.*`), Maven `artifactId` `xtrmETL`, and some env/topic defaults remain for compatibility.
+ - See `docs/rebrand-name-matrix.md`.
-### Changed
+### Added
-- The hourly pull-request disposition loop now requires at least one non-author approval anchored to
- the exact current head SHA; stale approvals, comment-only reviews, and absence of requested changes
- cannot authorize unattended merge.
-- The hourly OpenCode workflow now scopes write permissions to its maintenance job, installs an
- immutable checksum-pinned release archive, validates the exact archive shape, and uses a removable
- repository-local GitHub CLI credential helper while retaining `persist-credentials: false`.
-- The managed Jackson component set now uses the patched 2.21.5 BOM, closing CVE-2026-54515,
- CVE-2026-59889, and GHSA-mhm7-754m-9p8w while keeping managed artifacts aligned.
-- Durable `POST /api/etl/jobs` submissions return RFC 9110 `202 Accepted`, `Location` status-monitor
- metadata, stable replay metadata, and fail-closed intake activation without changing synchronous
- `/api/etl/process` behavior.
-- The durable job worker and all worker configuration aliases remain disabled by default. Operators
- may independently enable intake or execution for controlled drain and maintenance procedures.
-- Concurrent synchronous idempotency requests use PostgreSQL transaction advisory locks and return
- deterministic RFC 9457 conflict responses instead of waiting without a client-visible bound.
-- `POST /api/etl/process` supports authenticated-principal-scoped idempotency keys, atomic target and
- response-ledger writes, response replay, and payload-conflict rejection.
-- `Idempotency-Key` prefers the RFC 9651 quoted Structured Field String representation while retaining
- the normalized legacy safe-ASCII representation.
-- ETL request errors use non-sensitive RFC 9457 `application/problem+json` responses with stable
- error codes and explicit HTTP taxonomy.
-- ETL admission validates the complete bounded UTF-8 batch before the first JDBC write, preserves
- punctuation-bearing values, uses locale-independent conversion and deterministic decimal
- formatting, and retries only transient data-access failures.
-- User-facing documentation and recommended image tags use **mightyETL**. Legacy Java packages,
- Maven artifact identifiers, and selected environment/topic defaults remain compatibility surfaces
- documented in `docs/rebrand-name-matrix.md`.
+- PostgreSQL `FOR UPDATE SKIP LOCKED` durable-job claiming, per-process and per-claim lease fencing, expiry reclaim, bounded attempts, exact-live-lease transitions, terminal payload clearing, stable failure codes, and finite-cardinality worker metrics.
+- Hashed durable execution identity and domain-separated reuse of `etl_idempotency_records`, coupling response replay or creation, target writes, and terminal `SUCCEEDED` in one transaction without retaining or reconstructing raw principals or raw client idempotency keys.
+- Deterministic migration, concurrency, expiry, exhaustion, response-replay, integrity, stale-lease rollback, privacy, configuration-boundary, and operator-recovery tests plus `docs/operations/durable-job-worker.md`.
+- A separate fail-closed hourly OpenCode maintenance workflow pinned to OpenCode 1.18.13 and `nvidia/deepseek-ai/deepseek-v4-pro`, using only the existing `NVIDIA_NIM_API_KEY` through OpenCode's `NVIDIA_API_KEY` provider variable while preserving the independent review agent and deterministic merge-disposition workflow.
+- Supply-chain doctoring evidence for checksum binding, exact archive-member and entry-type validation, private extraction, post-extraction file checks, test-first regression evidence, and rollback in `docs/doctoring/opencode-archive-extraction-evidence.md`.
+- NVIDIA model-selection doctoring evidence for endpoint availability, deprecated-endpoint rejection, capability and context evidence, no-fallback semantics, test-first regression evidence, and replacement procedure in `docs/doctoring/nvidia-opencode-model-selection-evidence.md`.
+- Principal-scoped durable asynchronous ETL job intake and owner-scoped status resources, Flyway `etl_job_records` migration, deterministic replay/conflict coverage, and the authoritative lifecycle contract in `docs/etl/durable-job-intake.md`.
+- Durable idempotency ledger migration, PostgreSQL transaction advisory-lock adapter, deterministic concurrency/rollback coverage, and the operator/client contract `docs/etl/idempotent-retries.md`.
+- ETL problem-details client and operator contract: `docs/api/problem-details.md`.
+- Operator-configurable ETL admission limits under `mightyetl.etl.*` / `xtrmetl.etl.*`, backed by `ETL_MAX_PAYLOAD_BYTES` and `ETL_MAX_BATCH_RECORDS` environment variables with hard safety ceilings.
+- ETL transaction rollback integration coverage and the operator runbook `docs/etl/bounded-atomic-batches.md`.
+- Connector scaffolds (contracts + docs only): Qlik Sense, Databricks, Snowflake under `docs/connectors/` and `etl-service` SPI stubs.
+- Any-to-any CDC design notes and source SPI scaffold: `docs/cdc/any-to-any-cdc.md`, `cdc-service` SPI stubs.
+- CDC operations notes: `docs/cdc/ops-and-reliability.md`.
+- Product upgrade progress tracker: `docs/mightyETL-product-upgrade-progress.md`.
+- CDC status/sources API: `GET /api/cdc/status`, `GET /api/cdc/sources` (no secrets).
+- `DebeziumChangeRecordMapper` + `CanonicalChangeRecord` (mapper unit-tested; not on live publish path).
+- CDC target SPI registry (`kafka`, `jdbc-replica`) for any-to-any routing scaffold.
+- `etl-service` `xtrmetl.connectors.*` disabled config keys for Databricks/Snowflake/Qlik.
+- Dual-read config aliases: `mightyetl.*` preferred → `xtrmetl.*` (`MightyEtlConfigAliasEnvironmentPostProcessor`).
+- Configurable replica tables (`xtrmetl.replica.tables`) for `(id,data)`-shaped tables.
+- Optional CDC canonical-map counters (`xtrmetl.cdc.canonical-map-enabled`).
+- ETL connector catalog API `GET /api/etl/connectors` + scaffold enable guard.
+- CDC replication slot lag probe on `GET /api/cdc/status` (`ReplicationSlotProbe`).
+- CDC multi-source config list + `CdcSourceFactory` (declarative; single live engine).
+- `GET /api/cdc/targets` for target SPI discovery.
+- Actuator `cdcEngine` health indicator (engine running + slot details).
+- SPI lifecycle: `PostgresDebeziumCdcSource.start/stop` delegates to `CdcService`.
+- Scaffold CDC sources: `mysql-debezium`, `sqlserver-debezium` (discovery only).
+- Root POM `mightyETL` (artifactId remains `xtrmETL`).
+- README honest “Supported today” matrix; compose file product-name header.
### Security
-- Durable-worker telemetry excludes payloads, raw principals, raw idempotency keys, internal hashes,
- job and lease identifiers, SQL, exception messages, and unbounded exception labels.
-- Exact payload-digest and response-ledger conflicts fail closed with
- `etl_job_integrity_failure`; stale workers cannot commit target, ledger, or terminal-state effects.
+- Durable-worker metrics and ordinary logs exclude payloads, raw principals, raw idempotency keys, hashes, job and lease identifiers, SQL, exception messages, and unbounded exception labels.
+- Retained payload or response-ledger identity conflicts fail closed with `etl_job_integrity_failure`; an expired or superseded lease rolls back target, ledger, and terminal-state effects.
+
+### Added (historical)
+
+- Comprehensive documentation suite (2026-01-08)
+ - `README.md`: Quick start guide and project overview
+ - `PRD.md`: Product Requirements Document with detailed specifications
+ - `ARCHITECTURE.md`: System architecture and technical diagrams
+ - `SUMMARY_KR.md`: Korean language summary
+ - `CHANGELOG.md`: This file
## [1.0.0] - 2026-01-08
-### Added
+### Project Documentation Initiative
+
+This release focuses on reverse-engineering and documenting the
+existing xtrmETL platform.
+
+#### Added Documentation
+
+1. **README.md** (478 lines)
+ - Project overview and value proposition
+ - Quick start guide with prerequisites
+ - Service descriptions for all microservices
+ - Authentication flow and API examples
+ - Database setup scripts
+ - Testing instructions
+ - Monitoring setup with Zipkin
+ - Technology stack reference
+ - Development guidelines
+
+2. **PRD.md** (608 lines)
+ - Executive summary and product vision
+ - Problem statement analysis
+ - Solution overview with core capabilities
+ - Functional requirements (FR-CDC-1 through FR-GATE-1)
+ - Non-functional requirements (Performance, Reliability, Security, etc.)
+ - Complete data model specifications
+ - API specifications with examples
+ - Deployment architecture
+ - Use cases and scenarios
+ - Future enhancements roadmap
+ - Success metrics and KPIs
+ - Risk assessment and mitigation strategies
+ - Comprehensive glossary
+
+3. **ARCHITECTURE.md** (633 lines)
+ - High-level system architecture diagrams
+ - Service communication patterns (synchronous/asynchronous)
+ - Detailed data flow diagrams for:
+ - ETL processing
+ - CDC event capture
+ - Authentication flow
+ - Service discovery and registration
+ - Security architecture
+ - Monitoring and observability stack
+ - Deployment architectures (single-node and multi-node)
+ - Debezium integration details
+ - Spring Retry mechanism
+ - Network and port configuration
+ - Scalability considerations
+
+4. **SUMMARY_KR.md** (206 lines)
+ - Korean language summary for stakeholders
+ - Project purpose and goals
+ - Key features overview
+ - System architecture summary
+ - Technology stack
+ - Use cases
+ - API specifications
+ - Quick start guide
+ - Future improvements
+ - Technical debt assessment
+
+#### Project Understanding
+
+Through code analysis, identified the platform as:
+
+- **Enterprise ETL and CDC Platform**
+- Microservices-based architecture using Spring Cloud
+- Real-time Change Data Capture using Debezium
+- Data transformation pipelines with parallel processing
+- JWT-based security with role-based access control
+- Event streaming via Apache Kafka
+- Service discovery with Netflix Eureka
+- Distributed tracing with Zipkin
+
+#### Key Components Documented
+
+1. **CDC Service** (Port 8001)
+ - PostgreSQL change data capture
+ - Debezium embedded engine
+ - Kafka event publishing
+ - Real-time monitoring capabilities
+
+2. **ETL Service** (Port 8000)
+ - JSON data processing
+ - Parallel record processing
+ - Configurable transformations
+ - Automatic retry mechanism
+ - Target database loading
+
+3. **Zuul Gateway** (Port 8080)
+ - API Gateway with routing
+ - JWT authentication filter
+ - Load balancing
+ - Request routing to services
+
+4. **Eureka Server** (Port 8761)
+ - Service discovery
+ - Service registration
+ - Health monitoring
+
+5. **Config Server** (Port 8888)
+ - Centralized configuration (planned)
+
+6. **Zipkin** (Port 9412)
+ - Distributed tracing
+ - Performance monitoring
+
+#### Technology Stack Documented
+
+- Java 25
+- Spring Boot 2.7.14
+- Spring Cloud 2021.0.8
+- Debezium 2.3.x - 2.5.x
+- PostgreSQL 12+
+- Apache Kafka
+- Netflix Zuul
+- Netflix Eureka
+- Maven
+
+#### Identified Technical Debt
+
+- Common module referenced but not implemented
+- MyBatis dependencies present but unused
+- Redis integration configured but not utilized
+- Config Server implemented but not actively used
+- Missing Spring Boot Actuator health checks
+
+#### Future Enhancements Documented
+
+- Multi-database CDC support (MySQL, Oracle, SQL Server)
+- Custom transformation functions
+- Data quality validation
+- Web UI for configuration and monitoring
+- Schema registry integration
+- Dead Letter Queue for failed messages
+- Enhanced metrics dashboard
+
+### Files Changed
+
+- `CHANGELOG.md` (new)
+- `README.md` (new)
+- `PRD.md` (new)
+- `ARCHITECTURE.md` (new)
+- `SUMMARY_KR.md` (new)
+
+### Issue Resolved
+
+This release addresses the GitHub issue requesting reverse-engineering of the program's purpose and PRD creation. The issue noted: "이 프로그램이 무엇을 하고 싶었던 프로그램인지 역추적하고 PRD 작성. 아마도 데이터베이스 CDC 프로그램이었던 것 같음."
+
+**Confirmation**: Yes, this is a database CDC (Change Data Capture) program, specifically an enterprise-grade ETL and CDC platform for real-time data integration.
+
+### Documentation Statistics
+
+- Total lines of documentation: 1,925
+- Total files created: 4
+- Total size: ~75 KB
+- Languages: English (primary), Korean (summary)
+
+### Related Documents
+
+For more information, see:
+
+- [README.md](README.md) - Quick start guide
+- [PRD.md](PRD.md) - Product Requirements Document
+- [ARCHITECTURE.md](ARCHITECTURE.md) - Technical architecture
+- [SUMMARY_KR.md](SUMMARY_KR.md) - Korean summary
+- Original design notes (Korean) in project files
+
+---
+
+## Notes on Versioning
+
+Since this is documentation work on an existing codebase:
+
+- Version 1.0.0 represents the first documented release
+- The actual codebase existed before this documentation
+- Future versions will track both code and documentation changes
+
+## Changelog Maintenance
+
+This changelog will be updated:
-- Initial reverse-engineered product documentation: `README.md`, `PRD.md`, `ARCHITECTURE.md`, and
- `SUMMARY_KR.md`.
-- Baseline documentation for the Java/Spring microservice architecture, PostgreSQL ETL path,
- Debezium-based CDC path, Kafka publication, service discovery, gateway routing, and Zipkin tracing.
+- When new features are added
+- When bugs are fixed
+- When documentation is significantly updated
+- For each release or milestone
-### Known baseline limitations
+---
-- Several connector and multi-source capabilities were documented or scaffolded rather than live.
-- Config Server, Redis, and selected dependencies were present without complete production usage.
-- Operational health, security, idempotency, bounded admission, and durable asynchronous execution
- required the later unreleased hardening documented above.
+**Changelog Version**: 1.0
+**Last Updated**: 2026-08-05
+**Maintained By**: Development Team
\ No newline at end of file
From cafedcafafcc170fbb69a707c536afb85f4070fc Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 10:15:58 +0900
Subject: [PATCH 44/92] test(etl): enforce complete durable execution coverage
---
etl-service/pom.xml | 32 ++++++++++++++++++++++----------
1 file changed, 22 insertions(+), 10 deletions(-)
diff --git a/etl-service/pom.xml b/etl-service/pom.xml
index 00695de2..9e0e56b5 100644
--- a/etl-service/pom.xml
+++ b/etl-service/pom.xml
@@ -98,12 +98,6 @@
org.jacocojacoco-maven-plugin0.8.15
-
-
- com.xtrmetl.etl.job.*
- com.xtrmetl.etl.controller.EtlJobController*
-
- prepare-durable-job-coverage
@@ -118,6 +112,13 @@
report
+
+
+ com/xtrmetl/etl/job/*.class
+ com/xtrmetl/etl/controller/EtlJobController*.class
+ com/xtrmetl/etl/service/Sha256Digest*.class
+
+ check-durable-job-coverage
@@ -126,13 +127,24 @@
check
+
+ com/xtrmetl/etl/job/*.class
+ com/xtrmetl/etl/controller/EtlJobController*.class
+ com/xtrmetl/etl/service/Sha256Digest*.class
+
+
+ BUNDLE
+
+
+ INSTRUCTION
+ TOTALCOUNT
+ 1
+
+
+ CLASS
-
- com.xtrmetl.etl.job.*
- com.xtrmetl.etl.controller.EtlJobController*
- INSTRUCTION
From 956a392c28d1aaf04992af963300211c80892f6e Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 10:16:37 +0900
Subject: [PATCH 45/92] test(etl): verify complete execution coverage policy
---
.../etl/job/EtlJobCoveragePolicyTest.java | 203 +++++++++++++++---
1 file changed, 176 insertions(+), 27 deletions(-)
diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobCoveragePolicyTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobCoveragePolicyTest.java
index 4a728d75..675498ed 100644
--- a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobCoveragePolicyTest.java
+++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobCoveragePolicyTest.java
@@ -1,55 +1,204 @@
package com.xtrmetl.etl.job;
import org.junit.jupiter.api.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
-import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.HashSet;
+import java.util.Set;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
- * Keeps the durable-job production slice bound to an executable 100% coverage policy.
+ * Keeps the durable-job production slice bound to an executable, non-empty 100% coverage policy.
*
- *
The policy is intentionally scoped to the production classes introduced by the durable-job
- * intake slice. It requires current Java-compatible JaCoCo instrumentation and zero missed
- * instructions, lines, methods, or branches while the ordinary {@code mvn test} lifecycle runs.
+ *
JaCoCo's agent instrumentation filters and Maven report filters consume different name
+ * forms. A plugin-wide dotted include can therefore match neither compiled class-file paths nor
+ * the names seen by the agent, creating a report with zero analyzed classes that still satisfies
+ * zero-missed rules vacuously. This contract requires unrestricted test instrumentation,
+ * execution-specific class-file filters, and an explicit non-empty bundle check before the
+ * zero-missed instruction, line, method, and branch rules can pass.
The worker is disabled unless an operator explicitly enables it. A process-lifetime lease
- * owner identifier is generated when no external value is supplied. The identifier is deliberately
- * restricted to a short safe ASCII profile because it is persisted as operational metadata and
- * must never become a free-form log or database injection surface.
+ *
The worker is disabled unless an operator explicitly enables it. Polling delays and lease
+ * durations are capped at one day so a malformed environment value cannot create effectively
+ * permanent scheduling gaps, arithmetic overflow, or a lease that prevents timely crash recovery.
+ * A process-lifetime lease owner identifier is generated when no external value is supplied. The
+ * identifier is deliberately restricted to a short safe ASCII profile because it is persisted as
+ * operational metadata and must never become a free-form log or database injection surface.
*/
@ConfigurationProperties(prefix = "xtrmetl.etl.jobs.worker")
public class EtlJobWorkerProperties {
+ /** Maximum supported fixed or initial scheduler delay: one day in milliseconds. */
+ public static final long MAXIMUM_SCHEDULER_DELAY_MILLISECONDS = 86_400_000L;
+
+ /** Maximum supported durable-job lease duration: one day in seconds. */
+ public static final long MAXIMUM_LEASE_DURATION_SECONDS = 86_400L;
+
private static final Pattern SAFE_LEASE_OWNER_PATTERN = Pattern.compile(
"[A-Za-z0-9._:-]{8,128}"
);
@@ -56,7 +64,7 @@ public void setEnabled(boolean enabled) {
/**
* Returns the delay measured after one polling invocation completes.
*
- * @return positive fixed delay in milliseconds
+ * @return fixed delay from one millisecond through one day
*/
public long getFixedDelayMilliseconds() {
return fixedDelayMilliseconds;
@@ -65,12 +73,16 @@ public long getFixedDelayMilliseconds() {
/**
* Sets the delay measured after one polling invocation completes.
*
- * @param fixedDelayMilliseconds positive fixed delay in milliseconds
- * @throws IllegalArgumentException when the delay is zero or negative
+ * @param fixedDelayMilliseconds delay from one millisecond through one day
+ * @throws IllegalArgumentException when the delay is outside the supported range
*/
public void setFixedDelayMilliseconds(long fixedDelayMilliseconds) {
- if (fixedDelayMilliseconds <= 0L) {
- throw new IllegalArgumentException("fixedDelayMilliseconds must be positive");
+ if (fixedDelayMilliseconds < 1L
+ || fixedDelayMilliseconds > MAXIMUM_SCHEDULER_DELAY_MILLISECONDS) {
+ throw new IllegalArgumentException(
+ "fixedDelayMilliseconds must be between 1 and "
+ + MAXIMUM_SCHEDULER_DELAY_MILLISECONDS
+ );
}
this.fixedDelayMilliseconds = fixedDelayMilliseconds;
}
@@ -78,7 +90,7 @@ public void setFixedDelayMilliseconds(long fixedDelayMilliseconds) {
/**
* Returns the delay before the first polling invocation after application startup.
*
- * @return non-negative initial delay in milliseconds
+ * @return initial delay from zero milliseconds through one day
*/
public long getInitialDelayMilliseconds() {
return initialDelayMilliseconds;
@@ -87,12 +99,16 @@ public long getInitialDelayMilliseconds() {
/**
* Sets the delay before the first polling invocation after application startup.
*
- * @param initialDelayMilliseconds non-negative initial delay in milliseconds
- * @throws IllegalArgumentException when the delay is negative
+ * @param initialDelayMilliseconds delay from zero milliseconds through one day
+ * @throws IllegalArgumentException when the delay is outside the supported range
*/
public void setInitialDelayMilliseconds(long initialDelayMilliseconds) {
- if (initialDelayMilliseconds < 0L) {
- throw new IllegalArgumentException("initialDelayMilliseconds must not be negative");
+ if (initialDelayMilliseconds < 0L
+ || initialDelayMilliseconds > MAXIMUM_SCHEDULER_DELAY_MILLISECONDS) {
+ throw new IllegalArgumentException(
+ "initialDelayMilliseconds must be between 0 and "
+ + MAXIMUM_SCHEDULER_DELAY_MILLISECONDS
+ );
}
this.initialDelayMilliseconds = initialDelayMilliseconds;
}
@@ -100,7 +116,7 @@ public void setInitialDelayMilliseconds(long initialDelayMilliseconds) {
/**
* Returns how long one database claim remains valid without renewal.
*
- * @return positive lease duration in seconds
+ * @return lease duration from one second through one day
*/
public long getLeaseDurationSeconds() {
return leaseDurationSeconds;
@@ -109,12 +125,16 @@ public long getLeaseDurationSeconds() {
/**
* Sets how long one database claim remains valid without renewal.
*
- * @param leaseDurationSeconds positive lease duration in seconds
- * @throws IllegalArgumentException when the duration is zero or negative
+ * @param leaseDurationSeconds duration from one second through one day
+ * @throws IllegalArgumentException when the duration is outside the supported range
*/
public void setLeaseDurationSeconds(long leaseDurationSeconds) {
- if (leaseDurationSeconds <= 0L) {
- throw new IllegalArgumentException("leaseDurationSeconds must be positive");
+ if (leaseDurationSeconds < 1L
+ || leaseDurationSeconds > MAXIMUM_LEASE_DURATION_SECONDS) {
+ throw new IllegalArgumentException(
+ "leaseDurationSeconds must be between 1 and "
+ + MAXIMUM_LEASE_DURATION_SECONDS
+ );
}
this.leaseDurationSeconds = leaseDurationSeconds;
}
From 027375310770d0b3ae3cd8fa5d074d86253e5046 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 12:06:08 +0900
Subject: [PATCH 51/92] test(etl): reject overlong repository leases
---
.../EtlJobLeaseRepositoryValidationTest.java | 39 +++++++++++++++++++
1 file changed, 39 insertions(+)
create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobLeaseRepositoryValidationTest.java
diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobLeaseRepositoryValidationTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobLeaseRepositoryValidationTest.java
new file mode 100644
index 00000000..16f4ba5a
--- /dev/null
+++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobLeaseRepositoryValidationTest.java
@@ -0,0 +1,39 @@
+package com.xtrmetl.etl.job;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.transaction.PlatformTransactionManager;
+
+import java.time.Duration;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+/**
+ * Proves that the lease repository rejects unsafe public arguments before database access.
+ */
+class EtlJobLeaseRepositoryValidationTest {
+
+ @Test
+ void rejectsLeaseDurationsAboveTheOperationalSafetyCeilingBeforeDatabaseAccess() {
+ JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class);
+ PlatformTransactionManager transactionManager = mock(PlatformTransactionManager.class);
+ EtlJobLeaseRepository repository = new EtlJobLeaseRepository(
+ jdbcTemplate,
+ transactionManager
+ );
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> repository.claimNext(
+ "worker-alpha",
+ Duration.ofSeconds(
+ EtlJobWorkerProperties.MAXIMUM_LEASE_DURATION_SECONDS + 1L
+ ),
+ 3
+ )
+ );
+ verifyNoInteractions(jdbcTemplate, transactionManager);
+ }
+}
From 1377f6efffdd784c54873a64d349fdad9f399de7 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 12:07:18 +0900
Subject: [PATCH 52/92] fix(etl): enforce repository lease ceiling
---
.../xtrmetl/etl/job/EtlJobLeaseRepository.java | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
diff --git a/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobLeaseRepository.java b/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobLeaseRepository.java
index 776cb48c..7842d248 100644
--- a/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobLeaseRepository.java
+++ b/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobLeaseRepository.java
@@ -22,7 +22,8 @@
* oldest eligible row with {@code FOR UPDATE SKIP LOCKED}, and finally writes a fresh claim token,
* owner, expiry, and incremented attempt count before commit. State transitions repeat the exact
* claim token, owner, running status, and database-time expiry predicates so stale workers cannot
- * mutate lifecycle state.
+ * mutate lifecycle state. Public callers cannot create leases longer than the worker's one-day
+ * operational ceiling, even when they bypass Spring configuration binding.
*/
@Repository
public class EtlJobLeaseRepository {
@@ -174,7 +175,7 @@ public EtlJobLeaseRepository(
* Claims at most one oldest eligible job for one worker process.
*
* @param leaseOwnerId safe non-sensitive process identifier
- * @param leaseDuration positive duration applied to database claim time
+ * @param leaseDuration duration from one second through one day
* @param maxAttempts maximum permitted claim count from 1 through 100
* @return a fresh claim, or an empty result when no row is eligible
* @throws NullPointerException when an argument is {@code null}
@@ -318,8 +319,15 @@ private static Duration requirePositiveDuration(Duration leaseDuration) {
leaseDuration,
"leaseDuration must not be null"
);
- if (requiredDuration.isZero() || requiredDuration.isNegative()) {
- throw new IllegalArgumentException("leaseDuration must be positive");
+ Duration maximumDuration = Duration.ofSeconds(
+ EtlJobWorkerProperties.MAXIMUM_LEASE_DURATION_SECONDS
+ );
+ if (requiredDuration.isZero()
+ || requiredDuration.isNegative()
+ || requiredDuration.compareTo(maximumDuration) > 0) {
+ throw new IllegalArgumentException(
+ "leaseDuration must be between one second and one day"
+ );
}
return requiredDuration;
}
From 51d120232cedbf09bb0f012b68094477d9777c6a Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 12:08:08 +0900
Subject: [PATCH 53/92] docs(etl): document bounded worker timing controls
---
docs/operations/durable-job-worker.md | 18 +++++++++++++-----
1 file changed, 13 insertions(+), 5 deletions(-)
diff --git a/docs/operations/durable-job-worker.md b/docs/operations/durable-job-worker.md
index 04ab521a..bc0b31be 100644
--- a/docs/operations/durable-job-worker.md
+++ b/docs/operations/durable-job-worker.md
@@ -25,19 +25,26 @@ payloads are acceptable. Enable the worker without intake only to drain already
| Preferred property | Environment variable | Default | Constraint |
| --- | --- | ---: | --- |
| `mightyetl.etl.jobs.worker.enabled` | `ETL_JOB_WORKER_ENABLED` | `false` | explicit opt-in |
-| `mightyetl.etl.jobs.worker.fixed-delay-milliseconds` | `ETL_JOB_WORKER_FIXED_DELAY_MILLISECONDS` | `5000` | greater than zero |
-| `mightyetl.etl.jobs.worker.initial-delay-milliseconds` | `ETL_JOB_WORKER_INITIAL_DELAY_MILLISECONDS` | `5000` | zero or greater |
-| `mightyetl.etl.jobs.worker.lease-duration-seconds` | `ETL_JOB_WORKER_LEASE_DURATION_SECONDS` | `300` | greater than zero |
+| `mightyetl.etl.jobs.worker.fixed-delay-milliseconds` | `ETL_JOB_WORKER_FIXED_DELAY_MILLISECONDS` | `5000` | 1 through 86,400,000 |
+| `mightyetl.etl.jobs.worker.initial-delay-milliseconds` | `ETL_JOB_WORKER_INITIAL_DELAY_MILLISECONDS` | `5000` | 0 through 86,400,000 |
+| `mightyetl.etl.jobs.worker.lease-duration-seconds` | `ETL_JOB_WORKER_LEASE_DURATION_SECONDS` | `300` | 1 through 86,400 |
| `mightyetl.etl.jobs.worker.max-attempts` | `ETL_JOB_WORKER_MAX_ATTEMPTS` | `3` | 1 through 100 |
| `mightyetl.etl.jobs.worker.lease-owner-id` | deployment-specific | generated | 8–128 safe ASCII characters |
+Scheduler delays and lease durations have a one-day safety ceiling. Configuration binding and the
+lease repository enforce the same limit, so direct repository callers cannot bypass it. Values above
+the ceiling fail application binding or claim validation rather than creating an effectively
+permanent polling pause, arithmetic overflow, or multi-day stale-work recovery delay.
+
Set an explicit `lease-owner-id` only when the deployment platform can guarantee one stable,
non-sensitive value per process. Never use a hostname containing customer data, a pod annotation
containing credentials, an email address, a tenant identifier, or a raw infrastructure token.
Choose a lease duration longer than the normal high-percentile execution time plus database and
network variance. The current slice does not renew leases. A lease that expires during execution
-causes the final success transition to fail and rolls back target and response-ledger writes.
+causes the final success transition to fail and rolls back target and response-ledger writes. If a
+normal execution can exceed one day, do not increase the ceiling silently; implement and validate
+lease renewal as a separate fenced capability first.
## Claim, execution, and recovery
@@ -117,7 +124,8 @@ and query parameters as opt-in sensitive telemetry requiring a separate privacy
2. Check clock-independent database latency and long-running statements; lease decisions use database
time.
3. Verify every process has a safe, distinct lease owner identifier.
-4. Increase the lease duration only after confirming that crash recovery delay remains acceptable.
+4. Increase the lease duration only within the one-day ceiling and only after confirming that crash
+ recovery delay remains acceptable; implement lease renewal instead of exceeding the ceiling.
### Integrity failure
From 5af809f64ece959adaffcc37ec9ec03ed9f92f63 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 12:08:59 +0900
Subject: [PATCH 54/92] docs(config): expose worker timing ceilings
---
etl-service/src/main/resources/application.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/etl-service/src/main/resources/application.yml b/etl-service/src/main/resources/application.yml
index 642675d3..f5b566d5 100644
--- a/etl-service/src/main/resources/application.yml
+++ b/etl-service/src/main/resources/application.yml
@@ -32,8 +32,10 @@ xtrmetl:
worker:
# Execution remains fail-closed until an operator enables the worker explicitly.
enabled: ${ETL_JOB_WORKER_ENABLED:false}
+ # Scheduler delays are bounded to one day (86,400,000 milliseconds).
fixed-delay-milliseconds: ${ETL_JOB_WORKER_FIXED_DELAY_MILLISECONDS:5000}
initial-delay-milliseconds: ${ETL_JOB_WORKER_INITIAL_DELAY_MILLISECONDS:5000}
+ # Leases are bounded to one day (86,400 seconds); longer jobs require lease renewal.
lease-duration-seconds: ${ETL_JOB_WORKER_LEASE_DURATION_SECONDS:300}
max-attempts: ${ETL_JOB_WORKER_MAX_ATTEMPTS:3}
# Warehouse/BI targets: SPI + config binding + validation + catalog; writes remain SCAFFOLD.
From 8e91f69316a3db5698f42e59a5d8d4c4d4c68bda Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 12:18:26 +0900
Subject: [PATCH 55/92] test(etl): align runbook contract with lease-fenced
execution
---
.../job/EtlJobMigrationDocumentationTest.java | 16 +++++++++-------
1 file changed, 9 insertions(+), 7 deletions(-)
diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobMigrationDocumentationTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobMigrationDocumentationTest.java
index 46aa44bc..698acc9c 100644
--- a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobMigrationDocumentationTest.java
+++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobMigrationDocumentationTest.java
@@ -51,21 +51,23 @@ void migrationReservesStableWorkerStatesAndRequiresTerminalPayloadClearing() thr
}
@Test
- void runbookDocumentsAcceptedSemanticsOwnershipAndTheWorkerBoundary() throws IOException {
+ void runbookDocumentsAcceptedSemanticsOwnershipAndLeaseFencedExecution() throws IOException {
String runbook = read("docs/etl/durable-job-intake.md").replaceAll("\\s+", " ");
assertTrue(runbook.contains("202 Accepted"));
assertTrue(runbook.contains("Location: /api/etl/jobs/{job_record_id}"));
assertTrue(runbook.contains("same authenticated principal"));
- assertTrue(runbook.contains("byte-for-byte same JSON text"));
- assertTrue(runbook.contains("does not execute jobs yet"));
+ assertTrue(runbook.contains("byte-for-byte identical JSON text"));
+ assertTrue(runbook.contains("lease-fenced worker claims accepted jobs"));
+ assertTrue(runbook.contains("PostgreSQL, not scheduler uniqueness, distributes work"));
+ assertTrue(runbook.contains("same transaction"));
assertTrue(runbook.contains("request payload"));
- assertTrue(runbook.contains("worker and lease-fencing slice"));
assertTrue(runbook.contains("Cache-Control: no-store"));
assertTrue(runbook.contains("422 etl_job_submission_key_reused"));
- assertTrue(runbook.contains("disabled by default"));
- assertTrue(runbook.contains("mightyetl.etl.jobs.intake-enabled=true"));
- assertTrue(runbook.contains("xtrmetl.etl.jobs.intake-enabled=true"));
+ assertTrue(runbook.contains("fail-closed"));
+ assertTrue(runbook.contains("mightyetl.etl.jobs.intake-enabled=false"));
+ assertTrue(runbook.contains("mightyetl.etl.jobs.worker.enabled=false"));
+ assertTrue(runbook.contains("xtrmetl.*"));
}
private static String read(String relativePath) throws IOException {
From 5bafdd508c656f4656cb06c38b4db367cc391a3a Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 5 Aug 2026 12:21:18 +0900
Subject: [PATCH 56/92] test(etl): cover impossible locked-claim transition
---
.../EtlJobLeaseRepositoryValidationTest.java | 55 ++++++++++++++++++-
1 file changed, 54 insertions(+), 1 deletion(-)
diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobLeaseRepositoryValidationTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobLeaseRepositoryValidationTest.java
index 16f4ba5a..b56f85ae 100644
--- a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobLeaseRepositoryValidationTest.java
+++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobLeaseRepositoryValidationTest.java
@@ -2,16 +2,28 @@
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.TransactionDefinition;
+import org.springframework.transaction.TransactionStatus;
+import java.sql.ResultSet;
import java.time.Duration;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.List;
+import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
/**
- * Proves that the lease repository rejects unsafe public arguments before database access.
+ * Proves that the lease repository rejects unsafe arguments and impossible claim transitions.
*/
class EtlJobLeaseRepositoryValidationTest {
@@ -36,4 +48,45 @@ void rejectsLeaseDurationsAboveTheOperationalSafetyCeilingBeforeDatabaseAccess()
);
verifyNoInteractions(jdbcTemplate, transactionManager);
}
+
+ @Test
+ void failsClosedWhenALockedCandidateCannotBeUpdated() throws Exception {
+ JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class);
+ PlatformTransactionManager transactionManager = mock(PlatformTransactionManager.class);
+ TransactionStatus transactionStatus = mock(TransactionStatus.class);
+ ResultSet resultSet = mock(ResultSet.class);
+ UUID jobRecordId = UUID.randomUUID();
+
+ when(transactionManager.getTransaction(any(TransactionDefinition.class)))
+ .thenReturn(transactionStatus);
+ when(jdbcTemplate.update(anyString(), any(Object[].class))).thenReturn(0);
+ when(resultSet.getObject("job_record_id", UUID.class)).thenReturn(jobRecordId);
+ when(resultSet.getString("principal_scope_hash")).thenReturn("a".repeat(64));
+ when(resultSet.getString("submission_key_hash")).thenReturn("b".repeat(64));
+ when(resultSet.getString("request_digest")).thenReturn("c".repeat(64));
+ when(resultSet.getString("request_payload"))
+ .thenReturn("[{\"id\":\"record_alpha\"}]");
+ when(resultSet.getInt("attempt_count")).thenReturn(0);
+ when(resultSet.getObject("database_now", OffsetDateTime.class))
+ .thenReturn(OffsetDateTime.of(2026, 8, 5, 0, 0, 0, 0, ZoneOffset.UTC));
+ doAnswer(invocation -> {
+ @SuppressWarnings("unchecked")
+ RowMapper