From 2b4d323b9d187412c6f4e6f03e36983374eba254 Mon Sep 17 00:00:00 2001 From: Avital-Fine Date: Tue, 19 Apr 2022 13:46:23 +0300 Subject: [PATCH 1/5] Support OBJECT FREQ --- .../Interfaces/IDatabase.cs | 10 ++++++++++ .../Interfaces/IDatabaseAsync.cs | 10 ++++++++++ .../KeyspaceIsolation/DatabaseWrapper.cs | 3 +++ .../KeyspaceIsolation/WrapperBase.cs | 3 +++ src/StackExchange.Redis/PublicAPI.Shipped.txt | 2 ++ src/StackExchange.Redis/RedisDatabase.cs | 13 +++++++++++++ src/StackExchange.Redis/RedisLiterals.cs | 1 + .../DatabaseWrapperTests.cs | 7 +++++++ tests/StackExchange.Redis.Tests/Keys.cs | 19 +++++++++++++++++++ .../WrapperBaseTests.cs | 7 +++++++ 10 files changed, 75 insertions(+) diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 4c42fa498..9408e7284 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -728,6 +728,16 @@ public interface IDatabase : IRedis, IDatabaseAsync /// https://redis.io/commands/pexpiretime DateTime? KeyExpireTime(RedisKey key, CommandFlags flags = CommandFlags.None); + /// + /// Returns the logarithmic access frequency counter of the object stored at . + /// The command is only available when the `maxmemory-policy` configuration directive is set to one of the LFU policies. + /// + /// The key to get a frequency count for. + /// The flags to use for this operation. + /// The number of logarithmic access frequency counter, ( if the key does not exist). + /// https://redis.io/commands/object-freq + long? KeyFrequency(RedisKey key, CommandFlags flags = CommandFlags.None); + /// /// Returns the time since the object stored at the specified key is idle (not requested by read or write operations). /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index a54681ce5..afaccf12a 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -706,6 +706,16 @@ public interface IDatabaseAsync : IRedisAsync /// https://redis.io/commands/pexpiretime Task KeyExpireTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + /// + /// Returns the logarithmic access frequency counter of the object stored at . + /// The command is only available when the `maxmemory-policy` configuration directive is set to one of the LFU policies. + /// + /// The key to get a frequency count for. + /// The flags to use for this operation. + /// The number of logarithmic access frequency counter, ( if the key does not exist). + /// https://redis.io/commands/object-freq + Task KeyFrequencyAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + /// /// Returns the time since the object stored at the specified key is idle (not requested by read or write operations). /// diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index 7def559b2..6ea9123a4 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -182,6 +182,9 @@ public bool KeyExpire(RedisKey key, TimeSpan? expiry, ExpireWhen when, CommandFl public DateTime? KeyExpireTime(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.KeyExpireTime(ToInner(key), flags); + public long? KeyFrequency(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyFrequency(ToInner(key), flags); + public TimeSpan? KeyIdleTime(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.KeyIdleTime(ToInner(key), flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index 5a1437e33..9c2348f8a 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -193,6 +193,9 @@ public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, ExpireWhen when public Task KeyExpireTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.KeyExpireTimeAsync(ToInner(key), flags); + public Task KeyFrequencyAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyFrequencyAsync(ToInner(key), flags); + public Task KeyIdleTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.KeyIdleTimeAsync(ToInner(key), flags); diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 7b4dfb2df..7b40a25c3 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -550,6 +550,7 @@ StackExchange.Redis.IDatabase.KeyExpire(StackExchange.Redis.RedisKey key, System StackExchange.Redis.IDatabase.KeyExpire(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.KeyExpire(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.ExpireWhen when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.KeyExpireTime(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.DateTime? +StackExchange.Redis.IDatabase.KeyFrequency(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long? StackExchange.Redis.IDatabase.KeyIdleTime(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.TimeSpan? StackExchange.Redis.IDatabase.KeyMigrate(StackExchange.Redis.RedisKey key, System.Net.EndPoint! toServer, int toDatabase = 0, int timeoutMilliseconds = 0, StackExchange.Redis.MigrateOptions migrateOptions = StackExchange.Redis.MigrateOptions.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void StackExchange.Redis.IDatabase.KeyMove(StackExchange.Redis.RedisKey key, int database, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool @@ -763,6 +764,7 @@ StackExchange.Redis.IDatabaseAsync.KeyExpireAsync(StackExchange.Redis.RedisKey k StackExchange.Redis.IDatabaseAsync.KeyExpireAsync(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyExpireAsync(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.ExpireWhen when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyExpireTimeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyFrequencyAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyIdleTimeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyMigrateAsync(StackExchange.Redis.RedisKey key, System.Net.EndPoint! toServer, int toDatabase = 0, int timeoutMilliseconds = 0, StackExchange.Redis.MigrateOptions migrateOptions = StackExchange.Redis.MigrateOptions.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyMoveAsync(StackExchange.Redis.RedisKey key, int database, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 500fcc963..84acb96f9 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -877,11 +877,24 @@ public Task KeyExpireAsync(RedisKey key, DateTime? expire, ExpireWhen when return ExecuteAsync(msg, ResultProcessor.NullableDateTimeFromMilliseconds); } + public long? KeyFrequency(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.OBJECT, RedisLiterals.FREQ, key); + return ExecuteSync(msg, ResultProcessor.NullableInt64); + } + + public Task KeyFrequencyAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.OBJECT, RedisLiterals.FREQ, key); + return ExecuteAsync(msg, ResultProcessor.NullableInt64); + } + public TimeSpan? KeyIdleTime(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.OBJECT, RedisLiterals.IDLETIME, key); return ExecuteSync(msg, ResultProcessor.TimeSpanFromSeconds); } + public Task KeyIdleTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.OBJECT, RedisLiterals.IDLETIME, key); diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 3b405ae8b..44d4c60ac 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -63,6 +63,7 @@ public static readonly RedisValue EXAT = "EXAT", EXISTS = "EXISTS", FLUSH = "FLUSH", + FREQ = "FREQ", GET = "GET", GETNAME = "GETNAME", GT = "GT", diff --git a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs index e51805da3..38287971b 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs @@ -319,6 +319,13 @@ public void KeyExpireTime() mock.Verify(_ => _.KeyExpireTime("prefix:key", CommandFlags.None)); } + [Fact] + public void KeyFrequency() + { + wrapper.KeyFrequency("key", CommandFlags.None); + mock.Verify(_ => _.KeyFrequency("prefix:key", CommandFlags.None)); + } + [Fact] public void KeyMigrate() { diff --git a/tests/StackExchange.Redis.Tests/Keys.cs b/tests/StackExchange.Redis.Tests/Keys.cs index 65dd48c51..765e7d1a3 100644 --- a/tests/StackExchange.Redis.Tests/Keys.cs +++ b/tests/StackExchange.Redis.Tests/Keys.cs @@ -296,4 +296,23 @@ public async Task KeyRefCount() Assert.Null(db.KeyRefCount(keyNotExists)); Assert.Null(await db.KeyRefCountAsync(keyNotExists)); } + + [Fact] + public async Task KeyFrequency() + { + using var conn = Create(); + + var key = Me(); + var db = conn.GetDatabase(); + + db.KeyDelete(key, CommandFlags.FireAndForget); + db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + db.StringGet(key); + + var count = db.KeyFrequency(key); + Assert.True(count > 0); + + count = await db.KeyFrequencyAsync(key); + Assert.True(count > 0); + } } diff --git a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs index b29d36b1a..4457f0e07 100644 --- a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs +++ b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs @@ -280,6 +280,13 @@ public void KeyExpireTimeAsync() mock.Verify(_ => _.KeyExpireTimeAsync("prefix:key", CommandFlags.None)); } + [Fact] + public void KeyFrequencyAsync() + { + wrapper.KeyFrequencyAsync("key", CommandFlags.None); + mock.Verify(_ => _.KeyFrequencyAsync("prefix:key", CommandFlags.None)); + } + [Fact] public void KeyMigrateAsync() { From 4df18a296b555963591707d80a234db0af83a269 Mon Sep 17 00:00:00 2001 From: Avital-Fine Date: Tue, 19 Apr 2022 13:48:27 +0300 Subject: [PATCH 2/5] release notes --- docs/ReleaseNotes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 857cd5166..d5dee4a24 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -23,6 +23,7 @@ - Adds: Support for `GEOSEARCH` with `.GeoSearch()`/`.GeoSearchAsync()` ([#2089 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2089)) - Adds: Support for `GEOSEARCHSTORE` with `.GeoSearchAndStore()`/`.GeoSearchAndStoreAsync()` ([#2089 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2089)) - Adds: Support for `HRANDFIELD` with `.HashRandomField()`/`.HashRandomFieldAsync()`, `.HashRandomFields()`/`.HashRandomFieldsAsync()`, and `.HashRandomFieldsWithValues()`/`.HashRandomFieldsWithValuesAsync()` ([#2090 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2090)) +- Adds: Support for `OBJECT FREQ` with `.KeyFrequency()`/`.KeyFrequencyAsync()` ([#2105 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2105)) ## 2.5.61 From 3393986eba7171ceb32e1de43b30ef518f12711f Mon Sep 17 00:00:00 2001 From: Avital-Fine Date: Tue, 19 Apr 2022 14:12:34 +0300 Subject: [PATCH 3/5] add config set and test for non existing key --- tests/StackExchange.Redis.Tests/Keys.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/StackExchange.Redis.Tests/Keys.cs b/tests/StackExchange.Redis.Tests/Keys.cs index 765e7d1a3..54df70d02 100644 --- a/tests/StackExchange.Redis.Tests/Keys.cs +++ b/tests/StackExchange.Redis.Tests/Keys.cs @@ -305,6 +305,8 @@ public async Task KeyFrequency() var key = Me(); var db = conn.GetDatabase(); + await db.ExecuteAsync("CONFIG", "SET", "maxmemory-policy", "allkeys-lfu"); + db.KeyDelete(key, CommandFlags.FireAndForget); db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); db.StringGet(key); @@ -314,5 +316,13 @@ public async Task KeyFrequency() count = await db.KeyFrequencyAsync(key); Assert.True(count > 0); + + // Key not exists + db.KeyDelete(key, CommandFlags.FireAndForget); + var res = db.KeyFrequency(key); + Assert.Null(res); + + res = await db.KeyFrequencyAsync(key); + Assert.Null(res); } } From 3492c6e81221e935a35663000382af2f77c37400 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 19 Apr 2022 13:09:00 -0400 Subject: [PATCH 4/5] Fix comment merge --- src/StackExchange.Redis/Interfaces/IDatabase.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index c523b1cbe..5ca31542f 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -756,12 +756,13 @@ public interface IDatabase : IRedis, IDatabaseAsync /// /// Returns the logarithmic access frequency counter of the object stored at . - /// The command is only available when the `maxmemory-policy` configuration directive is set to one of the LFU policies. + /// The command is only available when the maxmemory-policy configuration directive is set to + /// one of the LFU policies. /// /// The key to get a frequency count for. /// The flags to use for this operation. - /// The number of logarithmic access frequency counter, ( if the key does not exist). - /// https://redis.io/commands/object-freq + /// The number of logarithmic access frequency counter, ( if the key does not exist). + /// long? KeyFrequency(RedisKey key, CommandFlags flags = CommandFlags.None); /// From 6a323ad1ba665841e24071ea5445e5fa1d07f2e5 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 19 Apr 2022 13:09:44 -0400 Subject: [PATCH 5/5] Fix tests We can't be changing configs in the middle of a test, this is hostile to other tests running on the same server (e.g. this will cause KeyTouch to fail). Instead, cope with either config - not worth setting up another test instance for, IMO. --- tests/StackExchange.Redis.Tests/Keys.cs | 38 +++++++++++++++++-------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/tests/StackExchange.Redis.Tests/Keys.cs b/tests/StackExchange.Redis.Tests/Keys.cs index 54df70d02..cd09cf9de 100644 --- a/tests/StackExchange.Redis.Tests/Keys.cs +++ b/tests/StackExchange.Redis.Tests/Keys.cs @@ -300,29 +300,43 @@ public async Task KeyRefCount() [Fact] public async Task KeyFrequency() { - using var conn = Create(); + using var conn = Create(allowAdmin: true, require: RedisFeatures.v4_0_0); var key = Me(); var db = conn.GetDatabase(); + var server = GetServer(conn); - await db.ExecuteAsync("CONFIG", "SET", "maxmemory-policy", "allkeys-lfu"); + var serverConfig = server.ConfigGet("maxmemory-policy"); + var maxMemoryPolicy = serverConfig.Length == 1 ? serverConfig[0].Value : ""; + Log($"maxmemory-policy detected as {maxMemoryPolicy}"); + var isLfu = maxMemoryPolicy.Contains("lfu"); db.KeyDelete(key, CommandFlags.FireAndForget); db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); db.StringGet(key); - var count = db.KeyFrequency(key); - Assert.True(count > 0); + if (isLfu) + { + var count = db.KeyFrequency(key); + Assert.True(count > 0); - count = await db.KeyFrequencyAsync(key); - Assert.True(count > 0); + count = await db.KeyFrequencyAsync(key); + Assert.True(count > 0); - // Key not exists - db.KeyDelete(key, CommandFlags.FireAndForget); - var res = db.KeyFrequency(key); - Assert.Null(res); + // Key not exists + db.KeyDelete(key, CommandFlags.FireAndForget); + var res = db.KeyFrequency(key); + Assert.Null(res); - res = await db.KeyFrequencyAsync(key); - Assert.Null(res); + res = await db.KeyFrequencyAsync(key); + Assert.Null(res); + } + else + { + var ex = Assert.Throws(() => db.KeyFrequency(key)); + Assert.Contains("An LFU maxmemory policy is not selected", ex.Message); + ex = await Assert.ThrowsAsync(() => db.KeyFrequencyAsync(key)); + Assert.Contains("An LFU maxmemory policy is not selected", ex.Message); + } } }