Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #162 RedisScript fails for the CultureInfo tr-TR (and use BacklogPolicy.FailFast for backward compatibility) #173

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/DistributedLock.Redis/DistributedLock.Redis.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="StackExchange.Redis" Version="2.2.4" />
<PackageReference Include="StackExchange.Redis" Version="2.6.122" />
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<PackageReference Include="Nullable" Version="1.2.1" Condition="'$(TargetFramework)' != 'netstandard2.1'">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ private RedisServer(int? port, bool allowAdmin)
.RedirectStandardErrorTo(Console.Error);
ActiveServersByPort.Add(this.Port, this);
}
this.Multiplexer = ConnectionMultiplexer.Connect($"localhost:{this.Port}{(allowAdmin ? ",allowAdmin=true" : string.Empty)}");
this.Multiplexer = ConnectionMultiplexer.Connect(new ConfigurationOptions
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/StackExchange/StackExchange.Redis/blob/5504ed9a93f729f204e6cb53280cb4efa1ccd390/docs/Configuration.md says that there are two ways to set configuration. I changed this to using ConfigurationOptions because BacklogPolicy is one of the code-only options.

{
EndPoints = { { "localhost", this.Port } },
AllowAdmin = allowAdmin,
BacklogPolicy = BacklogPolicy.FailFast, // for backward compatibility with pre-2.5 versions
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some tests started failing after upgrading to 2.5.27+ as noted here. I believe this is because of the following update (see the following excerpt from the release notes):

2.5.27 (prerelease)

  • Adds: a backlog/retry mechanism for commands issued while a connection isn’t available (#1912 by NickCraver)
    • Commands will be queued if a multiplexer isn’t yet connected to a Redis server.
    • Commands will be queued if a connection is lost and then sent to the server when the connection is restored.
    • All commands queued will only remain in the backlog for the duration of the configured timeout.
    • To revert to previous behavior, a new ConfigurationOptions.BacklogPolicy is available - old behavior is configured via options.BacklogPolicy = BacklogPolicy.FailFast. This backlogs nothing and fails commands immediately if no connection is available.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. Which tests fail without this? Based on the description I would expect it to be tests that reproduce things like connection failure.

My biggest concern is whether the need for this setting means that the library won't work for users who don't turn this on. Thoughts?

Finally, would be good to have a concise summary of the information in your comment + my questions above in the source itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For (my) future reference, I just updated StackExchange.Redis to 2.5.43 and noticed the following two failures:

Core_ReaderWriterAsMutex_RedisReaderWriter_Redis2x1Database_RedisSynchronizationStrategy_Redis2x1Database_RedisSynchronizationStrategy_Redis2x1DatabaseTest
 TestParallelism
 Source: DistributedLockCoreTestCases.cs line 162
 Duration: 30 sec
Message: 
Timed out! (only 3/100 completed)
Expected: not same as <System.Threading.Tasks.Task+DelayPromise>
But was: <System.Threading.Tasks.Task+DelayPromise>

Core_Redis_Redis2x1Database_RedisSynchronizationStrategy_Redis2x1DatabaseTest
 TestParallelism
 Source: DistributedLockCoreTestCases.cs line 162
 Duration: 30 sec
Message: 
Timed out! (only 0/100 completed)
Expected: not same as <System.Threading.Tasks.Task+DelayPromise>
But was: <System.Threading.Tasks.Task+DelayPromise>

Note that the number of completed tests can vary.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up figuring out what I think is a good path forward here: https://github.com/madelson/DistributedLock/blob/release-2.4/src/DistributedLock.Redis/RedLock/RedLockAcquire.cs#L155

Appreciate you bringing this to my attention.

});
// Clean the db to ensure it is empty. Running an arbitrary command also ensures that
// the db successfully spun up before we proceed (Connect seemingly can complete before that happens).
// This is particularly important for cross-process locking where the lock taker process
Expand Down
37 changes: 34 additions & 3 deletions src/DistributedLock.Tests/Tests/Redis/RedisDistributedLockTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
using Moq;
using NUnit.Framework;
using StackExchange.Redis;
using System.Globalization;

namespace Medallion.Threading.Tests.Redis;

[Category("CI")]
[NonParallelizable] // one of the tests temporarily changes CurrentCulture
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't need this. CurrentCulture is an asynclocal value (this is critical since otherwise you couldn't set different cultures for different concurrent web requests), so it shouldn't interfere with any concurrently-executing tests.

We only need to worry about this when setting truly-global values like environment variables that affect all threads at once.

public class RedisDistributedLockTest
{
[Test]
[Test, Category("CI")]
public void TestName()
{
const string Name = "\0🐉汉字\b\r\n\\";
Expand All @@ -17,7 +18,7 @@ public void TestName()
@lock.Key.ShouldEqual(new RedisKey(Name));
}

[Test]
[Test, Category("CI")]
public void TestValidatesConstructorParameters()
{
var database = new Mock<IDatabase>(MockBehavior.Strict).Object;
Expand All @@ -28,4 +29,34 @@ public void TestValidatesConstructorParameters()
Assert.Throws<ArgumentNullException>(() => new RedisDistributedLock("key", new[] { database, null! }));
Assert.Throws<ArgumentException>(() => new RedisDistributedLock("key", Enumerable.Empty<IDatabase>()));
}

/// <summary>
/// Reproduces the bug in https://github.com/madelson/DistributedLock/issues/162
/// where a Redis lock couldn't be acquired if the current CultureInfo was tr-TR,
/// due to a bug in the underlying StackExchange.Redis package.
///
/// This is because there are both "dotted i" and "dotless i" in some Turkic languages:
/// https://en.wikipedia.org/wiki/Dotted_and_dotless_I_in_computing
/// </summary>
[Test] // exclude from CI because RedisServer is local-only
public void TestCanAcquireLockWhenCurrentCultureIsTurkishTurkey()
{
var originalCultureInfo = Thread.CurrentThread.CurrentCulture;

try
{
Thread.CurrentThread.CurrentCulture = new("tr-TR");
CultureInfo.CurrentCulture.ClearCachedData();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect these 2 lines to just be:

CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("tr-TR");


var @lock = new RedisDistributedLock(
TestHelper.UniqueName,
RedisServer.GetDefaultServer(0).Multiplexer.GetDatabase()
);
@lock.Acquire().Dispose();
}
finally
{
Thread.CurrentThread.CurrentCulture = originalCultureInfo;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CultureInfo.CurrentCulture = originalCultureInfo;

}
}
}