Skip to content

Commit ceddafa

Browse files
authored
Implement support for suspend/resume (#195)
Also: Fix minor issue with DateTimeKind
1 parent 8d26b5e commit ceddafa

File tree

8 files changed

+109
-27
lines changed

8 files changed

+109
-27
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@
44

55
(Add new notes here)
66

7+
## v1.2.1
8+
9+
### New
10+
11+
* Support suspend/resume of orchestrations
12+
13+
### Updates
14+
15+
* SqlOrchestrationService.WaitForInstanceAsync no longer throws `TimeoutException` - only `OperationCanceledException` (previously could be either, depending on timing)
16+
* Fix default DateTime values to have DateTimeKind of UTC (instead of Unspecified)
17+
718
## v1.2.0
819

920
### New

src/DurableTask.SqlServer/Scripts/logic.sql

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,6 @@ BEGIN
630630
E.[InstanceID] = I.[InstanceID]
631631
WHERE
632632
I.TaskHub = @TaskHub AND
633-
I.[RuntimeStatus] NOT IN ('Suspended') AND
634633
(I.[LockExpiration] IS NULL OR I.[LockExpiration] < @now) AND
635634
(E.[VisibleTime] IS NULL OR E.[VisibleTime] < @now)
636635

src/DurableTask.SqlServer/SqlOrchestrationService.cs

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ public override Task DeleteAsync(bool deleteInstanceStore)
205205
currentStatus = SqlUtils.GetRuntimeStatus(reader);
206206
isRunning =
207207
currentStatus == OrchestrationStatus.Running ||
208+
currentStatus == OrchestrationStatus.Suspended ||
208209
currentStatus == OrchestrationStatus.Pending;
209210
}
210211
else
@@ -570,19 +571,7 @@ public override async Task<OrchestrationState> WaitForOrchestrationAsync(
570571
return state;
571572
}
572573

573-
try
574-
{
575-
await Task.Delay(TimeSpan.FromSeconds(1), combinedCts.Token);
576-
}
577-
catch (TaskCanceledException)
578-
{
579-
if (timeoutCts.Token.IsCancellationRequested)
580-
{
581-
throw new TimeoutException($"A caller-specified timeout of {timeout} has expired, but instance '{instanceId}' is still in an {state?.OrchestrationStatus.ToString() ?? "unknown"} state.");
582-
}
583-
584-
throw;
585-
}
574+
await Task.Delay(TimeSpan.FromSeconds(1), combinedCts.Token);
586575
}
587576
}
588577

src/DurableTask.SqlServer/SqlUtils.cs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,12 @@ public static HistoryEvent GetHistoryEvent(this DbDataReader reader, bool isOrch
207207
TimerId = GetTaskId(reader),
208208
};
209209
break;
210+
case EventType.ExecutionSuspended:
211+
historyEvent = new ExecutionSuspendedEvent(eventId, GetPayloadText(reader));
212+
break;
213+
case EventType.ExecutionResumed:
214+
historyEvent = new ExecutionResumedEvent(eventId, GetPayloadText(reader));
215+
break;
210216
default:
211217
throw new InvalidOperationException($"Don't know how to interpret '{eventType}'.");
212218
}
@@ -247,10 +253,10 @@ public static OrchestrationState GetOrchestrationState(this DbDataReader reader)
247253

248254
var state = new OrchestrationState
249255
{
250-
CompletedTime = reader.GetUtcDateTimeOrNull(reader.GetOrdinal("CompletedTime")) ?? default,
251-
CreatedTime = reader.GetUtcDateTimeOrNull(reader.GetOrdinal("CreatedTime")) ?? default,
256+
CompletedTime = GetUtcDateTime(reader, "CompletedTime"),
257+
CreatedTime = GetUtcDateTime(reader, "CreatedTime"),
252258
Input = reader.GetStringOrNull(reader.GetOrdinal("InputText")),
253-
LastUpdatedTime = reader.GetUtcDateTimeOrNull(reader.GetOrdinal("LastUpdatedTime")) ?? default,
259+
LastUpdatedTime = GetUtcDateTime(reader, "LastUpdatedTime"),
254260
Name = GetName(reader),
255261
Version = GetVersion(reader),
256262
OrchestrationInstance = new OrchestrationInstance
@@ -411,23 +417,28 @@ internal static string GetInstanceId(DbDataReader reader)
411417

412418
static DateTime GetVisibleTime(DbDataReader reader)
413419
{
414-
int ordinal = reader.GetOrdinal("VisibleTime");
415-
return GetUtcDateTime(reader, ordinal);
420+
return GetUtcDateTime(reader, "VisibleTime");
416421
}
417422

418423
static DateTime GetTimestamp(DbDataReader reader)
419424
{
420-
int ordinal = reader.GetOrdinal("Timestamp");
421-
return GetUtcDateTime(reader, ordinal);
425+
return GetUtcDateTime(reader, "Timestamp");
422426
}
423427

424-
static DateTime? GetUtcDateTimeOrNull(this DbDataReader reader, int columnIndex)
428+
static DateTime GetUtcDateTime(DbDataReader reader, string columnName)
425429
{
426-
return reader.IsDBNull(columnIndex) ? (DateTime?)null : GetUtcDateTime(reader, columnIndex);
430+
int ordinal = reader.GetOrdinal(columnName);
431+
return GetUtcDateTime(reader, ordinal);
427432
}
428433

429434
static DateTime GetUtcDateTime(DbDataReader reader, int ordinal)
430435
{
436+
if (reader.IsDBNull(ordinal))
437+
{
438+
// Note that some serializers (like protobuf) won't accept non-UTC DateTime objects.
439+
return DateTime.SpecifyKind(default, DateTimeKind.Utc);
440+
}
441+
431442
// The SQL client always assumes DateTimeKind.Unspecified. We need to modify the result so that it knows it is UTC.
432443
return DateTime.SpecifyKind(reader.GetDateTime(ordinal), DateTimeKind.Utc);
433444
}

src/common.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
<PropertyGroup>
1818
<MajorVersion>1</MajorVersion>
1919
<MinorVersion>2</MinorVersion>
20-
<PatchVersion>0</PatchVersion>
20+
<PatchVersion>1</PatchVersion>
2121
<VersionPrefix>$(MajorVersion).$(MinorVersion).$(PatchVersion)</VersionPrefix>
2222
<VersionSuffix></VersionSuffix>
2323
<AssemblyVersion>$(MajorVersion).$(MinorVersion).0.0</AssemblyVersion>

test/DurableTask.SqlServer.Tests/Integration/DatabaseManagement.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ async Task ValidateDatabaseSchemaAsync(TestDatabase database, string schemaName
504504
schemaName);
505505
Assert.Equal(1, currentSchemaVersion.Major);
506506
Assert.Equal(2, currentSchemaVersion.Minor);
507-
Assert.Equal(0, currentSchemaVersion.Patch);
507+
Assert.Equal(1, currentSchemaVersion.Patch);
508508
}
509509

510510
sealed class TestDatabase : IDisposable

test/DurableTask.SqlServer.Tests/Integration/Orchestrations.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,5 +768,62 @@ public async Task TraceContextFlowCorrectly()
768768
Assert.True(activitySpan.Duration > delay);
769769
Assert.True(activitySpan.Duration < delay * 2);
770770
}
771+
772+
[Fact]
773+
public async Task SuspendAndResumeInstance()
774+
{
775+
TaskCompletionSource<int> tcs = null;
776+
777+
const int EventCount = 5;
778+
string orchestrationName = "SuspendResumeOrchestration";
779+
780+
TestInstance<string> instance = await this.testService.RunOrchestration<int, string>(
781+
null,
782+
orchestrationName,
783+
implementation: async (ctx, _) =>
784+
{
785+
tcs = new TaskCompletionSource<int>();
786+
787+
int i;
788+
for (i = 0; i < EventCount; i++)
789+
{
790+
await tcs.Task;
791+
tcs = new TaskCompletionSource<int>();
792+
}
793+
794+
return i;
795+
},
796+
onEvent: (ctx, name, value) =>
797+
{
798+
Assert.Equal("Event" + value, name);
799+
tcs.TrySetResult(int.Parse(value));
800+
});
801+
802+
// Wait for the orchestration to finish starting
803+
await instance.WaitForStart();
804+
805+
// Suspend the orchestration so that it won't process any new events
806+
await instance.SuspendAsync();
807+
808+
// Raise the events, which should get buffered but not consumed
809+
for (int i = 0; i < EventCount; i++)
810+
{
811+
await instance.RaiseEventAsync($"Event{i}", i);
812+
}
813+
814+
// Make sure that the orchestration *doesn't* complete
815+
await Assert.ThrowsAnyAsync<OperationCanceledException>(
816+
() => instance.WaitForCompletion(TimeSpan.FromSeconds(3), doNotAdjustTimeout: true));
817+
818+
// Confirm that the orchestration is in a suspended state
819+
OrchestrationState state = await instance.GetStateAsync();
820+
Assert.Equal(OrchestrationStatus.Suspended, state.OrchestrationStatus);
821+
822+
// Resume the orchestration
823+
await instance.ResumeAsync();
824+
825+
// Now the orchestration should complete immediately
826+
await instance.WaitForCompletion(timeout: TimeSpan.FromSeconds(3), expectedOutput: EventCount);
827+
}
771828
}
772829
}

test/DurableTask.SqlServer.Tests/Utils/TestInstance.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,13 @@ public async Task<OrchestrationState> WaitForCompletion(
7171
OrchestrationStatus expectedStatus = OrchestrationStatus.Completed,
7272
object expectedOutput = null,
7373
string expectedOutputRegex = null,
74-
bool continuedAsNew = false)
74+
bool continuedAsNew = false,
75+
bool doNotAdjustTimeout = false)
7576
{
76-
AdjustTimeout(ref timeout);
77+
if (!doNotAdjustTimeout)
78+
{
79+
AdjustTimeout(ref timeout);
80+
}
7781

7882
OrchestrationState state = await this.client.WaitForOrchestrationAsync(this.GetInstanceForAnyExecution(), timeout);
7983
Assert.NotNull(state);
@@ -158,6 +162,17 @@ internal async Task RestartAsync(TInput newInput, OrchestrationStatus[] dedupeSt
158162
this.instance.ExecutionId = newInstance.ExecutionId;
159163
}
160164

165+
166+
internal Task SuspendAsync(string reason = null)
167+
{
168+
return this.client.SuspendInstanceAsync(this.instance, reason);
169+
}
170+
171+
internal Task ResumeAsync(string reason = null)
172+
{
173+
return this.client.ResumeInstanceAsync(this.instance, reason);
174+
}
175+
161176
static void AdjustTimeout(ref TimeSpan timeout)
162177
{
163178
timeout = timeout.AdjustForDebugging();

0 commit comments

Comments
 (0)