From 48d7ca8b3bb2609a5d1a6c8235ec9f309ce0c367 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 09:41:06 +0900 Subject: [PATCH 1/2] test(execution): specify worker lease fencing --- .../execution/ConversionWorkerLeaseTest.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/test/java/com/clearfolio/viewer/execution/ConversionWorkerLeaseTest.java diff --git a/src/test/java/com/clearfolio/viewer/execution/ConversionWorkerLeaseTest.java b/src/test/java/com/clearfolio/viewer/execution/ConversionWorkerLeaseTest.java new file mode 100644 index 00000000..2bd82ebd --- /dev/null +++ b/src/test/java/com/clearfolio/viewer/execution/ConversionWorkerLeaseTest.java @@ -0,0 +1,50 @@ +package com.clearfolio.viewer.execution; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +class ConversionWorkerLeaseTest { + + @Test + void leaseCarriesImmutableJobGenerationAndLeaseIdentity() { + UUID jobId = UUID.randomUUID(); + UUID leaseId = UUID.randomUUID(); + + ConversionWorkerLease lease = ConversionWorkerLease.issue(jobId, 7L, leaseId); + + assertEquals(jobId, lease.jobId()); + assertEquals(7L, lease.generation()); + assertEquals(leaseId, lease.leaseId()); + } + + @Test + void authorizationRequiresExactJobGenerationAndLeaseIdentity() { + UUID jobId = UUID.randomUUID(); + UUID leaseId = UUID.randomUUID(); + ConversionWorkerLease lease = ConversionWorkerLease.issue(jobId, 2L, leaseId); + + assertTrue(lease.authorizes(jobId, 2L, leaseId)); + assertFalse(lease.authorizes(UUID.randomUUID(), 2L, leaseId)); + assertFalse(lease.authorizes(jobId, 3L, leaseId)); + assertFalse(lease.authorizes(jobId, 2L, UUID.randomUUID())); + assertFalse(lease.authorizes(null, 2L, leaseId)); + assertFalse(lease.authorizes(jobId, 2L, null)); + } + + @Test + void leaseIssuanceFailsClosedForMissingOrNonPositiveAuthority() { + UUID jobId = UUID.randomUUID(); + UUID leaseId = UUID.randomUUID(); + + assertThrows(NullPointerException.class, () -> ConversionWorkerLease.issue(null, 1L, leaseId)); + assertThrows(NullPointerException.class, () -> ConversionWorkerLease.issue(jobId, 1L, null)); + assertThrows(IllegalArgumentException.class, () -> ConversionWorkerLease.issue(jobId, 0L, leaseId)); + assertThrows(IllegalArgumentException.class, () -> ConversionWorkerLease.issue(jobId, -1L, leaseId)); + } +} From 29863b9c0ee820bedb1fac96181aeec830d67a68 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 09:42:33 +0900 Subject: [PATCH 2/2] feat(execution): add generation-fenced worker lease --- .../execution/ConversionWorkerLease.java | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/main/java/com/clearfolio/viewer/execution/ConversionWorkerLease.java diff --git a/src/main/java/com/clearfolio/viewer/execution/ConversionWorkerLease.java b/src/main/java/com/clearfolio/viewer/execution/ConversionWorkerLease.java new file mode 100644 index 00000000..d39e9723 --- /dev/null +++ b/src/main/java/com/clearfolio/viewer/execution/ConversionWorkerLease.java @@ -0,0 +1,88 @@ +package com.clearfolio.viewer.execution; + +import java.util.Objects; +import java.util.UUID; + +/** + * Immutable authority token for one conversion-worker claim. + * + *

The lease binds a worker to one permanently identified conversion job, + * one positive lifecycle generation, and one unique lease identifier. A later + * publication boundary can therefore reject work produced by an expired or + * superseded claim instead of trusting job identity alone.

+ */ +public final class ConversionWorkerLease { + + private final UUID jobId; + private final long generation; + private final UUID leaseId; + + private ConversionWorkerLease(UUID jobId, long generation, UUID leaseId) { + this.jobId = jobId; + this.generation = generation; + this.leaseId = leaseId; + } + + /** + * Issues a lease identity after validating all authority components. + * + * @param jobId permanently reserved conversion-job identifier + * @param generation positive lifecycle generation owned by this claim + * @param leaseId unique identifier for this exact worker claim + * @return immutable worker lease + * @throws NullPointerException when {@code jobId} or {@code leaseId} is null + * @throws IllegalArgumentException when {@code generation} is not positive + */ + public static ConversionWorkerLease issue(UUID jobId, long generation, UUID leaseId) { + UUID requiredJobId = Objects.requireNonNull(jobId, "jobId"); + UUID requiredLeaseId = Objects.requireNonNull(leaseId, "leaseId"); + if (generation <= 0L) { + throw new IllegalArgumentException("generation must be positive"); + } + return new ConversionWorkerLease(requiredJobId, generation, requiredLeaseId); + } + + /** + * Returns the conversion job whose work this lease may authorize. + * + * @return permanently reserved conversion-job identifier + */ + public UUID jobId() { + return jobId; + } + + /** + * Returns the lifecycle generation fenced by this lease. + * + * @return positive lifecycle generation + */ + public long generation() { + return generation; + } + + /** + * Returns the unique identifier for this exact worker claim. + * + * @return worker lease identifier + */ + public UUID leaseId() { + return leaseId; + } + + /** + * Checks whether candidate publication authority exactly matches this lease. + * + * @param candidateJobId candidate permanently reserved job identifier + * @param candidateGeneration candidate lifecycle generation + * @param candidateLeaseId candidate worker lease identifier + * @return true only when all three authority components exactly match + */ + public boolean authorizes( + UUID candidateJobId, + long candidateGeneration, + UUID candidateLeaseId) { + return jobId.equals(candidateJobId) + && generation == candidateGeneration + && leaseId.equals(candidateLeaseId); + } +}