diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 5a1b9aa64..bf057512f 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,7 @@ Current package versions: ## Unreleased +- Add support for hash field expiration (see [#2715](https://github.com/StackExchange/StackExchange.Redis/issues/2715)) ([#2716 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2716])) ## 2.8.0 diff --git a/src/StackExchange.Redis/Enums/ExpireResult.cs b/src/StackExchange.Redis/Enums/ExpireResult.cs new file mode 100644 index 000000000..6211492e6 --- /dev/null +++ b/src/StackExchange.Redis/Enums/ExpireResult.cs @@ -0,0 +1,27 @@ +namespace StackExchange.Redis; + +/// +/// Specifies the result of operation to set expire time. +/// +public enum ExpireResult +{ + /// + /// Field deleted because the specified expiration time is due. + /// + Due = 2, + + /// + /// Expiration time/duration updated successfully. + /// + Success = 1, + + /// + /// Expiration not set because of a specified NX | XX | GT | LT condition not met. + /// + ConditionNotMet = 0, + + /// + /// No such field. + /// + NoSuchField = -2, +} diff --git a/src/StackExchange.Redis/Enums/PersistResult.cs b/src/StackExchange.Redis/Enums/PersistResult.cs new file mode 100644 index 000000000..91fdf9fa7 --- /dev/null +++ b/src/StackExchange.Redis/Enums/PersistResult.cs @@ -0,0 +1,22 @@ +namespace StackExchange.Redis; + +/// +/// Specifies the result of operation to remove the expire time. +/// +public enum PersistResult +{ + /// + /// Expiration removed successfully. + /// + Success = 1, + + /// + /// Expiration not removed because of a specified NX | XX | GT | LT condition not met. + /// + ConditionNotMet = -1, + + /// + /// No such field. + /// + NoSuchField = -2, +} diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 728b88a76..a4647d7eb 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -66,6 +66,9 @@ internal enum RedisCommand HDEL, HELLO, HEXISTS, + HEXPIRE, + HEXPIREAT, + HEXPIRETIME, HGET, HGETALL, HINCRBY, @@ -74,6 +77,11 @@ internal enum RedisCommand HLEN, HMGET, HMSET, + HPERSIST, + HPEXPIRE, + HPEXPIREAT, + HPEXPIRETIME, + HPTTL, HRANDFIELD, HSCAN, HSET, @@ -279,9 +287,14 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.GETEX: case RedisCommand.GETSET: case RedisCommand.HDEL: + case RedisCommand.HEXPIRE: + case RedisCommand.HEXPIREAT: case RedisCommand.HINCRBY: case RedisCommand.HINCRBYFLOAT: case RedisCommand.HMSET: + case RedisCommand.HPERSIST: + case RedisCommand.HPEXPIRE: + case RedisCommand.HPEXPIREAT: case RedisCommand.HSET: case RedisCommand.HSETNX: case RedisCommand.INCR: @@ -378,11 +391,14 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.GETRANGE: case RedisCommand.HELLO: case RedisCommand.HEXISTS: + case RedisCommand.HEXPIRETIME: case RedisCommand.HGET: case RedisCommand.HGETALL: case RedisCommand.HKEYS: case RedisCommand.HLEN: case RedisCommand.HMGET: + case RedisCommand.HPEXPIRETIME: + case RedisCommand.HPTTL: case RedisCommand.HRANDFIELD: case RedisCommand.HSCAN: case RedisCommand.HSTRLEN: diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 6178051d0..c192f07f9 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -325,6 +325,165 @@ public interface IDatabase : IRedis, IDatabaseAsync /// bool HashExists(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); + /// + /// Set the remaining time to live in milliseconds for the given set of fields of hash + /// After the timeout has expired, the field of the hash will automatically be deleted. + /// + /// The key of the hash. + /// The fields in the hash to set expire time. + /// The timeout to set. + /// under which condition the expiration will be set using . + /// The flags to use for this operation. + /// + /// Empty array if the key does not exist. Otherwise returns an array where each item is the result of operation for given fields: + /// + /// + /// Result + /// Description + /// + /// + /// 2 + /// Field deleted because the specified expiration time is due. + /// + /// + /// 1 + /// Expiration time set/updated. + /// + /// + /// 0 + /// Expiration time is not set/update (a specified ExpireWhen condition is not met). + /// + /// + /// -1 + /// No such field exists. + /// + /// + /// + ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None); + + /// + /// Set the time out on a field of the given set of fields of hash. + /// After the timeout has expired, the field of the hash will automatically be deleted. + /// + /// The key of the hash. + /// The fields in the hash to set expire time. + /// The exact date to expiry to set. + /// under which condition the expiration will be set using . + /// The flags to use for this operation. + /// + /// Empty array if the key does not exist. Otherwise returns an array where each item is the result of operation for given fields: + /// + /// + /// Result + /// Description + /// + /// + /// 2 + /// Field deleted because the specified expiration time is due. + /// + /// + /// 1 + /// Expiration time set/updated. + /// + /// + /// 0 + /// Expiration time is not set/update (a specified ExpireWhen condition is not met). + /// + /// + /// -1 + /// No such field exists. + /// + /// + /// + ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None); + + /// + /// For each specified field, it gets the expiration time as a Unix timestamp in milliseconds (milliseconds since the Unix epoch). + /// + /// The key of the hash. + /// The fields in the hash to get expire time. + /// The flags to use for this operation. + /// + /// Empty array if the key does not exist. Otherwise returns the result of operation for given fields: + /// + /// + /// Result + /// Description + /// + /// + /// > 0 + /// Expiration time, as a Unix timestamp in milliseconds. + /// + /// + /// -1 + /// Field has no associated expiration time. + /// + /// + /// -2 + /// No such field exists. + /// + /// + /// + long[] HashFieldGetExpireDateTime(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None); + + /// + /// For each specified field, it removes the expiration time. + /// + /// The key of the hash. + /// The fields in the hash to remove expire time. + /// The flags to use for this operation. + /// + /// Empty array if the key does not exist. Otherwise returns the result of operation for given fields: + /// + /// + /// Result + /// Description + /// + /// + /// 1 + /// Expiration time was removed. + /// + /// + /// -1 + /// Field has no associated expiration time. + /// + /// + /// -2 + /// No such field exists. + /// + /// + /// + PersistResult[] HashFieldPersist(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None); + + /// + /// For each specified field, it gets the remaining time to live in milliseconds. + /// + /// The key of the hash. + /// The fields in the hash to get expire time. + /// The flags to use for this operation. + /// + /// Empty array if the key does not exist. Otherwise returns the result of operation for given fields: + /// + /// + /// Result + /// Description + /// + /// + /// > 0 + /// Time to live, in milliseconds. + /// + /// + /// -1 + /// Field has no associated expiration time. + /// + /// + /// -2 + /// No such field exists. + /// + /// + /// + long[] HashFieldGetTimeToLive(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None); + /// /// Returns the value associated with field in the hash stored at key. /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 103b7151b..8600a5a1a 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -84,6 +84,21 @@ public interface IDatabaseAsync : IRedisAsync /// Task HashExistsAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); + /// + Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None); + + /// + Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None); + + /// + Task HashFieldGetExpireDateTimeAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None); + + /// + Task HashFieldPersistAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None); + + /// + Task HashFieldGetTimeToLiveAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None); + /// Task HashGetAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index f6821af1d..f18e74512 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -84,6 +84,21 @@ public Task HashDeleteAsync(RedisKey key, RedisValue hashField, CommandFla public Task HashExistsAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => Inner.HashExistsAsync(ToInner(key), hashField, flags); + public Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldExpireAsync(ToInner(key), hashFields, expiry, when, flags); + + public Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldExpireAsync(ToInner(key), hashFields, expiry, when, flags); + + public Task HashFieldGetExpireDateTimeAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags) => + Inner.HashFieldGetExpireDateTimeAsync(ToInner(key), hashFields, flags); + + public Task HashFieldPersistAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags) => + Inner.HashFieldPersistAsync(ToInner(key), hashFields, flags); + + public Task HashFieldGetTimeToLiveAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags) => + Inner.HashFieldGetTimeToLiveAsync(ToInner(key), hashFields, flags); + public Task HashGetAllAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.HashGetAllAsync(ToInner(key), flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index e2b484935..8f570edd6 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -81,6 +81,21 @@ public bool HashDelete(RedisKey key, RedisValue hashField, CommandFlags flags = public bool HashExists(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => Inner.HashExists(ToInner(key), hashField, flags); + public ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldExpire(ToInner(key), hashFields, expiry, when, flags); + + public ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldExpire(ToInner(key), hashFields, expiry, when, flags); + + public long[] HashFieldGetExpireDateTime(RedisKey key, RedisValue[] hashFields, CommandFlags flags) => + Inner.HashFieldGetExpireDateTime(ToInner(key), hashFields, flags); + + public PersistResult[] HashFieldPersist(RedisKey key, RedisValue[] hashFields, CommandFlags flags) => + Inner.HashFieldPersist(ToInner(key), hashFields, flags); + + public long[] HashFieldGetTimeToLive(RedisKey key, RedisValue[] hashFields, CommandFlags flags) => + Inner.HashFieldGetTimeToLive(ToInner(key), hashFields, flags); + public HashEntry[] HashGetAll(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.HashGetAll(ToInner(key), flags); diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 8707cc1b4..7b0a515df 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -412,6 +412,11 @@ StackExchange.Redis.Exclude.Both = StackExchange.Redis.Exclude.Start | StackExch StackExchange.Redis.Exclude.None = 0 -> StackExchange.Redis.Exclude StackExchange.Redis.Exclude.Start = 1 -> StackExchange.Redis.Exclude StackExchange.Redis.Exclude.Stop = 2 -> StackExchange.Redis.Exclude +StackExchange.Redis.ExpireResult +StackExchange.Redis.ExpireResult.ConditionNotMet = 0 -> StackExchange.Redis.ExpireResult +StackExchange.Redis.ExpireResult.Due = 2 -> StackExchange.Redis.ExpireResult +StackExchange.Redis.ExpireResult.NoSuchField = -2 -> StackExchange.Redis.ExpireResult +StackExchange.Redis.ExpireResult.Success = 1 -> StackExchange.Redis.ExpireResult StackExchange.Redis.ExpireWhen StackExchange.Redis.ExpireWhen.Always = 0 -> StackExchange.Redis.ExpireWhen StackExchange.Redis.ExpireWhen.GreaterThanCurrentExpiry = 1 -> StackExchange.Redis.ExpireWhen @@ -558,6 +563,11 @@ StackExchange.Redis.IDatabase.HashDecrement(StackExchange.Redis.RedisKey key, St StackExchange.Redis.IDatabase.HashDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.HashDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.HashExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.HashFieldExpire(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, System.DateTime expiry, StackExchange.Redis.ExpireWhen when = StackExchange.Redis.ExpireWhen.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ExpireResult[]! +StackExchange.Redis.IDatabase.HashFieldExpire(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, System.TimeSpan expiry, StackExchange.Redis.ExpireWhen when = StackExchange.Redis.ExpireWhen.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ExpireResult[]! +StackExchange.Redis.IDatabase.HashFieldGetExpireDateTime(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long[]! +StackExchange.Redis.IDatabase.HashFieldGetTimeToLive(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long[]! +StackExchange.Redis.IDatabase.HashFieldPersist(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.PersistResult[]! StackExchange.Redis.IDatabase.HashGet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.HashGet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.HashGetAll(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.HashEntry[]! @@ -789,6 +799,13 @@ StackExchange.Redis.IDatabaseAsync.HashDecrementAsync(StackExchange.Redis.RedisK StackExchange.Redis.IDatabaseAsync.HashDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.HashDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.HashExistsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! + +StackExchange.Redis.IDatabaseAsync.HashFieldExpireAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, System.DateTime expiry, StackExchange.Redis.ExpireWhen when = StackExchange.Redis.ExpireWhen.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashFieldExpireAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, System.TimeSpan expiry, StackExchange.Redis.ExpireWhen when = StackExchange.Redis.ExpireWhen.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashFieldGetExpireDateTimeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashFieldGetTimeToLiveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashFieldPersistAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! + StackExchange.Redis.IDatabaseAsync.HashGetAllAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.HashGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.HashGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! @@ -1248,6 +1265,10 @@ StackExchange.Redis.NameValueEntry.Value.get -> StackExchange.Redis.RedisValue StackExchange.Redis.Order StackExchange.Redis.Order.Ascending = 0 -> StackExchange.Redis.Order StackExchange.Redis.Order.Descending = 1 -> StackExchange.Redis.Order +StackExchange.Redis.PersistResult +StackExchange.Redis.PersistResult.ConditionNotMet = -1 -> StackExchange.Redis.PersistResult +StackExchange.Redis.PersistResult.NoSuchField = -2 -> StackExchange.Redis.PersistResult +StackExchange.Redis.PersistResult.Success = 1 -> StackExchange.Redis.PersistResult StackExchange.Redis.Profiling.IProfiledCommand StackExchange.Redis.Profiling.IProfiledCommand.Command.get -> string! StackExchange.Redis.Profiling.IProfiledCommand.CommandCreated.get -> System.DateTime diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 288e263d2..0e5eada53 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -387,6 +387,83 @@ public Task HashExistsAsync(RedisKey key, RedisValue hashField, CommandFla return ExecuteAsync(msg, ResultProcessor.Boolean); } + public ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) + { + long milliseconds = expiry.Ticks / TimeSpan.TicksPerMillisecond; + return HashFieldExpireExecute(key, milliseconds, when, PickExpireCommandByPrecision, SyncCustomArrExecutor>, ResultProcessor.ExpireResultArray, flags, hashFields); + } + + public ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) + { + long milliseconds = GetMillisecondsUntil(expiry); + return HashFieldExpireExecute(key, milliseconds, when, PickExpireAtCommandByPrecision, SyncCustomArrExecutor>, ResultProcessor.ExpireResultArray, flags, hashFields); + } + + public Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) + { + long milliseconds = expiry.Ticks / TimeSpan.TicksPerMillisecond; + return HashFieldExpireExecute(key, milliseconds, when, PickExpireCommandByPrecision, AsyncCustomArrExecutor>, ResultProcessor.ExpireResultArray, flags, hashFields); + } + + public Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) + { + long milliseconds = GetMillisecondsUntil(expiry); + return HashFieldExpireExecute(key, milliseconds, when, PickExpireAtCommandByPrecision, AsyncCustomArrExecutor>, ResultProcessor.ExpireResultArray, flags, hashFields); + } + + private T HashFieldExpireExecute(RedisKey key, long milliseconds, ExpireWhen when, Func getCmd, CustomExecutor executor, TProcessor processor, CommandFlags flags, params RedisValue[] hashFields) + { + if (hashFields == null) throw new ArgumentNullException(nameof(hashFields)); + var useSeconds = milliseconds % 1000 == 0; + var cmd = getCmd(useSeconds); + long expiry = useSeconds ? (milliseconds / 1000) : milliseconds; + + var values = when switch + { + ExpireWhen.Always => new List { expiry, RedisLiterals.FIELDS, hashFields.Length }, + _ => new List { expiry, when.ToLiteral(), RedisLiterals.FIELDS, hashFields.Length }, + }; + values.AddRange(hashFields); + var msg = Message.Create(Database, flags, cmd, key, values.ToArray()); + return executor(msg, processor); + } + + private static RedisCommand PickExpireCommandByPrecision(bool useSeconds) => useSeconds ? RedisCommand.HEXPIRE : RedisCommand.HPEXPIRE; + + private static RedisCommand PickExpireAtCommandByPrecision(bool useSeconds) => useSeconds ? RedisCommand.HEXPIREAT : RedisCommand.HPEXPIREAT; + + private T HashFieldExecute(RedisCommand cmd, RedisKey key, CustomExecutor executor, TProcessor processor, CommandFlags flags = CommandFlags.None, params RedisValue[] hashFields) + { + var values = new List { RedisLiterals.FIELDS, hashFields.Length }; + values.AddRange(hashFields); + var msg = Message.Create(Database, flags, cmd, key, values.ToArray()); + return executor(msg, processor); + } + + private delegate T CustomExecutor(Message msg, TProcessor processor); + + private T[] SyncCustomArrExecutor(Message msg, TProcessor processor) where TProcessor : ResultProcessor => ExecuteSync(msg, processor)!; + + private Task AsyncCustomArrExecutor(Message msg, TProcessor processor) where TProcessor : ResultProcessor => ExecuteAsync(msg, processor)!; + + public long[] HashFieldGetExpireDateTime(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + HashFieldExecute(RedisCommand.HPEXPIRETIME, key, SyncCustomArrExecutor>, ResultProcessor.Int64Array, flags, hashFields); + + public Task HashFieldGetExpireDateTimeAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + HashFieldExecute(RedisCommand.HPEXPIRETIME, key, AsyncCustomArrExecutor>, ResultProcessor.Int64Array, flags, hashFields); + + public PersistResult[] HashFieldPersist(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + HashFieldExecute(RedisCommand.HPERSIST, key, SyncCustomArrExecutor>, ResultProcessor.PersistResultArray, flags, hashFields); + + public Task HashFieldPersistAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + HashFieldExecute(RedisCommand.HPERSIST, key, AsyncCustomArrExecutor>, ResultProcessor.PersistResultArray, flags, hashFields); + + public long[] HashFieldGetTimeToLive(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + HashFieldExecute(RedisCommand.HPTTL, key, SyncCustomArrExecutor>, ResultProcessor.Int64Array, flags, hashFields); + + public Task HashFieldGetTimeToLiveAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + HashFieldExecute(RedisCommand.HPTTL, key, AsyncCustomArrExecutor>, ResultProcessor.Int64Array, flags, hashFields); + public RedisValue HashGet(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.HGET, key, hashField); diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index e81043010..225516433 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -43,7 +43,8 @@ namespace StackExchange.Redis v6_2_0 = new Version(6, 2, 0), v7_0_0_rc1 = new Version(6, 9, 240), // 7.0 RC1 is version 6.9.240 v7_2_0_rc1 = new Version(7, 1, 240), // 7.2 RC1 is version 7.1.240 - v7_4_0_rc1 = new Version(7, 3, 240); // 7.4 RC1 is version 7.3.240 + v7_4_0_rc1 = new Version(7, 3, 240), // 7.4 RC1 is version 7.3.240 + v7_4_0_rc2 = new Version(7, 3, 241); // 7.4 RC2 is version 7.3.241 #pragma warning restore SA1310 // Field names should not contain underscore #pragma warning restore SA1311 // Static readonly fields should begin with upper-case letter diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index b77649402..a076ff3fe 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -78,6 +78,7 @@ public static readonly RedisValue EX = "EX", EXAT = "EXAT", EXISTS = "EXISTS", + FIELDS = "FIELDS", FILTERBY = "FILTERBY", FLUSH = "FLUSH", FREQ = "FREQ", diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 894e6f5f9..7a69f7429 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -68,6 +68,10 @@ public static readonly ResultProcessor public static readonly ResultProcessor NullableInt64 = new NullableInt64Processor(); + public static readonly ResultProcessor ExpireResultArray = new ExpireResultArrayProcessor(); + + public static readonly ResultProcessor PersistResultArray = new PersistResultArrayProcessor(); + public static readonly ResultProcessor RedisChannelArrayLiteral = new RedisChannelArrayProcessor(RedisChannel.PatternMode.Literal); @@ -1452,6 +1456,47 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes return true; } break; + case ResultType.Array: + var items = result.GetItems(); + if (items.Length == 1) + { // treat an array of 1 like a single reply + if (items[0].TryGetInt64(out long value)) + { + SetResult(message, value); + return true; + } + } + break; + } + return false; + } + } + + private sealed class ExpireResultArrayProcessor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Resp2TypeArray == ResultType.Array || result.IsNull) + { + var arr = result.ToArray((in RawResult x) => (ExpireResult)(long)x.AsRedisValue())!; + + SetResult(message, arr); + return true; + } + return false; + } + } + + private sealed class PersistResultArrayProcessor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Resp2TypeArray == ResultType.Array || result.IsNull) + { + var arr = result.ToArray((in RawResult x) => (PersistResult)(long)x.AsRedisValue())!; + + SetResult(message, arr); + return true; } return false; } diff --git a/tests/StackExchange.Redis.Tests/HashFieldTests.cs b/tests/StackExchange.Redis.Tests/HashFieldTests.cs new file mode 100644 index 000000000..e50cd0546 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/HashFieldTests.cs @@ -0,0 +1,305 @@ +using System; +using System.Linq; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests; + +/// +/// Tests for . +/// +[RunPerProtocol] +[Collection(SharedConnectionFixture.Key)] +public class HashFieldTests : TestBase +{ + private readonly DateTime nextCentury = new DateTime(2101, 1, 1, 0, 0, 0, DateTimeKind.Utc); + private readonly TimeSpan oneYearInMs = TimeSpan.FromMilliseconds(31536000000); + + private readonly HashEntry[] entries = new HashEntry[] { new("f1", 1), new("f2", 2) }; + + private readonly RedisValue[] fields = new RedisValue[] { "f1", "f2" }; + + public HashFieldTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) + { + } + + [Fact] + public void HashFieldExpire() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + + var fieldsResult = db.HashFieldExpire(hashKey, fields, oneYearInMs); + Assert.Equal(new[] { ExpireResult.Success, ExpireResult.Success }, fieldsResult); + + fieldsResult = db.HashFieldExpire(hashKey, fields, nextCentury); + Assert.Equal(new[] { ExpireResult.Success, ExpireResult.Success, }, fieldsResult); + } + + [Fact] + public void HashFieldExpireNoKey() + { + var db = Create(require: RedisFeatures.v7_4_0_rc2).GetDatabase(); + var hashKey = Me(); + + var fieldsResult = db.HashFieldExpire(hashKey, fields, oneYearInMs); + Assert.Equal(new[] { ExpireResult.NoSuchField, ExpireResult.NoSuchField }, fieldsResult); + + fieldsResult = db.HashFieldExpire(hashKey, fields, nextCentury); + Assert.Equal(new[] { ExpireResult.NoSuchField, ExpireResult.NoSuchField }, fieldsResult); + } + + [Fact] + public async void HashFieldExpireAsync() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + + var fieldsResult = await db.HashFieldExpireAsync(hashKey, fields, oneYearInMs); + Assert.Equal(new[] { ExpireResult.Success, ExpireResult.Success }, fieldsResult); + + fieldsResult = await db.HashFieldExpireAsync(hashKey, fields, nextCentury); + Assert.Equal(new[] { ExpireResult.Success, ExpireResult.Success }, fieldsResult); + } + + [Fact] + public async void HashFieldExpireAsyncNoKey() + { + var db = Create(require: RedisFeatures.v7_4_0_rc2).GetDatabase(); + var hashKey = Me(); + + var fieldsResult = await db.HashFieldExpireAsync(hashKey, fields, oneYearInMs); + Assert.Equal(new[] { ExpireResult.NoSuchField, ExpireResult.NoSuchField }, fieldsResult); + + fieldsResult = await db.HashFieldExpireAsync(hashKey, fields, nextCentury); + Assert.Equal(new[] { ExpireResult.NoSuchField, ExpireResult.NoSuchField }, fieldsResult); + } + + [Fact] + public void HashFieldGetExpireDateTimeIsDue() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + + var result = db.HashFieldExpire(hashKey, new RedisValue[] { "f1" }, new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + Assert.Equal(new[] { ExpireResult.Due }, result); + } + + [Fact] + public void HashFieldExpireNoField() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + + var result = db.HashFieldExpire(hashKey, new RedisValue[] { "nonExistingField" }, oneYearInMs); + Assert.Equal(new[] { ExpireResult.NoSuchField }, result); + } + + [Fact] + public void HashFieldExpireConditionsSatisfied() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.KeyDelete(hashKey); + db.HashSet(hashKey, entries); + db.HashSet(hashKey, new HashEntry[] { new("f3", 3), new("f4", 4) }); + var initialExpire = db.HashFieldExpire(hashKey, new RedisValue[] { "f2", "f3", "f4" }, new DateTime(2050, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + Assert.Equal(new[] { ExpireResult.Success, ExpireResult.Success, ExpireResult.Success }, initialExpire); + + var result = db.HashFieldExpire(hashKey, new RedisValue[] { "f1" }, oneYearInMs, ExpireWhen.HasNoExpiry); + Assert.Equal(new[] { ExpireResult.Success }, result); + + result = db.HashFieldExpire(hashKey, new RedisValue[] { "f2" }, oneYearInMs, ExpireWhen.HasExpiry); + Assert.Equal(new[] { ExpireResult.Success }, result); + + result = db.HashFieldExpire(hashKey, new RedisValue[] { "f3" }, nextCentury, ExpireWhen.GreaterThanCurrentExpiry); + Assert.Equal(new[] { ExpireResult.Success }, result); + + result = db.HashFieldExpire(hashKey, new RedisValue[] { "f4" }, oneYearInMs, ExpireWhen.LessThanCurrentExpiry); + Assert.Equal(new[] { ExpireResult.Success }, result); + } + + [Fact] + public void HashFieldExpireConditionsNotSatisfied() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.KeyDelete(hashKey); + db.HashSet(hashKey, entries); + db.HashSet(hashKey, new HashEntry[] { new("f3", 3), new("f4", 4) }); + var initialExpire = db.HashFieldExpire(hashKey, new RedisValue[] { "f2", "f3", "f4" }, new DateTime(2050, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + Assert.Equal(new[] { ExpireResult.Success, ExpireResult.Success, ExpireResult.Success }, initialExpire); + + var result = db.HashFieldExpire(hashKey, new RedisValue[] { "f1" }, oneYearInMs, ExpireWhen.HasExpiry); + Assert.Equal(new[] { ExpireResult.ConditionNotMet }, result); + + result = db.HashFieldExpire(hashKey, new RedisValue[] { "f2" }, oneYearInMs, ExpireWhen.HasNoExpiry); + Assert.Equal(new[] { ExpireResult.ConditionNotMet }, result); + + result = db.HashFieldExpire(hashKey, new RedisValue[] { "f3" }, nextCentury, ExpireWhen.LessThanCurrentExpiry); + Assert.Equal(new[] { ExpireResult.ConditionNotMet }, result); + + result = db.HashFieldExpire(hashKey, new RedisValue[] { "f4" }, oneYearInMs, ExpireWhen.GreaterThanCurrentExpiry); + Assert.Equal(new[] { ExpireResult.ConditionNotMet }, result); + } + + [Fact] + public void HashFieldGetExpireDateTime() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + db.HashFieldExpire(hashKey, fields, nextCentury); + long ms = new DateTimeOffset(nextCentury).ToUnixTimeMilliseconds(); + + var result = db.HashFieldGetExpireDateTime(hashKey, new RedisValue[] { "f1" }); + Assert.Equal(new[] { ms }, result); + + var fieldsResult = db.HashFieldGetExpireDateTime(hashKey, fields); + Assert.Equal(new[] { ms, ms }, fieldsResult); + } + + [Fact] + public void HashFieldExpireFieldNoExpireTime() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + + var result = db.HashFieldGetExpireDateTime(hashKey, new RedisValue[] { "f1" }); + Assert.Equal(new[] { -1L }, result); + + var fieldsResult = db.HashFieldGetExpireDateTime(hashKey, fields); + Assert.Equal(new long[] { -1, -1, }, fieldsResult); + } + + [Fact] + public void HashFieldGetExpireDateTimeNoKey() + { + var db = Create(require: RedisFeatures.v7_4_0_rc2).GetDatabase(); + var hashKey = Me(); + + var fieldsResult = db.HashFieldGetExpireDateTime(hashKey, fields); + Assert.Equal(new long[] { -2, -2, }, fieldsResult); + } + + [Fact] + public void HashFieldGetExpireDateTimeNoField() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + db.HashFieldExpire(hashKey, fields, oneYearInMs); + + var fieldsResult = db.HashFieldGetExpireDateTime(hashKey, new RedisValue[] { "notExistingField1", "notExistingField2" }); + Assert.Equal(new long[] { -2, -2, }, fieldsResult); + } + + [Fact] + public void HashFieldGetTimeToLive() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + db.HashFieldExpire(hashKey, fields, oneYearInMs); + long ms = new DateTimeOffset(nextCentury).ToUnixTimeMilliseconds(); + + var result = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" }); + Assert.NotNull(result); + Assert.True(result.Length == 1); + Assert.True(result[0] > 0); + + var fieldsResult = db.HashFieldGetTimeToLive(hashKey, fields); + Assert.NotNull(fieldsResult); + Assert.True(fieldsResult.Length > 0); + Assert.True(fieldsResult.All(x => x > 0)); + } + + [Fact] + public void HashFieldGetTimeToLiveNoExpireTime() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + + var fieldsResult = db.HashFieldGetTimeToLive(hashKey, fields); + Assert.Equal(new long[] { -1, -1, }, fieldsResult); + } + + [Fact] + public void HashFieldGetTimeToLiveNoKey() + { + var db = Create(require: RedisFeatures.v7_4_0_rc2).GetDatabase(); + var hashKey = Me(); + + var fieldsResult = db.HashFieldGetTimeToLive(hashKey, fields); + Assert.Equal(new long[] { -2, -2, }, fieldsResult); + } + + [Fact] + public void HashFieldGetTimeToLiveNoField() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + db.HashFieldExpire(hashKey, fields, oneYearInMs); + + var fieldsResult = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "notExistingField1", "notExistingField2" }); + Assert.Equal(new long[] { -2, -2, }, fieldsResult); + } + + [Fact] + public void HashFieldPersist() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + db.HashFieldExpire(hashKey, fields, oneYearInMs); + long ms = new DateTimeOffset(nextCentury).ToUnixTimeMilliseconds(); + + var result = db.HashFieldPersist(hashKey, new RedisValue[] { "f1" }); + Assert.Equal(new[] { PersistResult.Success }, result); + + db.HashFieldExpire(hashKey, fields, oneYearInMs); + + var fieldsResult = db.HashFieldPersist(hashKey, fields); + Assert.Equal(new[] { PersistResult.Success, PersistResult.Success }, fieldsResult); + } + + [Fact] + public void HashFieldPersistNoExpireTime() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + + var fieldsResult = db.HashFieldPersist(hashKey, fields); + Assert.Equal(new[] { PersistResult.ConditionNotMet, PersistResult.ConditionNotMet }, fieldsResult); + } + + [Fact] + public void HashFieldPersistNoKey() + { + var db = Create(require: RedisFeatures.v7_4_0_rc2).GetDatabase(); + var hashKey = Me(); + + var fieldsResult = db.HashFieldPersist(hashKey, fields); + Assert.Equal(new[] { PersistResult.NoSuchField, PersistResult.NoSuchField }, fieldsResult); + } + + [Fact] + public void HashFieldPersistNoField() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + db.HashFieldExpire(hashKey, fields, oneYearInMs); + + var fieldsResult = db.HashFieldPersist(hashKey, new RedisValue[] { "notExistingField1", "notExistingField2" }); + Assert.Equal(new[] { PersistResult.NoSuchField, PersistResult.NoSuchField }, fieldsResult); + } +}