Skip to content

Commit

Permalink
add RedisChannel UseImplicitAutoPattern and IsPatternBased
Browse files Browse the repository at this point in the history
  • Loading branch information
mgravell committed Jun 13, 2023
1 parent ae6419a commit 3f0be76
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 12 deletions.
1 change: 1 addition & 0 deletions docs/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Current package versions:

## Unreleased

- Fix [#2479](https://github.com/StackExchange/StackExchange.Redis/issues/2479): Add `RedisChannel.UseImplicitAutoPattern` (global) and `RedisChannel.IsPatternBased` ([#2480 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2480))
- Fix [#2449](https://github.com/StackExchange/StackExchange.Redis/issues/2449): Update `Pipelines.Sockets.Unofficial` to `v2.2.8` to support native AOT ([#2456 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2456))

## 2.6.111
Expand Down
3 changes: 3 additions & 0 deletions src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1259,6 +1259,7 @@ StackExchange.Redis.Proxy.Twemproxy = 1 -> StackExchange.Redis.Proxy
StackExchange.Redis.RedisChannel
StackExchange.Redis.RedisChannel.Equals(StackExchange.Redis.RedisChannel other) -> bool
StackExchange.Redis.RedisChannel.IsNullOrEmpty.get -> bool
StackExchange.Redis.RedisChannel.IsPatternBased.get -> bool
StackExchange.Redis.RedisChannel.PatternMode
StackExchange.Redis.RedisChannel.PatternMode.Auto = 0 -> StackExchange.Redis.RedisChannel.PatternMode
StackExchange.Redis.RedisChannel.PatternMode.Literal = 1 -> StackExchange.Redis.RedisChannel.PatternMode
Expand Down Expand Up @@ -1667,6 +1668,8 @@ static StackExchange.Redis.RedisChannel.operator ==(StackExchange.Redis.RedisCha
static StackExchange.Redis.RedisChannel.operator ==(StackExchange.Redis.RedisChannel x, StackExchange.Redis.RedisChannel y) -> bool
static StackExchange.Redis.RedisChannel.operator ==(StackExchange.Redis.RedisChannel x, string! y) -> bool
static StackExchange.Redis.RedisChannel.operator ==(string! x, StackExchange.Redis.RedisChannel y) -> bool
static StackExchange.Redis.RedisChannel.UseImplicitAutoPattern.get -> bool
static StackExchange.Redis.RedisChannel.UseImplicitAutoPattern.set -> void
static StackExchange.Redis.RedisFeatures.operator !=(StackExchange.Redis.RedisFeatures left, StackExchange.Redis.RedisFeatures right) -> bool
static StackExchange.Redis.RedisFeatures.operator ==(StackExchange.Redis.RedisFeatures left, StackExchange.Redis.RedisFeatures right) -> bool
static StackExchange.Redis.RedisKey.implicit operator byte[]?(StackExchange.Redis.RedisKey key) -> byte[]?
Expand Down
49 changes: 38 additions & 11 deletions src/StackExchange.Redis/RedisChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,50 @@ namespace StackExchange.Redis
public readonly struct RedisChannel : IEquatable<RedisChannel>
{
internal readonly byte[]? Value;
internal readonly bool IsPatternBased;
internal readonly bool _isPatternBased;

/// <summary>
/// Indicates whether the channel-name is either null or a zero-length value.
/// </summary>
public bool IsNullOrEmpty => Value == null || Value.Length == 0;

/// <summary>
/// Indicates whether this channel represents a wildcard pattern (see <c>PSUBSCRIBE</c>)
/// </summary>
public bool IsPatternBased => _isPatternBased;

internal bool IsNull => Value == null;


/// <summary>
/// Indicates whether channels should use <see cref="PatternMode.Auto"/> when no <see cref="PatternMode"/>
/// is specified; this is enabled by default, but can be disabled to avoid unexpected wildcard scenarios.
/// </summary>
public static bool UseImplicitAutoPattern
{
get => s_DefaultPatternMode == PatternMode.Auto;
set => s_DefaultPatternMode = value ? PatternMode.Auto : PatternMode.Literal;
}
private static PatternMode s_DefaultPatternMode = PatternMode.Auto;

/// <summary>
/// Create a new redis channel from a buffer, explicitly controlling the pattern mode.
/// </summary>
/// <param name="value">The name of the channel to create.</param>
/// <param name="mode">The mode for name matching.</param>
public RedisChannel(byte[]? value, PatternMode mode) : this(value, DeterminePatternBased(value, mode)) {}
public RedisChannel(byte[]? value, PatternMode mode) : this(value, DeterminePatternBased(value, mode)) { }

/// <summary>
/// Create a new redis channel from a string, explicitly controlling the pattern mode.
/// </summary>
/// <param name="value">The string name of the channel to create.</param>
/// <param name="mode">The mode for name matching.</param>
public RedisChannel(string value, PatternMode mode) : this(value == null ? null : Encoding.UTF8.GetBytes(value), mode) {}
public RedisChannel(string value, PatternMode mode) : this(value == null ? null : Encoding.UTF8.GetBytes(value), mode) { }

private RedisChannel(byte[]? value, bool isPatternBased)
{
Value = value;
IsPatternBased = isPatternBased;
_isPatternBased = isPatternBased;
}

private static bool DeterminePatternBased(byte[]? value, PatternMode mode) => mode switch
Expand Down Expand Up @@ -87,7 +104,7 @@ private RedisChannel(byte[]? value, bool isPatternBased)
/// <param name="x">The first <see cref="RedisChannel"/> to compare.</param>
/// <param name="y">The second <see cref="RedisChannel"/> to compare.</param>
public static bool operator ==(RedisChannel x, RedisChannel y) =>
x.IsPatternBased == y.IsPatternBased && RedisValue.Equals(x.Value, y.Value);
x._isPatternBased == y._isPatternBased && RedisValue.Equals(x.Value, y.Value);

/// <summary>
/// Indicate whether two channel names are equal.
Expand Down Expand Up @@ -135,10 +152,10 @@ private RedisChannel(byte[]? value, bool isPatternBased)
/// Indicate whether two channel names are equal.
/// </summary>
/// <param name="other">The <see cref="RedisChannel"/> to compare to.</param>
public bool Equals(RedisChannel other) => IsPatternBased == other.IsPatternBased && RedisValue.Equals(Value, other.Value);
public bool Equals(RedisChannel other) => _isPatternBased == other._isPatternBased && RedisValue.Equals(Value, other.Value);

/// <inheritdoc/>
public override int GetHashCode() => RedisValue.GetHashCode(Value) + (IsPatternBased ? 1 : 0);
public override int GetHashCode() => RedisValue.GetHashCode(Value) + (_isPatternBased ? 1 : 0);

/// <summary>
/// Obtains a string representation of the channel name.
Expand Down Expand Up @@ -187,7 +204,7 @@ public enum PatternMode
public static implicit operator RedisChannel(string key)
{
if (key == null) return default;
return new RedisChannel(Encoding.UTF8.GetBytes(key), PatternMode.Auto);
return new RedisChannel(Encoding.UTF8.GetBytes(key), s_DefaultPatternMode);
}

/// <summary>
Expand All @@ -197,20 +214,20 @@ public static implicit operator RedisChannel(string key)
public static implicit operator RedisChannel(byte[]? key)
{
if (key == null) return default;
return new RedisChannel(key, PatternMode.Auto);
return new RedisChannel(key, s_DefaultPatternMode);
}

/// <summary>
/// Obtain the channel name as a <see cref="T:byte[]"/>.
/// </summary>
/// <param name="key">The channel to get a byte[] from.</param>
public static implicit operator byte[]? (RedisChannel key) => key.Value;
public static implicit operator byte[]?(RedisChannel key) => key.Value;

/// <summary>
/// Obtain the channel name as a <see cref="string"/>.
/// </summary>
/// <param name="key">The channel to get a string from.</param>
public static implicit operator string? (RedisChannel key)
public static implicit operator string?(RedisChannel key)
{
var arr = key.Value;
if (arr == null)
Expand All @@ -226,5 +243,15 @@ public static implicit operator RedisChannel(byte[]? key)
return BitConverter.ToString(arr);
}
}

#if DEBUG
// these exist *purely* to ensure that we never add them later *without*
// giving due consideration to the default pattern mode (UseImplicitAutoPattern)
// (since we don't ship them, we don't need them in release)
[Obsolete("Watch for " + nameof(UseImplicitAutoPattern), error: true)]
private RedisChannel(string value) => throw new NotSupportedException();
[Obsolete("Watch for " + nameof(UseImplicitAutoPattern), error: true)]
private RedisChannel(byte[]? value) => throw new NotSupportedException();
#endif
}
}
2 changes: 1 addition & 1 deletion src/StackExchange.Redis/RedisSubscriber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ public Subscription(CommandFlags flags)
/// </summary>
internal Message GetMessage(RedisChannel channel, SubscriptionAction action, CommandFlags flags, bool internalCall)
{
var isPattern = channel.IsPatternBased;
var isPattern = channel._isPatternBased;
var command = action switch
{
SubscriptionAction.Subscribe when isPattern => RedisCommand.PSUBSCRIBE,
Expand Down
112 changes: 112 additions & 0 deletions tests/StackExchange.Redis.Tests/ChannelTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using System.Text;
using Xunit;

namespace StackExchange.Redis.Tests
{
public class ChannelTests
{
[Fact]
public void UseImplicitAutoPattern_OnByDefault()
{
Assert.True(RedisChannel.UseImplicitAutoPattern);
}

[Theory]
[InlineData("abc", true, false)]
[InlineData("abc*def", true, true)]
[InlineData("abc", false, false)]
[InlineData("abc*def", false, false)]
public void ValidateAutoPatternModeString(string name, bool useImplicitAutoPattern, bool isPatternBased)
{
bool oldValue = RedisChannel.UseImplicitAutoPattern;
try
{
RedisChannel.UseImplicitAutoPattern = useImplicitAutoPattern;
RedisChannel channel = name;
Assert.Equal(isPatternBased, channel.IsPatternBased);
}
finally
{
RedisChannel.UseImplicitAutoPattern = oldValue;
}
}

[Theory]
[InlineData("abc", RedisChannel.PatternMode.Auto, true, false)]
[InlineData("abc*def", RedisChannel.PatternMode.Auto, true, true)]
[InlineData("abc", RedisChannel.PatternMode.Literal, true, false)]
[InlineData("abc*def", RedisChannel.PatternMode.Literal, true, false)]
[InlineData("abc", RedisChannel.PatternMode.Pattern, true, true)]
[InlineData("abc*def", RedisChannel.PatternMode.Pattern, true, true)]
[InlineData("abc", RedisChannel.PatternMode.Auto, false, false)]
[InlineData("abc*def", RedisChannel.PatternMode.Auto, false, true)]
[InlineData("abc", RedisChannel.PatternMode.Literal, false, false)]
[InlineData("abc*def", RedisChannel.PatternMode.Literal, false, false)]
[InlineData("abc", RedisChannel.PatternMode.Pattern, false, true)]
[InlineData("abc*def", RedisChannel.PatternMode.Pattern, false, true)]
public void ValidateModeSpecifiedIgnoresGlobalSetting(string name, RedisChannel.PatternMode mode, bool useImplicitAutoPattern, bool isPatternBased)
{
bool oldValue = RedisChannel.UseImplicitAutoPattern;
try
{
RedisChannel.UseImplicitAutoPattern = useImplicitAutoPattern;
RedisChannel channel = new(name, mode);
Assert.Equal(isPatternBased, channel.IsPatternBased);
}
finally
{
RedisChannel.UseImplicitAutoPattern = oldValue;
}
}

[Theory]
[InlineData("abc", true, false)]
[InlineData("abc*def", true, true)]
[InlineData("abc", false, false)]
[InlineData("abc*def", false, false)]
public void ValidateAutoPatternModeBytes(string name, bool useImplicitAutoPattern, bool isPatternBased)
{
var bytes = Encoding.UTF8.GetBytes(name);
bool oldValue = RedisChannel.UseImplicitAutoPattern;
try
{
RedisChannel.UseImplicitAutoPattern = useImplicitAutoPattern;
RedisChannel channel = bytes;
Assert.Equal(isPatternBased, channel.IsPatternBased);
}
finally
{
RedisChannel.UseImplicitAutoPattern = oldValue;
}
}

[Theory]
[InlineData("abc", RedisChannel.PatternMode.Auto, true, false)]
[InlineData("abc*def", RedisChannel.PatternMode.Auto, true, true)]
[InlineData("abc", RedisChannel.PatternMode.Literal, true, false)]
[InlineData("abc*def", RedisChannel.PatternMode.Literal, true, false)]
[InlineData("abc", RedisChannel.PatternMode.Pattern, true, true)]
[InlineData("abc*def", RedisChannel.PatternMode.Pattern, true, true)]
[InlineData("abc", RedisChannel.PatternMode.Auto, false, false)]
[InlineData("abc*def", RedisChannel.PatternMode.Auto, false, true)]
[InlineData("abc", RedisChannel.PatternMode.Literal, false, false)]
[InlineData("abc*def", RedisChannel.PatternMode.Literal, false, false)]
[InlineData("abc", RedisChannel.PatternMode.Pattern, false, true)]
[InlineData("abc*def", RedisChannel.PatternMode.Pattern, false, true)]
public void ValidateModeSpecifiedIgnoresGlobalSettingBytes(string name, RedisChannel.PatternMode mode, bool useImplicitAutoPattern, bool isPatternBased)
{
var bytes = Encoding.UTF8.GetBytes(name);
bool oldValue = RedisChannel.UseImplicitAutoPattern;
try
{
RedisChannel.UseImplicitAutoPattern = useImplicitAutoPattern;
RedisChannel channel = new(bytes, mode);
Assert.Equal(isPatternBased, channel.IsPatternBased);
}
finally
{
RedisChannel.UseImplicitAutoPattern = oldValue;
}
}
}
}

0 comments on commit 3f0be76

Please sign in to comment.