Skip to content

Commit 8e038c6

Browse files
committed
Change operation IDs to hashed values
1 parent 447bf3e commit 8e038c6

5 files changed

Lines changed: 77 additions & 42 deletions

File tree

sdk/src/main/java/software/amazon/lambda/durable/DurableContext.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@
22
// SPDX-License-Identifier: Apache-2.0
33
package software.amazon.lambda.durable;
44

5-
import com.amazonaws.services.lambda.runtime.Context;
5+
import java.nio.charset.StandardCharsets;
6+
import java.security.MessageDigest;
7+
import java.security.NoSuchAlgorithmException;
68
import java.time.Duration;
9+
import java.util.HexFormat;
710
import java.util.Objects;
811
import java.util.concurrent.atomic.AtomicInteger;
912
import java.util.function.Function;
1013
import java.util.function.Supplier;
14+
1115
import org.slf4j.LoggerFactory;
16+
17+
import com.amazonaws.services.lambda.runtime.Context;
18+
1219
import software.amazon.lambda.durable.execution.ExecutionManager;
1320
import software.amazon.lambda.durable.logging.DurableLogger;
1421
import software.amazon.lambda.durable.operation.CallbackOperation;
@@ -309,12 +316,22 @@ public DurableLogger getLogger() {
309316
}
310317

311318
/**
312-
* Get the next operationId. For root contexts, returns sequential IDs like "1", "2", "3". For child contexts,
313-
* prefixes with the contextId to ensure global uniqueness, e.g. "1-1", "1-2" for operations inside child context
314-
* "1". This matches the JavaScript SDK's stepPrefix convention and prevents ID collisions in checkpoint batches.
319+
* Get the next operationId.
320+
* Returns a globally unique operation ID by hashing a sequential operation counter.
321+
* For root contexts, the counter value is hashed directly (e.g. "1", "2", "3").
322+
* For child contexts, the values are prefixed with the parent hashed contextId
323+
* (e.g. "<hash>-1", "<hash>-2" inside parent context <hash>).
324+
* This matches the JavaScript SDK's stepPrefix convention and prevents ID collisions in checkpoint batches.
315325
*/
316326
private String nextOperationId() {
317327
var counter = String.valueOf(operationCounter.incrementAndGet());
318-
return getContextId() != null ? getContextId() + "-" + counter : counter;
328+
var rawId = getContextId() != null ? getContextId() + "-" + counter : counter;
329+
try {
330+
var messageDigest = MessageDigest.getInstance("SHA-256");
331+
var hash = messageDigest.digest(rawId.getBytes(StandardCharsets.UTF_8));
332+
return HexFormat.of().formatHex(hash);
333+
} catch (NoSuchAlgorithmException e) {
334+
throw new RuntimeException("failed to get next operation id, SHA-256 not available", e);
335+
}
319336
}
320337
}

sdk/src/test/java/software/amazon/lambda/durable/DurableContextTest.java

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33
package software.amazon.lambda.durable;
4-
54
import static org.junit.jupiter.api.Assertions.*;
6-
75
import java.time.Duration;
86
import java.util.ArrayList;
97
import java.util.List;
8+
109
import org.junit.jupiter.api.Test;
10+
1111
import software.amazon.awssdk.services.lambda.model.*;
1212
import software.amazon.lambda.durable.execution.ExecutionManager;
1313
import software.amazon.lambda.durable.execution.SuspendExecutionException;
@@ -17,7 +17,7 @@
1717

1818
class DurableContextTest {
1919
private static final Operation EXECUTION_OP = Operation.builder()
20-
.id("0")
20+
.id(TestUtils.hashOperationId("0"))
2121
.type(OperationType.EXECUTION)
2222
.status(OperationStatus.STARTED)
2323
.build();
@@ -80,7 +80,7 @@ void testStepExecution() {
8080
void testStepReplay() {
8181
// Create context with existing operation
8282
var existingOp = Operation.builder()
83-
.id("1")
83+
.id(TestUtils.hashOperationId("1"))
8484
.status(OperationStatus.SUCCEEDED)
8585
.stepDetails(StepDetails.builder().result("\"Cached Result\"").build())
8686
.build();
@@ -106,7 +106,7 @@ void testStepAsync() throws Exception {
106106
void testStepAsyncReplay() throws Exception {
107107
// Create context with existing operation
108108
var existingOp = Operation.builder()
109-
.id("1")
109+
.id(TestUtils.hashOperationId("1"))
110110
.status(OperationStatus.SUCCEEDED)
111111
.stepDetails(
112112
StepDetails.builder().result("\"Cached Async Result\"").build())
@@ -132,7 +132,7 @@ void testWait() {
132132
void testWaitReplay() {
133133
// Create context with completed wait operation
134134
var existingOp =
135-
Operation.builder().id("1").status(OperationStatus.SUCCEEDED).build();
135+
Operation.builder().id(TestUtils.hashOperationId("1")).status(OperationStatus.SUCCEEDED).build();
136136
var context = createTestContext(List.of(existingOp));
137137

138138
// Wait should complete immediately (no exception)
@@ -168,17 +168,17 @@ void testCombinedSyncAsyncWait() throws Exception {
168168
void testCombinedReplay() throws Exception {
169169
// Create context with all operations completed
170170
var syncOp = Operation.builder()
171-
.id("1")
171+
.id(TestUtils.hashOperationId("1"))
172172
.status(OperationStatus.SUCCEEDED)
173173
.stepDetails(StepDetails.builder().result("\"Replayed Sync\"").build())
174174
.build();
175175
var asyncOp = Operation.builder()
176-
.id("2")
176+
.id(TestUtils.hashOperationId("2"))
177177
.status(OperationStatus.SUCCEEDED)
178178
.stepDetails(StepDetails.builder().result("100").build())
179179
.build();
180180
var waitOp =
181-
Operation.builder().id("3").status(OperationStatus.SUCCEEDED).build();
181+
Operation.builder().id(TestUtils.hashOperationId("3")).status(OperationStatus.SUCCEEDED).build();
182182
var context = createTestContext(List.of(syncOp, asyncOp, waitOp));
183183

184184
// All operations should replay from cache
@@ -229,7 +229,7 @@ void testStepWithTypeToken() {
229229
void testStepWithTypeTokenReplay() {
230230
// Create context with existing operation
231231
var existingOp = Operation.builder()
232-
.id("1")
232+
.id(TestUtils.hashOperationId("1"))
233233
.status(OperationStatus.SUCCEEDED)
234234
.stepDetails(StepDetails.builder()
235235
.result("[\"cached1\",\"cached2\"]")
@@ -280,7 +280,7 @@ void testStepAsyncWithTypeToken() throws Exception {
280280
void testStepAsyncWithTypeTokenReplay() throws Exception {
281281
// Create context with existing operation
282282
var existingOp = Operation.builder()
283-
.id("1")
283+
.id(TestUtils.hashOperationId("1"))
284284
.status(OperationStatus.SUCCEEDED)
285285
.stepDetails(StepDetails.builder()
286286
.result("[\"async-cached1\",\"async-cached2\"]")

sdk/src/test/java/software/amazon/lambda/durable/DurableExecutionTest.java

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@
22
// SPDX-License-Identifier: Apache-2.0
33
package software.amazon.lambda.durable;
44

5+
import java.util.List;
6+
import java.util.concurrent.ExecutorService;
7+
58
import static org.junit.jupiter.api.Assertions.assertEquals;
69
import static org.junit.jupiter.api.Assertions.assertFalse;
710
import static org.junit.jupiter.api.Assertions.assertNotNull;
811
import static org.junit.jupiter.api.Assertions.assertNull;
912
import static org.junit.jupiter.api.Assertions.assertThrows;
1013
import static org.junit.jupiter.api.Assertions.assertTrue;
11-
12-
import java.util.List;
13-
import java.util.concurrent.ExecutorService;
1414
import org.junit.jupiter.api.Test;
15+
1516
import software.amazon.awssdk.services.lambda.model.CheckpointUpdatedExecutionState;
1617
import software.amazon.awssdk.services.lambda.model.ExecutionDetails;
1718
import software.amazon.awssdk.services.lambda.model.Operation;
@@ -32,7 +33,7 @@ private DurableConfig configWithMockClient() {
3233
@Test
3334
void testExecuteSuccess() {
3435
var executionOp = Operation.builder()
35-
.id("0")
36+
.id(TestUtils.hashOperationId("0"))
3637
.type(OperationType.EXECUTION)
3738
.status(OperationStatus.STARTED)
3839
.executionDetails(ExecutionDetails.builder()
@@ -62,7 +63,7 @@ void testExecuteSuccess() {
6263
@Test
6364
void testExecutePending() {
6465
var executionOp = Operation.builder()
65-
.id("0")
66+
.id(TestUtils.hashOperationId("0"))
6667
.type(OperationType.EXECUTION)
6768
.status(OperationStatus.STARTED)
6869
.executionDetails(ExecutionDetails.builder()
@@ -95,7 +96,7 @@ void testExecutePending() {
9596
@Test
9697
void testExecuteFailure() {
9798
var executionOp = Operation.builder()
98-
.id("0")
99+
.id(TestUtils.hashOperationId("0"))
99100
.type(OperationType.EXECUTION)
100101
.status(OperationStatus.STARTED)
101102
.executionDetails(ExecutionDetails.builder()
@@ -128,7 +129,7 @@ void testExecuteFailure() {
128129
@Test
129130
void testExecuteReplay() {
130131
var executionOp = Operation.builder()
131-
.id("0")
132+
.id(TestUtils.hashOperationId("0"))
132133
.type(OperationType.EXECUTION)
133134
.status(OperationStatus.STARTED)
134135
.executionDetails(ExecutionDetails.builder()
@@ -137,7 +138,7 @@ void testExecuteReplay() {
137138
.build();
138139

139140
var completedStep = Operation.builder()
140-
.id("1")
141+
.id(TestUtils.hashOperationId("1"))
141142
.name("step1")
142143
.type(OperationType.STEP)
143144
.status(OperationStatus.SUCCEEDED)
@@ -180,7 +181,7 @@ void testValidationNoOperations() {
180181
@Test
181182
void testValidationWrongFirstOperation() {
182183
var stepOp = Operation.builder()
183-
.id("1")
184+
.id(TestUtils.hashOperationId("1"))
184185
.type(OperationType.STEP)
185186
.status(OperationStatus.SUCCEEDED)
186187
.stepDetails(StepDetails.builder().result("\"result\"").build())
@@ -204,7 +205,7 @@ void testValidationWrongFirstOperation() {
204205
@Test
205206
void testValidationMissingExecutionDetails() {
206207
var executionOp = Operation.builder()
207-
.id("0")
208+
.id(TestUtils.hashOperationId("0"))
208209
.type(OperationType.EXECUTION)
209210
.status(OperationStatus.STARTED)
210211
.build();
@@ -234,7 +235,7 @@ void testExecutorNotShutdownAfterMultipleHandlerInvocations() {
234235
assertFalse(sharedExecutor.isShutdown(), "Executor should not be shutdown initially");
235236

236237
var executionOp = Operation.builder()
237-
.id("0")
238+
.id(TestUtils.hashOperationId("0"))
238239
.type(OperationType.EXECUTION)
239240
.status(OperationStatus.STARTED)
240241
.executionDetails(ExecutionDetails.builder()
@@ -262,7 +263,7 @@ void testExecutorNotShutdownAfterMultipleHandlerInvocations() {
262263

263264
// Create second input with different execution operation
264265
var executionOp2 = Operation.builder()
265-
.id("0")
266+
.id(TestUtils.hashOperationId("0"))
266267
.type(OperationType.EXECUTION)
267268
.status(OperationStatus.STARTED)
268269
.executionDetails(ExecutionDetails.builder()

sdk/src/test/java/software/amazon/lambda/durable/ReplayValidationTest.java

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
// SPDX-License-Identifier: Apache-2.0
33
package software.amazon.lambda.durable;
44

5-
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
6-
import static org.junit.jupiter.api.Assertions.assertThrows;
7-
import static org.junit.jupiter.api.Assertions.assertTrue;
8-
95
import java.time.Duration;
106
import java.util.List;
117
import java.util.stream.Stream;
8+
9+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
10+
import static org.junit.jupiter.api.Assertions.assertThrows;
11+
import static org.junit.jupiter.api.Assertions.assertTrue;
1212
import org.junit.jupiter.api.Test;
13+
1314
import software.amazon.awssdk.services.lambda.model.CheckpointUpdatedExecutionState;
1415
import software.amazon.awssdk.services.lambda.model.Operation;
1516
import software.amazon.awssdk.services.lambda.model.OperationStatus;
@@ -23,7 +24,7 @@ class ReplayValidationTest {
2324
private DurableContext createTestContext(List<Operation> initialOperations) {
2425
var client = TestUtils.createMockClient();
2526
var executionOp = Operation.builder()
26-
.id("0")
27+
.id(TestUtils.hashOperationId("0"))
2728
.type(OperationType.EXECUTION)
2829
.status(OperationStatus.STARTED)
2930
.build();
@@ -53,7 +54,7 @@ void shouldPassValidationWhenNoCheckpointExists() {
5354
void shouldPassValidationWhenStepTypeAndNameMatch() {
5455
// Given: Existing STEP operation with matching name
5556
var existingOp = Operation.builder()
56-
.id("1")
57+
.id(TestUtils.hashOperationId("1"))
5758
.name("test")
5859
.type(OperationType.STEP)
5960
.status(OperationStatus.SUCCEEDED)
@@ -70,7 +71,7 @@ void shouldPassValidationWhenStepTypeAndNameMatch() {
7071
void shouldPassValidationWhenWaitTypeMatches() {
7172
// Given: Existing WAIT operation
7273
var existingOp = Operation.builder()
73-
.id("1")
74+
.id(TestUtils.hashOperationId("1"))
7475
.type(OperationType.WAIT)
7576
.status(OperationStatus.SUCCEEDED)
7677
.build();
@@ -85,7 +86,7 @@ void shouldPassValidationWhenWaitTypeMatches() {
8586
void shouldThrowWhenOperationTypeMismatches() {
8687
// Given: Existing WAIT operation but current is STEP
8788
var existingOp = Operation.builder()
88-
.id("1")
89+
.id(TestUtils.hashOperationId("1"))
8990
.name("test")
9091
.type(OperationType.WAIT)
9192
.status(OperationStatus.SUCCEEDED)
@@ -106,7 +107,7 @@ void shouldThrowWhenOperationTypeMismatches() {
106107
void shouldThrowWhenOperationNameMismatches() {
107108
// Given: Existing STEP operation with different name
108109
var existingOp = Operation.builder()
109-
.id("1")
110+
.id(TestUtils.hashOperationId("1"))
110111
.name("original")
111112
.type(OperationType.STEP)
112113
.status(OperationStatus.SUCCEEDED)
@@ -128,7 +129,7 @@ void shouldThrowWhenOperationNameMismatches() {
128129
void shouldHandleNullNamesCorrectly() {
129130
// Given: Existing STEP operation with null name
130131
var existingOp = Operation.builder()
131-
.id("1")
132+
.id(TestUtils.hashOperationId("1"))
132133
.name(null)
133134
.type(OperationType.STEP)
134135
.status(OperationStatus.SUCCEEDED)
@@ -145,7 +146,7 @@ void shouldHandleNullNamesCorrectly() {
145146
void shouldThrowWhenNameChangesFromNullToValue() {
146147
// Given: Existing STEP operation with null name
147148
var existingOp = Operation.builder()
148-
.id("1")
149+
.id(TestUtils.hashOperationId("1"))
149150
.name(null)
150151
.type(OperationType.STEP)
151152
.status(OperationStatus.SUCCEEDED)
@@ -167,7 +168,7 @@ void shouldThrowWhenNameChangesFromNullToValue() {
167168
void shouldThrowWhenNameChangesFromValueToNull() {
168169
// Given: Existing STEP operation with a name
169170
var existingOp = Operation.builder()
170-
.id("1")
171+
.id(TestUtils.hashOperationId("1"))
171172
.name("existingName")
172173
.type(OperationType.STEP)
173174
.status(OperationStatus.SUCCEEDED)
@@ -189,7 +190,7 @@ void shouldThrowWhenNameChangesFromValueToNull() {
189190
void shouldValidateStepAsyncOperations() {
190191
// Given: Existing WAIT operation but current is STEP (async)
191192
var existingOp = Operation.builder()
192-
.id("1")
193+
.id(TestUtils.hashOperationId("1"))
193194
.name("test")
194195
.type(OperationType.WAIT)
195196
.status(OperationStatus.SUCCEEDED)
@@ -211,7 +212,7 @@ void shouldValidateStepAsyncOperations() {
211212
void shouldSkipValidationWhenOperationTypeIsNull() {
212213
// Given: Existing operation with null type (edge case)
213214
var existingOp = Operation.builder()
214-
.id("1")
215+
.id(TestUtils.hashOperationId("1"))
215216
.name("test")
216217
.type((OperationType) null)
217218
.status(OperationStatus.SUCCEEDED)

sdk/src/test/java/software/amazon/lambda/durable/TestUtils.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@
22
// SPDX-License-Identifier: Apache-2.0
33
package software.amazon.lambda.durable;
44

5+
import java.nio.charset.StandardCharsets;
6+
import java.security.MessageDigest;
7+
import java.security.NoSuchAlgorithmException;
8+
59
import static org.mockito.ArgumentMatchers.*;
610
import static org.mockito.Mockito.*;
711

812
import java.util.ArrayList;
13+
import java.util.HexFormat;
914
import java.util.List;
1015
import java.util.UUID;
16+
1117
import software.amazon.awssdk.services.lambda.model.*;
1218
import software.amazon.lambda.durable.client.DurableExecutionClient;
1319

@@ -61,4 +67,14 @@ public static DurableExecutionClient createMockClient() {
6167
});
6268
return client;
6369
}
70+
71+
public static String hashOperationId(String rawId) {
72+
try {
73+
var messageDigest = MessageDigest.getInstance("SHA-256");
74+
var hash = messageDigest.digest(rawId.getBytes(StandardCharsets.UTF_8));
75+
return HexFormat.of().formatHex(hash);
76+
} catch (NoSuchAlgorithmException e) {
77+
throw new AssertionError("SHA-256 not available", e);
78+
}
79+
}
6480
}

0 commit comments

Comments
 (0)