Conversation
…ovements, and ambiguous method detection
…desState for .NET parity
client/src/test/java/com/microsoft/durabletask/EntityQueryPageableTest.java
Fixed
Show fixed
Hide fixed
client/src/test/java/com/microsoft/durabletask/EntityQueryPageableTest.java
Fixed
Show fixed
Hide fixed
client/src/main/java/com/microsoft/durabletask/TaskOrchestrationContext.java
Fixed
Show fixed
Hide fixed
There was a problem hiding this comment.
Pull request overview
Adds Durable Entities support to the Java SDK, including worker-side entity execution, orchestration↔entity APIs, client management/query APIs, Azure Functions integration, and samples/tests demonstrating the feature set.
Changes:
- Add core entity programming model APIs (entities, entity context/state/operation abstractions, entity client, options, query/paging, storage cleanup).
- Integrate entity messaging + locking into orchestration execution and worker-side work-item processing.
- Add Azure Functions entity trigger + middleware, plus multiple new samples and extensive unit/integration tests.
Reviewed changes
Copilot reviewed 56 out of 56 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| samples/src/main/java/io/durabletask/samples/LowLevelEntitySample.java | Sample: low-level entity + state dispatch + implicit delete |
| samples/src/main/java/io/durabletask/samples/EntityTimeoutSample.java | Sample: callEntity timeouts + scheduled signals |
| samples/src/main/java/io/durabletask/samples/EntityQuerySample.java | Sample: entity query + storage cleanup |
| samples/src/main/java/io/durabletask/samples/EntityCommunicationSample.java | Sample: entity→entity signaling + entity-started orchestrations |
| samples/src/main/java/io/durabletask/samples/CounterEntitySample.java | Sample: basic counter entity usage |
| samples/src/main/java/io/durabletask/samples/BankAccountSample.java | Sample: entity locking + atomic transfer orchestration |
| internal/durabletask-protobuf/protos/orchestrator_service.proto | Proto updates for worker request filtering |
| internal/durabletask-protobuf/PROTO_SOURCE_COMMIT_HASH | Tracks upstream proto sync commit |
| client/src/test/java/com/microsoft/durabletask/TypedEntityMetadataTest.java | Tests for typed entity metadata wrapper |
| client/src/test/java/com/microsoft/durabletask/TaskEntityTest.java | Tests for reflection dispatch, implicit delete, state dispatch behavior |
| client/src/test/java/com/microsoft/durabletask/TaskEntityExecutorTest.java | Tests for batch execution semantics, rollback, actions |
| client/src/test/java/com/microsoft/durabletask/IntegrationTestBase.java | Test worker builder support for entity registration |
| client/src/test/java/com/microsoft/durabletask/EntityRegistrationTest.java | Tests for worker builder addEntity overloads |
| client/src/test/java/com/microsoft/durabletask/EntityQueryTest.java | Tests for query normalization/defaults/chaining |
| client/src/test/java/com/microsoft/durabletask/EntityQueryResultTest.java | Tests for query result container |
| client/src/test/java/com/microsoft/durabletask/EntityQueryPageableTest.java | Tests for auto-pagination behavior |
| client/src/test/java/com/microsoft/durabletask/EntityOptionsTest.java | Tests for signal/call entity options |
| client/src/test/java/com/microsoft/durabletask/EntityMetadataTest.java | Tests for entity metadata parsing/state deserialization |
| client/src/test/java/com/microsoft/durabletask/EntityIntegrationTests.java | Sidecar-backed integration tests for entities |
| client/src/test/java/com/microsoft/durabletask/EntityInstanceIdTest.java | Tests for entity ID parsing/format/ordering |
| client/src/test/java/com/microsoft/durabletask/CleanEntityStorageResultTest.java | Tests for cleanup result model |
| client/src/test/java/com/microsoft/durabletask/CleanEntityStorageRequestTest.java | Tests for cleanup request model |
| client/src/main/java/com/microsoft/durabletask/TypedEntityMetadata.java | Typed state wrapper for entity metadata |
| client/src/main/java/com/microsoft/durabletask/TaskOrchestrationExecutor.java | Orchestrator integration: entity signaling/calling/locking + event handling |
| client/src/main/java/com/microsoft/durabletask/TaskOrchestrationContext.java | Public orchestration APIs for entity operations + locking |
| client/src/main/java/com/microsoft/durabletask/TaskEntityState.java | Entity state wrapper with commit/rollback semantics |
| client/src/main/java/com/microsoft/durabletask/TaskEntityOperation.java | Entity operation input/state/context container |
| client/src/main/java/com/microsoft/durabletask/TaskEntityFactory.java | Functional factory for entity instantiation |
| client/src/main/java/com/microsoft/durabletask/TaskEntityExecutor.java | Worker-side executor for entity batches + transactional semantics |
| client/src/main/java/com/microsoft/durabletask/TaskEntityContext.java | Entity context for signaling/starting orchestrations |
| client/src/main/java/com/microsoft/durabletask/TaskEntity.java | Reflection-based entity dispatch base class |
| client/src/main/java/com/microsoft/durabletask/SignalEntityOptions.java | Options for scheduled entity signals |
| client/src/main/java/com/microsoft/durabletask/ITaskEntity.java | Entity interface contract |
| client/src/main/java/com/microsoft/durabletask/GrpcDurableEntityClient.java | gRPC implementation of DurableEntityClient management APIs |
| client/src/main/java/com/microsoft/durabletask/EntityRunner.java | Helper for executing entity batches from encoded protobuf payloads |
| client/src/main/java/com/microsoft/durabletask/EntityQueryResult.java | Entity query result model |
| client/src/main/java/com/microsoft/durabletask/EntityQueryPageable.java | Auto-pagination support for entity queries |
| client/src/main/java/com/microsoft/durabletask/EntityQuery.java | Entity query model + prefix normalization |
| client/src/main/java/com/microsoft/durabletask/EntityOperationFailedException.java | Exception type for failed two-way entity calls |
| client/src/main/java/com/microsoft/durabletask/EntityMetadata.java | Entity metadata model + state deserialization |
| client/src/main/java/com/microsoft/durabletask/EntityInstanceId.java | Entity instance identifier type and parsing |
| client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcWorkerBuilder.java | Worker builder support for registering entities + concurrency option |
| client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcWorker.java | Worker runtime support for entity work items |
| client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java | Expose entity client from gRPC client |
| client/src/main/java/com/microsoft/durabletask/DurableTaskClient.java | Add entity API surface + deprecated forwarding helpers |
| client/src/main/java/com/microsoft/durabletask/DurableEntityClient.java | Durable entity management client abstraction |
| client/src/main/java/com/microsoft/durabletask/CleanEntityStorageResult.java | Model for cleanup results |
| client/src/main/java/com/microsoft/durabletask/CleanEntityStorageRequest.java | Model for cleanup requests |
| client/src/main/java/com/microsoft/durabletask/CallEntityOptions.java | Options for callEntity timeouts |
| client/build.gradle | Windows-friendly java/javac path handling for tests |
| azuremanaged/build.gradle | Windows-friendly javac path handling for tests |
| azurefunctions/src/main/resources/META-INF/services/com.microsoft.azure.functions.internal.spi.middleware.Middleware | Register entity middleware |
| azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/internal/middleware/EntityMiddleware.java | Azure Functions middleware for entity triggers |
| azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/DurableEntityTrigger.java | Azure Functions entity trigger annotation |
| azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/DurableClientContext.java | Entity management APIs surfaced via DurableClientContext |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
…letask-java into vabachu/entities
client/src/test/java/com/microsoft/durabletask/TaskOrchestrationEntityEventTest.java
Fixed
Show fixed
Hide fixed
client/src/main/java/com/microsoft/durabletask/TaskOrchestrationContext.java
Fixed
Show fixed
Hide fixed
client/src/test/java/com/microsoft/durabletask/TaskEntityTest.java
Dismissed
Show dismissed
Hide dismissed
client/src/main/java/com/microsoft/durabletask/TaskOrchestrationContext.java
Fixed
Show fixed
Hide fixed
client/src/test/java/com/microsoft/durabletask/TaskOrchestrationEntityEventTest.java
Fixed
Show fixed
Hide fixed
client/src/main/java/com/microsoft/durabletask/TaskOrchestrationContext.java
Fixed
Show fixed
Hide fixed
client/src/main/java/com/microsoft/durabletask/TaskOrchestrationEntityFeature.java
Fixed
Show fixed
Hide fixed
client/src/main/java/com/microsoft/durabletask/TaskOrchestrationContext.java
Fixed
Show fixed
Hide fixed
YunchuWang
left a comment
There was a problem hiding this comment.
Code Review: Durable Entities for Java SDK
Thorough review of the 78-file PR adding Durable Entities support. The overall design is solid and well-documented, with good alignment to the .NET SDK patterns. However, I found several issues ranging from a Java 8 compatibility breakage to unused concurrency controls and excessive logging. Details in inline comments below.
Summary of Findings
Critical (must fix):
Class.getPackageName()breaks Java 8 compatibility (project targets Java 8)maxConcurrentEntityWorkItemsis accepted but never enforced- Activity execution silently changed from synchronous to unbounded-concurrent
Medium:
4. EntityMetadata.includesState incorrectly derived from proto field presence
5. EntityQueryPageable.cloneQuery() duplicates EntityQuery.copy() and double-normalizes
6. TaskEntityOperation constructor should be package-private
Low / Code Quality:
7. INFO-level logging on orchestration replay hot path will flood production logs
8. Inconsistent indentation in ENTITYREQUESTV2 handler and findMethodUncached
9. Field declared after methods that reference it in CleanEntityStorageRequest
10. Missing newline at end of several files
client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcWorker.java
Outdated
Show resolved
Hide resolved
client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcWorker.java
Outdated
Show resolved
Hide resolved
client/src/main/java/com/microsoft/durabletask/GrpcDurableEntityClient.java
Show resolved
Hide resolved
client/src/main/java/com/microsoft/durabletask/EntityQueryPageable.java
Outdated
Show resolved
Hide resolved
client/src/main/java/com/microsoft/durabletask/TaskEntityOperation.java
Outdated
Show resolved
Hide resolved
client/src/main/java/com/microsoft/durabletask/TaskOrchestrationExecutor.java
Outdated
Show resolved
Hide resolved
client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcWorker.java
Show resolved
Hide resolved
client/src/main/java/com/microsoft/durabletask/CleanEntityStorageRequest.java
Outdated
Show resolved
Hide resolved
YunchuWang
left a comment
There was a problem hiding this comment.
🔍 Strict Code Review — Durable Entities (Java SDK)
This is a thorough review of the 12,394-line, 78-file Durable Entities feature addition. The overall architecture is solid and well-aligned with the .NET SDK design. However, there are several correctness, safety, and API-design issues that should be addressed before merge.
I've organized findings into tiers — please prioritize the Critical/High items.
🔴 Critical / High Priority
1. Lock release close() is not idempotent — can cause non-deterministic replay (TaskOrchestrationExecutor.java)
The AutoCloseable returned from lockEntities() sends unlock messages and increments sequenceNumber every time close() is called. If a user calls close() twice (very common with try-with-resources + explicit close, or exception handling double-close), this will: (a) send duplicate unlock messages corrupting entity state, and (b) increment sequenceNumber again, causing non-deterministic replay failures on subsequent executions. Fix: Add a boolean guard to make close() idempotent.
2. Critical section enforcement bypassed when lockedEntityIds is null (TaskOrchestrationExecutor.java)
In callEntity():
if (this.isInCriticalSection && this.lockedEntityIds != null && !this.lockedEntityIds.contains(...))handleEntityLockGranted() sets this.lockedEntityIds = this.pendingLockSets.remove(criticalSectionId), which returns null if the key is missing. Combined with isInCriticalSection = true, the null short-circuits the safety check, allowing calls to any entity — defeating deadlock prevention. Fix: Throw if pendingLockSets.remove() returns null.
3. Activity completion failures silently lost (DurableTaskGrpcWorker.java)
Activities are now dispatched to workItemExecutor, but sidecarClient.completeActivityTask() inside the Runnable has no outer try-catch for the gRPC call itself. If the gRPC call fails (network issue, sidecar crash), the exception hits only the thread pool's uncaught exception handler — the activity result is permanently lost with no retry or abandon. Entity work items have an abandon fallback, but activities do not.
4. setState() silently deletes entity state on serialization failure (TaskEntityState.java)
this.serializedState = this.dataConverter.serialize(state);
if (this.serializedState == null) {
deleteState(); // <-- silent data loss!
}Since the outer if already guarantees state != null, serialize() returning null means a broken DataConverter. Silently calling deleteState() is a data-loss bug. Should throw IllegalStateException.
5. Unbounded thread pool (DurableTaskGrpcWorker.java)
Executors.newCachedThreadPool() has no upper bound. Despite maxConcurrentEntityWorkItems being sent to the sidecar, the local thread pool is unbounded. Activities AND entities both use this pool. A burst of work items can create unbounded threads. Use a bounded ThreadPoolExecutor or separate pools.
6. Activity execution model silently changed from sync to async (DurableTaskGrpcWorker.java)
Activities were previously executed synchronously on the main dispatch thread. Moving them to the thread pool is a silent behavioral breaking change: (a) activities using ThreadLocal state will break, (b) activities relying on sequential execution relative to dispatch will break, (c) gRPC context propagation may be lost. This deserves a CHANGELOG entry.
7. gRPC exceptions not wrapped in GrpcDurableEntityClient.java
Existing DurableTaskGrpcClient methods catch StatusRuntimeException and translate to domain exceptions. All new entity gRPC calls let raw gRPC exceptions escape. This breaks the DurableEntityClient abstraction — callers shouldn't need to catch gRPC types.
8. Excessive INFO-level logging will flood production logs (TaskOrchestrationExecutor.java)
Multiple log statements changed from FINE to INFO: every orchestrator yield, every entity call/response, every new event processed, every buffered event. In production with hundreds of orchestrations, this will flood logs. Most should remain at FINE/DEBUG.
🟡 Medium Priority
9. getCommittedActions() iterates ALL pending actions, not committed count (TaskEntityExecutor.java)
The method iterates all pendingActions rather than bounding by committedActionCount. It relies on a fragile invariant that both are equal at call time. If that invariant is ever violated (e.g., future refactoring), uncommitted actions leak into the result. Bound the loop by committedActionCount.
10. resultNode.asText() loses data for non-string JSON nodes (TaskOrchestrationExecutor.java)
JsonNode.asText() returns "" for object/array nodes. If the DTFx ResponseMessage "result" field ever contains a raw JSON object, the result is silently lost. Use resultNode.isTextual() ? resultNode.asText() : resultNode.toString().
11. Legacy callEntity path omits "signal" field
Legacy signalEntity JSON explicitly sets "signal": true, but callEntity does not set "signal": false. If DTFx defaults absence to true, all calls would be misrouted as fire-and-forget signals.
12. EntityQueryPageable — hasNext() triggers network calls (side-effect violation)
Both the constructor and hasNext() eagerly call fetchNextPage(), which executes gRPC queries. The Iterator.hasNext() contract should be a pure observation. Defer first page fetch to first hasNext() call.
13. TypedEntityMetadata eagerly deserializes — single bad entity poisons entire query
If readStateAs() throws for one entity, TypedEntityMetadata construction fails, halting iteration over the entire result set. No way to skip corrupt entities.
14. Javadoc says "excluding state" but behavior includes state (DurableTaskClient.java)
The deprecated getEntityMetadata(entityId) doc says "excluding its state" but delegates to getEntities().getEntityMetadata(entityId) which defaults to includeState = true.
15. EntityRunner silently swallows malformed instance IDs
try {
entityName = EntityInstanceId.fromString(instanceId).getName();
} catch (Exception e) {
entityName = instanceId; // Silent fallback
}This masks the real problem and produces a confusing "No entity named '...' is registered" error later.
16. EntityProxy.getRawClass() doesn't handle WildcardType
Task<? extends Foo> silently falls back to Object.class, losing the upper-bound type.
17. EntityQueryResult.entities list not defensively copied
The constructor stores the list reference directly. Callers can mutate via getEntities().add(...). Use Collections.unmodifiableList.
18. Missing interrupt flag restoration in close() (DurableTaskGrpcWorker.java)
When InterruptedException is caught, Thread.currentThread().interrupt() is not called, violating Java's interrupt contract.
19. operationInfos/results size mismatch silently truncated (DurableTaskGrpcWorker.java, V2 entity path)
If sizes differ, excess infos are quietly dropped or results have no routing info. Neither case is logged. Can cause silent data loss or mis-routed responses.
20. Lazy cachedEntityInstanceId without volatile (EntityMetadata.java)
The field is lazily initialized in getEntityInstanceId(). Without volatile, it's not safe for cross-thread visibility under the Java Memory Model.
🟠 Low Priority / Design
21. ITaskEntity.runAsync() is synchronous — The method returns Object, not CompletableFuture. The Async suffix is misleading per universal Java convention. Rename to run().
22. ITaskEntity uses I prefix — Inconsistent with existing SDK naming (DataConverter, TaskOrchestrationContext).
23. TaskOrchestrationEntityFeature.lockEntities(EntityInstanceId...) is needlessly abstract — Should be a concrete convenience method delegating to the List overload.
24. getEntities() allocates a new object on every call — The .NET SDK caches this. Cache or document.
25. currentCriticalSectionId is set but never read — Dead state. Remove or use.
26. lockEntities doesn't validate duplicate entity IDs or null elements — Same ID twice = double-lock. Null element = cryptic NPE from Collections.sort.
27. No timeout mechanism for lockEntities — Unlike callEntity with CallEntityOptions.getTimeout(), locks can block forever.
28. Worker threads all share the same name "durabletask-worker" — Thread dumps become useless. Use a counter.
29. EntityQuery.setInstanceIdStartsWith("@") produces degenerate prefix — Matches ALL entities.
30. EntityQueryPageable.cloneQuery() duplicates EntityQuery.copy() logic — Maintenance hazard if a field is added.
31. TaskEntityOperation and TaskEntityState constructors are public — These are internal types that users should never construct. Make package-private.
32. cleanEntityStorage in GrpcDurableEntityClient has no max-iteration guard — If the sidecar has a bug returning non-null tokens forever, this is an unbounded blocking call.
PR Checklist
⚠️ CHANGELOG.md is NOT updated (PR checklist item unchecked)⚠️ Merge state is "dirty" — rebase needed- ✅ Extensive unit tests added
- ✅ E2E tests added
Questions for @bachuv
-
Re: #1 (lock close): Is there a reason the lock release wasn't guarded with a boolean flag?
try (AutoCloseable lock = ...)makes double-close very likely. -
Re: #6 (activity model change): Was the sync→async activity execution change intentional? This is a behavioral breaking change that should at minimum be in the CHANGELOG.
-
Re: #8 (logging): Were the
FINE → INFOlogging changes intentional for debugging, or should they be reverted before merge? -
Re: #11 (missing "signal": false): Can you confirm DTFx extension behavior when the
"signal"field is absent from the RequestMessage JSON? -
Re: EVENTSENT uncommented:
handleEventSent()now removes pending actions for ALLEventSentevents, not just entity-related ones. Has this been regression-tested against existing orchestrations usingsendEvent? -
Re: #27 (lock timeout): Is lock timeout deferred to a follow-up? The .NET SDK has this capability.
client/src/main/java/com/microsoft/durabletask/TaskOrchestrationExecutor.java
Show resolved
Hide resolved
client/src/main/java/com/microsoft/durabletask/TaskOrchestrationExecutor.java
Show resolved
Hide resolved
client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcWorker.java
Outdated
Show resolved
Hide resolved
client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcWorker.java
Outdated
Show resolved
Hide resolved
|
First set of feedback: Critical (must fix):
Medium: Low / Code Quality: Second set of feedback: 🔴 Critical / High Priority
🟡 Medium Priority 🟠 Low Priority / Design Questions Re: #1 (lock close): This was an oversight. The boolean guard has now been added. Double-close is idempotent: first call emits unlocks, subsequent calls are no-ops. Re: #6 (activity model change): This has been reverted so activities are back to synchronous execution on the dispatch thread, identical to main. No behavioral change, no CHANGELOG entry needed. The thread pool is now used exclusively for entity work items. Re: #8 (logging): The FINE → INFO changes were not intentional for production use. They were debug-time changes that should have been reverted. All hot-path logs have been restored to FINE. Only entity operation failures remain at INFO since those are genuinely noteworthy. Re: #11 (missing "signal": false): DTFx RequestMessage.IsSignal is a C# bool property with [DataMember(Name = "signal", EmitDefaultValue = false)]. It defaults to false, and EmitDefaultValue = false means the .NET serializer omits the field when the value is false. So absent and false are functionally identical. DTFx treats both as "this is a call, not a signal." We now explicitly set "signal": false for clarity. Re: EVENTSENT uncommented: Could you point to the specific lines? I want to verify the exact change before answering. The handleEventSent handler should only process entity-related events. If it's broader than that, it needs to be scoped. Re: #27 (lock timeout): Yes, deferred. The .NET SDK's LockEntitiesAsync does not have a timeout parameter. It returns Task with no timeout overload. This would be a new feature for both SDKs, not a parity gap. |
Issue describing the changes in this PR
Adding Durable Entities support for the Java SDK.
resolves #31
Pull request checklist
CHANGELOG.md