Skip to content

Fix entity locking deserialization and add Jackson support for EntityInstanceId/EntityMetadata#281

Open
bachuv wants to merge 1 commit intomainfrom
vabachu/entity-lock-serialization
Open

Fix entity locking deserialization and add Jackson support for EntityInstanceId/EntityMetadata#281
bachuv wants to merge 1 commit intomainfrom
vabachu/entity-lock-serialization

Conversation

@bachuv
Copy link
Copy Markdown
Contributor

@bachuv bachuv commented Apr 20, 2026

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 EntityInstanceId JSON 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 an EventRaised history event. During replay, handleEventRaised() attempts to deserialize the event payload as AutoCloseable.class via Jackson. Since AutoCloseable is an interface, Jackson throws:

Cannot construct instance of `java.lang.AutoCloseable` (no Creators, like default constructor, exist): 
abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information

This caused any orchestration using lockEntities() to fail on replay in Azure Functions. The DTS/emulator path was unaffected because lock grants arrive as proto EntityLockGranted events there, handled by a separate code path.

Fix: Added a branch in handleEventRaised() that checks matchingTaskRecord.getDataType() == AutoCloseable.class. When detected, it skips Jackson deserialization and instead sets the critical section state (isInCriticalSection, lockedEntityIds) and completes the task with null — mirroring what handleEntityLockGranted() does for the proto path. The null value flows into the thenApply lambda in lockEntities(), which ignores it and returns the AutoCloseable unlock handle.

Files changed: TaskOrchestrationExecutor.java
Tests added: lockEntities_lockGrantedViaEventRaised_succeeds in TaskOrchestrationEntityEventTest.java


2. EntityInstanceId fails Jackson deserialization when embedded in orchestration payloads

Bug: EntityInstanceId had no Jackson annotations, no default constructor, and no setters — only an immutable two-arg constructor. When users embed an EntityInstanceId in an orchestration input POJO (e.g., CounterPayload), Jackson cannot deserialize it during orchestration replay. The .NET SDK avoids this by registering a custom JsonConverter that serializes EntityInstanceId as a compact "@name@key" string.

Fix: Added @JsonSerialize and @JsonDeserialize annotations to EntityInstanceId with inner Serializer/Deserializer classes. These serialize to "@name@key" and deserialize via EntityInstanceId.fromString(), matching the .NET SDK's compact string representation.

Files changed: EntityInstanceId.java
Tests added: jacksonSerialization_* and jacksonDeserialization_* tests in EntityInstanceIdTest.java


3. TypedEntityMetadata cannot be serialized in Azure Functions HTTP responses

Bug: 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> stateTypeUnsupportedOperationException: Attempted to serialize java.lang.Class
  • DataConverter dataConverter → not serializable

Fix (SDK): Added Jackson annotations to EntityMetadata and TypedEntityMetadata:

  • @JsonIgnore on serializedState, dataConverter, stateType, and raw instanceId
  • @JsonProperty("entityId") on getEntityInstanceId() (serializes via the new EntityInstanceId serializer)
  • @JsonProperty("state") on getState()

Fix (Samples): Changed CounterFunctions, BankAccountFunctions, and LifetimeFunctions to serialize via new 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.java
Tests added: jacksonSerialization_* tests in TypedEntityMetadataTest.java

Testing

  • All existing unit tests pass (no breaking changes)
  • All entity integration tests pass against DTS emulator (11 tests)
  • Azure Functions TransferFunds sample verified end-to-end — entity locking now works correctly

Pull request checklist

  • My changes do not require documentation changes
    • Otherwise: Documentation issue linked to PR
  • My changes are added to the CHANGELOG.md
  • I have added all required tests (Unit tests, E2E tests)

Copilot AI review requested due to automatic review settings April 20, 2026 20:17
@bachuv bachuv requested a review from a team as a code owner April 20, 2026 20:17
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 AutoCloseable lock tasks in handleEventRaised.
  • Add Jackson serialization/deserialization support for EntityInstanceId, and add Jackson-friendly JSON surface for EntityMetadata / TypedEntityMetadata with 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.

Comment on lines +169 to +186
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();
}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
// 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];;
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
google.protobuf.Int32Value chunkIndex = 9 [deprecated=true];;
google.protobuf.Int32Value chunkIndex = 9 [deprecated=true];

Copilot uses AI. Check for mistakes.
Comment thread samples/build.gradle
}

// --- 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
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
// 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

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +54
String authType = endpoint.startsWith("http://localhost") || endpoint.startsWith("http://127.")
? "None" : "DefaultAzure";
return String.format("Endpoint=%s;TaskHub=%s;Authentication=%s", endpoint, taskHub, authType);
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +8
import com.microsoft.durabletask.AbstractTaskEntity;
import com.microsoft.durabletask.TaskEntityContext;
import com.microsoft.durabletask.TaskEntityOperation;

Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file has several unused imports (AbstractTaskEntity, TaskEntityContext, TaskEntityOperation). They add noise and can trigger style/analysis warnings; please remove the unused imports.

Suggested change
import com.microsoft.durabletask.AbstractTaskEntity;
import com.microsoft.durabletask.TaskEntityContext;
import com.microsoft.durabletask.TaskEntityOperation;

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants