Skip to content

Commit c87f2da

Browse files
authored
Bugfix - Enumerating collection with documents expiring causes early 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
1 parent fa80a84 commit c87f2da

File tree

5 files changed

+192
-5
lines changed

5 files changed

+192
-5
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ We'd love your contributions! If you want to contribute please read our [Contrib
364364
* [@imansafari1991](https://github.com/imansafari1991)
365365
* [@AndersenGans](https://github.com/AndersenGans)
366366
* [@mdrakib](https://github.com/mdrakib)
367+
* [@jrpavoncello](https://github.com/jrpavoncello)
367368

368369
<!-- Logo -->
369370
[Logo]: images/logo.svg

src/Redis.OM/Redis.OM.csproj

+3-3
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
<RootNamespace>Redis.OM</RootNamespace>
77
<Nullable>enable</Nullable>
88
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
9-
<PackageVersion>0.5.4</PackageVersion>
10-
<Version>0.5.4</Version>
11-
<PackageReleaseNotes>https://github.com/redis/redis-om-dotnet/releases/tag/v0.5.4</PackageReleaseNotes>
9+
<PackageVersion>0.5.5</PackageVersion>
10+
<Version>0.5.5</Version>
11+
<PackageReleaseNotes>https://github.com/redis/redis-om-dotnet/releases/tag/v0.5.5</PackageReleaseNotes>
1212
<Description>Object Mapping and More for Redis</Description>
1313
<Title>Redis OM</Title>
1414
<Authors>Steve Lorello</Authors>

src/Redis.OM/Searching/RedisCollectionEnumerator.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public bool MoveNext()
9494
switch (_started)
9595
{
9696
case true when _limited:
97-
case true when _records.Documents.Count < _query!.Limit!.Number:
97+
case true when _records.Documents.Count < _query!.Limit!.Number && _records.DocumentsSkippedCount == 0:
9898
return false;
9999
default:
100100
return GetNextChunk();
@@ -113,7 +113,7 @@ public async ValueTask<bool> MoveNextAsync()
113113
switch (_started)
114114
{
115115
case true when _limited:
116-
case true when _records.Documents.Count < _query!.Limit!.Number:
116+
case true when _records.Documents.Count < _query!.Limit!.Number && _records.DocumentsSkippedCount == 0:
117117
return false;
118118
default:
119119
return await GetNextChunkAsync();

src/Redis.OM/Searching/SearchResponse.cs

+11
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,18 @@ public SearchResponse(RedisReply val)
124124
var obj = RedisObjectHandler.FromHashSet<T>(documentHash);
125125
Documents.Add(docId, obj);
126126
}
127+
else
128+
{
129+
DocumentsSkippedCount++; // needed when a key expired while it was being enumerated by Redis.
130+
}
127131
}
128132
}
129133
}
130134

131135
private SearchResponse()
132136
{
133137
DocumentCount = 0;
138+
DocumentsSkippedCount = 0;
134139
Documents = new Dictionary<string, T>();
135140
}
136141

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

147+
/// <summary>
148+
/// Gets the number of documents skipped while enumerating the search result set.
149+
/// This can be indicative of documents that have expired during enumeration.
150+
/// </summary>
151+
public int DocumentsSkippedCount { get; private set; }
152+
142153
/// <summary>
143154
/// Gets the documents.
144155
/// </summary>

test/Redis.OM.Unit.Tests/RediSearchTests/SearchTests.cs

+175
Original file line numberDiff line numberDiff line change
@@ -3474,5 +3474,180 @@ public void TestConstantExpressionContains()
34743474
_ = collection.Where(lambada).ToList();
34753475
_substitute.Received().Execute("FT.SEARCH", "person-idx", "(@TagField:{James|Bond})", "LIMIT", "0", "100");
34763476
}
3477+
3478+
[Fact]
3479+
public async Task EnumerateAllWhenKeyExpires()
3480+
{
3481+
RedisReply firstReply = new RedisReply[]
3482+
{
3483+
new(2),
3484+
new("Redis.OM.Unit.Tests.RediSearchTests.Person:E912BED67BD64386B4FDC7322D"),
3485+
new(new RedisReply[]
3486+
{
3487+
"$",
3488+
"{\"Name\":\"Steve\",\"Age\":32,\"Height\":71.0, \"Id\":\"E912BED67BD64386B4FDC7322D\"}"
3489+
}),
3490+
new("Redis.OM.Unit.Tests.RediSearchTests.Person:01FVN836BNQGYMT80V7RCVY73N"),
3491+
// Key expired while executing the search
3492+
new(Array.Empty<RedisReply>())
3493+
};
3494+
RedisReply secondReply = new RedisReply[]
3495+
{
3496+
new(2),
3497+
new("Redis.OM.Unit.Tests.RediSearchTests.Person:4F6AE0A9BAE044E4B2D2186044"),
3498+
new(new RedisReply[]
3499+
{
3500+
"$",
3501+
"{\"Name\":\"Josh\",\"Age\":30,\"Height\":12.0, \"Id\":\"4F6AE0A9BAE044E4B2D2186044\"}"
3502+
})
3503+
};
3504+
RedisReply finalEmptyResult = new RedisReply[]
3505+
{
3506+
new(0),
3507+
};
3508+
3509+
_substitute.ClearSubstitute();
3510+
_substitute.ExecuteAsync(
3511+
"FT.SEARCH",
3512+
"person-idx",
3513+
"*",
3514+
"LIMIT",
3515+
"0",
3516+
"2").Returns(firstReply);
3517+
_substitute.ExecuteAsync(
3518+
"FT.SEARCH",
3519+
"person-idx",
3520+
"*",
3521+
"LIMIT",
3522+
"2",
3523+
"2").Returns(secondReply);
3524+
_substitute.ExecuteAsync(
3525+
"FT.SEARCH",
3526+
"person-idx",
3527+
"*",
3528+
"LIMIT",
3529+
"4",
3530+
"2").Returns(finalEmptyResult);
3531+
3532+
var people = new List<Person>();
3533+
// Chunk size 2 induces the iterator to call FT.SEARCH 3 times
3534+
await foreach (var person in new RedisCollection<Person>(_substitute, 2))
3535+
{
3536+
people.Add(person);
3537+
}
3538+
3539+
Assert.Equal(2, people.Count);
3540+
3541+
Assert.Equal("Steve", people[0].Name);
3542+
Assert.Equal("Josh", people[1].Name);
3543+
}
3544+
3545+
[Fact]
3546+
public async Task EnumerateAllWhenKeyExpiresAtEnd()
3547+
{
3548+
RedisReply firstReply = new RedisReply[]
3549+
{
3550+
new(2),
3551+
new("Redis.OM.Unit.Tests.RediSearchTests.Person:E912BED67BD64386B4FDC7322D"),
3552+
new(new RedisReply[]
3553+
{
3554+
"$",
3555+
"{\"Name\":\"Steve\",\"Age\":32,\"Height\":71.0, \"Id\":\"E912BED67BD64386B4FDC7322D\"}"
3556+
}),
3557+
new("Redis.OM.Unit.Tests.RediSearchTests.Person:4F6AE0A9BAE044E4B2D2186044"),
3558+
new(new RedisReply[]
3559+
{
3560+
"$",
3561+
"{\"Name\":\"Josh\",\"Age\":30,\"Height\":12.0, \"Id\":\"4F6AE0A9BAE044E4B2D2186044\"}"
3562+
})
3563+
};
3564+
RedisReply secondReply = new RedisReply[]
3565+
{
3566+
new(1),
3567+
new("Redis.OM.Unit.Tests.RediSearchTests.Person:01FVN836BNQGYMT80V7RCVY73N"),
3568+
// Key expired while executing the search
3569+
new(Array.Empty<RedisReply>())
3570+
};
3571+
RedisReply finalEmptyResult = new RedisReply[]
3572+
{
3573+
new(0),
3574+
};
3575+
3576+
_substitute.ClearSubstitute();
3577+
_substitute.ExecuteAsync(
3578+
"FT.SEARCH",
3579+
"person-idx",
3580+
"*",
3581+
"LIMIT",
3582+
"0",
3583+
"2").Returns(firstReply);
3584+
_substitute.ExecuteAsync(
3585+
"FT.SEARCH",
3586+
"person-idx",
3587+
"*",
3588+
"LIMIT",
3589+
"2",
3590+
"2").Returns(secondReply);
3591+
_substitute.ExecuteAsync(
3592+
"FT.SEARCH",
3593+
"person-idx",
3594+
"*",
3595+
"LIMIT",
3596+
"4",
3597+
"2").Returns(finalEmptyResult);
3598+
3599+
var people = new List<Person>();
3600+
// Chunk size 2 induces the iterator to call FT.SEARCH 3 times
3601+
await foreach (var person in new RedisCollection<Person>(_substitute, 2))
3602+
{
3603+
people.Add(person);
3604+
}
3605+
3606+
Assert.Equal(2, people.Count);
3607+
3608+
Assert.Equal("Steve", people[0].Name);
3609+
Assert.Equal("Josh", people[1].Name);
3610+
}
3611+
3612+
[Fact]
3613+
public async Task EnumerateAllButAllExpired()
3614+
{
3615+
RedisReply firstReply = new RedisReply[]
3616+
{
3617+
new(1),
3618+
new("Redis.OM.Unit.Tests.RediSearchTests.Person:01FVN836BNQGYMT80V7RCVY73N"),
3619+
// Key expired while executing the search
3620+
new(Array.Empty<RedisReply>())
3621+
};
3622+
RedisReply finalEmptyResult = new RedisReply[]
3623+
{
3624+
new(0),
3625+
};
3626+
3627+
_substitute.ClearSubstitute();
3628+
_substitute.ExecuteAsync(
3629+
"FT.SEARCH",
3630+
"person-idx",
3631+
"*",
3632+
"LIMIT",
3633+
"0",
3634+
"2").Returns(firstReply);
3635+
_substitute.ExecuteAsync(
3636+
"FT.SEARCH",
3637+
"person-idx",
3638+
"*",
3639+
"LIMIT",
3640+
"4",
3641+
"2").Returns(finalEmptyResult);
3642+
3643+
var people = new List<Person>();
3644+
// Chunk size 2 induces the iterator to call FT.SEARCH twice
3645+
await foreach (var person in new RedisCollection<Person>(_substitute, 2))
3646+
{
3647+
people.Add(person);
3648+
}
3649+
3650+
Assert.Empty(people);
3651+
}
34773652
}
34783653
}

0 commit comments

Comments
 (0)