Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 18
Implement suspend and resume client APIs#104
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Uh oh!
There was an error while loading. Please reload this page.
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
cdddff4
fix comments to align with official doc
kaibocai a0f3eb3
Merge branch 'main' of https://github.com/microsoft/durabletask-java
kaibocai e8e343f
implement suspend and resume client apis
kaibocai 6b6314f
add integration test
kaibocai 1daa2f6
add integration tests
kaibocai f212e30
update integratio tests
kaibocai 390bd52
update CHANGELOG.md
kaibocai e3f59c4
fix potential NPE of terminate method - update unit tests for suspend…
kaibocai 9ece50b
update release notes - minior refactor unit test
kaibocai 740f688
update sidecar image for testing
kaibocai File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
30 changes: 30 additions & 0 deletions
30 client/src/main/java/com/microsoft/durabletask/DurableTaskClient.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 24 additions & 1 deletion
25 client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcClient.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 10 additions & 1 deletion
11 client/src/main/java/com/microsoft/durabletask/OrchestrationRuntimeStatus.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
145 changes: 87 additions & 58 deletions
145 client/src/main/java/com/microsoft/durabletask/TaskOrchestrationExecutor.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -66,21 +66,22 @@ private class ContextImplTask implements TaskOrchestrationContext { | ||
| private String instanceId; | ||
| private Instant currentInstant; | ||
| private boolean isComplete; | ||
| private boolean isSuspended; | ||
| private boolean isReplaying = true; | ||
| // LinkedHashMap to maintain insertion order when returning the list of pending actions | ||
| private final LinkedHashMap<Integer, OrchestratorAction> pendingActions = new LinkedHashMap<>(); | ||
| private final HashMap<Integer, TaskRecord<?>> openTasks = new HashMap<>(); | ||
| private final LinkedHashMap<String, Queue<TaskRecord<?>>> outstandingEvents = new LinkedHashMap<>(); | ||
| private final LinkedList<HistoryEvent> unprocessedEvents = new LinkedList<>(); | ||
| private final Queue<HistoryEvent> eventsWhileSuspended = new ArrayDeque<>(); | ||
| private final DataConverter dataConverter = TaskOrchestrationExecutor.this.dataConverter; | ||
| private final Logger logger = TaskOrchestrationExecutor.this.logger; | ||
| private final OrchestrationHistoryIterator historyEventPlayer; | ||
| private int sequenceNumber; | ||
| private boolean continuedAsNew; | ||
| private Object continuedAsNewInput; | ||
| private boolean preserveUnprocessedEvents; | ||
| private Object customStatus; | ||
| public ContextImplTask(List<HistoryEvent> pastEvents, List<HistoryEvent> newEvents) { | ||
| @@ -524,6 +525,23 @@ private void handleEventRaised(HistoryEvent e) { | ||
| task.complete(result); | ||
| } | ||
| private void handleEventWhileSuspended (HistoryEvent historyEvent){ | ||
| if (historyEvent.getEventTypeCase() != HistoryEvent.EventTypeCase.EXECUTIONSUSPENDED) { | ||
| eventsWhileSuspended.offer(historyEvent); | ||
| } | ||
| } | ||
| private void handleExecutionSuspended(HistoryEvent historyEvent) { | ||
| this.isSuspended = true; | ||
| } | ||
| private void handleExecutionResumed(HistoryEvent historyEvent) { | ||
| this.isSuspended = false; | ||
| while (!eventsWhileSuspended.isEmpty()) { | ||
| this.processEvent(eventsWhileSuspended.poll()); | ||
| } | ||
| } | ||
| public Task<Void> createTimer(Duration duration) { | ||
| Helpers.throwIfOrchestratorComplete(this.isComplete); | ||
| Helpers.throwIfArgumentNull(duration, "duration"); | ||
| @@ -717,75 +735,86 @@ private boolean processNextEvent() { | ||
| } | ||
| private void processEvent(HistoryEvent e) { | ||
| switch (e.getEventTypeCase()) { | ||
| case ORCHESTRATORSTARTED: | ||
| Instant instant = DataConverter.getInstantFromTimestamp(e.getTimestamp()); | ||
| this.setCurrentInstant(instant); | ||
| break; | ||
| case ORCHESTRATORCOMPLETED: | ||
| // No action | ||
| break; | ||
| case EXECUTIONSTARTED: | ||
| ExecutionStartedEvent startedEvent = e.getExecutionStarted(); | ||
| String name = startedEvent.getName(); | ||
| this.setName(name); | ||
| String instanceId = startedEvent.getOrchestrationInstance().getInstanceId(); | ||
| this.setInstanceId(instanceId); | ||
| String input = startedEvent.getInput().getValue(); | ||
| this.setInput(input); | ||
| TaskOrchestrationFactory factory = TaskOrchestrationExecutor.this.orchestrationFactories.get(name); | ||
| if (factory == null) { | ||
| // Try getting the default orchestrator | ||
| factory = TaskOrchestrationExecutor.this.orchestrationFactories.get("*"); | ||
| } | ||
| // TODO: Throw if the factory is null (orchestration by that name doesn't exist) | ||
| TaskOrchestration orchestrator = factory.create(); | ||
| orchestrator.run(this); | ||
| break; | ||
| boolean overrideSuspension = e.getEventTypeCase() == HistoryEvent.EventTypeCase.EXECUTIONRESUMED || e.getEventTypeCase() == HistoryEvent.EventTypeCase.EXECUTIONTERMINATED; | ||
kaibocai marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| if (this.isSuspended && !overrideSuspension) { | ||
| this.handleEventWhileSuspended(e); | ||
| } else { | ||
| switch (e.getEventTypeCase()) { | ||
| case ORCHESTRATORSTARTED: | ||
| Instant instant = DataConverter.getInstantFromTimestamp(e.getTimestamp()); | ||
| this.setCurrentInstant(instant); | ||
| break; | ||
| case ORCHESTRATORCOMPLETED: | ||
| // No action | ||
| break; | ||
| case EXECUTIONSTARTED: | ||
| ExecutionStartedEvent startedEvent = e.getExecutionStarted(); | ||
| String name = startedEvent.getName(); | ||
| this.setName(name); | ||
| String instanceId = startedEvent.getOrchestrationInstance().getInstanceId(); | ||
| this.setInstanceId(instanceId); | ||
| String input = startedEvent.getInput().getValue(); | ||
| this.setInput(input); | ||
| TaskOrchestrationFactory factory = TaskOrchestrationExecutor.this.orchestrationFactories.get(name); | ||
| if (factory == null) { | ||
| // Try getting the default orchestrator | ||
| factory = TaskOrchestrationExecutor.this.orchestrationFactories.get("*"); | ||
| } | ||
| // TODO: Throw if the factory is null (orchestration by that name doesn't exist) | ||
| TaskOrchestration orchestrator = factory.create(); | ||
| orchestrator.run(this); | ||
| break; | ||
| // case EXECUTIONCOMPLETED: | ||
| // break; | ||
| // case EXECUTIONFAILED: | ||
| // break; | ||
| case EXECUTIONTERMINATED: | ||
| this.handleExecutionTerminated(e); | ||
| break; | ||
| case TASKSCHEDULED: | ||
| this.handleTaskScheduled(e); | ||
| break; | ||
| case TASKCOMPLETED: | ||
| this.handleTaskCompleted(e); | ||
| break; | ||
| case TASKFAILED: | ||
| this.handleTaskFailed(e); | ||
| break; | ||
| case TIMERCREATED: | ||
| this.handleTimerCreated(e); | ||
| break; | ||
| case TIMERFIRED: | ||
| this.handleTimerFired(e); | ||
| break; | ||
| case SUBORCHESTRATIONINSTANCECREATED: | ||
| this.handleSubOrchestrationCreated(e); | ||
| break; | ||
| case SUBORCHESTRATIONINSTANCECOMPLETED: | ||
| this.handleSubOrchestrationCompleted(e); | ||
| break; | ||
| case SUBORCHESTRATIONINSTANCEFAILED: | ||
| this.handleSubOrchestrationFailed(e); | ||
| break; | ||
| case EXECUTIONTERMINATED: | ||
| this.handleExecutionTerminated(e); | ||
| break; | ||
| case TASKSCHEDULED: | ||
| this.handleTaskScheduled(e); | ||
| break; | ||
| case TASKCOMPLETED: | ||
| this.handleTaskCompleted(e); | ||
| break; | ||
| case TASKFAILED: | ||
| this.handleTaskFailed(e); | ||
| break; | ||
| case TIMERCREATED: | ||
| this.handleTimerCreated(e); | ||
| break; | ||
| case TIMERFIRED: | ||
| this.handleTimerFired(e); | ||
| break; | ||
| case SUBORCHESTRATIONINSTANCECREATED: | ||
| this.handleSubOrchestrationCreated(e); | ||
| break; | ||
| case SUBORCHESTRATIONINSTANCECOMPLETED: | ||
| this.handleSubOrchestrationCompleted(e); | ||
| break; | ||
| case SUBORCHESTRATIONINSTANCEFAILED: | ||
| this.handleSubOrchestrationFailed(e); | ||
| break; | ||
| // case EVENTSENT: | ||
| // break; | ||
| case EVENTRAISED: | ||
| this.handleEventRaised(e); | ||
| break; | ||
| case EVENTRAISED: | ||
| this.handleEventRaised(e); | ||
| break; | ||
| // case GENERICEVENT: | ||
| // break; | ||
| // case HISTORYSTATE: | ||
| // break; | ||
| // case EVENTTYPE_NOT_SET: | ||
| // break; | ||
| default: | ||
| throw new IllegalStateException("Don't know how to handle history type " + e.getEventTypeCase()); | ||
| case EXECUTIONSUSPENDED: | ||
| this.handleExecutionSuspended(e); | ||
| break; | ||
| case EXECUTIONRESUMED: | ||
| this.handleExecutionResumed(e); | ||
| break; | ||
| default: | ||
| throw new IllegalStateException("Don't know how to handle history type " + e.getEventTypeCase()); | ||
| } | ||
| } | ||
| } | ||
66 changes: 66 additions & 0 deletions
66 client/src/test/java/com/microsoft/durabletask/IntegrationTests.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -319,6 +319,72 @@ void termination() throws TimeoutException { | ||
| } | ||
| } | ||
| @Test | ||
| void suspendResumeOrchestration() throws TimeoutException, InterruptedException { | ||
| final String orchestratorName = "suspend"; | ||
| final String eventName = "MyEvent"; | ||
| final String eventPayload = "testPayload"; | ||
| final Duration suspendTimeout = Duration.ofSeconds(5); | ||
| DurableTaskGrpcWorker worker = this.createWorkerBuilder() | ||
| .addOrchestrator(orchestratorName, ctx -> { | ||
| String payload = ctx.waitForExternalEvent(eventName, String.class).await(); | ||
| ctx.complete(payload); | ||
| }) | ||
| .buildAndStart(); | ||
| DurableTaskClient client = new DurableTaskGrpcClientBuilder().build(); | ||
| try (worker; client) { | ||
| String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName); | ||
| client.suspendInstance(instanceId); | ||
| OrchestrationMetadata instance = client.waitForInstanceStart(instanceId, defaultTimeout); | ||
| assertNotNull(instance); | ||
| assertEquals(OrchestrationRuntimeStatus.SUSPENDED, instance.getRuntimeStatus()); | ||
| client.raiseEvent(instanceId, eventName, eventPayload); | ||
| assertThrows( | ||
| TimeoutException.class, | ||
| () -> client.waitForInstanceCompletion(instanceId, suspendTimeout, false), | ||
| "Expected to throw TimeoutException, but it didn't" | ||
| ); | ||
| String resumeReason = "Resume for testing."; | ||
| client.resumeInstance(instanceId, resumeReason); | ||
kaibocai marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, true); | ||
| assertNotNull(instance); | ||
| assertEquals(instanceId, instance.getInstanceId()); | ||
| assertEquals(eventPayload, instance.readOutputAs(String.class)); | ||
| assertEquals(OrchestrationRuntimeStatus.COMPLETED, instance.getRuntimeStatus()); | ||
| } | ||
| } | ||
| @Test | ||
| void terminateSuspendOrchestration() throws TimeoutException, InterruptedException { | ||
| final String orchestratorName = "suspendResume"; | ||
| final String eventName = "MyEvent"; | ||
| final String eventPayload = "testPayload"; | ||
| DurableTaskGrpcWorker worker = this.createWorkerBuilder() | ||
| .addOrchestrator(orchestratorName, ctx -> { | ||
| String payload = ctx.waitForExternalEvent(eventName, String.class).await(); | ||
| ctx.complete(payload); | ||
| }) | ||
| .buildAndStart(); | ||
| DurableTaskClient client = new DurableTaskGrpcClientBuilder().build(); | ||
| try (worker; client) { | ||
| String instanceId = client.scheduleNewOrchestrationInstance(orchestratorName); | ||
| String suspendReason = "Suspend for testing."; | ||
| client.suspendInstance(instanceId, suspendReason); | ||
| client.terminate(instanceId, null); | ||
| OrchestrationMetadata instance = client.waitForInstanceCompletion(instanceId, defaultTimeout, false); | ||
| assertNotNull(instance); | ||
| assertEquals(instanceId, instance.getInstanceId()); | ||
| assertEquals(OrchestrationRuntimeStatus.TERMINATED, instance.getRuntimeStatus()); | ||
| } | ||
| } | ||
| @Test | ||
| void activityFanOut() throws IOException, TimeoutException { | ||
| final String orchestratorName = "ActivityFanOut"; | ||
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.