Fix race condition with less than 1 seconds left (#84)

* Special case

* return

* test

* readme
This commit is contained in:
Matias Quaranta 2024-08-30 10:05:36 -07:00 коммит произвёл GitHub
Родитель f0ae628599
Коммит edf7ff1259
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
4 изменённых файлов: 61 добавлений и 1 удалений

Просмотреть файл

@ -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

Просмотреть файл

@ -129,6 +129,13 @@ namespace Microsoft.Extensions.Caching.Cosmos
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;
@ -224,6 +231,13 @@ namespace Microsoft.Extensions.Caching.Cosmos
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;

Просмотреть файл

@ -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>

Просмотреть файл

@ -715,6 +715,46 @@ namespace Microsoft.Extensions.Caching.Cosmos.Tests
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>();