Skip to content

Commit

Permalink
Bugfix - Enumerating collection with documents expiring causes early …
Browse files Browse the repository at this point in the history
…return (#422)

Fix bug where enumerating a collection while documents are actively expiring can result in an early return with less documents than expected in the result
  • Loading branch information
jrpavoncello authored Nov 27, 2023
1 parent fa80a84 commit c87f2da
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 5 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ We'd love your contributions! If you want to contribute please read our [Contrib
* [@imansafari1991](https://github.com/imansafari1991)
* [@AndersenGans](https://github.com/AndersenGans)
* [@mdrakib](https://github.com/mdrakib)
* [@jrpavoncello](https://github.com/jrpavoncello)

<!-- Logo -->
[Logo]: images/logo.svg
Expand Down
6 changes: 3 additions & 3 deletions src/Redis.OM/Redis.OM.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
<RootNamespace>Redis.OM</RootNamespace>
<Nullable>enable</Nullable>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<PackageVersion>0.5.4</PackageVersion>
<Version>0.5.4</Version>
<PackageReleaseNotes>https://github.com/redis/redis-om-dotnet/releases/tag/v0.5.4</PackageReleaseNotes>
<PackageVersion>0.5.5</PackageVersion>
<Version>0.5.5</Version>
<PackageReleaseNotes>https://github.com/redis/redis-om-dotnet/releases/tag/v0.5.5</PackageReleaseNotes>
<Description>Object Mapping and More for Redis</Description>
<Title>Redis OM</Title>
<Authors>Steve Lorello</Authors>
Expand Down
4 changes: 2 additions & 2 deletions src/Redis.OM/Searching/RedisCollectionEnumerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public bool MoveNext()
switch (_started)
{
case true when _limited:
case true when _records.Documents.Count < _query!.Limit!.Number:
case true when _records.Documents.Count < _query!.Limit!.Number && _records.DocumentsSkippedCount == 0:
return false;
default:
return GetNextChunk();
Expand All @@ -113,7 +113,7 @@ public async ValueTask<bool> MoveNextAsync()
switch (_started)
{
case true when _limited:
case true when _records.Documents.Count < _query!.Limit!.Number:
case true when _records.Documents.Count < _query!.Limit!.Number && _records.DocumentsSkippedCount == 0:
return false;
default:
return await GetNextChunkAsync();
Expand Down
11 changes: 11 additions & 0 deletions src/Redis.OM/Searching/SearchResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,13 +124,18 @@ public SearchResponse(RedisReply val)
var obj = RedisObjectHandler.FromHashSet<T>(documentHash);
Documents.Add(docId, obj);
}
else
{
DocumentsSkippedCount++; // needed when a key expired while it was being enumerated by Redis.
}
}
}
}

private SearchResponse()
{
DocumentCount = 0;
DocumentsSkippedCount = 0;
Documents = new Dictionary<string, T>();
}

Expand All @@ -139,6 +144,12 @@ private SearchResponse()
/// </summary>
public long DocumentCount { get; set; }

/// <summary>
/// Gets the number of documents skipped while enumerating the search result set.
/// This can be indicative of documents that have expired during enumeration.
/// </summary>
public int DocumentsSkippedCount { get; private set; }

/// <summary>
/// Gets the documents.
/// </summary>
Expand Down
175 changes: 175 additions & 0 deletions test/Redis.OM.Unit.Tests/RediSearchTests/SearchTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3474,5 +3474,180 @@ public void TestConstantExpressionContains()
_ = collection.Where(lambada).ToList();
_substitute.Received().Execute("FT.SEARCH", "person-idx", "(@TagField:{James|Bond})", "LIMIT", "0", "100");
}

[Fact]
public async Task EnumerateAllWhenKeyExpires()
{
RedisReply firstReply = new RedisReply[]
{
new(2),
new("Redis.OM.Unit.Tests.RediSearchTests.Person:E912BED67BD64386B4FDC7322D"),
new(new RedisReply[]
{
"$",
"{\"Name\":\"Steve\",\"Age\":32,\"Height\":71.0, \"Id\":\"E912BED67BD64386B4FDC7322D\"}"
}),
new("Redis.OM.Unit.Tests.RediSearchTests.Person:01FVN836BNQGYMT80V7RCVY73N"),
// Key expired while executing the search
new(Array.Empty<RedisReply>())
};
RedisReply secondReply = new RedisReply[]
{
new(2),
new("Redis.OM.Unit.Tests.RediSearchTests.Person:4F6AE0A9BAE044E4B2D2186044"),
new(new RedisReply[]
{
"$",
"{\"Name\":\"Josh\",\"Age\":30,\"Height\":12.0, \"Id\":\"4F6AE0A9BAE044E4B2D2186044\"}"
})
};
RedisReply finalEmptyResult = new RedisReply[]
{
new(0),
};

_substitute.ClearSubstitute();
_substitute.ExecuteAsync(
"FT.SEARCH",
"person-idx",
"*",
"LIMIT",
"0",
"2").Returns(firstReply);
_substitute.ExecuteAsync(
"FT.SEARCH",
"person-idx",
"*",
"LIMIT",
"2",
"2").Returns(secondReply);
_substitute.ExecuteAsync(
"FT.SEARCH",
"person-idx",
"*",
"LIMIT",
"4",
"2").Returns(finalEmptyResult);

var people = new List<Person>();
// Chunk size 2 induces the iterator to call FT.SEARCH 3 times
await foreach (var person in new RedisCollection<Person>(_substitute, 2))
{
people.Add(person);
}

Assert.Equal(2, people.Count);

Assert.Equal("Steve", people[0].Name);
Assert.Equal("Josh", people[1].Name);
}

[Fact]
public async Task EnumerateAllWhenKeyExpiresAtEnd()
{
RedisReply firstReply = new RedisReply[]
{
new(2),
new("Redis.OM.Unit.Tests.RediSearchTests.Person:E912BED67BD64386B4FDC7322D"),
new(new RedisReply[]
{
"$",
"{\"Name\":\"Steve\",\"Age\":32,\"Height\":71.0, \"Id\":\"E912BED67BD64386B4FDC7322D\"}"
}),
new("Redis.OM.Unit.Tests.RediSearchTests.Person:4F6AE0A9BAE044E4B2D2186044"),
new(new RedisReply[]
{
"$",
"{\"Name\":\"Josh\",\"Age\":30,\"Height\":12.0, \"Id\":\"4F6AE0A9BAE044E4B2D2186044\"}"
})
};
RedisReply secondReply = new RedisReply[]
{
new(1),
new("Redis.OM.Unit.Tests.RediSearchTests.Person:01FVN836BNQGYMT80V7RCVY73N"),
// Key expired while executing the search
new(Array.Empty<RedisReply>())
};
RedisReply finalEmptyResult = new RedisReply[]
{
new(0),
};

_substitute.ClearSubstitute();
_substitute.ExecuteAsync(
"FT.SEARCH",
"person-idx",
"*",
"LIMIT",
"0",
"2").Returns(firstReply);
_substitute.ExecuteAsync(
"FT.SEARCH",
"person-idx",
"*",
"LIMIT",
"2",
"2").Returns(secondReply);
_substitute.ExecuteAsync(
"FT.SEARCH",
"person-idx",
"*",
"LIMIT",
"4",
"2").Returns(finalEmptyResult);

var people = new List<Person>();
// Chunk size 2 induces the iterator to call FT.SEARCH 3 times
await foreach (var person in new RedisCollection<Person>(_substitute, 2))
{
people.Add(person);
}

Assert.Equal(2, people.Count);

Assert.Equal("Steve", people[0].Name);
Assert.Equal("Josh", people[1].Name);
}

[Fact]
public async Task EnumerateAllButAllExpired()
{
RedisReply firstReply = new RedisReply[]
{
new(1),
new("Redis.OM.Unit.Tests.RediSearchTests.Person:01FVN836BNQGYMT80V7RCVY73N"),
// Key expired while executing the search
new(Array.Empty<RedisReply>())
};
RedisReply finalEmptyResult = new RedisReply[]
{
new(0),
};

_substitute.ClearSubstitute();
_substitute.ExecuteAsync(
"FT.SEARCH",
"person-idx",
"*",
"LIMIT",
"0",
"2").Returns(firstReply);
_substitute.ExecuteAsync(
"FT.SEARCH",
"person-idx",
"*",
"LIMIT",
"4",
"2").Returns(finalEmptyResult);

var people = new List<Person>();
// Chunk size 2 induces the iterator to call FT.SEARCH twice
await foreach (var person in new RedisCollection<Person>(_substitute, 2))
{
people.Add(person);
}

Assert.Empty(people);
}
}
}

0 comments on commit c87f2da

Please sign in to comment.