diff --git a/sdk/src/main/java/software/amazon/lambda/durable/DurableContext.java b/sdk/src/main/java/software/amazon/lambda/durable/DurableContext.java index 33ff6afac..153d30cb5 100644 --- a/sdk/src/main/java/software/amazon/lambda/durable/DurableContext.java +++ b/sdk/src/main/java/software/amazon/lambda/durable/DurableContext.java @@ -3,19 +3,15 @@ package software.amazon.lambda.durable; import com.amazonaws.services.lambda.runtime.Context; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.time.Duration; -import java.util.HexFormat; import java.util.Objects; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Supplier; import org.slf4j.LoggerFactory; import software.amazon.awssdk.services.lambda.model.OperationType; import software.amazon.lambda.durable.execution.ExecutionManager; +import software.amazon.lambda.durable.execution.OperationIdGenerator; import software.amazon.lambda.durable.execution.ThreadType; import software.amazon.lambda.durable.logging.DurableLogger; import software.amazon.lambda.durable.model.OperationIdentifier; @@ -32,7 +28,7 @@ public class DurableContext extends BaseContext { private static final String WAIT_FOR_CALLBACK_SUBMITTER_SUFFIX = "-submitter"; private static final int MAX_WAIT_FOR_CALLBACK_NAME_LENGTH = ParameterValidator.MAX_OPERATION_NAME_LENGTH - Math.max(WAIT_FOR_CALLBACK_CALLBACK_SUFFIX.length(), WAIT_FOR_CALLBACK_SUBMITTER_SUFFIX.length()); - private final AtomicInteger operationCounter; + private final OperationIdGenerator operationIdGenerator; private volatile DurableLogger logger; /** Shared initialization — sets all fields. */ @@ -43,7 +39,7 @@ private DurableContext( String contextId, String contextName) { super(executionManager, durableConfig, lambdaContext, contextId, contextName, ThreadType.CONTEXT); - this.operationCounter = new AtomicInteger(0); + operationIdGenerator = new OperationIdGenerator(contextId); } /** @@ -490,14 +486,6 @@ public void close() { * matches the Python SDK's stepPrefix convention and prevents ID collisions in checkpoint batches. */ private String nextOperationId() { - var counter = String.valueOf(operationCounter.incrementAndGet()); - var rawId = getContextId() != null ? getContextId() + "-" + counter : counter; - try { - var messageDigest = MessageDigest.getInstance("SHA-256"); - var hash = messageDigest.digest(rawId.getBytes(StandardCharsets.UTF_8)); - return HexFormat.of().formatHex(hash); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("failed to get next operation id, SHA-256 not available", e); - } + return operationIdGenerator.nextOperationId(); } } diff --git a/sdk/src/main/java/software/amazon/lambda/durable/execution/OperationIdGenerator.java b/sdk/src/main/java/software/amazon/lambda/durable/execution/OperationIdGenerator.java new file mode 100644 index 000000000..138c50cd7 --- /dev/null +++ b/sdk/src/main/java/software/amazon/lambda/durable/execution/OperationIdGenerator.java @@ -0,0 +1,38 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.lambda.durable.execution; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.concurrent.atomic.AtomicInteger; + +/** Generates operation IDs for the durable operations. */ +public class OperationIdGenerator { + private final AtomicInteger operationCounter; + private final String contextId; + + public OperationIdGenerator(String contextId) { + this.operationCounter = new AtomicInteger(0); + this.contextId = contextId; + } + + /** + * Get the next operationId. Returns a globally unique operation ID by hashing a sequential operation counter. For + * root contexts, the counter value is hashed directly (e.g. "1", "2", "3"). For child contexts, the values are + * prefixed with the parent hashed contextId (e.g. "-1", "-2" inside parent context ). This + * matches the Python SDK's stepPrefix convention and prevents ID collisions in checkpoint batches. + */ + public String nextOperationId() { + var counter = String.valueOf(operationCounter.incrementAndGet()); + var rawId = contextId != null ? contextId + "-" + counter : counter; + try { + var messageDigest = MessageDigest.getInstance("SHA-256"); + var hash = messageDigest.digest(rawId.getBytes(StandardCharsets.UTF_8)); + return HexFormat.of().formatHex(hash); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("failed to get next operation id, SHA-256 not available", e); + } + } +}