Skip to content

Commit f199c74

Browse files
committed
refined implementation and addressed feedback
1 parent a7e173b commit f199c74

29 files changed

Lines changed: 718 additions & 543 deletions

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,7 @@ private static Object invokeMethodDirect(
329329
}
330330

331331
try {
332+
makeDeclaringClassAccessible(method);
332333
return method.invoke(target, args);
333334
} catch (InvocationTargetException e) {
334335
Throwable cause = e.getTargetException();
@@ -421,6 +422,7 @@ private static Object invokeMethod(Method method, Object target, TaskEntityOpera
421422
}
422423

423424
try {
425+
makeDeclaringClassAccessible(method);
424426
return method.invoke(target, args);
425427
} catch (InvocationTargetException e) {
426428
// Unwrap the target exception
@@ -431,4 +433,10 @@ private static Object invokeMethod(Method method, Object target, TaskEntityOpera
431433
throw new RuntimeException(cause);
432434
}
433435
}
436+
437+
private static void makeDeclaringClassAccessible(Method method) {
438+
if (!Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
439+
method.setAccessible(true);
440+
}
441+
}
434442
}

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,23 @@ protected Class<Integer> getStateType() {
4242
}
4343
}
4444

45+
/** A non-public entity implementation, matching internal feature entities. */
46+
private static class PrivateEntity extends AbstractTaskEntity<String> {
47+
public String get() {
48+
return this.state;
49+
}
50+
51+
@Override
52+
protected String initializeState(TaskEntityOperation operation) {
53+
return "private-state";
54+
}
55+
56+
@Override
57+
protected Class<String> getStateType() {
58+
return String.class;
59+
}
60+
}
61+
4562
/**
4663
* Entity that accepts a TaskEntityContext as a method parameter.
4764
*/
@@ -247,6 +264,12 @@ void reflectionDispatch_methodWithReturnValue() throws Exception {
247264
assertEquals(42, result);
248265
}
249266

267+
@Test
268+
void reflectionDispatch_publicOperationOnPrivateEntityClass() throws Exception {
269+
Object result = new PrivateEntity().run(createOperation("get"));
270+
assertEquals("private-state", result);
271+
}
272+
250273
@Test
251274
void reflectionDispatch_caseInsensitive() throws Exception {
252275
CounterEntity entity = new CounterEntity();

exporthistory/src/main/java/com/microsoft/durabletask/exporthistory/CommitCheckpointRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* When {@link #getCheckpoint()} is non-null the cursor moves forward (successful batch); when {@code null} the
1212
* cursor is retained (failed batch eligible for retry).
1313
*/
14-
public final class CommitCheckpointRequest {
14+
final class CommitCheckpointRequest {
1515

1616
private long scannedInstances;
1717
private long exportedInstances;

exporthistory/src/main/java/com/microsoft/durabletask/exporthistory/ExecuteExportJobOperationOrchestrator.java

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@
22
// Licensed under the MIT License.
33
package com.microsoft.durabletask.exporthistory;
44

5+
import com.microsoft.durabletask.EntityInstanceId;
56
import com.microsoft.durabletask.TaskOrchestration;
67
import com.microsoft.durabletask.TaskOrchestrationContext;
78

9+
import javax.annotation.Nullable;
10+
811
/**
912
* Orchestrator that executes a single operation on an export job entity and returns its result.
1013
* <p>
1114
* The client schedules this orchestrator (rather than signaling the entity directly) so it can await completion and
1215
* surface validation errors.
1316
*/
14-
public final class ExecuteExportJobOperationOrchestrator implements TaskOrchestration {
17+
final class ExecuteExportJobOperationOrchestrator implements TaskOrchestration {
1518

1619
/** The registered orchestration name. */
1720
public static final String NAME = "ExecuteExportJobOperationOrchestrator";
@@ -25,3 +28,74 @@ public void run(TaskOrchestrationContext ctx) {
2528
ctx.complete(result);
2629
}
2730
}
31+
32+
/**
33+
* Request to execute a single operation on an export job entity, scheduled by the client through
34+
* {@link ExecuteExportJobOperationOrchestrator} so the caller can await completion and surface validation errors.
35+
*/
36+
final class ExportJobOperationRequest {
37+
38+
private EntityInstanceId entityId;
39+
private String operationName;
40+
private Object input;
41+
42+
/** Creates an empty {@code ExportJobOperationRequest} (for deserialization). */
43+
public ExportJobOperationRequest() {
44+
}
45+
46+
/**
47+
* Creates an {@code ExportJobOperationRequest}.
48+
*
49+
* @param entityId the target entity ID
50+
* @param operationName the operation name
51+
* @param input the operation input, or {@code null}
52+
*/
53+
public ExportJobOperationRequest(EntityInstanceId entityId, String operationName, @Nullable Object input) {
54+
this.entityId = entityId;
55+
this.operationName = operationName;
56+
this.input = input;
57+
}
58+
59+
/** @return the target entity ID. */
60+
public EntityInstanceId getEntityId() {
61+
return this.entityId;
62+
}
63+
64+
/**
65+
* Sets the target entity ID.
66+
*
67+
* @param entityId the entity ID
68+
*/
69+
public void setEntityId(EntityInstanceId entityId) {
70+
this.entityId = entityId;
71+
}
72+
73+
/** @return the operation name. */
74+
public String getOperationName() {
75+
return this.operationName;
76+
}
77+
78+
/**
79+
* Sets the operation name.
80+
*
81+
* @param operationName the operation name
82+
*/
83+
public void setOperationName(String operationName) {
84+
this.operationName = operationName;
85+
}
86+
87+
/** @return the operation input, or {@code null}. */
88+
@Nullable
89+
public Object getInput() {
90+
return this.input;
91+
}
92+
93+
/**
94+
* Sets the operation input.
95+
*
96+
* @param input the input, or {@code null}
97+
*/
98+
public void setInput(@Nullable Object input) {
99+
this.input = input;
100+
}
101+
}

exporthistory/src/main/java/com/microsoft/durabletask/exporthistory/ExportBlobNaming.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.time.OffsetDateTime;
1010
import java.time.ZoneOffset;
1111
import java.time.format.DateTimeFormatter;
12+
import java.util.Locale;
1213

1314
/**
1415
* Computes export blob names and paths. The blob name is a lowercase-hex SHA-256 hash of
@@ -51,7 +52,7 @@ static String blobFileName(Instant completedTimestamp, String instanceId, Export
5152
static String formatTimestamp(Instant instant) {
5253
OffsetDateTime utc = instant.atOffset(ZoneOffset.UTC);
5354
long ticks = utc.getNano() / 100L;
54-
return TIMESTAMP_DATE_TIME.format(utc) + "." + String.format("%07d", ticks) + "+00:00";
55+
return TIMESTAMP_DATE_TIME.format(utc) + "." + String.format(Locale.ROOT, "%07d", ticks) + "+00:00";
5556
}
5657

5758
/**

exporthistory/src/main/java/com/microsoft/durabletask/exporthistory/ExportFailure.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
/**
88
* Failure of a specific instance export.
99
*/
10-
public final class ExportFailure {
10+
final class ExportFailure {
1111

1212
private String instanceId;
1313
private String reason;

exporthistory/src/main/java/com/microsoft/durabletask/exporthistory/ExportHistoryConstants.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
/**
66
* Constants used throughout the export history functionality.
77
*/
8-
public final class ExportHistoryConstants {
8+
final class ExportHistoryConstants {
99

1010
/**
1111
* The prefix used for generating export job orchestrator instance IDs.

exporthistory/src/main/java/com/microsoft/durabletask/exporthistory/ExportHistoryJobClient.java

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import java.time.Duration;
1313
import java.util.Locale;
1414
import java.util.concurrent.TimeoutException;
15+
import java.util.logging.Level;
16+
import java.util.logging.Logger;
1517

1618
/**
1719
* Client for managing a single export job via entity operations routed through
@@ -20,6 +22,7 @@
2022
public final class ExportHistoryJobClient {
2123

2224
private static final Duration OPERATION_TIMEOUT = Duration.ofSeconds(60);
25+
private static final Logger LOGGER = Logger.getLogger(ExportHistoryJobClient.class.getName());
2326

2427
private final DurableTaskClient durableTaskClient;
2528
private final String jobId;
@@ -96,14 +99,32 @@ public ExportJobDescription describe() {
9699
return ExportJobDescription.fromState(this.jobId, metadata.getState());
97100
}
98101

99-
/**
100-
* Deletes the export job entity. The export orchestrator self-exits on its next cycle once it observes the job
101-
* is gone.
102-
*/
102+
/** Deletes the export job entity, then terminates and purges its linked export orchestrator. */
103103
public void delete() {
104104
ExportJobOperationRequest request = new ExportJobOperationRequest(
105105
this.entityId, ExportJobTransitions.OP_DELETE, null);
106-
scheduleAndWait(request);
106+
OrchestrationMetadata result = scheduleAndWait(request);
107+
if (result.getRuntimeStatus() != OrchestrationRuntimeStatus.COMPLETED) {
108+
FailureDetails failure = result.getFailureDetails();
109+
String detail = failure == null ? "" : failure.getErrorMessage();
110+
throw new ExportJobClientValidationException(
111+
"Failed to delete export job '" + this.jobId + "': " + detail);
112+
}
113+
114+
terminateAndPurgeOrchestrator();
115+
}
116+
117+
private void terminateAndPurgeOrchestrator() {
118+
String orchestratorInstanceId = ExportHistoryConstants.getOrchestratorInstanceId(this.jobId);
119+
try {
120+
this.durableTaskClient.terminate(orchestratorInstanceId, "Export job deleted");
121+
this.durableTaskClient.waitForInstanceCompletion(
122+
orchestratorInstanceId, OPERATION_TIMEOUT, false);
123+
this.durableTaskClient.purgeInstance(orchestratorInstanceId);
124+
} catch (RuntimeException | TimeoutException ex) {
125+
LOGGER.log(Level.WARNING,
126+
"Failed to terminate or purge export orchestrator '" + orchestratorInstanceId + "'.", ex);
127+
}
107128
}
108129

109130
private OrchestrationMetadata scheduleAndWait(ExportJobOperationRequest request) {

exporthistory/src/main/java/com/microsoft/durabletask/exporthistory/ExportHistoryStorageOptions.java

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ public final class ExportHistoryStorageOptions {
3737
private TokenCredential credential;
3838
private String containerName = "";
3939
private String prefix;
40-
private ExportFormat format = ExportFormat.getDefault();
4140

4241
/**
4342
* Gets the Azure Storage connection string.
@@ -145,24 +144,4 @@ public ExportHistoryStorageOptions setPrefix(@Nullable String prefix) {
145144
this.prefix = prefix;
146145
return this;
147146
}
148-
149-
/**
150-
* Gets the export format. Defaults to {@link ExportFormat#getDefault()} (JSONL + gzip).
151-
*
152-
* @return the export format
153-
*/
154-
public ExportFormat getFormat() {
155-
return this.format;
156-
}
157-
158-
/**
159-
* Sets the export format.
160-
*
161-
* @param format the export format
162-
* @return this options object
163-
*/
164-
public ExportHistoryStorageOptions setFormat(ExportFormat format) {
165-
this.format = format;
166-
return this;
167-
}
168147
}

exporthistory/src/main/java/com/microsoft/durabletask/exporthistory/ExportHistoryWorkerExtensions.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public static DurableTaskGrpcWorkerBuilder useExportHistory(
4444

4545
BlobExportWriter writer = new BlobExportWriter(storage);
4646

47-
builder.addEntity(ExportJob.NAME, ExportJob.class);
47+
builder.addEntity(ExportJob.NAME, ExportJob::new);
4848

4949
builder.addOrchestration(orchestrationFactory(
5050
ExportJobOrchestrator.NAME, ExportJobOrchestrator::new));

0 commit comments

Comments
 (0)