Skip to content

Commit

Permalink
Fix race condition with less than 1 seconds left (#84)
Browse files Browse the repository at this point in the history
* Special case

* return

* test

* readme
  • Loading branch information
ealsur authored Aug 30, 2024
1 parent f0ae628 commit edf7ff1
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 1 deletion.
6 changes: 6 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## Unreleased

## <a name="1.6.2"/> 1.6.2 - 2024-08-30

### Fixed

- [#84](https://github.com/Azure/Microsoft.Extensions.Caching.Cosmos/pull/84) Fix race condition with less than 1 seconds left on sliding expiration

## <a name="1.6.1"/> 1.6.1 - 2024-03-27

### Fixed
Expand Down
14 changes: 14 additions & 0 deletions src/CosmosCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ public byte[] Get(string key)
else
{
double pendingSeconds = (absoluteExpiration - DateTimeOffset.UtcNow).TotalSeconds;
if (pendingSeconds == 0)
{
// Cosmos DB TTL works on seconds granularity and this item has less than a second to live.
// Return the content because it does exist, but it will be cleaned up by the TTL shortly after.
return cosmosCacheSessionResponse.Resource.Content;
}

if (pendingSeconds < ttl)
{
cosmosCacheSessionResponse.Resource.TimeToLive = (long)pendingSeconds;
Expand Down Expand Up @@ -224,6 +231,13 @@ public void Refresh(string key)
else
{
double pendingSeconds = (absoluteExpiration - DateTimeOffset.UtcNow).TotalSeconds;
if (pendingSeconds == 0)
{
// Cosmos DB TTL works on seconds granularity and this item has less than a second to live.
// Treat it as a cache-miss.
return;
}

if (pendingSeconds < ttl)
{
cosmosCacheSessionResponse.Resource.TimeToLive = (long)pendingSeconds;
Expand Down
2 changes: 1 addition & 1 deletion src/CosmosDistributedCache.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<Copyright>© Microsoft Corporation. All rights reserved.</Copyright>
<CurrentDate>$([System.DateTime]::Now.ToString(yyyyMMdd))</CurrentDate>
<NeutralLanguage>en-US</NeutralLanguage>
<ClientVersion>1.6.1</ClientVersion>
<ClientVersion>1.6.2</ClientVersion>
<VersionSuffix Condition=" '$(IsPreview)' == 'true' ">preview</VersionSuffix>
<Version Condition=" '$(VersionSuffix)' == '' ">$(ClientVersion)</Version>
<Version Condition=" '$(VersionSuffix)' != '' ">$(ClientVersion)-$(VersionSuffix)</Version>
Expand Down
40 changes: 40 additions & 0 deletions tests/unit/CosmosCacheTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,46 @@ public async Task SlidingExpirationWithAbsoluteExpirationOnReplaceNotFound()
mockedContainer.Verify(c => c.ReplaceItemAsync<CosmosCacheSession>(It.Is<CosmosCacheSession>(item => item.TimeToLive == ttlSliding), It.Is<string>(id => id == "key"), It.IsAny<PartitionKey?>(), It.IsAny<ItemRequestOptions>(), It.IsAny<CancellationToken>()), Times.Once);
}

[Fact]
public async Task SlidingExpirationWithAbsoluteExpirationOnAlmostExpiredRead()
{
const int ttlSliding = 20;
const int ttlAbsolute = 500;
string etag = "etag";
CosmosCacheSession existingSession = new CosmosCacheSession();
existingSession.SessionKey = "key";
existingSession.Content = new byte[0];
existingSession.IsSlidingExpiration = true;
existingSession.TimeToLive = ttlSliding;
existingSession.AbsoluteSlidingExpiration = DateTimeOffset.UtcNow.AddMilliseconds(ttlAbsolute).ToUnixTimeSeconds();
Mock<ItemResponse<CosmosCacheSession>> mockedItemResponse = new Mock<ItemResponse<CosmosCacheSession>>();
Mock<CosmosClient> mockedClient = new Mock<CosmosClient>();
Mock<Container> mockedContainer = new Mock<Container>();
Mock<Database> mockedDatabase = new Mock<Database>();
Mock<ContainerResponse> mockedResponse = new Mock<ContainerResponse>();
mockedItemResponse.Setup(c => c.Resource).Returns(existingSession);
mockedItemResponse.Setup(c => c.ETag).Returns(etag);
mockedResponse.Setup(c => c.StatusCode).Returns(HttpStatusCode.OK);
mockedContainer.Setup(c => c.ReadContainerAsync(It.IsAny<ContainerRequestOptions>(), It.IsAny<CancellationToken>())).ReturnsAsync(mockedResponse.Object);
mockedContainer.Setup(c => c.ReadItemAsync<CosmosCacheSession>(It.Is<string>(id => id == "key"), It.IsAny<PartitionKey>(), It.IsAny<ItemRequestOptions>(), It.IsAny<CancellationToken>())).ReturnsAsync(mockedItemResponse.Object);
mockedClient.Setup(c => c.GetContainer(It.IsAny<string>(), It.IsAny<string>())).Returns(mockedContainer.Object);
mockedClient.Setup(c => c.GetDatabase(It.IsAny<string>())).Returns(mockedDatabase.Object);
mockedClient.Setup(x => x.Endpoint).Returns(new Uri("http://localhost"));
CosmosCache cache = new CosmosCache(Options.Create(new CosmosCacheOptions()
{
DatabaseName = "something",
ContainerName = "something",
CreateIfNotExists = true,
CosmosClient = mockedClient.Object
}));

Assert.NotNull(await cache.GetAsync("key"));
// Checks for Db existence due to CreateIfNotExists
mockedClient.Verify(c => c.CreateDatabaseIfNotExistsAsync(It.IsAny<string>(), It.IsAny<int?>(), It.IsAny<RequestOptions>(), It.IsAny<CancellationToken>()), Times.Once);
mockedContainer.Verify(c => c.ReadItemAsync<CosmosCacheSession>(It.Is<string>(id => id == "key"), It.IsAny<PartitionKey>(), It.IsAny<ItemRequestOptions>(), It.IsAny<CancellationToken>()), Times.Once);
mockedContainer.Verify(c => c.ReplaceItemAsync<CosmosCacheSession>(It.Is<CosmosCacheSession>(item => item.TimeToLive == ttlSliding), It.Is<string>(id => id == "key"), It.IsAny<PartitionKey?>(), It.IsAny<ItemRequestOptions>(), It.IsAny<CancellationToken>()), Times.Never);
}

private class DiagnosticsSink
{
private List<CosmosDiagnostics> capturedDiagnostics = new List<CosmosDiagnostics>();
Expand Down

0 comments on commit edf7ff1

Please sign in to comment.