Skip to content

Commit a0a6a5f

Browse files
authored
Fix continue-as-new data corruption issue (#150)
Specifically fixes issues related to external event handling and especially Durable Entities.
1 parent 9adbf23 commit a0a6a5f

File tree

5 files changed

+45
-3
lines changed

5 files changed

+45
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
* Synchronous reads for improved performance for large payloads ([#134](https://github.com/microsoft/durabletask-mssql/pull/134)) - contributed by [@bhugot](https://github.com/bhugot)
88
* Fix for sub-orchestration handling over gRPC ([#149](https://github.com/microsoft/durabletask-mssql/pull/149))
9+
* Fix continue-as-new data corruption race condition ([#150](https://github.com/microsoft/durabletask-mssql/pull/150))
910

1011
## v1.1.0
1112

src/DurableTask.SqlServer/Scripts/logic.sql

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,7 @@ BEGIN
635635

636636
DECLARE @InputPayloadID uniqueidentifier
637637
DECLARE @CustomStatusPayloadID uniqueidentifier
638+
DECLARE @ExistingOutputPayloadID uniqueidentifier
638639
DECLARE @ExistingCustomStatusPayload varchar(MAX)
639640
DECLARE @ExistingExecutionID varchar(50)
640641

@@ -644,6 +645,7 @@ BEGIN
644645
SELECT TOP 1
645646
@InputPayloadID = I.[InputPayloadID],
646647
@CustomStatusPayloadID = I.[CustomStatusPayloadID],
648+
@ExistingOutputPayloadID = I.[OutputPayloadID],
647649
@ExistingCustomStatusPayload = P.[Text],
648650
@ExistingExecutionID = I.[ExecutionID]
649651
FROM Payloads P RIGHT OUTER JOIN Instances I ON
@@ -652,15 +654,24 @@ BEGIN
652654
P.[PayloadID] = I.[CustomStatusPayloadID]
653655
WHERE I.[TaskHub] = @TaskHub AND I.[InstanceID] = @InstanceID
654656

655-
-- ContinueAsNew case: delete all existing runtime state (history and payloads)
657+
-- ContinueAsNew case: delete all existing runtime state (history and payloads), but be careful
658+
-- not to delete payloads of unprocessed state, like new events.
656659
DECLARE @IsContinueAsNew BIT = 0
657660
IF @ExistingExecutionID IS NOT NULL AND @ExistingExecutionID <> @ExecutionID
658661
BEGIN
662+
DECLARE @PayloadIDsToDelete TABLE ([PayloadID] uniqueidentifier NULL)
663+
INSERT INTO @PayloadIDsToDelete
664+
VALUES (@InputPayloadID), (@CustomStatusPayloadID), (@ExistingOutputPayloadID)
665+
659666
DELETE FROM History
667+
OUTPUT DELETED.[DataPayloadID] INTO @PayloadIDsToDelete
660668
WHERE [TaskHub] = @TaskHub AND [InstanceID] = @InstanceID
661669

662670
DELETE FROM Payloads
663-
WHERE [TaskHub] = @TaskHub AND [InstanceID] = @InstanceID
671+
WHERE
672+
[TaskHub] = @TaskHub AND
673+
[InstanceID] = @InstanceID AND
674+
[PayloadID] IN (SELECT [PayloadID] FROM @PayloadIDsToDelete)
664675

665676
-- The existing payload got purged in the previous statement
666677
SET @ExistingCustomStatusPayload = NULL

test/DurableTask.SqlServer.AzureFunctions.Tests/CoreScenarios.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public async Task CanOrchestrateEntities()
7777
}
7878

7979
[Fact]
80-
public async Task CanInteractWithEntities()
80+
public async Task CanClientInteractWithEntities()
8181
{
8282
IDurableClient client = this.GetDurableClient();
8383

@@ -98,6 +98,14 @@ await Task.WhenAll(
9898
Assert.Equal(7, result.EntityState);
9999
}
100100

101+
[Fact]
102+
public async Task CanOrchestrationInteractWithEntities()
103+
{
104+
DurableOrchestrationStatus status = await this.RunOrchestrationAsync(nameof(Functions.IncrementThenGet));
105+
Assert.Equal(OrchestrationRuntimeStatus.Completed, status.RuntimeStatus);
106+
Assert.Equal(1, (int)status.Output);
107+
}
108+
101109
[Fact]
102110
public async Task SingleInstanceQuery()
103111
{

test/DurableTask.SqlServer.AzureFunctions.Tests/Functions.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,26 @@ public static void Counter([EntityTrigger] IDurableEntityContext ctx)
114114
}
115115
}
116116

117+
[FunctionName(nameof(IncrementThenGet))]
118+
public static async Task<int> IncrementThenGet([OrchestrationTrigger] IDurableOrchestrationContext context)
119+
{
120+
// Key needs to be pseudo-random to avoid conflicts with multiple test runs.
121+
string key = context.NewGuid().ToString().Substring(0, 8);
122+
EntityId entityId = new EntityId(nameof(Counter), key);
123+
124+
context.SignalEntity(entityId, "add", 1);
125+
126+
// Invoking a sub-orchestration as a regression test for https://github.com/microsoft/durabletask-mssql/issues/146
127+
return await context.CallSubOrchestratorAsync<int>(nameof(GetEntityAsync), entityId);
128+
}
129+
130+
[FunctionName(nameof(GetEntityAsync))]
131+
public static async Task<int> GetEntityAsync([OrchestrationTrigger] IDurableOrchestrationContext context)
132+
{
133+
EntityId entityId = context.GetInput<EntityId>();
134+
return await context.CallEntityAsync<int>(entityId, "get");
135+
}
136+
117137
[FunctionName(nameof(WaitForEvent))]
118138
public static Task<object> WaitForEvent([OrchestrationTrigger] IDurableOrchestrationContext ctx)
119139
{

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ public static async Task<object> ExecuteSqlAsync(string commandText, string conn
8484
public static async Task InitializeDatabaseAsync(string schema = DefaultSchema)
8585
{
8686
var options = new SqlOrchestrationServiceSettings(GetDefaultConnectionString(), schemaName: schema);
87+
options.CreateDatabaseIfNotExists = true;
88+
8789
var service = new SqlOrchestrationService(options);
8890
await service.CreateIfNotExistsAsync();
8991
}

0 commit comments

Comments
 (0)