Skip to content

Commit 5a5bb01

Browse files
committed
align .NET parity for cleanup defaults and orchestration entity accessors
1 parent 3cd0c07 commit 5a5bb01

File tree

6 files changed

+289
-7
lines changed

6 files changed

+289
-7
lines changed

client/src/main/java/com/microsoft/durabletask/CleanEntityStorageRequest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212
*/
1313
public final class CleanEntityStorageRequest {
1414
private String continuationToken;
15-
private boolean removeEmptyEntities;
16-
private boolean releaseOrphanedLocks;
15+
private boolean removeEmptyEntities = true;
16+
private boolean releaseOrphanedLocks = true;
1717

1818
/**
1919
* Creates a new {@code CleanEntityStorageRequest} with default settings.
20-
* By default, both {@code removeEmptyEntities} and {@code releaseOrphanedLocks} are {@code false}.
20+
* By default, both {@code removeEmptyEntities} and {@code releaseOrphanedLocks} are {@code true}.
2121
*/
2222
public CleanEntityStorageRequest() {
2323
}

client/src/main/java/com/microsoft/durabletask/TaskOrchestrationContext.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,29 @@ default <V> Task<V> waitForExternalEvent(String name, Class<V> dataType) {
559559
}
560560
}
561561

562+
/**
563+
* Gets the durable entity feature for this orchestration context.
564+
* <p>
565+
* This mirrors the .NET SDK's {@code TaskOrchestrationContext.Entities} surface,
566+
* adapted to Java as a method-based accessor.
567+
*
568+
* @return the entity feature for this orchestration context
569+
*/
570+
default TaskOrchestrationEntityFeature getEntities() {
571+
return new ContextBackedTaskOrchestrationEntityFeature(this);
572+
}
573+
574+
/**
575+
* Gets the durable entity feature for this orchestration context.
576+
* <p>
577+
* This is an alias of {@link #getEntities()}.
578+
*
579+
* @return the entity feature for this orchestration context
580+
*/
581+
default TaskOrchestrationEntityFeature entities() {
582+
return this.getEntities();
583+
}
584+
562585
// region Entity integration methods
563586

564587
/**
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
package com.microsoft.durabletask;
4+
5+
import javax.annotation.Nonnull;
6+
import javax.annotation.Nullable;
7+
import java.util.List;
8+
9+
/**
10+
* Feature for interacting with durable entities from an orchestration.
11+
* <p>
12+
* This mirrors the .NET SDK's {@code TaskOrchestrationContext.Entities} shape, adapted to Java.
13+
*/
14+
public abstract class TaskOrchestrationEntityFeature {
15+
16+
/**
17+
* Calls an operation on an entity and waits for it to complete.
18+
*
19+
* @param entityId the target entity
20+
* @param operationName the name of the operation
21+
* @param input the operation input, or {@code null}
22+
* @param returnType the expected return type
23+
* @param <TResult> the result type
24+
* @return a task that completes with the operation result
25+
*/
26+
public abstract <TResult> Task<TResult> callEntity(
27+
@Nonnull EntityInstanceId entityId,
28+
@Nonnull String operationName,
29+
@Nullable Object input,
30+
@Nonnull Class<TResult> returnType);
31+
32+
/**
33+
* Calls an operation on an entity and waits for it to complete, with options.
34+
*
35+
* @param entityId the target entity
36+
* @param operationName the name of the operation
37+
* @param input the operation input, or {@code null}
38+
* @param returnType the expected return type
39+
* @param options the call options, or {@code null}
40+
* @param <TResult> the result type
41+
* @return a task that completes with the operation result
42+
*/
43+
public abstract <TResult> Task<TResult> callEntity(
44+
@Nonnull EntityInstanceId entityId,
45+
@Nonnull String operationName,
46+
@Nullable Object input,
47+
@Nonnull Class<TResult> returnType,
48+
@Nullable CallEntityOptions options);
49+
50+
/**
51+
* Calls an operation on an entity and waits for it to complete.
52+
*
53+
* @param entityId the target entity
54+
* @param operationName the name of the operation
55+
* @param <TResult> the result type
56+
* @return a task that completes with the operation result
57+
*/
58+
public <TResult> Task<TResult> callEntity(
59+
@Nonnull EntityInstanceId entityId,
60+
@Nonnull String operationName,
61+
@Nonnull Class<TResult> returnType) {
62+
return this.callEntity(entityId, operationName, null, returnType, null);
63+
}
64+
65+
/**
66+
* Signals an entity operation without waiting for completion.
67+
*
68+
* @param entityId the target entity
69+
* @param operationName the operation name
70+
* @param input the operation input, or {@code null}
71+
* @param options signal options, or {@code null}
72+
*/
73+
public abstract void signalEntity(
74+
@Nonnull EntityInstanceId entityId,
75+
@Nonnull String operationName,
76+
@Nullable Object input,
77+
@Nullable SignalEntityOptions options);
78+
79+
/**
80+
* Signals an entity operation without waiting for completion.
81+
*
82+
* @param entityId the target entity
83+
* @param operationName the operation name
84+
*/
85+
public void signalEntity(@Nonnull EntityInstanceId entityId, @Nonnull String operationName) {
86+
this.signalEntity(entityId, operationName, null, null);
87+
}
88+
89+
/**
90+
* Signals an entity operation without waiting for completion.
91+
*
92+
* @param entityId the target entity
93+
* @param operationName the operation name
94+
* @param input the operation input, or {@code null}
95+
*/
96+
public void signalEntity(
97+
@Nonnull EntityInstanceId entityId,
98+
@Nonnull String operationName,
99+
@Nullable Object input) {
100+
this.signalEntity(entityId, operationName, input, null);
101+
}
102+
103+
/**
104+
* Acquires one or more entity locks.
105+
*
106+
* @param entityIds the entity IDs to lock
107+
* @return a task that completes with an {@link AutoCloseable} used to release locks
108+
*/
109+
public abstract Task<AutoCloseable> lockEntities(@Nonnull List<EntityInstanceId> entityIds);
110+
111+
/**
112+
* Acquires one or more entity locks.
113+
*
114+
* @param entityIds the entity IDs to lock
115+
* @return a task that completes with an {@link AutoCloseable} used to release locks
116+
*/
117+
public abstract Task<AutoCloseable> lockEntities(@Nonnull EntityInstanceId... entityIds);
118+
119+
/**
120+
* Gets whether this orchestration is currently inside a critical section.
121+
*
122+
* @return {@code true} if inside a critical section, otherwise {@code false}
123+
*/
124+
public abstract boolean isInCriticalSection();
125+
126+
/**
127+
* Gets the currently locked entity IDs.
128+
*
129+
* @return the list of currently locked entities, or an empty list
130+
*/
131+
public abstract List<EntityInstanceId> getLockedEntities();
132+
}
133+
134+
final class ContextBackedTaskOrchestrationEntityFeature extends TaskOrchestrationEntityFeature {
135+
private final TaskOrchestrationContext context;
136+
137+
ContextBackedTaskOrchestrationEntityFeature(TaskOrchestrationContext context) {
138+
this.context = context;
139+
}
140+
141+
@Override
142+
public <TResult> Task<TResult> callEntity(
143+
@Nonnull EntityInstanceId entityId,
144+
@Nonnull String operationName,
145+
@Nullable Object input,
146+
@Nonnull Class<TResult> returnType) {
147+
return this.context.callEntity(entityId, operationName, input, returnType);
148+
}
149+
150+
@Override
151+
public <TResult> Task<TResult> callEntity(
152+
@Nonnull EntityInstanceId entityId,
153+
@Nonnull String operationName,
154+
@Nullable Object input,
155+
@Nonnull Class<TResult> returnType,
156+
@Nullable CallEntityOptions options) {
157+
return this.context.callEntity(entityId, operationName, input, returnType, options);
158+
}
159+
160+
@Override
161+
public void signalEntity(
162+
@Nonnull EntityInstanceId entityId,
163+
@Nonnull String operationName,
164+
@Nullable Object input,
165+
@Nullable SignalEntityOptions options) {
166+
this.context.signalEntity(entityId, operationName, input, options);
167+
}
168+
169+
@Override
170+
public Task<AutoCloseable> lockEntities(@Nonnull List<EntityInstanceId> entityIds) {
171+
return this.context.lockEntities(entityIds);
172+
}
173+
174+
@Override
175+
public Task<AutoCloseable> lockEntities(@Nonnull EntityInstanceId... entityIds) {
176+
return this.context.lockEntities(entityIds);
177+
}
178+
179+
@Override
180+
public boolean isInCriticalSection() {
181+
return this.context.isInCriticalSection();
182+
}
183+
184+
@Override
185+
public List<EntityInstanceId> getLockedEntities() {
186+
return this.context.getLockedEntities();
187+
}
188+
}

client/src/test/java/com/microsoft/durabletask/CleanEntityStorageRequestTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
public class CleanEntityStorageRequestTest {
1313

1414
@Test
15-
void defaults_allFalseAndNull() {
15+
void defaults_matchDotNetDefaults() {
1616
CleanEntityStorageRequest request = new CleanEntityStorageRequest();
1717
assertNull(request.getContinuationToken());
18-
assertFalse(request.isRemoveEmptyEntities());
19-
assertFalse(request.isReleaseOrphanedLocks());
18+
assertTrue(request.isRemoveEmptyEntities());
19+
assertTrue(request.isReleaseOrphanedLocks());
2020
assertFalse(request.isContinueUntilComplete());
2121
}
2222

client/src/test/java/com/microsoft/durabletask/TaskOrchestrationEntityEventTest.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,39 @@ void signalEntity_producesSendEntityMessageAction() {
218218
assertTrue(hasComplete, "Expected a completeOrchestration action");
219219
}
220220

221+
@Test
222+
void getEntities_signalEntity_producesSendEntityMessageAction() {
223+
final String orchestratorName = "SignalViaEntitiesFeatureOrchestration";
224+
EntityInstanceId entityId = new EntityInstanceId("Counter", "c1");
225+
226+
TaskOrchestrationExecutor executor = createExecutor(orchestratorName, ctx -> {
227+
ctx.getEntities().signalEntity(entityId, "add", 7);
228+
ctx.complete("done");
229+
});
230+
231+
List<HistoryEvent> pastEvents = Arrays.asList(
232+
orchestratorStarted(),
233+
executionStarted(orchestratorName, "null"));
234+
List<HistoryEvent> newEvents = Collections.singletonList(orchestratorCompleted());
235+
236+
TaskOrchestratorResult result = executor.execute(pastEvents, newEvents);
237+
238+
boolean hasSignal = false;
239+
for (OrchestratorAction action : result.getActions()) {
240+
if (action.hasSendEntityMessage()) {
241+
SendEntityMessageAction msg = action.getSendEntityMessage();
242+
if (msg.hasEntityOperationSignaled()) {
243+
EntityOperationSignaledEvent signal = msg.getEntityOperationSignaled();
244+
assertEquals("add", signal.getOperation());
245+
assertEquals("@counter@c1", signal.getTargetInstanceId().getValue());
246+
hasSignal = true;
247+
}
248+
}
249+
}
250+
251+
assertTrue(hasSignal, "Expected a sendEntityMessage action with signal");
252+
}
253+
221254
@Test
222255
void signalEntity_replayPassesNonDeterminismCheck() {
223256
final String orchestratorName = "SignalEntityReplay";
@@ -296,6 +329,44 @@ void callEntity_producesActionAndWaitsForResponse() {
296329
assertFalse(hasComplete, "Should not complete while waiting for entity response");
297330
}
298331

332+
@Test
333+
void entities_callEntity_producesActionAndWaitsForResponse() {
334+
final String orchestratorName = "CallViaEntitiesFeatureOrchestration";
335+
EntityInstanceId entityId = new EntityInstanceId("Counter", "c1");
336+
337+
TaskOrchestrationExecutor executor = createExecutor(orchestratorName, ctx -> {
338+
int value = ctx.entities().callEntity(entityId, "get", null, int.class).await();
339+
ctx.complete(value);
340+
});
341+
342+
List<HistoryEvent> pastEvents = Arrays.asList(
343+
orchestratorStarted(),
344+
executionStarted(orchestratorName, "null"));
345+
List<HistoryEvent> newEvents = Collections.singletonList(orchestratorCompleted());
346+
347+
TaskOrchestratorResult result = executor.execute(pastEvents, newEvents);
348+
349+
boolean hasCall = false;
350+
boolean hasComplete = false;
351+
for (OrchestratorAction action : result.getActions()) {
352+
if (action.hasSendEntityMessage()) {
353+
SendEntityMessageAction msg = action.getSendEntityMessage();
354+
if (msg.hasEntityOperationCalled()) {
355+
EntityOperationCalledEvent call = msg.getEntityOperationCalled();
356+
assertEquals("get", call.getOperation());
357+
assertEquals("@counter@c1", call.getTargetInstanceId().getValue());
358+
hasCall = true;
359+
}
360+
}
361+
if (action.hasCompleteOrchestration()) {
362+
hasComplete = true;
363+
}
364+
}
365+
366+
assertTrue(hasCall, "Expected a sendEntityMessage action with call");
367+
assertFalse(hasComplete, "Should not complete while waiting for entity response");
368+
}
369+
299370
@Test
300371
void callEntity_completesWhenResponseArrives() {
301372
final String orchestratorName = "CallEntityComplete";
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
026329c53fe6363985655857b9ca848ec7238bd2n
1+
1caadbd7ecfdf5f2309acbeac28a3e36d16aa156

0 commit comments

Comments
 (0)