diff --git a/Lib9c b/Lib9c index 7f504d197..275f31998 160000 --- a/Lib9c +++ b/Lib9c @@ -1 +1 @@ -Subproject commit 7f504d197f0f159275f3901c820de4b087260445 +Subproject commit 275f3199855b319bbca490789d4bad3607fcceb5 diff --git a/NineChronicles.Headless.Tests/GraphTypes/StateQueryTest.cs b/NineChronicles.Headless.Tests/GraphTypes/StateQueryTest.cs index 038763d82..78303997c 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.TryGetSheet(cacheKey, out byte[] _)); + if (cached) + { + Assert.Equal(expected, data["cachedSheet"]); + } + else + { + Assert.Null(data["cachedSheet"]); + } + } + private static IEnumerable GetMemberDataOfGarages() { var agentAddr = new PrivateKey().ToAddress(); diff --git a/NineChronicles.Headless.Tests/GraphTypes/TransactionHeadlessQueryTest.cs b/NineChronicles.Headless.Tests/GraphTypes/TransactionHeadlessQueryTest.cs index 3a6cb5a0d..031d81040 100644 --- a/NineChronicles.Headless.Tests/GraphTypes/TransactionHeadlessQueryTest.cs +++ b/NineChronicles.Headless.Tests/GraphTypes/TransactionHeadlessQueryTest.cs @@ -342,6 +342,41 @@ public async Task TransactionResultIsSuccess() Assert.Equal("SUCCESS", txStatus); } + [Fact] + public async Task TransactionResults() + { + var privateKey = new PrivateKey(); + // Because `AddActivatedAccount` doesn't need any prerequisites. + var action = new AddActivatedAccount(default); + Transaction tx = _blockChain.MakeTransaction(privateKey, new ActionBase[] { action }); + var action2 = new DailyReward + { + avatarAddress = default + }; + Transaction tx2 = _blockChain.MakeTransaction(new PrivateKey(), new ActionBase[] { action2 }); + Block block = _blockChain.ProposeBlock(_proposer); + _blockChain.Append(block, GenerateBlockCommit(block.Index, block.Hash, _proposer)); + var queryFormat = @"query {{ + transactionResults(txIds: [""{0}"", ""{1}""]) {{ + blockHash + txStatus + }} + }}"; + var result = await ExecuteAsync(string.Format( + queryFormat, + tx.Id.ToString(), + tx2.Id.ToString() + )); + Assert.NotNull(result.Data); + var transactionResults = + (object[])((Dictionary)((ExecutionNode)result.Data!).ToValue()!)["transactionResults"]; + Assert.Equal(2, transactionResults.Length); + var txStatus = (string)((Dictionary)transactionResults[0])["txStatus"]; + Assert.Equal("SUCCESS", txStatus); + txStatus = (string)((Dictionary)transactionResults[1])["txStatus"]; + Assert.Equal("FAILURE", txStatus); + } + [Fact] public async Task NcTransactionsOnTip() { 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..a05b128f7 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) @@ -223,7 +221,7 @@ public UnaryResult> GetSheets( foreach (var b in addressBytesList) { var address = new Address(b); - if (_memoryCache.TryGetValue(address.ToString(), out byte[] cached)) + if (_memoryCache.TryGetSheet(address.ToString(), out byte[] cached)) { result.TryAdd(b, cached); } @@ -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..4f0e96193 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/GraphTypes/TransactionHeadlessQuery.cs b/NineChronicles.Headless/GraphTypes/TransactionHeadlessQuery.cs index 8474db53d..0632d7ca9 100644 --- a/NineChronicles.Headless/GraphTypes/TransactionHeadlessQuery.cs +++ b/NineChronicles.Headless/GraphTypes/TransactionHeadlessQuery.cs @@ -192,42 +192,22 @@ public TransactionHeadlessQuery(StandaloneContext standaloneContext) ), resolve: context => { - if (!(standaloneContext.BlockChain is BlockChain blockChain)) - { - throw new ExecutionError( - $"{nameof(StandaloneContext)}.{nameof(StandaloneContext.BlockChain)} was not set yet!"); - } - - if (!(standaloneContext.Store is IStore store)) - { - throw new ExecutionError( - $"{nameof(StandaloneContext)}.{nameof(StandaloneContext.Store)} was not set yet!"); - } - - TxId txId = context.GetArgument("txId"); - if (!(store.GetFirstTxIdBlockHashIndex(txId) is { } txExecutedBlockHash)) - { - return blockChain.GetStagedTransactionIds().Contains(txId) - ? new TxResult(TxStatus.STAGING, null, null, null, null, null) - : new TxResult(TxStatus.INVALID, null, null, null, null, null); - } + var txId = context.GetArgument("txId"); + return TxResult(standaloneContext, txId); + }); - try - { - TxExecution execution = blockChain.GetTxExecution(txExecutedBlockHash, txId); - Block txExecutedBlock = blockChain[txExecutedBlockHash]; - return new TxResult( - execution.Fail ? TxStatus.FAILURE : TxStatus.SUCCESS, - txExecutedBlock.Index, - txExecutedBlock.Hash.ToString(), - execution.InputState, - execution.OutputState, - execution.ExceptionNames); - } - catch (Exception) - { - return new TxResult(TxStatus.INVALID, null, null, null, null, null); - } + Field>>( + name: "transactionResults", + arguments: new QueryArguments( + new QueryArgument>> + { Name = "txIds", Description = "transaction ids." } + ), + resolve: context => + { + return context.GetArgument>("txIds") + .AsParallel() + .AsOrdered() + .Select(txId => TxResult(standaloneContext, txId)); } ); @@ -312,6 +292,45 @@ public TransactionHeadlessQuery(StandaloneContext standaloneContext) ); } + private static object? TxResult(StandaloneContext standaloneContext, TxId txId) + { + if (!(standaloneContext.BlockChain is BlockChain blockChain)) + { + throw new ExecutionError( + $"{nameof(StandaloneContext)}.{nameof(StandaloneContext.BlockChain)} was not set yet!"); + } + + if (!(standaloneContext.Store is IStore store)) + { + throw new ExecutionError( + $"{nameof(StandaloneContext)}.{nameof(StandaloneContext.Store)} was not set yet!"); + } + + if (!(store.GetFirstTxIdBlockHashIndex(txId) is { } txExecutedBlockHash)) + { + return blockChain.GetStagedTransactionIds().Contains(txId) + ? new TxResult(TxStatus.STAGING, null, null, null, null, null) + : new TxResult(TxStatus.INVALID, null, null, null, null, null); + } + + try + { + TxExecution execution = blockChain.GetTxExecution(txExecutedBlockHash, txId); + Block txExecutedBlock = blockChain[txExecutedBlockHash]; + return new TxResult( + execution.Fail ? TxStatus.FAILURE : TxStatus.SUCCESS, + txExecutedBlock.Index, + txExecutedBlock.Hash.ToString(), + execution.InputState, + execution.OutputState, + execution.ExceptionNames); + } + catch (Exception) + { + return new TxResult(TxStatus.INVALID, null, null, null, null, null); + } + } + private IEnumerable ListBlocks(BlockChain chain, long from, long limit) { if (chain.Tip.Index < from) diff --git a/NineChronicles.Headless/MemoryCacheExtensions.cs b/NineChronicles.Headless/MemoryCacheExtensions.cs new file mode 100644 index 000000000..3cda79d22 --- /dev/null +++ b/NineChronicles.Headless/MemoryCacheExtensions.cs @@ -0,0 +1,35 @@ +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 bool TryGetSheet(this MemoryCache cache, string cacheKey, out T cached) + { + return cache.TryGetValue(cacheKey, out cached); + } + + public static string? GetSheet(this MemoryCache cache, string cacheKey) + { + if (cache.TryGetSheet(cacheKey, out byte[] cached)) + { + return (Text)Codec.Decode(MessagePackSerializer.Deserialize(cached, Lz4Options)); + } + + return null; + } +}