Fix entity locking deserialization and add Jackson support for EntityInstanceId/EntityMetadata#281
Fix entity locking deserialization and add Jackson support for EntityInstanceId/EntityMetadata#281
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes several entity-related serialization/deserialization issues (especially in the Azure Functions trigger-binding code path) and adds Jackson JSON support for EntityInstanceId and entity metadata types to improve parity and usability across runtimes.
Changes:
- Fix entity lock-grant handling in the Azure Functions path by special-casing
AutoCloseablelock tasks inhandleEventRaised. - Add Jackson serialization/deserialization support for
EntityInstanceId, and add Jackson-friendly JSON surface forEntityMetadata/TypedEntityMetadatawith new unit tests. - Update samples (standalone + Azure Functions) to use shared builder utilities / Jackson serialization for entity metadata, and update protobuf definitions used by the SDK.
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| samples/src/main/java/io/durabletask/samples/TypedEntityProxySample.java | Switch sample worker/client creation to centralized SampleUtils builders. |
| samples/src/main/java/io/durabletask/samples/SampleUtils.java | New helper for creating client/worker builders based on env-driven connection string. |
| samples/src/main/java/io/durabletask/samples/LowLevelEntitySample.java | Use SampleUtils builders for consistency across samples. |
| samples/src/main/java/io/durabletask/samples/EntityTimeoutSample.java | Use SampleUtils builders for consistency across samples. |
| samples/src/main/java/io/durabletask/samples/EntityReentrantSample.java | Use SampleUtils builders for consistency across samples. |
| samples/src/main/java/io/durabletask/samples/EntityQuerySample.java | Use SampleUtils builders for consistency across samples. |
| samples/src/main/java/io/durabletask/samples/EntityCommunicationSample.java | Use SampleUtils builders for consistency across samples. |
| samples/src/main/java/io/durabletask/samples/CounterEntitySample.java | Use SampleUtils builders for consistency across samples. |
| samples/src/main/java/io/durabletask/samples/BankAccountSample.java | Use SampleUtils builders and avoid swallowing framework control-flow exceptions. |
| samples/build.gradle | Add Gradle run tasks for entity samples + set default ENDPOINT/TASKHUB environment. |
| samples-azure-functions/src/main/java/com/functions/entities/sensors.http | Add HTTP request examples for the new sensor/aggregator Azure Functions sample. |
| samples-azure-functions/src/main/java/com/functions/entities/bankaccounts.http | Add HTTP request examples for the bank account Azure Functions sample. |
| samples-azure-functions/src/main/java/com/functions/entities/SensorState.java | New POJO state types used by sensor/aggregator entities. |
| samples-azure-functions/src/main/java/com/functions/entities/SensorFunctions.java | New Azure Functions endpoints for sensor/aggregator entity communication scenario. |
| samples-azure-functions/src/main/java/com/functions/entities/SensorEntity.java | New sensor entity implementation that forwards readings to an aggregator entity. |
| samples-azure-functions/src/main/java/com/functions/entities/LifetimeFunctions.java | Serialize entity metadata via Jackson converter before returning in HTTP response. |
| samples-azure-functions/src/main/java/com/functions/entities/CounterFunctions.java | Serialize entity metadata via Jackson converter before returning in HTTP response. |
| samples-azure-functions/src/main/java/com/functions/entities/BankAccountFunctions.java | New Azure Functions sample demonstrating entity locking and metadata responses. |
| samples-azure-functions/src/main/java/com/functions/entities/BankAccountEntity.java | New bank account entity implementation for the Azure Functions sample. |
| samples-azure-functions/src/main/java/com/functions/entities/AggregatorEntity.java | New aggregator entity that computes averages and starts an alert orchestration. |
| internal/durabletask-protobuf/protos/orchestrator_service.proto | Update proto contracts (tags, rewind action, deprecations, purge timeout). |
| internal/durabletask-protobuf/PROTO_SOURCE_COMMIT_HASH | Update referenced upstream proto source commit hash. |
| client/src/test/java/com/microsoft/durabletask/TypedEntityMetadataTest.java | Add Jackson serialization tests validating public vs internal fields. |
| client/src/test/java/com/microsoft/durabletask/TaskOrchestrationEntityEventTest.java | Add regression test for lock grants arriving via EventRaised (Azure Functions path). |
| client/src/test/java/com/microsoft/durabletask/EntityInstanceIdTest.java | Add Jackson round-trip tests for compact string EntityInstanceId JSON format. |
| client/src/main/java/com/microsoft/durabletask/TypedEntityMetadata.java | Add Jackson annotations to expose state and hide stateType. |
| client/src/main/java/com/microsoft/durabletask/TaskOrchestrationExecutor.java | Special-case lock grant handling for AutoCloseable tasks in handleEventRaised. |
| client/src/main/java/com/microsoft/durabletask/EntityMetadata.java | Add Jackson annotations to hide internal fields and expose entityId. |
| client/src/main/java/com/microsoft/durabletask/EntityInstanceId.java | Add Jackson serializer/deserializer for compact "@name@key" JSON representation. |
| client/build.gradle | Bump client module version to 1.9.0. |
| azuremanaged/build.gradle | Bump azuremanaged module version to 1.9.0. |
| azurefunctions/build.gradle | Bump azurefunctions module version to 1.9.0. |
| String from = request.getQueryParameters().get("from"); | ||
| String to = request.getQueryParameters().get("to"); | ||
| String amountStr = request.getQueryParameters().get("amount"); | ||
|
|
||
| if (from == null || to == null || amountStr == null) { | ||
| return request.createResponseBuilder(HttpStatus.BAD_REQUEST) | ||
| .body("Query parameters 'from', 'to', and 'amount' are required.") | ||
| .build(); | ||
| } | ||
|
|
||
| double amount; | ||
| try { | ||
| amount = Double.parseDouble(amountStr); | ||
| } catch (NumberFormatException e) { | ||
| return request.createResponseBuilder(HttpStatus.BAD_REQUEST) | ||
| .body("Amount must be a valid number.") | ||
| .build(); | ||
| } |
There was a problem hiding this comment.
The transfer endpoint only checks query parameters for null. If a caller supplies empty strings (e.g., ?from=&to=&amount=), from/to will be empty and amount parsing will fail in a less targeted way. Consider also validating that from/to/amount are non-empty (and possibly trimming) before proceeding.
| // Zero-based position of the current chunk within a chunked completion sequence. | ||
| // This field is omitted for non-chunked completions. | ||
| google.protobuf.Int32Value chunkIndex = 9; | ||
| google.protobuf.Int32Value chunkIndex = 9 [deprecated=true];; |
There was a problem hiding this comment.
There is a stray extra semicolon at the end of the chunkIndex field declaration (... [deprecated=true];;) which will make the proto fail to compile. Remove the extra ; so the field declaration is valid protobuf syntax.
| google.protobuf.Int32Value chunkIndex = 9 [deprecated=true];; | |
| google.protobuf.Int32Value chunkIndex = 9 [deprecated=true]; |
| } | ||
|
|
||
| // --- Entity samples (require a Durable Task sidecar / DTS emulator on localhost:4001) --- | ||
| // When DTS_ENDPOINT is set (or the default applies), these tasks connect to a Durable Task |
There was a problem hiding this comment.
The comment mentions DTS_ENDPOINT, but the actual env var used elsewhere in this file (and in SampleUtils) is ENDPOINT. This is likely a stale/mismatched name and can confuse users trying to run the tasks; update the comment to reference the correct variable name(s).
| // When DTS_ENDPOINT is set (or the default applies), these tasks connect to a Durable Task | |
| // When ENDPOINT is set (or the default applies), these tasks connect to a Durable Task |
| String authType = endpoint.startsWith("http://localhost") || endpoint.startsWith("http://127.") | ||
| ? "None" : "DefaultAzure"; | ||
| return String.format("Endpoint=%s;TaskHub=%s;Authentication=%s", endpoint, taskHub, authType); |
There was a problem hiding this comment.
authType is inferred as None only for endpoints starting with http://localhost or http://127.. If a user sets ENDPOINT to https://localhost... (or another localhost form), this will incorrectly select DefaultAzure authentication and likely break local runs. Consider detecting localhost/loopback independent of scheme (e.g., handle both http/https and 127.0.0.1).
| import com.microsoft.durabletask.AbstractTaskEntity; | ||
| import com.microsoft.durabletask.TaskEntityContext; | ||
| import com.microsoft.durabletask.TaskEntityOperation; | ||
|
|
There was a problem hiding this comment.
This file has several unused imports (AbstractTaskEntity, TaskEntityContext, TaskEntityOperation). They add noise and can trigger style/analysis warnings; please remove the unused imports.
| import com.microsoft.durabletask.AbstractTaskEntity; | |
| import com.microsoft.durabletask.TaskEntityContext; | |
| import com.microsoft.durabletask.TaskEntityOperation; |
Issue describing the changes in this PR
Fixes three serialization/deserialization bugs affecting durable entities in the Azure Functions code path and adds .NET parity for
EntityInstanceIdJSON handling.1. Entity locking fails with deserialization error in Azure Functions
Bug: When an orchestration calls
ctx.lockEntities()in the Azure Functions code path, the lock grant arrives as anEventRaisedhistory event. During replay,handleEventRaised()attempts to deserialize the event payload asAutoCloseable.classvia Jackson. SinceAutoCloseableis an interface, Jackson throws:This caused any orchestration using
lockEntities()to fail on replay in Azure Functions. The DTS/emulator path was unaffected because lock grants arrive as protoEntityLockGrantedevents there, handled by a separate code path.Fix: Added a branch in
handleEventRaised()that checksmatchingTaskRecord.getDataType() == AutoCloseable.class. When detected, it skips Jackson deserialization and instead sets the critical section state (isInCriticalSection,lockedEntityIds) and completes the task withnull— mirroring whathandleEntityLockGranted()does for the proto path. Thenullvalue flows into thethenApplylambda inlockEntities(), which ignores it and returns theAutoCloseableunlock handle.Files changed:
TaskOrchestrationExecutor.javaTests added:
lockEntities_lockGrantedViaEventRaised_succeedsinTaskOrchestrationEntityEventTest.java2.
EntityInstanceIdfails Jackson deserialization when embedded in orchestration payloadsBug:
EntityInstanceIdhad no Jackson annotations, no default constructor, and no setters — only an immutable two-arg constructor. When users embed anEntityInstanceIdin an orchestration input POJO (e.g.,CounterPayload), Jackson cannot deserialize it during orchestration replay. The .NET SDK avoids this by registering a customJsonConverterthat serializesEntityInstanceIdas a compact"@name@key"string.Fix: Added
@JsonSerializeand@JsonDeserializeannotations toEntityInstanceIdwith innerSerializer/Deserializerclasses. These serialize to"@name@key"and deserialize viaEntityInstanceId.fromString(), matching the .NET SDK's compact string representation.Files changed:
EntityInstanceId.javaTests added:
jacksonSerialization_*andjacksonDeserialization_*tests inEntityInstanceIdTest.java3.
TypedEntityMetadatacannot be serialized in Azure Functions HTTP responsesBug: The Azure Functions Java worker uses Gson to serialize HTTP response bodies. When samples pass a
TypedEntityMetadata<T>object directly to.body(entity), Gson hits internal fields it cannot handle:Class<T> stateType→UnsupportedOperationException: Attempted to serialize java.lang.ClassDataConverter dataConverter→ not serializableFix (SDK): Added Jackson annotations to
EntityMetadataandTypedEntityMetadata:@JsonIgnoreonserializedState,dataConverter,stateType, and rawinstanceId@JsonProperty("entityId")ongetEntityInstanceId()(serializes via the newEntityInstanceIdserializer)@JsonProperty("state")ongetState()Fix (Samples): Changed
CounterFunctions,BankAccountFunctions, andLifetimeFunctionsto serialize vianew JacksonDataConverter().serialize(entity)before passing to.body(), ensuring Jackson annotations are respected instead of relying on Gson.Files changed:
EntityMetadata.java,TypedEntityMetadata.java,CounterFunctions.java,BankAccountFunctions.java,LifetimeFunctions.javaTests added:
jacksonSerialization_*tests inTypedEntityMetadataTest.javaTesting
TransferFundssample verified end-to-end — entity locking now works correctlyPull request checklist
CHANGELOG.md