Fix etag handling on read before write. (#351)

This commit is contained in:
Aditya Mandaleeka
2025-08-06 08:31:32 -07:00
committed by GitHub
Unverified
parent f43939d803
commit 8dcc8533a6
2 changed files with 91 additions and 2 deletions
@@ -230,8 +230,8 @@ public class CosmosActorStateStorage : IActorStateStorage
}
catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
// No root document means no actor state exists
return Guid.NewGuid().ToString("N");
// No root document means no actor state exists - return initial ETag
return InitialEtag;
}
}
}
@@ -247,4 +247,93 @@ public class CosmosActorStateStorageConcurrencyTests
Assert.NotNull(getValue);
Assert.Equal("updatedValue", getValue.Value?.GetString());
}
[Fact]
public async Task ReadThenWrite_OnNonExistentActor_ShouldWorkCorrectlyAsync()
{
// 1. Read state from a non-existent actor (gets initial ETag)
// 2. Write with that ETag (should succeed)
// Arrange
using var cts = new CancellationTokenSource(s_defaultTimeout);
var cancellationToken = cts.Token;
var storage = new CosmosActorStateStorage(this._fixture.Container);
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString()); // Fresh actor
var key = "testKey";
var value = JsonSerializer.SerializeToElement("testValue");
// Act - Read state from non-existent actor (this calls GetActorETagAsync internally)
var readOperations = new List<ActorStateReadOperation>
{
new GetValueOperation(key)
};
var readResult = await storage.ReadStateAsync(testActorId, readOperations, cancellationToken);
// Assert - Read should succeed but return null value and initial ETag
Assert.Single(readResult.Results);
var getValue = readResult.Results[0] as GetValueResult;
Assert.NotNull(getValue);
Assert.Null(getValue.Value); // No value exists yet
Assert.Equal("0", readResult.ETag); // Should return initial ETag for non-existent actor
// Act - Write using the ETag from the read operation
var writeOperations = new List<ActorStateWriteOperation>
{
new SetValueOperation(key, value)
};
var writeResult = await storage.WriteStateAsync(testActorId, writeOperations, readResult.ETag, cancellationToken);
// Assert - Write should succeed
Assert.True(writeResult.Success);
Assert.NotNull(writeResult.ETag);
Assert.NotEqual("0", writeResult.ETag); // Should get a real ETag after write
// Act - Verify the value was written
var verifyReadResult = await storage.ReadStateAsync(testActorId, readOperations, cancellationToken);
var verifyGetValue = verifyReadResult.Results[0] as GetValueResult;
// Assert - Value should now exist
Assert.NotNull(verifyGetValue);
Assert.NotNull(verifyGetValue.Value);
Assert.Equal("testValue", verifyGetValue.Value?.GetString());
Assert.Equal(writeResult.ETag, verifyReadResult.ETag); // ETags should match
}
[Fact]
public async Task WriteStateAsync_WithInvalidETag_ShouldFailAsync()
{
// Arrange
using var cts = new CancellationTokenSource(s_defaultTimeout);
var cancellationToken = cts.Token;
var storage = new CosmosActorStateStorage(this._fixture.Container);
var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString()); // Non-existent actor
var key = "testKey";
var value = JsonSerializer.SerializeToElement("testValue");
var operations = new List<ActorStateWriteOperation>
{
new SetValueOperation(key, value)
};
// Act - Try to write with a completely fabricated/invalid ETag (no document exists)
var fabricatedETag = "\"fabricated-etag-12345\""; // Made-up ETag for non-existent document
var resultWithFabricatedETag = await storage.WriteStateAsync(testActorId, operations, fabricatedETag, cancellationToken);
// Assert - The write should fail due to ETag mismatch (document doesn't exist)
Assert.False(resultWithFabricatedETag.Success);
Assert.Empty(resultWithFabricatedETag.ETag);
// Verify no document was created
var readOperations = new List<ActorStateReadOperation>
{
new GetValueOperation(key)
};
var readResult = await storage.ReadStateAsync(testActorId, readOperations, cancellationToken);
var getValue = readResult.Results[0] as GetValueResult;
Assert.NotNull(getValue);
Assert.Null(getValue.Value); // Should be null since no document exists
}
}