Skip to content

Commit 466c4b7

Browse files
authored
Stabilize CI, especially on Windows (#3109)
* move KeyIdle[Async]Tests to a server without replication * stabilize Windows CI * don't run the `OBJECT IDLETIME` tests in WSL * skip sentinel tests on Windows CI
1 parent 8f47bf6 commit 466c4b7

6 files changed

Lines changed: 67 additions & 7 deletions

File tree

tests/StackExchange.Redis.Tests/ClusterTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ public async Task TestIdentity()
146146
[Fact]
147147
public async Task IntentionalWrongServer()
148148
{
149+
SkipOnWindowsRelease();
149150
static string? StringGet(IServer server, RedisKey key, CommandFlags flags = CommandFlags.None)
150151
=> (string?)server.Execute(0, "GET", [key], flags);
151152

tests/StackExchange.Redis.Tests/KeyIdleAsyncTests.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,35 @@ namespace StackExchange.Redis.Tests;
88
[RunPerProtocol]
99
public class KeyIdleAsyncTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture)
1010
{
11+
// Target the standalone secure server (6381) rather than the default primary (6379).
12+
// OBJECT IDLETIME is reset via the value's LRU access clock, but Redis deliberately
13+
// suppresses that update while a save/AOF/replication-sync child is active (copy-on-write
14+
// avoidance). The default primary has a replica attached, and every replica full-sync forks
15+
// such a child; if one overlaps a touch/read in these tests, the idle time isn't reset and the
16+
// test flakes (observed on Windows CI). 6381 has no replica and no persistence, so no fork ever
17+
// suppresses the reset and the behaviour is deterministic. Overriding GetConfiguration here also
18+
// opts these tests out of the shared connection fixture, giving each a dedicated 6381 connection.
19+
protected override string GetConfiguration()
20+
=> TestConfig.Current.SecureServerAndPort + ",password=" + TestConfig.Current.SecurePassword;
21+
1122
[Fact]
1223
public async Task IdleTimeAsync()
1324
{
25+
SkipOnWindowsRelease("WSL exacerbates replication time");
1426
await using var conn = Create();
1527

1628
RedisKey key = Me();
1729
var db = conn.GetDatabase();
1830
db.KeyDelete(key, CommandFlags.FireAndForget);
1931
db.StringSet(key, "new value", flags: CommandFlags.FireAndForget);
32+
var timer = Stopwatch.StartNew();
2033
await Task.Delay(2000).ForAwait();
2134
var idleTime = await db.KeyIdleTimeAsync(key).ForAwait();
22-
Assert.True(idleTime > TimeSpan.Zero, "First check");
35+
Assert.True(idleTime > TimeSpan.Zero, $"First check: {idleTime} should be > 0; elapsed: {timer.ElapsedMilliseconds}ms");
2336

2437
db.StringSet(key, "new value2", flags: CommandFlags.FireAndForget);
2538
var idleTime2 = await db.KeyIdleTimeAsync(key).ForAwait();
26-
Assert.True(idleTime2 < idleTime, "Second check");
39+
Assert.True(idleTime2 < idleTime, $"Second check: {idleTime2} should be < {idleTime}; elapsed: {timer.ElapsedMilliseconds}ms");
2740

2841
db.KeyDelete(key);
2942
var idleTime3 = await db.KeyIdleTimeAsync(key).ForAwait();
@@ -33,6 +46,7 @@ public async Task IdleTimeAsync()
3346
[Fact]
3447
public async Task TouchIdleTimeAsync()
3548
{
49+
SkipOnWindowsRelease("WSL exacerbates replication time");
3650
await using var conn = Create(require: RedisFeatures.v3_2_1);
3751

3852
RedisKey key = Me();
Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Diagnostics;
23
using System.Threading.Tasks;
34
using Xunit;
45

@@ -7,22 +8,35 @@ namespace StackExchange.Redis.Tests;
78
[RunPerProtocol]
89
public class KeyIdleTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture)
910
{
11+
// Target the standalone secure server (6381) rather than the default primary (6379).
12+
// OBJECT IDLETIME is reset via the value's LRU access clock, but Redis deliberately
13+
// suppresses that update while a save/AOF/replication-sync child is active (copy-on-write
14+
// avoidance). The default primary has a replica attached, and every replica full-sync forks
15+
// such a child; if one overlaps a touch/read in these tests, the idle time isn't reset and the
16+
// test flakes (observed on Windows CI). 6381 has no replica and no persistence, so no fork ever
17+
// suppresses the reset and the behaviour is deterministic. Overriding GetConfiguration here also
18+
// opts these tests out of the shared connection fixture, giving each a dedicated 6381 connection.
19+
protected override string GetConfiguration()
20+
=> TestConfig.Current.SecureServerAndPort + ",password=" + TestConfig.Current.SecurePassword;
21+
1022
[Fact]
1123
public async Task IdleTime()
1224
{
25+
SkipOnWindowsRelease("WSL exacerbates replication time");
1326
await using var conn = Create();
1427

1528
RedisKey key = Me();
1629
var db = conn.GetDatabase();
1730
db.KeyDelete(key, CommandFlags.FireAndForget);
1831
db.StringSet(key, "new value", flags: CommandFlags.FireAndForget);
32+
var timer = Stopwatch.StartNew();
1933
await Task.Delay(2000).ForAwait();
2034
var idleTime = db.KeyIdleTime(key);
21-
Assert.True(idleTime > TimeSpan.Zero);
35+
Assert.True(idleTime > TimeSpan.Zero, $"First check: {idleTime} should be > 0; elapsed: {timer.ElapsedMilliseconds}ms");
2236

2337
db.StringSet(key, "new value2", flags: CommandFlags.FireAndForget);
2438
var idleTime2 = db.KeyIdleTime(key);
25-
Assert.True(idleTime2 < idleTime);
39+
Assert.True(idleTime2 < idleTime, $"Second check: {idleTime2} should be < {idleTime}; elapsed: {timer.ElapsedMilliseconds}ms");
2640

2741
db.KeyDelete(key);
2842
var idleTime3 = db.KeyIdleTime(key);
@@ -32,18 +46,20 @@ public async Task IdleTime()
3246
[Fact]
3347
public async Task TouchIdleTime()
3448
{
49+
SkipOnWindowsRelease("WSL exacerbates replication time");
3550
await using var conn = Create(require: RedisFeatures.v3_2_1);
3651

3752
RedisKey key = Me();
3853
var db = conn.GetDatabase();
3954
db.KeyDelete(key, CommandFlags.FireAndForget);
4055
db.StringSet(key, "new value", flags: CommandFlags.FireAndForget);
56+
var timer = Stopwatch.StartNew();
4157
await Task.Delay(2000).ForAwait();
4258
var idleTime = db.KeyIdleTime(key);
43-
Assert.True(idleTime > TimeSpan.Zero, "First check");
59+
Assert.True(idleTime > TimeSpan.Zero, $"First check: {idleTime} should be > 0; elapsed: {timer.ElapsedMilliseconds}ms");
4460

45-
Assert.True(db.KeyTouch(key), "Second check");
61+
Assert.True(db.KeyTouch(key), $"Second check: should be True; elapsed: {timer.ElapsedMilliseconds}ms");
4662
var idleTime1 = db.KeyIdleTime(key);
47-
Assert.True(idleTime1 < idleTime, "Third check");
63+
Assert.True(idleTime1 < idleTime, $"Third check: {idleTime1} should be < {idleTime}; elapsed: {timer.ElapsedMilliseconds}ms");
4864
}
4965
}

tests/StackExchange.Redis.Tests/RoleTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public class Roles(ITestOutputHelper output, SharedConnectionFixture fixture) :
1313
[InlineData(false)]
1414
public async Task PrimaryRole(bool allowAdmin) // should work with or without admin now
1515
{
16+
SkipOnWindowsRelease();
1617
await using var conn = Create(allowAdmin: allowAdmin);
1718
var servers = conn.GetServers();
1819
Log("Server list:");

tests/StackExchange.Redis.Tests/SentinelTests.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public class SentinelTests(ITestOutputHelper output) : SentinelBase(output)
1212
[Fact]
1313
public async Task PrimaryConnectTest()
1414
{
15+
SkipOnWindowsRelease();
1516
var connectionString = $"{TestConfig.Current.SentinelServer},serviceName={ServiceOptions.ServiceName},allowAdmin=true";
1617

1718
var conn = ConnectionMultiplexer.Connect(connectionString);
@@ -49,6 +50,7 @@ public async Task PrimaryConnectTest()
4950
[Fact]
5051
public async Task PrimaryConnectAsyncTest()
5152
{
53+
SkipOnWindowsRelease();
5254
var connectionString = $"{TestConfig.Current.SentinelServer},serviceName={ServiceOptions.ServiceName},allowAdmin=true";
5355
var conn = await ConnectionMultiplexer.ConnectAsync(connectionString);
5456

@@ -86,6 +88,7 @@ public async Task PrimaryConnectAsyncTest()
8688
[RunPerProtocol]
8789
public async Task SentinelConnectTest()
8890
{
91+
SkipOnWindowsRelease();
8992
var options = ServiceOptions.Clone();
9093
options.EndPoints.Add(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA);
9194
await using var conn = ConnectionMultiplexer.SentinelConnect(options);
@@ -98,6 +101,7 @@ public async Task SentinelConnectTest()
98101
[Fact]
99102
public async Task SentinelRepeatConnectTest()
100103
{
104+
SkipOnWindowsRelease();
101105
var options = ConfigurationOptions.Parse($"{TestConfig.Current.SentinelServer}:{TestConfig.Current.SentinelPortA}");
102106
options.ServiceName = ServiceName;
103107
options.AllowAdmin = true;
@@ -130,6 +134,7 @@ public async Task SentinelRepeatConnectTest()
130134
[Fact]
131135
public async Task SentinelConnectAsyncTest()
132136
{
137+
SkipOnWindowsRelease();
133138
var options = ServiceOptions.Clone();
134139
options.EndPoints.Add(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA);
135140
var conn = await ConnectionMultiplexer.SentinelConnectAsync(options);
@@ -142,6 +147,7 @@ public async Task SentinelConnectAsyncTest()
142147
[Fact]
143148
public void SentinelRole()
144149
{
150+
SkipOnWindowsRelease();
145151
foreach (var server in SentinelsServers)
146152
{
147153
var role = server.Role();
@@ -155,6 +161,7 @@ public void SentinelRole()
155161
[Fact]
156162
public async Task PingTest()
157163
{
164+
SkipOnWindowsRelease();
158165
var test = await SentinelServerA.PingAsync();
159166
Log("ping to sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA, test.TotalMilliseconds);
160167
test = await SentinelServerB.PingAsync();
@@ -166,6 +173,7 @@ public async Task PingTest()
166173
[Fact]
167174
public void SentinelGetPrimaryAddressByNameTest()
168175
{
176+
SkipOnWindowsRelease();
169177
foreach (var server in SentinelsServers)
170178
{
171179
var primary = server.SentinelMaster(ServiceName);
@@ -182,6 +190,7 @@ public void SentinelGetPrimaryAddressByNameTest()
182190
[Fact]
183191
public async Task SentinelGetPrimaryAddressByNameAsyncTest()
184192
{
193+
SkipOnWindowsRelease();
185194
foreach (var server in SentinelsServers)
186195
{
187196
var primary = server.SentinelMaster(ServiceName);
@@ -198,6 +207,7 @@ public async Task SentinelGetPrimaryAddressByNameAsyncTest()
198207
[Fact]
199208
public void SentinelGetMasterAddressByNameNegativeTest()
200209
{
210+
SkipOnWindowsRelease();
201211
foreach (var server in SentinelsServers)
202212
{
203213
var endpoint = server.SentinelGetMasterAddressByName("FakeServiceName");
@@ -208,6 +218,7 @@ public void SentinelGetMasterAddressByNameNegativeTest()
208218
[Fact]
209219
public async Task SentinelGetMasterAddressByNameAsyncNegativeTest()
210220
{
221+
SkipOnWindowsRelease();
211222
foreach (var server in SentinelsServers)
212223
{
213224
var endpoint = await server.SentinelGetMasterAddressByNameAsync("FakeServiceName").ForAwait();
@@ -218,6 +229,7 @@ public async Task SentinelGetMasterAddressByNameAsyncNegativeTest()
218229
[Fact]
219230
public void SentinelPrimaryTest()
220231
{
232+
SkipOnWindowsRelease();
221233
foreach (var server in SentinelsServers)
222234
{
223235
var dict = server.SentinelMaster(ServiceName).ToDictionary();
@@ -233,6 +245,7 @@ public void SentinelPrimaryTest()
233245
[Fact]
234246
public async Task SentinelPrimaryAsyncTest()
235247
{
248+
SkipOnWindowsRelease();
236249
foreach (var server in SentinelsServers)
237250
{
238251
var results = await server.SentinelMasterAsync(ServiceName).ForAwait();
@@ -248,6 +261,7 @@ public async Task SentinelPrimaryAsyncTest()
248261
[Fact]
249262
public void SentinelSentinelsTest()
250263
{
264+
SkipOnWindowsRelease();
251265
var sentinels = SentinelServerA.SentinelSentinels(ServiceName);
252266

253267
var expected = new List<string?>
@@ -305,6 +319,7 @@ public void SentinelSentinelsTest()
305319
[Fact]
306320
public async Task SentinelSentinelsAsyncTest()
307321
{
322+
SkipOnWindowsRelease();
308323
var sentinels = await SentinelServerA.SentinelSentinelsAsync(ServiceName).ForAwait();
309324
var expected = new List<string?>
310325
{
@@ -363,6 +378,7 @@ public async Task SentinelSentinelsAsyncTest()
363378
[Fact]
364379
public void SentinelPrimariesTest()
365380
{
381+
SkipOnWindowsRelease();
366382
var primaryConfigs = SentinelServerA.SentinelMasters();
367383
Assert.Single(primaryConfigs);
368384
Assert.True(primaryConfigs[0].ToDictionary().ContainsKey("name"), "replicaConfigs contains 'name'");
@@ -380,6 +396,7 @@ public void SentinelPrimariesTest()
380396
[Fact]
381397
public async Task SentinelPrimariesAsyncTest()
382398
{
399+
SkipOnWindowsRelease();
383400
var primaryConfigs = await SentinelServerA.SentinelMastersAsync().ForAwait();
384401
Assert.Single(primaryConfigs);
385402
Assert.True(primaryConfigs[0].ToDictionary().ContainsKey("name"), "replicaConfigs contains 'name'");
@@ -397,6 +414,7 @@ public async Task SentinelPrimariesAsyncTest()
397414
[Fact]
398415
public async Task SentinelReplicasTest()
399416
{
417+
SkipOnWindowsRelease();
400418
// Give previous test run a moment to reset when multi-framework failover is in play.
401419
await UntilConditionAsync(TimeSpan.FromSeconds(5), () => SentinelServerA.SentinelReplicas(ServiceName).Length > 0);
402420

@@ -417,6 +435,7 @@ public async Task SentinelReplicasTest()
417435
[Fact]
418436
public async Task SentinelReplicasAsyncTest()
419437
{
438+
SkipOnWindowsRelease();
420439
// Give previous test run a moment to reset when multi-framework failover is in play.
421440
await UntilConditionAsync(TimeSpan.FromSeconds(5), () => SentinelServerA.SentinelReplicas(ServiceName).Length > 0);
422441

@@ -436,6 +455,7 @@ public async Task SentinelReplicasAsyncTest()
436455
[Fact]
437456
public async Task SentinelGetSentinelAddressesTest()
438457
{
458+
SkipOnWindowsRelease();
439459
var addresses = await SentinelServerA.SentinelGetSentinelAddressesAsync(ServiceName).ForAwait();
440460
Assert.Contains(SentinelServerB.EndPoint, addresses);
441461
Assert.Contains(SentinelServerC.EndPoint, addresses);
@@ -452,6 +472,7 @@ public async Task SentinelGetSentinelAddressesTest()
452472
[Fact]
453473
public async Task ReadOnlyConnectionReplicasTest()
454474
{
475+
SkipOnWindowsRelease();
455476
var replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName);
456477
if (replicas.Length == 0)
457478
{

tests/StackExchange.Redis.Tests/TestBase.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Linq;
66
using System.Runtime;
77
using System.Runtime.CompilerServices;
8+
using System.Runtime.InteropServices;
89
using System.Threading;
910
using System.Threading.Tasks;
1011
using StackExchange.Redis.Configuration;
@@ -702,4 +703,10 @@ public ValueTask DisposeAsync()
702703
return default;
703704
}
704705
}
706+
707+
[Conditional("RELEASE")]
708+
protected void SkipOnWindowsRelease(string? message = null) // typically used for tests that are super brittle on the Windows CI
709+
{
710+
Assert.SkipWhen(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), message ?? "skipping on Windows");
711+
}
705712
}

0 commit comments

Comments
 (0)