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

BITFIELD and BITFIELD_RO feature #2107

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
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
224 changes: 224 additions & 0 deletions src/StackExchange.Redis/APITypes/BitfieldOperation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace StackExchange.Redis;

/// <summary>
/// Represents a single Bitfield Operation.
/// </summary>
public struct BitfieldOperation
{
private static string CreateOffset(bool offsetByBit, long offset) => $"{(offsetByBit ? string.Empty : "#")}{offset}";
private static readonly string[] Encodings = Enumerable.Range(0, 127).Select(x => // 0?
{
var size = x % 64;
var signedness = x < 65 ? "i" : "u";
return $"{signedness}{size}";
}).ToArray();

private static string CreateEncoding(bool unsigned, byte size)
{
if (size == 0)
{
throw new ArgumentException("Invalid encoding, size must be non-zero", nameof(size));
}

if (unsigned && size > 63)
{
throw new ArgumentException(
$"Invalid Encoding, unsigned bitfield operations support a maximum size of 63, provided size: {size}", nameof(size));
}

if (size > 64)
{
throw new ArgumentException(
$"Invalid Encoding, signed bitfield operations support a maximum size of 64, provided size: {size}", nameof(size));
}

return Encodings[size + (!unsigned ? 0 : 64)];
}

internal string Offset;
internal long? Value;
internal BitFieldSubCommand SubCommand;
internal string Encoding;
internal BitfieldOverflowHandling? BitfieldOverflowHandling;

/// <summary>
/// Creates a Get Bitfield Subcommand struct to retrieve a single integer from the bitfield.
/// </summary>
/// <param name="offset">The offset into the bitfield to address.</param>
/// <param name="width">The width of the encoding to interpret the bitfield width.</param>
/// <param name="offsetByBit">Whether or not to offset into the bitfield by bits vs encoding.</param>
/// <param name="unsigned">Whether or not to interpret the number gotten as an unsigned integer.</param>
/// <returns></returns>
public static BitfieldOperation Get(long offset, byte width, bool offsetByBit = true, bool unsigned = false)
{
var offsetValue = CreateOffset(offsetByBit, offset);
return new BitfieldOperation
{
Offset = offsetValue,
Value = null,
SubCommand = BitFieldSubCommand.Get,
Encoding = CreateEncoding(unsigned, width)
};
}

/// <summary>
/// Creates a Set Bitfield SubCommand to set a single integer from the bitfield.
/// </summary>
/// <param name="offset">The offset into the bitfield to address.</param>
/// <param name="width">The width of the encoding to interpret the bitfield width.</param>
/// <param name="value">The value to set the addressed bits to.</param>
/// <param name="offsetByBit">Whether or not to offset into the bitfield by bits vs encoding.</param>
/// <param name="unsigned">Whether or not to interpret the number gotten as an unsigned integer.</param>
/// <returns></returns>
public static BitfieldOperation Set(long offset, byte width, long value, bool offsetByBit = true, bool unsigned = false)
{
var offsetValue = CreateOffset(offsetByBit, offset);
return new BitfieldOperation
{
Offset = offsetValue,
Value = value,
SubCommand = BitFieldSubCommand.Set,
Encoding = CreateEncoding(unsigned, width)
};
}

/// <summary>
/// Creates an Increment Bitfield SubCommand to increment a single integer from the bitfield.
/// </summary>
/// <param name="offset">The offset into the bitfield to address.</param>
/// <param name="width">The width of the encoding to interpret the bitfield width.</param>
/// <param name="increment">The value to set the addressed bits to.</param>
/// <param name="offsetByBit">Whether or not to offset into the bitfield by bits vs encoding.</param>
/// <param name="unsigned">Whether or not to interpret the number gotten as an unsigned integer.</param>
/// <param name="overflowHandling">How to handle overflows.</param>
/// <returns></returns>
public static BitfieldOperation Increment(long offset, byte width, long increment, bool offsetByBit = true, bool unsigned = false, BitfieldOverflowHandling overflowHandling = Redis.BitfieldOverflowHandling.Wrap)
{
var offsetValue = CreateOffset(offsetByBit, offset);
return new BitfieldOperation
{
Offset = offsetValue,
Value = increment,
SubCommand = BitFieldSubCommand.Increment,
Encoding = CreateEncoding(unsigned, width),
BitfieldOverflowHandling = overflowHandling
};
}

internal IEnumerable<RedisValue> EnumerateArgs()
{
if (SubCommand != BitFieldSubCommand.Get)
{
if (BitfieldOverflowHandling is not null && BitfieldOverflowHandling != Redis.BitfieldOverflowHandling.Wrap)
{
yield return RedisLiterals.OVERFLOW;
yield return BitfieldOverflowHandling.Value.AsRedisValue();
}
}

yield return SubCommand.AsRedisValue();
yield return Encoding;
yield return Offset;
if (SubCommand != BitFieldSubCommand.Get)
{
if (Value is null)
{
throw new ArgumentNullException($"Value must not be null for {SubCommand.AsRedisValue()} commands");
}

yield return Value;
}
}

internal int NumArgs()
{
var numArgs = 3;
if (SubCommand != BitFieldSubCommand.Get)
{
numArgs += BitfieldOverflowHandling is not null && BitfieldOverflowHandling != Redis.BitfieldOverflowHandling.Wrap ? 3 : 1;
}

return numArgs;
}
}

internal static class BitfieldOperationExtensions
{
internal static BitfieldCommandMessage BuildMessage(this BitfieldOperation[] subCommands, int db, RedisKey key,
CommandFlags flags, RedisBase redisBase, out ServerEndPoint? server)
{
var eligibleForReadOnly = subCommands.All(x => x.SubCommand == BitFieldSubCommand.Get);
var features = redisBase.GetFeatures(key, flags, eligibleForReadOnly ? RedisCommand.BITFIELD_RO : RedisCommand.BITFIELD, out server);
var command = eligibleForReadOnly && features.ReadOnlyBitfield ? RedisCommand.BITFIELD_RO : RedisCommand.BITFIELD;
return new BitfieldCommandMessage(db, flags, key, command, subCommands.SelectMany(x=>x.EnumerateArgs()).ToArray());
}

internal static BitfieldCommandMessage BuildMessage(this BitfieldOperation subCommand, int db, RedisKey key,
CommandFlags flags, RedisBase redisBase, out ServerEndPoint? server)
{
var eligibleForReadOnly = subCommand.SubCommand == BitFieldSubCommand.Get;
var features = redisBase.GetFeatures(key, flags, eligibleForReadOnly ? RedisCommand.BITFIELD_RO : RedisCommand.BITFIELD, out server);
var command = eligibleForReadOnly && features.ReadOnlyBitfield ? RedisCommand.BITFIELD_RO : RedisCommand.BITFIELD;
return new BitfieldCommandMessage(db, flags, key, command, subCommand.EnumerateArgs().ToArray());
}
}

/// <summary>
/// Bitfield subcommands.
/// </summary>
public enum BitFieldSubCommand
{
/// <summary>
/// Subcommand to get the bitfield value.
/// </summary>
Get,

/// <summary>
/// Subcommand to set the bitfield value.
/// </summary>
Set,

/// <summary>
/// Subcommand to increment the bitfield value
/// </summary>
Increment
}

internal static class BitfieldSubCommandExtensions
{
internal static RedisValue AsRedisValue(this BitFieldSubCommand subCommand) =>
subCommand switch
{
BitFieldSubCommand.Get => RedisLiterals.GET,
BitFieldSubCommand.Set => RedisLiterals.SET,
BitFieldSubCommand.Increment => RedisLiterals.INCRBY,
_ => throw new ArgumentOutOfRangeException(nameof(subCommand))
};
}

internal class BitfieldCommandMessage : Message
{
private readonly IEnumerable<RedisValue> _args;
private readonly RedisKey _key;
public BitfieldCommandMessage(int db, CommandFlags flags, RedisKey key, RedisCommand command, RedisValue[] args) : base(db, flags, command)
{
_key = key;
_args = args;
}

public override int ArgCount => 1 + _args.Count();

protected override void WriteImpl(PhysicalConnection physical)
{
physical.WriteHeader(Command, ArgCount);
physical.Write(_key);
foreach (var arg in _args)
{
physical.WriteBulkString(arg);
}
}
}
33 changes: 33 additions & 0 deletions src/StackExchange.Redis/Enums/BitfieldOverflowHandling.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;

namespace StackExchange.Redis;

/// <summary>
/// Defines the overflow behavior of a BITFIELD command.
/// </summary>
public enum BitfieldOverflowHandling
{
/// <summary>
/// Wraps around to the most negative value of signed integers, or zero for unsigned integers
/// </summary>
Wrap,
/// <summary>
/// Uses saturation arithmetic, stopping at the highest possible value for overflows, and the lowest possible value for underflows.
/// </summary>
Saturate,
/// <summary>
/// If an overflow is encountered, associated subcommand fails, and the result will be NULL.
/// </summary>
Fail,
}

internal static class BitfieldOverflowHandlingExtensions
{
internal static RedisValue AsRedisValue(this BitfieldOverflowHandling handling) => handling switch
{
BitfieldOverflowHandling.Fail => RedisLiterals.FAIL,
BitfieldOverflowHandling.Saturate => RedisLiterals.SAT,
BitfieldOverflowHandling.Wrap => RedisLiterals.WRAP,
_ => throw new ArgumentOutOfRangeException(nameof(handling))
};
}
4 changes: 4 additions & 0 deletions src/StackExchange.Redis/Enums/RedisCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ internal enum RedisCommand
BGREWRITEAOF,
BGSAVE,
BITCOUNT,
BITFIELD,
BITFIELD_RO,
BITOP,
BITPOS,
BLPOP,
Expand Down Expand Up @@ -263,6 +265,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command)
// for example spreading load via a .DemandReplica flag in the caller.
// Basically: would it fail on a read-only replica in 100% of cases? Then it goes in the list.
case RedisCommand.APPEND:
case RedisCommand.BITFIELD:
case RedisCommand.BITOP:
case RedisCommand.BLPOP:
case RedisCommand.BRPOP:
Expand Down Expand Up @@ -350,6 +353,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command)
case RedisCommand.BGREWRITEAOF:
case RedisCommand.BGSAVE:
case RedisCommand.BITCOUNT:
case RedisCommand.BITFIELD_RO:
case RedisCommand.BITPOS:
case RedisCommand.CLIENT:
case RedisCommand.CLUSTER:
Expand Down
60 changes: 60 additions & 0 deletions src/StackExchange.Redis/Interfaces/IDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2594,6 +2594,66 @@ IEnumerable<SortedSetEntry> SortedSetScan(RedisKey key,
/// <remarks><seealso href="https://redis.io/commands/bitcount"/></remarks>
long StringBitCount(RedisKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Executes a set of Bitfield subcommands as constructed by the <paramref name="subCommands"/> against the bitfield at the provided <paramref name="key"/>.
/// Will run as a <c>BITFIELD_RO</c> if all operations are read-only and the command is available.
/// </summary>
/// <param name="key">The key of the string.</param>
/// <param name="subCommands">The subcommands to execute against the bitfield.</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <returns>An array of numbers corresponding to the result of each sub-command. For increment subcommands, these can be null.</returns>
/// <remarks>
/// <seealso href="https://redis.io/commands/bitfield"/>,
/// <seealso href="https://redis.io/commands/bitfield_ro"/>
/// </remarks>
long?[] StringBitfield(RedisKey key, BitfieldOperation[] subCommands, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Pulls a single number out of a bitfield of the provided encoding at the given offset.
/// Will execute a <c>BITFIELD_RO</c> if possible.
/// </summary>
/// <param name="key">The key for the string.</param>
/// <param name="offset">The offset into the bitfield to pull the number from.</param>
/// <param name="width">The width of the encoding to interpret the bitfield width.</param>
/// <param name="offsetByBit">Whether or not to offset into the bitfield by bits vs encoding.</param>
/// <param name="unsigned">Whether or not to interpret the number gotten as an unsigned integer.</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <returns>The number of the given encoding at the provided <paramref name="offset"/>.</returns>
/// <remarks>
/// <seealso href="https://redis.io/commands/bitfield"/>,
/// <seealso href="https://redis.io/commands/bitfield_ro"/>
/// </remarks>
long StringBitfieldGet(RedisKey key, long offset, byte width, bool offsetByBit = true, bool unsigned = false, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Sets a single number in a bitfield at the provided offset to the <paramref name="value"/> provided, in the given encoding.
/// </summary>
/// <param name="key">The key for the string.</param>
/// <param name="offset">The offset into the bitfield to address.</param>
/// <param name="width">The width of the encoding to interpret the bitfield width.</param>
/// <param name="value">The value to set the addressed bits to.</param>
/// <param name="offsetByBit">Whether or not to offset into the bitfield by bits vs encoding.</param>
/// <param name="unsigned">Whether or not to interpret the number gotten as an unsigned integer.</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <returns>The previous value as an <see cref="long"/> at the provided <paramref name="offset"/>.</returns>
/// <remarks><seealso href="https://redis.io/commands/bitfield"/></remarks>
long StringBitfieldSet(RedisKey key, long offset, byte width, long value, bool offsetByBit = true, bool unsigned = false, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Increments a single number in a bitfield at the provided <paramref name="offset"/> in the provided encoding by the given <paramref name="increment"/>.
/// </summary>
/// <param name="key">The key for the string.</param>
/// <param name="offset">The offset into the bitfield to address.</param>
/// <param name="width">The width of the encoding to interpret the bitfield width.</param>
/// <param name="increment">The value to set the addressed bits to.</param>
/// <param name="offsetByBit">Whether or not to offset into the bitfield by bits vs encoding.</param>
/// <param name="unsigned">Whether or not to interpret the number gotten as an unsigned integer.</param>
/// <param name="overflowHandling">How to handle overflows.</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <returns>The new value of the given at the provided <paramref name="offset"/> after the <c>INCRBY</c> is applied, represented as an <see cref="long"/>. Returns <see langword="null"/> if the operation fails.</returns>
/// <remarks><seealso href="https://redis.io/commands/bitfield"/></remarks>
long? StringBitfieldIncrement(RedisKey key, long offset, byte width, long increment, bool offsetByBit = true, bool unsigned = false, BitfieldOverflowHandling overflowHandling = Redis.BitfieldOverflowHandling.Wrap, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Perform a bitwise operation between multiple keys (containing string values) and store the result in the destination key.
/// The BITOP command supports four bitwise operations; note that NOT is a unary operator: the second key should be omitted in this case
Expand Down
Loading