From 16faa43bcdb56b09fd2011def9587e561a498dfe Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Fri, 4 Sep 2020 18:26:05 +0900 Subject: [PATCH] DelayedActionRenderer class --- .vscode/settings.json | 3 + CHANGES.md | 1 + .../Renderers/DelayedActionRendererTest.cs | 379 +++++++++++++++++ .../Renderers/DelayedRendererTest.cs | 16 +- Libplanet.Tests/Libplanet.Tests.csproj | 1 + Libplanet.sln.DotSettings | 3 + .../Renderers/DelayedActionRenderer.cs | 392 ++++++++++++++++++ .../Blockchain/Renderers/DelayedRenderer.cs | 38 +- 8 files changed, 813 insertions(+), 20 deletions(-) create mode 100644 Libplanet.Tests/Blockchain/Renderers/DelayedActionRendererTest.cs create mode 100644 Libplanet/Blockchain/Renderers/DelayedActionRenderer.cs diff --git a/.vscode/settings.json b/.vscode/settings.json index 44eb0d61ac..294b546e28 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,5 +19,8 @@ "struct", "unrender", "unrendered", + "unrenderer", + "unrendering", + "unrenders", ] } diff --git a/CHANGES.md b/CHANGES.md index 036ce816cd..9b5a586468 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -180,6 +180,7 @@ To be released. - Added `AnonymousRenderer` class. [[#959], [#963]] - Added `AnonymousActionRenderer` interface. [[#959], [#967], [#970]] - Added `DelayedRenderer` class. [[#980]] + - Added `DelayedActionRenderer` class. [[#980]] - Added `LoggedRenderer` class. [[#959], [#963]] - Added `LoggedActionRenderer` interface. [[#959], [#967], [#970]] - Added `BlockChain.Renderers` property. [[#945], [#959], [#963]] diff --git a/Libplanet.Tests/Blockchain/Renderers/DelayedActionRendererTest.cs b/Libplanet.Tests/Blockchain/Renderers/DelayedActionRendererTest.cs new file mode 100644 index 0000000000..9b9000a64b --- /dev/null +++ b/Libplanet.Tests/Blockchain/Renderers/DelayedActionRendererTest.cs @@ -0,0 +1,379 @@ +using System; +using System.Collections.Generic; +using Libplanet.Action; +using Libplanet.Blockchain.Renderers; +using Libplanet.Blocks; +using Libplanet.Tests.Common.Action; +using Xunit; +using Xunit.Abstractions; + +namespace Libplanet.Tests.Blockchain.Renderers +{ + public class DelayedActionRendererTest : DelayedRendererTest + { + private static readonly IAccountStateDelta _emptyStates = + new AccountStateDeltaImpl(_ => null, (_, __) => default, default); + + public DelayedActionRendererTest(ITestOutputHelper output) + : base(output) + { + } + + [Fact] + public override void BlocksBeingAppended() + { + // FIXME: Eliminate duplication between this and DelayedRendererTest + var blockLogs = new List<(Block OldTip, Block NewTip)>(); + var actionEvaluations = new List(); + uint unintendedCalls = 0; + var innerRenderer = new AnonymousActionRenderer + { + BlockRenderer = (oldTip, newTip) => blockLogs.Add((oldTip, newTip)), + ActionRenderer = (action, context, nextStates) => + actionEvaluations.Add(new ActionEvaluation(action, context, nextStates)), + ActionErrorRenderer = (action, context, e) => + actionEvaluations.Add( + new ActionEvaluation(action, context, context.PreviousStates, e) + ), + ReorgRenderer = (oldTip, newTip, bp) => + { + // Note that this callback should not be invoked in this test case. + unintendedCalls++; + }, + ActionUnrenderer = (action, context, nextStates) => + { + // Note that this callback should not be invoked in this test case. + unintendedCalls++; + }, + ActionErrorUnrenderer = (action, context, nextStates) => + { + // Note that this callback should not be invoked in this test case. + unintendedCalls++; + }, + }; + + var renderer = new DelayedActionRenderer(innerRenderer, _store, 3); + Assert.Null(renderer.Tip); + Assert.Empty(blockLogs); + Assert.Empty(actionEvaluations); + Assert.Equal(0U, unintendedCalls); + + // #0 -> 1 confirm; #1 -> no confirms; #2 -> no confirms + renderer.RenderBlock(_chainA[0], _chainA[1]); + renderer.RenderAction(new DumbAction(default, "#1.1"), FakeContext(), _emptyStates); + renderer.RenderAction(new DumbAction(default, "#1.2"), FakeContext(), _emptyStates); + Assert.Null(renderer.Tip); + Assert.Empty(blockLogs); + Assert.Empty(actionEvaluations); + Assert.Equal(0U, unintendedCalls); + + // #0 -> 2 confirms; #1 -> 1 confirm; #2 -> no confirms + renderer.RenderBlock(_chainA[1], _chainA[2]); + renderer.RenderActionError( + new DumbAction(default, "#2.1"), + FakeContext(), + new ThrowException.SomeException("#2.1") + ); + renderer.RenderAction(new DumbAction(default, "#2.2"), FakeContext(), _emptyStates); + Assert.Null(renderer.Tip); + Assert.Empty(blockLogs); + Assert.Empty(actionEvaluations); + Assert.Equal(0U, unintendedCalls); + + // #0 -> 3 confirms; #1 -> 2 confirms; #2 -> 1 confirm; tip changed -> #0 + renderer.RenderBlock(_chainA[2], _chainA[3]); + renderer.RenderAction(new DumbAction(default, "#3.1"), FakeContext(), _emptyStates); + Assert.Equal(_chainA[0], renderer.Tip); + Assert.Empty(blockLogs); + Assert.Empty(actionEvaluations); + Assert.Equal(0U, unintendedCalls); + + // #0 -> gone; #1 -> 3 confirms; #2 -> 2 confirms; tip changed -> #1; render(#0, #1) + renderer.RenderBlock(_chainA[3], _chainA[4]); + renderer.RenderAction(new DumbAction(default, "#4.1"), FakeContext(), _emptyStates); + Assert.Equal(_chainA[1], renderer.Tip); + Assert.Single(blockLogs); + Assert.Equal((_chainA[0], _chainA[1]), blockLogs[0]); + Assert.Equal(2, actionEvaluations.Count); + Assert.Equal("#1.1", ((DumbAction)actionEvaluations[0].Action).Item); + Assert.Null(actionEvaluations[0].Exception); + Assert.Equal("#1.2", ((DumbAction)actionEvaluations[1].Action).Item); + Assert.Null(actionEvaluations[1].Exception); + Assert.Equal(0U, unintendedCalls); + + // #0 -> gone; #1 -> gone; #2 -> 3 confirms; tip changed -> #2; render(#1, #2) + renderer.RenderBlock(_chainA[4], _chainA[5]); + Assert.Equal(_chainA[2], renderer.Tip); + Assert.Equal(2, blockLogs.Count); + Assert.Equal((_chainA[0], _chainA[1]), blockLogs[0]); + Assert.Equal((_chainA[1], _chainA[2]), blockLogs[1]); + Assert.Equal(4, actionEvaluations.Count); + Assert.Equal("#1.1", ((DumbAction)actionEvaluations[0].Action).Item); + Assert.Null(actionEvaluations[0].Exception); + Assert.Equal("#1.2", ((DumbAction)actionEvaluations[1].Action).Item); + Assert.Null(actionEvaluations[1].Exception); + Assert.Equal("#2.1", ((DumbAction)actionEvaluations[2].Action).Item); + Assert.NotNull(actionEvaluations[2].Exception); + Assert.IsType(actionEvaluations[2].Exception); + Assert.Equal("#2.2", ((DumbAction)actionEvaluations[3].Action).Item); + Assert.Null(actionEvaluations[3].Exception); + Assert.Equal(0U, unintendedCalls); + } + + [Fact] + public override void BlocksBeingAppendedInParallel() + { + // FIXME: Eliminate duplication between this and DelayedRendererTest + var blockLogs = new List<(Block OldTip, Block NewTip)>(); + var reorgLogs = new List<( + Block OldTip, + Block NewTip, + Block Branchpoint + )>(); + var renderLogs = new List<(bool Unrender, ActionEvaluation Evaluation)>(); + var innerRenderer = new AnonymousActionRenderer + { + BlockRenderer = (oldTip, newTip) => blockLogs.Add((oldTip, newTip)), + ReorgRenderer = (oldTip, newTip, bp) => reorgLogs.Add((oldTip, newTip, bp)), + ActionRenderer = (action, context, nextStates) => + renderLogs.Add((false, new ActionEvaluation(action, context, nextStates))), + ActionErrorRenderer = (act, ctx, e) => + renderLogs.Add((false, new ActionEvaluation(act, ctx, ctx.PreviousStates, e))), + ActionUnrenderer = (action, context, nextStates) => + renderLogs.Add((true, new ActionEvaluation(action, context, nextStates))), + ActionErrorUnrenderer = (act, ctx, e) => + renderLogs.Add((true, new ActionEvaluation(act, ctx, ctx.PreviousStates, e))), + }; + var renderer = new DelayedActionRenderer(innerRenderer, _store, 3); + Assert.Null(renderer.Tip); + Assert.Empty(blockLogs); + Assert.Empty(reorgLogs); + + // Some explanation on the fixture: there are two chains that shares a certain block + // as a branchpoint: _chainA and _chainB. The topmost mutual block, i.e., branchpoint, + // between _chainA and _chainB is _chainA[4] (== _chainB[4]). + // In this test, we tries to simulate blocks from two parallel chains being "appended" + // (discovered) to a peer. The order of discovery can be drawn as below (the prime + // [apostrophe] after the block index number like #N' shows it's from _chainB; the bare + // block index without that like #N means it's from _chainA): + // + // #4 (1st) + // | + // / \ + // #5 (2nd) #5' (3rd) + // | | + // #6 (3rd) #6' (4th) + // | | + // #7 (5th) #7' (6th) + // | | + // #8 (7th) #8' (8th) + // | + // #9' (9th) + + // #4 -> 1 confirm + // #5 -> no confirms + // #5' -> no confirms + renderer.RenderBlock(_chainA[4], _chainA[5]); + renderer.RenderAction(new DumbAction(default, "#5.1"), FakeContext(5), _emptyStates); + Assert.Null(renderer.Tip); + Assert.Empty(reorgLogs); + Assert.Empty(blockLogs); + Assert.Empty(renderLogs); + + // #4 -> 2 confirms + // #5 -> no confirms + // #5' -> no confirms + renderer.RenderReorg(_chainA[5], _chainB[5], _branchpoint); + renderer.RenderBlock(_chainA[5], _chainB[5]); + renderer.UnrenderAction(new DumbAction(default, "#5.1"), FakeContext(5), _emptyStates); + renderer.RenderAction(new DumbAction(default, "#5'.1"), FakeContext(5), _emptyStates); + renderer.RenderActionError( + new DumbAction(default, "#5'.2"), + FakeContext(5), + new ThrowException.SomeException("#5'.2") + ); + Assert.Null(renderer.Tip); + Assert.Empty(reorgLogs); + Assert.Empty(blockLogs); + Assert.Empty(renderLogs); + + // #4 -> 3 confirms; tip changed -> #4 + // #5 -> 1 confirm; #6 -> no confirm + // #5' -> no confirms + renderer.RenderReorg(_chainB[5], _chainA[6], _branchpoint); + renderer.RenderBlock(_chainB[5], _chainA[6]); + renderer.UnrenderActionError( + new DumbAction(default, "#5'.2"), + FakeContext(5), + new ThrowException.SomeException("#5'.2") + ); + renderer.UnrenderAction(new DumbAction(default, "#5'.1"), FakeContext(5), _emptyStates); + renderer.RenderAction(new DumbAction(default, "#5.1"), FakeContext(5), _emptyStates); + renderer.RenderAction(new DumbAction(default, "#6.1"), FakeContext(6), _emptyStates); + Assert.Equal(_chainA[4], renderer.Tip); + Assert.Empty(reorgLogs); + Assert.Empty(blockLogs); + Assert.Empty(renderLogs); + + // #4 -> gone but still is tip + // #5 -> 1 confirm; #6 -> no confirm + // #5' -> 1 confirm; #6' -> no confirm + renderer.RenderReorg(_chainA[6], _chainB[6], _branchpoint); + renderer.RenderBlock(_chainA[6], _chainB[6]); + renderer.UnrenderAction(new DumbAction(default, "#6.1"), FakeContext(6), _emptyStates); + renderer.UnrenderAction(new DumbAction(default, "#5.1"), FakeContext(5), _emptyStates); + renderer.RenderAction(new DumbAction(default, "#5'.1"), FakeContext(5), _emptyStates); + renderer.RenderActionError( + new DumbAction(default, "#5'.2"), + FakeContext(5), + new ThrowException.SomeException("#5'.2") + ); + renderer.RenderAction(new DumbAction(default, "#6'.1"), FakeContext(6), _emptyStates); + Assert.Equal(_chainA[4], renderer.Tip); + Assert.Empty(reorgLogs); + Assert.Empty(blockLogs); + Assert.Empty(renderLogs); + + // #4 -> gone but still is tip + // #5 -> 2 confirms; #6 -> 1 confirm; #7 -> no confirm + // #5' -> 1 confirm; #6' -> no confirm + renderer.RenderReorg(_chainB[6], _chainA[7], _branchpoint); + renderer.RenderBlock(_chainB[6], _chainA[7]); + renderer.UnrenderAction(new DumbAction(default, "#6'.1"), FakeContext(6), _emptyStates); + renderer.UnrenderActionError( + new DumbAction(default, "#5'.2"), + FakeContext(5), + new ThrowException.SomeException("#5'.2") + ); + renderer.UnrenderAction(new DumbAction(default, "#5'.1"), FakeContext(5), _emptyStates); + renderer.RenderAction(new DumbAction(default, "#5.1"), FakeContext(5), _emptyStates); + renderer.RenderAction(new DumbAction(default, "#6.1"), FakeContext(6), _emptyStates); + renderer.RenderAction(new DumbAction(default, "#7.1"), FakeContext(7), _emptyStates); + Assert.Equal(_chainA[4], renderer.Tip); + Assert.Empty(reorgLogs); + Assert.Empty(blockLogs); + Assert.Empty(renderLogs); + + // #4 -> gone but still is tip + // #5 -> 2 confirms; #6 -> 1 confirm; #7 -> no confirm + // #5' -> 2 confirms; #6' -> 1 confirm; #7' -> no confirm + renderer.RenderReorg(_chainA[7], _chainB[7], _branchpoint); + renderer.RenderBlock(_chainA[7], _chainB[7]); + renderer.UnrenderAction(new DumbAction(default, "#7.1"), FakeContext(7), _emptyStates); + renderer.UnrenderAction(new DumbAction(default, "#6.1"), FakeContext(6), _emptyStates); + renderer.UnrenderAction(new DumbAction(default, "#5.1"), FakeContext(5), _emptyStates); + renderer.RenderAction(new DumbAction(default, "#5'.1"), FakeContext(5), _emptyStates); + renderer.RenderActionError( + new DumbAction(default, "#5'.2"), + FakeContext(5), + new ThrowException.SomeException("#5'.2") + ); + renderer.RenderAction(new DumbAction(default, "#6'.1"), FakeContext(6), _emptyStates); + renderer.RenderAction(new DumbAction(default, "#7'.1"), FakeContext(7), _emptyStates); + Assert.Equal(_chainA[4], renderer.Tip); + Assert.Empty(reorgLogs); + Assert.Empty(blockLogs); + Assert.Empty(renderLogs); + + // #4 -> gone; tip changed -> #5; render(#4, #5) + // #5 -> 3 confirms; #6 -> 2 confirms; #7 -> 1 confirm; #8 -> no confirm + // #5' -> 2 confirms; #6' -> 1 confirm; #7' -> no confirm + renderer.RenderReorg(_chainB[7], _chainA[8], _branchpoint); + renderer.RenderBlock(_chainB[7], _chainA[8]); + renderer.UnrenderAction(new DumbAction(default, "#7'.1"), FakeContext(7), _emptyStates); + renderer.UnrenderAction(new DumbAction(default, "#6'.1"), FakeContext(6), _emptyStates); + renderer.UnrenderActionError( + new DumbAction(default, "#5'.2"), + FakeContext(5), + new ThrowException.SomeException("#5'.2") + ); + renderer.UnrenderAction(new DumbAction(default, "#5'.1"), FakeContext(5), _emptyStates); + renderer.RenderAction(new DumbAction(default, "#5.1"), FakeContext(5), _emptyStates); + renderer.RenderAction(new DumbAction(default, "#6.1"), FakeContext(6), _emptyStates); + renderer.RenderAction(new DumbAction(default, "#7.1"), FakeContext(7), _emptyStates); + renderer.RenderAction(new DumbAction(default, "#8.1"), FakeContext(8), _emptyStates); + Assert.Equal(_chainA[5], renderer.Tip); + Assert.Empty(reorgLogs); + Assert.Equal(new[] { (_chainA[4], _chainA[5]) }, blockLogs); + Assert.Single(renderLogs); + Assert.False(renderLogs[0].Unrender); + Assert.Equal("#5.1", ((DumbAction)renderLogs[0].Evaluation.Action).Item); + Assert.Null(renderLogs[0].Evaluation.Exception); + + // tip changed -> #5'; render(#5, #5'); reorg(#5, #5', #4) + // #5 -> 3 confirms; #6 -> 2 confirms; #7 -> 1 confirm; #8 -> no confirm + // #5' -> 3 confirms; #6' -> 2 confirms; #7' -> 1 confirm; #8' -> no confirm + renderer.RenderReorg(_chainA[8], _chainB[8], _branchpoint); + renderer.RenderBlock(_chainA[8], _chainB[8]); + renderer.UnrenderAction(new DumbAction(default, "#8.1"), FakeContext(8), _emptyStates); + renderer.UnrenderAction(new DumbAction(default, "#7.1"), FakeContext(7), _emptyStates); + renderer.UnrenderAction(new DumbAction(default, "#6.1"), FakeContext(6), _emptyStates); + renderer.UnrenderAction(new DumbAction(default, "#5.1"), FakeContext(5), _emptyStates); + renderer.RenderAction(new DumbAction(default, "#5'.1"), FakeContext(5), _emptyStates); + renderer.RenderActionError( + new DumbAction(default, "#5'.2"), + FakeContext(5), + new ThrowException.SomeException("#5'.2") + ); + renderer.RenderAction(new DumbAction(default, "#6'.1"), FakeContext(6), _emptyStates); + renderer.RenderAction(new DumbAction(default, "#7'.1"), FakeContext(7), _emptyStates); + renderer.RenderAction(new DumbAction(default, "#8'.1"), FakeContext(8), _emptyStates); + Assert.Equal(_chainB[5], renderer.Tip); + Assert.Equal(new[] { (_chainA[5], _chainB[5], _branchpoint) }, reorgLogs); + Assert.Equal(new[] { (_chainA[4], _chainA[5]), (_chainA[5], _chainB[5]) }, blockLogs); + Assert.Equal(1 + 1 + 2, renderLogs.Count); + Assert.True(renderLogs[1].Unrender); + Assert.Equal("#5.1", ((DumbAction)renderLogs[1].Evaluation.Action).Item); + Assert.Null(renderLogs[1].Evaluation.Exception); + Assert.False(renderLogs[2].Unrender); + Assert.Equal("#5'.1", ((DumbAction)renderLogs[2].Evaluation.Action).Item); + Assert.Null(renderLogs[2].Evaluation.Exception); + Assert.False(renderLogs[3].Unrender); + Assert.Equal("#5'.2", ((DumbAction)renderLogs[3].Evaluation.Action).Item); + Assert.NotNull(renderLogs[3].Evaluation.Exception); + + // tip changed -> #6'; render(#5', #6') + // #5' -> gone; #6' -> 3 confirms; #7' -> 2 confirm; #8' -> 1 confirm; #9' -> 1 confirm + renderer.RenderBlock(_chainB[8], _chainB[9]); + renderer.RenderAction(new DumbAction(default, "#9'.1"), FakeContext(9), _emptyStates); + Assert.Equal(_chainB[6], renderer.Tip); + Assert.Single(reorgLogs); + Assert.Equal( + new[] + { + (_chainA[4], _chainA[5]), + (_chainA[5], _chainB[5]), + (_chainB[5], _chainB[6]), + }, + blockLogs + ); + Assert.Equal(1 + 1 + 2 + 1, renderLogs.Count); + Assert.False(renderLogs[4].Unrender); + Assert.Equal("#6'.1", ((DumbAction)renderLogs[4].Evaluation.Action).Item); + Assert.Null(renderLogs[4].Evaluation.Exception); + } + + [Fact] + public void LocateBlockPath() + { + var renderer = new DelayedActionRenderer( + new AnonymousActionRenderer(), + _store, + 3 + ); + Assert.Equal( + new[] { _chainA[5].Hash, _chainA[6].Hash, _chainA[7].Hash }, + renderer.LocateBlockPath(_chainA[4], _chainA[7]) + ); + Assert.Throws( + () => renderer.LocateBlockPath(_chainA[5], _chainA[4]) + ); + Assert.Throws( + () => renderer.LocateBlockPath(_chainA[4], _chainA[4]) + ); + } + + private IActionContext FakeContext(long blockIndex = 1) => + new ActionContext(default, default, blockIndex, _emptyStates, 0); + } +} diff --git a/Libplanet.Tests/Blockchain/Renderers/DelayedRendererTest.cs b/Libplanet.Tests/Blockchain/Renderers/DelayedRendererTest.cs index 7f8be7eada..88327be201 100644 --- a/Libplanet.Tests/Blockchain/Renderers/DelayedRendererTest.cs +++ b/Libplanet.Tests/Blockchain/Renderers/DelayedRendererTest.cs @@ -13,11 +13,11 @@ namespace Libplanet.Tests.Blockchain.Renderers { public class DelayedRendererTest { - private static readonly IReadOnlyList> _chainA; - private static readonly IReadOnlyList> _chainB; - private static readonly Block _branchpoint; - private IStore _store; - private ILogger _logger; + protected static readonly IReadOnlyList> _chainA; + protected static readonly IReadOnlyList> _chainB; + protected static readonly Block _branchpoint; + protected IStore _store; + protected ILogger _logger; #pragma warning disable S3963 static DelayedRendererTest() @@ -48,7 +48,7 @@ public DelayedRendererTest(ITestOutputHelper output) .Enrich.WithThreadId() .WriteTo.TestOutput(output) .CreateLogger() - .ForContext(); + .ForContext(GetType()); Log.Logger = _logger; _logger.CompareBothChains( LogEventLevel.Debug, @@ -66,7 +66,7 @@ public DelayedRendererTest(ITestOutputHelper output) } [Fact] - public void BlocksBeingAppended() + public virtual void BlocksBeingAppended() { var blockLogs = new List<(Block OldTip, Block NewTip)>(); uint reorgs = 0; @@ -119,7 +119,7 @@ public void BlocksBeingAppended() } [Fact] - public void BlocksBeingAppendedInParallel() + public virtual void BlocksBeingAppendedInParallel() { var blockLogs = new List<(Block OldTip, Block NewTip)>(); var reorgLogs = new List<( diff --git a/Libplanet.Tests/Libplanet.Tests.csproj b/Libplanet.Tests/Libplanet.Tests.csproj index 6e836c8e87..664a35f028 100644 --- a/Libplanet.Tests/Libplanet.Tests.csproj +++ b/Libplanet.Tests/Libplanet.Tests.csproj @@ -8,6 +8,7 @@ true true 7.1 + $(NoWarn);SA1401 ..\Libplanet.ruleset diff --git a/Libplanet.sln.DotSettings b/Libplanet.sln.DotSettings index 61efa45af7..3406b2dc77 100644 --- a/Libplanet.sln.DotSettings +++ b/Libplanet.sln.DotSettings @@ -23,4 +23,7 @@ True True True + True + True + True diff --git a/Libplanet/Blockchain/Renderers/DelayedActionRenderer.cs b/Libplanet/Blockchain/Renderers/DelayedActionRenderer.cs new file mode 100644 index 0000000000..2f3de13f7d --- /dev/null +++ b/Libplanet/Blockchain/Renderers/DelayedActionRenderer.cs @@ -0,0 +1,392 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Security.Cryptography; +using Libplanet.Action; +using Libplanet.Blocks; +using Libplanet.Store; + +namespace Libplanet.Blockchain.Renderers +{ + /// + /// An version of . + /// Decorates an instance and delays the events until + /// blocks are confirmed the certain number of blocks. When blocks are recognized + /// the delayed events relevant to these blocks are relayed to the decorated + /// . + /// + /// An type. It should match to + /// 's type parameter. + /// + /// actionRenderer = new SomeActionRenderer(); + /// // Wraps the actionRenderer with DelayedActionRenderer; the SomeActionRenderer instance + /// // becomes to receive event messages only after the relevent blocks are confirmed + /// // by 3+ blocks. + /// actionRenderer = new DelayedActionRenderer( + /// actionRenderer, + /// store, + /// confirmations: 3); + /// // You must pass the same store to the BlockChain() constructor: + /// var chain = new BlockChain( + /// ..., + /// store: store, + /// renderers: new[] { actionRenderer }); + /// ]]> + /// + public class DelayedActionRenderer : DelayedRenderer, IActionRenderer + where T : IAction, new() + { + private readonly Dictionary, List> + _bufferedActionRenders; + + private readonly Dictionary, List> + _bufferedActionUnrenders; + + private HashDigest? _eventReceivingBlock; + private Reorg? _eventReceivingReorg; + + /// + /// Creates a new instance decorating the given + /// . + /// + /// The renderer to decorate which has the actual + /// implementations and receives delayed events. + /// The same store to what uses. + /// The required number of confirmations to recognize a block. + /// See also the property. + public DelayedActionRenderer(IActionRenderer renderer, IStore store, uint confirmations) + : base(renderer, store, confirmations) + { + ActionRenderer = renderer; + _bufferedActionRenders = + new Dictionary, List>(); + _bufferedActionUnrenders = + new Dictionary, List>(); + } + + /// + /// The inner action renderer which has the actual implementations and receives + /// delayed events. + /// + public IActionRenderer ActionRenderer { get; } + + /// + public override void RenderBlock(Block oldTip, Block newTip) + { + base.RenderBlock(oldTip, newTip); + if (_eventReceivingReorg is Reorg reorg) + { + foreach (HashDigest block in reorg.Unrendered) + { + if (_bufferedActionUnrenders.TryGetValue(block, out List? b)) + { + b?.Clear(); + } + } + + foreach (HashDigest block in reorg.Rendered) + { + if (_bufferedActionRenders.TryGetValue(block, out List? buf)) + { + buf?.Clear(); + } + } + + if (!(reorg.OldTip.Equals(oldTip) && reorg.NewTip.Equals(newTip))) + { + _eventReceivingReorg = null; + } + } + + _eventReceivingBlock = newTip.Hash; + _bufferedActionRenders[newTip.Hash] = new List(); + } + + /// + public override void RenderReorg(Block oldTip, Block newTip, Block branchpoint) + { + base.RenderReorg(oldTip, newTip, branchpoint); + _eventReceivingBlock = null; + _eventReceivingReorg = new Reorg( + LocateBlockPath(branchpoint, oldTip), + LocateBlockPath(branchpoint, newTip), + oldTip, + newTip, + branchpoint + ); + } + + /// + public void RenderAction( + IAction action, + IActionContext context, + IAccountStateDelta nextStates + ) => + DelayRenderingAction(new ActionEvaluation(action, context, nextStates)); + + /// + public void RenderActionError(IAction action, IActionContext context, Exception exception) + { + var eval = new ActionEvaluation(action, context, context.PreviousStates, exception); + DelayRenderingAction(eval); + } + + /// + public void UnrenderAction( + IAction action, + IActionContext context, + IAccountStateDelta nextStates + ) => + DelayUnrenderingAction(new ActionEvaluation(action, context, nextStates)); + + /// + public void UnrenderActionError(IAction action, IActionContext context, Exception exception) + { + var eval = new ActionEvaluation(action, context, context.PreviousStates, exception); + DelayUnrenderingAction(eval); + } + + /// + /// Lists all descendants from (exclusive) to + /// (inclusive). + /// + /// The block to get its descendants (excluding it). + /// The block to get its ancestors (including it). + /// Block hashes from to . + /// Lower block hashes go first, and upper block hashes go last. + /// Does not contain 's hash but 's one. + /// + /// Thrown when block's index + /// is not greater than block's index. + internal ImmutableArray> LocateBlockPath(Block lower, Block upper) + { + if (lower.Index >= upper.Index) + { + throw new ArgumentException( + $"The {nameof(upper)} block must has the greater index than " + + $"the {nameof(lower)} block.", + nameof(upper) + ); + } + + IEnumerable> Iterate() + { + for ( + Block? b = upper; + b is Block && b.Index > lower.Index; + b = b.PreviousHash is HashDigest prev ? Store.GetBlock(prev) : null + ) + { + yield return b.Hash; + } + } + + return Iterate().Reverse().ToImmutableArray(); + } + + /// + protected override Block? OnTipChanged(Block? oldTip, Block newTip) + { + if (oldTip is null) + { + return null; + } + + Block? branchpoint = base.OnTipChanged(oldTip, newTip); + if (branchpoint is null) + { + RenderBufferedActionEvaluations(newTip.Hash, unrender: false); + } + else + { + var blocksToUnrender = LocateBlockPath(branchpoint, oldTip).Reverse(); + foreach (HashDigest hash in blocksToUnrender) + { + RenderBufferedActionEvaluations(hash, unrender: true); + } + + var blocksToRender = LocateBlockPath(branchpoint, newTip); + foreach (HashDigest hash in blocksToRender) + { + RenderBufferedActionEvaluations(hash, unrender: false); + } + } + + return branchpoint; + } + + private void DelayRenderingAction(ActionEvaluation eval) + { + if (_eventReceivingReorg is Reorg reorg) + { + long blockIndex = eval.InputContext.BlockIndex; + HashDigest blockHash = reorg.IndexHash(blockIndex, unrender: false); + if (!_bufferedActionRenders.TryGetValue(blockHash, out List? buf)) + { + _bufferedActionRenders[blockHash] = buf = new List(); + } + + buf.Add(eval); + Logger.Verbose( + "Delayed an action render from #{BlockIndex} {BlockHash} (buffer: {Buffer}).", + blockIndex, + blockHash, + buf.Count + ); + } + else if (_eventReceivingBlock is HashDigest h && + _bufferedActionRenders.TryGetValue(h, out List? b) && + b is List buffer) + { + buffer.Add(eval); + Logger.Verbose( + "Delayed an action render from #{BlockIndex} {BlockHash} (buffer: {Buffer}).", + eval.InputContext.BlockIndex, + h, + buffer.Count + ); + } + else + { + const string msg = + "An action render {@Action} from the block #{BlockIndex} was ignored due " + + "unexpected internal state."; + Logger.Warning(msg, eval.Action, eval.InputContext.BlockIndex); + } + } + + private void DelayUnrenderingAction(ActionEvaluation eval) + { + if (_eventReceivingReorg is Reorg reorg) + { + long blockIndex = eval.InputContext.BlockIndex; + HashDigest blockHash = reorg.IndexHash(blockIndex, unrender: true); + if (!_bufferedActionUnrenders.TryGetValue(blockHash, out List? b)) + { + _bufferedActionUnrenders[blockHash] = b = new List(); + } + + b.Add(eval); + Logger.Verbose( + "Delayed an action unrender from #{BlockIndex} {BlockHash} (buffer: {Buffer}).", + blockIndex, + blockHash, + b.Count + ); + } + else + { + const string msg = + "An action unrender {@Action} from the block #{BlockIndex} was ignored due " + + "unexpected internal state."; + Logger.Warning(msg, eval.Action, eval.InputContext.BlockIndex); + } + } + + private void RenderBufferedActionEvaluations(HashDigest blockHash, bool unrender) + { + Dictionary, List> bufferMap + = unrender + ? _bufferedActionUnrenders + : _bufferedActionRenders; + string verb = unrender ? "unrender" : "render"; + if (bufferMap.TryGetValue(blockHash, out List? b) && + b is List buffer) + { + Logger.Debug( + $"Starts to {verb} {{BufferCount}} buffered actions from the block " + + "{BlockHash}...", + buffer.Count, + blockHash + ); + RenderActionEvaluations(buffer, unrender); + bufferMap.Remove(blockHash); + } + else + { + Logger.Debug( + $"There are no buffered actions to {verb} for the block {{BlockHash}}.", + blockHash + ); + } + } + + private void RenderActionEvaluations( + IEnumerable evaluations, + bool unrender + ) + { + foreach (ActionEvaluation eval in evaluations) + { + if (eval.Exception is Exception e) + { + if (unrender) + { + ActionRenderer.UnrenderActionError(eval.Action, eval.InputContext, e); + } + else + { + ActionRenderer.RenderActionError(eval.Action, eval.InputContext, e); + } + } + else + { + if (unrender) + { + ActionRenderer.UnrenderAction( + eval.Action, + eval.InputContext, + eval.OutputStates + ); + } + else + { + ActionRenderer.RenderAction( + eval.Action, + eval.InputContext, + eval.OutputStates + ); + } + } + } + } + + private readonly struct Reorg + { + public readonly ImmutableArray> Unrendered; + public readonly ImmutableArray> Rendered; + public readonly Block OldTip; + public readonly Block NewTip; + public readonly Block Branchpoint; + + public Reorg( + ImmutableArray> unrendered, + ImmutableArray> rendered, + Block oldTip, + Block newTip, + Block branchpoint + ) + { + Unrendered = unrendered; + Rendered = rendered; + OldTip = oldTip; + NewTip = newTip; + Branchpoint = branchpoint; + } + + public HashDigest IndexHash(long index, bool unrender) + { + int offset = (int)(index - Branchpoint.Index); + return (unrender ? Unrendered : Rendered)[offset - 1]; + } + } + } +} diff --git a/Libplanet/Blockchain/Renderers/DelayedRenderer.cs b/Libplanet/Blockchain/Renderers/DelayedRenderer.cs index 6f14899dd1..2acbca6e35 100644 --- a/Libplanet/Blockchain/Renderers/DelayedRenderer.cs +++ b/Libplanet/Blockchain/Renderers/DelayedRenderer.cs @@ -28,10 +28,14 @@ namespace Libplanet.Blockchain.Renderers /// var chain = new BlockChain(..., store: store, renderers: new[] { renderer }); /// ]]> /// + /// Since is a subtype of , + /// constructor can take + /// an instance as well. However, even it takes an action + /// renderer, action-level fine-grained events won't hear. For action renderers, + /// please use instead. public class DelayedRenderer : IRenderer where T : IAction, new() { - private ILogger _logger; private ConcurrentDictionary, uint> _confirmed; private Block? _tip; @@ -46,7 +50,7 @@ public class DelayedRenderer : IRenderer /// See also the property. public DelayedRenderer(IRenderer renderer, IStore store, uint confirmations) { - _logger = Log.ForContext>(); + Logger = Log.ForContext(GetType()); Renderer = renderer; Store = store; Confirmations = confirmations; @@ -88,7 +92,7 @@ private set if (_tip is null) { - _logger.Verbose( + Logger.Verbose( $"{nameof(DelayedRenderer)}.{nameof(Tip)} is tried to be updated to " + "#{NewTipIndex} {NewTipHash} (from null).", newTip.Index, @@ -97,7 +101,7 @@ private set } else { - _logger.Verbose( + Logger.Verbose( $"{nameof(DelayedRenderer)}.{nameof(Tip)} is tried to be updated to " + "#{NewTipIndex} {NewTipHash} (from #{OldTipIndex} {OldTipHash}).", newTip.Index, @@ -111,7 +115,7 @@ private set _tip = newTip; if (oldTip is null) { - _logger.Debug( + Logger.Debug( $"{nameof(DelayedRenderer)}.{nameof(Tip)} was updated to " + "#{NewTipIndex} {NewTipHash} (from null).", newTip.Index, @@ -120,7 +124,7 @@ private set } else { - _logger.Debug( + Logger.Debug( $"{nameof(DelayedRenderer)}.{nameof(Tip)} was updated to " + "#{NewTipIndex} {NewTipHash} (from #{OldTipIndex} {OldTipHash}).", newTip.Index, @@ -134,15 +138,20 @@ private set } } + /// + /// The logger to record internal state changes. + /// + protected ILogger Logger { get; } + /// - public void RenderBlock(Block oldTip, Block newTip) + public virtual void RenderBlock(Block oldTip, Block newTip) { _confirmed.TryAdd(oldTip.Hash, 0); DiscoverBlock(newTip); } /// - public void RenderReorg(Block oldTip, Block newTip, Block branchpoint) + public virtual void RenderReorg(Block oldTip, Block newTip, Block branchpoint) { _confirmed.TryAdd(branchpoint.Hash, 0); } @@ -153,19 +162,24 @@ public void RenderReorg(Block oldTip, Block newTip, Block branchpoint) /// /// The previously recognized topmost block. /// The topmost block recognized this time. - protected virtual void OnTipChanged(Block? oldTip, Block newTip) + /// A branchpoint between and + /// if the tip change is a reorg. Otherwise returns null. + protected virtual Block? OnTipChanged(Block? oldTip, Block newTip) { if (oldTip is null) { - return; + return null; } + Block? branchpoint = null; if (!newTip.PreviousHash.Equals(oldTip.Hash)) { - Renderer.RenderReorg(oldTip, newTip, FindBranchpoint(oldTip, newTip)); + branchpoint = FindBranchpoint(oldTip, newTip); + Renderer.RenderReorg(oldTip, newTip, branchpoint); } Renderer.RenderBlock(oldTip, newTip); + return branchpoint; } private void DiscoverBlock(Block block) @@ -188,7 +202,7 @@ private void DiscoverBlock(Block block) } uint c = _confirmed.AddOrUpdate(prevHash, k => 1U, (k, v) => v + 1U); - _logger.Verbose( + Logger.Verbose( "The block #{BlockIndex} {BlockHash} has {Confirmations} confirmations.", prevBlock.Index, prevBlock.Hash,