diff --git a/NineChronicles.Headless.Tests/GraphTypes/StateQueryTest.cs b/NineChronicles.Headless.Tests/GraphTypes/StateQueryTest.cs index 8d7df65d9..dbc0120a6 100644 --- a/NineChronicles.Headless.Tests/GraphTypes/StateQueryTest.cs +++ b/NineChronicles.Headless.Tests/GraphTypes/StateQueryTest.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; @@ -15,6 +16,7 @@ using Nekoyume.Model.Garages; using Nekoyume.Model.Item; using Nekoyume.Model.State; +using Nekoyume.TableData; using NineChronicles.Headless.GraphTypes; using NineChronicles.Headless.GraphTypes.States; using NineChronicles.Headless.Tests.Common; @@ -222,6 +224,38 @@ public async Task Garage( } } + [Theory] + [InlineData(true, "expected")] + [InlineData(false, null)] + public async Task CachedSheet(bool cached, string? expected) + { + var tableName = nameof(ItemRequirementSheet); + var cache = new StateMemoryCache(); + var cacheKey = Addresses.GetSheetAddress(tableName).ToString(); + if (cached) + { + cache.SheetCache.SetSheet(cacheKey, (Text)expected, TimeSpan.FromMinutes(1)); + } + var query = $"{{ cachedSheet(tableName: \"{tableName}\") }}"; + MockState mockState = MockState.Empty; + var queryResult = await ExecuteQueryAsync( + query, + source: new StateContext( + mockState, + 0L, cache)); + Assert.Null(queryResult.Errors); + var data = (Dictionary)((ExecutionNode)queryResult.Data!).ToValue()!; + Assert.Equal(cached, cache.SheetCache.TryGetValue(cacheKey, out _)); + if (cached) + { + Assert.Equal(expected, data["cachedSheet"]); + } + else + { + Assert.Null(data["cachedSheet"]); + } + } + private static IEnumerable GetMemberDataOfGarages() { var agentAddr = new PrivateKey().Address; diff --git a/NineChronicles.Headless.Tests/MemoryCacheExtensionsTest.cs b/NineChronicles.Headless.Tests/MemoryCacheExtensionsTest.cs new file mode 100644 index 000000000..d5d016d7e --- /dev/null +++ b/NineChronicles.Headless.Tests/MemoryCacheExtensionsTest.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading.Tasks; +using Bencodex; +using Bencodex.Types; +using MessagePack; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Nekoyume; +using Nekoyume.TableData; +using Xunit; + +namespace NineChronicles.Headless.Tests; + +public class MemoryCacheExtensionsTest +{ + [Fact] + public async Task Sheet() + { + var codec = new Codec(); + var lz4Options = MessagePackSerializerOptions.Standard.WithCompression(MessagePackCompression.Lz4BlockArray); + var cache = new MemoryCache(new OptionsWrapper(new MemoryCacheOptions + { + SizeLimit = null + })); + + var sheets = TableSheetsImporter.ImportSheets(); + var tableName = nameof(ItemRequirementSheet); + var csv = sheets[tableName]; + var cacheKey = Addresses.GetSheetAddress(tableName).ToString(); + var value = (Text)csv; + var compressed = MessagePackSerializer.Serialize(codec.Encode(value), lz4Options); + cache.SetSheet(cacheKey, value, TimeSpan.FromMilliseconds(100)); + Assert.True(cache.TryGetValue(cacheKey, out byte[] cached)); + Assert.Equal(compressed, cached); + Assert.Equal(csv, cache.GetSheet(cacheKey)); + await Task.Delay(100); + Assert.False(cache.TryGetValue(cacheKey, out byte[] _)); + } +} diff --git a/NineChronicles.Headless/ActionEvaluationPublisher.cs b/NineChronicles.Headless/ActionEvaluationPublisher.cs index e3003a3c5..f6f2142a3 100644 --- a/NineChronicles.Headless/ActionEvaluationPublisher.cs +++ b/NineChronicles.Headless/ActionEvaluationPublisher.cs @@ -118,7 +118,7 @@ public ActionEvaluationPublisher( { var action = ev.Action; var sheetAddress = Addresses.GetSheetAddress(action.TableName); - _memoryCache.Set(sheetAddress.ToString(), (Text)action.TableCsv); + _memoryCache.SetSheet(sheetAddress.ToString(), (Text)action.TableCsv, TimeSpan.FromMinutes(1)); } }); } diff --git a/NineChronicles.Headless/BlockChainService.cs b/NineChronicles.Headless/BlockChainService.cs index ccd26cbfd..82fb5cb18 100644 --- a/NineChronicles.Headless/BlockChainService.cs +++ b/NineChronicles.Headless/BlockChainService.cs @@ -44,7 +44,6 @@ public class BlockChainService : ServiceBase, IBlockChainSer private ActionEvaluationPublisher _publisher; private ConcurrentDictionary _sentryTraces; private MemoryCache _memoryCache; - private readonly MessagePackSerializerOptions _lz4Options; public BlockChainService( BlockChain blockChain, @@ -64,7 +63,6 @@ StateMemoryCache cache _publisher = actionEvaluationPublisher; _sentryTraces = sentryTraces; _memoryCache = cache.SheetCache; - _lz4Options = MessagePackSerializerOptions.Standard.WithCompression(MessagePackCompression.Lz4BlockArray); } public UnaryResult PutTransaction(byte[] txBytes) @@ -245,9 +243,8 @@ public UnaryResult> GetSheets( for (int i = 0; i < addresses.Count; i++) { var address = addresses[i]; - var value = _codec.Encode(values[i] ?? Null.Value); - var compressed = MessagePackSerializer.Serialize(value, _lz4Options); - _memoryCache.Set(address.ToString(), compressed); + var value = values[i] ?? Null.Value; + var compressed = _memoryCache.SetSheet(address.ToString(), value, TimeSpan.FromMinutes(1)); result.TryAdd(address.ToByteArray(), compressed); } } diff --git a/NineChronicles.Headless/GraphTypes/StateQuery.cs b/NineChronicles.Headless/GraphTypes/StateQuery.cs index c2f6d197c..d088fb5af 100644 --- a/NineChronicles.Headless/GraphTypes/StateQuery.cs +++ b/NineChronicles.Headless/GraphTypes/StateQuery.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Bencodex; using Bencodex.Types; using GraphQL; using GraphQL.Types; @@ -11,7 +12,6 @@ using Nekoyume; using Nekoyume.Action; using Nekoyume.Arena; -using Nekoyume.Battle; using Nekoyume.Extensions; using Nekoyume.Model.Arena; using Nekoyume.Model.EnumType; @@ -32,6 +32,8 @@ namespace NineChronicles.Headless.GraphTypes { public partial class StateQuery : ObjectGraphType { + private readonly Codec _codec = new Codec(); + public StateQuery() { Name = "StateQuery"; @@ -699,6 +701,22 @@ public StateQuery() return result; } ); + + Field( + name: "cachedSheet", + arguments: new QueryArguments( + new QueryArgument> + { + Name = "tableName" + } + ), + resolve: context => + { + var tableName = context.GetArgument("tableName"); + var cacheKey = Addresses.GetSheetAddress(tableName).ToString(); + return context.Source.StateMemoryCache.SheetCache.GetSheet(cacheKey); + } + ); } public static List GetRuneOptions( diff --git a/NineChronicles.Headless/MemoryCacheExtensions.cs b/NineChronicles.Headless/MemoryCacheExtensions.cs new file mode 100644 index 000000000..2c6366d83 --- /dev/null +++ b/NineChronicles.Headless/MemoryCacheExtensions.cs @@ -0,0 +1,30 @@ +using System; +using Bencodex; +using Bencodex.Types; +using MessagePack; +using Microsoft.Extensions.Caching.Memory; + +namespace NineChronicles.Headless; + +public static class MemoryCacheExtensions +{ + private static readonly Codec Codec = new Codec(); + private static readonly MessagePackSerializerOptions Lz4Options = MessagePackSerializerOptions.Standard.WithCompression(MessagePackCompression.Lz4BlockArray); + + public static byte[] SetSheet(this MemoryCache cache, string cacheKey, IValue value, TimeSpan ex) + { + var compressed = MessagePackSerializer.Serialize(Codec.Encode(value), Lz4Options); + cache.Set(cacheKey, compressed, ex); + return compressed; + } + + public static string? GetSheet(this MemoryCache cache, string cacheKey) + { + if (cache.TryGetValue(cacheKey, out byte[] cached)) + { + return (Text)Codec.Decode(MessagePackSerializer.Deserialize(cached, Lz4Options)); + } + + return null; + } +}