Skip to content

Commit

Permalink
Merge pull request #1353 from greymistcube/feature/block-stats-cli
Browse files Browse the repository at this point in the history
New stats command added to planet cli
  • Loading branch information
longfin authored Jul 6, 2021
2 parents a14e7c8 + b98fe2f commit 6308ac9
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,8 @@ To be released.
to retrieve the data from the store. [[#1316], [#1340]]
- Added `planet store build-index-tx-block` commands
to build index from TxId to BlockHash. [[#1316], [#1340]]
- Added `planet stats summary` command to retrieve a state summary of a
stored chain in a CSV format. [[#1353]]

[#1156]: https://github.com/planetarium/libplanet/issues/1156
[#1192]: https://github.com/planetarium/libplanet/issues/1192
Expand Down Expand Up @@ -326,6 +328,7 @@ To be released.
[#1349]: https://github.com/planetarium/libplanet/issues/1349
[#1350]: https://github.com/planetarium/libplanet/pull/1350
[#1351]: https://github.com/planetarium/libplanet/pull/1351
[#1353]: https://github.com/planetarium/libplanet/pull/1353
[#1360]: https://github.com/planetarium/libplanet/pull/1360


Expand Down
98 changes: 98 additions & 0 deletions Libplanet.Extensions.Cocona.Tests/Commands/StatsCommandTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System;
using System.Collections.Immutable;
using System.IO;
using Libplanet.Extensions.Cocona.Commands;
using Libplanet.RocksDBStore.Tests;
using Libplanet.Tests.Store;
using Xunit;

namespace Libplanet.Extensions.Cocona.Tests.Commands
{
public class StatsCommandTest : IDisposable
{
private readonly ImmutableArray<StoreFixture> _storeFixtures;
private readonly StatsCommand _command;
private readonly TextWriter _originalWriter;

public StatsCommandTest()
{
_command = new StatsCommand();
_originalWriter = Console.Out;
try
{
_storeFixtures = ImmutableArray.Create<StoreFixture>(
new DefaultStoreFixture(false),
new RocksDBStoreFixture(),
new MonoRocksDBStoreFixture()
);
}
catch (TypeInitializationException)
{
throw new SkipException("RocksDB is not available.");
}

foreach (var storeFixture in _storeFixtures)
{
var guid = Guid.NewGuid();
storeFixture.Store.SetCanonicalChainId(guid);
storeFixture.Store.PutBlock(storeFixture.Block1);
storeFixture.Store.AppendIndex(guid, storeFixture.Block1.Hash);
storeFixture.Store.PutTransaction(storeFixture.Transaction1);
}
}

[SkippableFact]
public void SummaryInvalidArguments()
{
string badPathFormat = "rocksdb+foo+bar://" + "/bar";
string badPathScheme = "foo://" + "/bar";
long badOffset = long.MaxValue;
long badLimit = 0;

foreach (var storeFixture in _storeFixtures)
{
_command.Summary(
store: storeFixture.Store,
header: true,
offset: 0,
limit: 1);

Assert.Throws<ArgumentException>(() =>
_command.Summary(
path: badPathFormat,
header: true,
offset: 0,
limit: 1));
Assert.Throws<NotSupportedException>(() =>
_command.Summary(
path: badPathScheme,
header: true,
offset: 0,
limit: 1));
Assert.Throws<ArgumentException>(() =>
_command.Summary(
store: storeFixture.Store,
header: true,
offset: badOffset,
limit: 1));
Assert.Throws<ArgumentException>(() =>
_command.Summary(
store: storeFixture.Store,
header: true,
offset: 0,
limit: badLimit));
}
}

public void Dispose()
{
foreach (var storeFixture in _storeFixtures)
{
(storeFixture.Store as IDisposable)?.Dispose();
(storeFixture.StateStore as IDisposable)?.Dispose();
}

Console.SetOut(_originalWriter);
}
}
}
3 changes: 3 additions & 0 deletions Libplanet.Extensions.Cocona/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Libplanet.Extensions.Cocona.Tests")]
127 changes: 127 additions & 0 deletions Libplanet.Extensions.Cocona/Commands/StatsCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using Cocona;
using Libplanet.Blocks;
using Libplanet.RocksDBStore;
using Libplanet.Store;

namespace Libplanet.Extensions.Cocona.Commands
{
public class StatsCommand
{
private const string StoreArgumentDescription =
"The URI denoting the type and the path of a concrete class for " +
nameof(IStore) + "; " +
"given as <store-type>://<store-path>";

private readonly IImmutableDictionary<string, Func<string, IStore>>
_storeConstructors =
new Dictionary<string, Func<string, IStore>>
{
["default"] = storePath => new DefaultStore(storePath),
["rocksdb"] = storePath => new RocksDBStore.RocksDBStore(storePath),
["monorocksdb"] = storePath => new MonoRocksDBStore(storePath),
}.ToImmutableSortedDictionary();

[Command(Description = "Outputs a summary of a stored chain in a CSV format.")]
public void Summary(
[Option('p', Description = StoreArgumentDescription)]
string path,
[Option('r', Description = "Whether to include header row")]
bool header,
[Option('o', Description =
"Starting index offset; " +
"supports negative indexing")]
long offset = 0,
[Option('l', Description =
"Maximum number of results to return; " +
"no limit by default")]
long? limit = null) => Summary(
store: LoadStoreFromUri(path),
header: header,
offset: offset,
limit: limit);

internal void Summary(
IStore store,
bool header,
long offset,
long? limit)
{
if (limit is { } && limit < 1)
{
throw new ArgumentException($"limit must be at least 1: {limit}");
}

Guid chainId = store.GetCanonicalChainId()
?? throw Utils.Error($"Failed to find the canonical chain in store.");
long chainLength = store.CountIndex(chainId);

if (offset >= chainLength || (offset < 0 && chainLength + offset < 0))
{
throw new ArgumentException(
$"invalid offset value {offset} for found chain length {chainLength}");
}

IEnumerable<BlockHash> hashes = store.IterateIndexes(
chainId,
offset: offset >= 0
? (int)offset
: (int)(chainLength + offset),
limit: (int?)limit);

if (header)
{
Console.WriteLine("index,hash,difficulty,miner,txCount,timestamp,perceivedTime");
}

foreach (var hash in hashes)
{
var block = store.GetBlock<Utils.DummyAction>(hash);
var perceivedTime = store.GetBlockPerceivedTime(hash);

Console.WriteLine(
$"{block.Index}," +
$"{block.Hash}," +
$"{block.Difficulty}," +
$"{block.Miner}," +
$"{block.Transactions.Count}," +
$"{block.Timestamp.ToUnixTimeMilliseconds()}," +
$"{perceivedTime?.ToUnixTimeMilliseconds()}");
}
}

private IStore LoadStoreFromUri(string rawUri)
{
var uri = new Uri(rawUri);
var scheme = uri.Scheme;
var splitScheme = scheme.Split('+');
if (splitScheme.Length <= 0 || splitScheme.Length > 2)
{
const string? exceptionMessage = "A key-value store URI must have a scheme, " +
"e.g., default://, rocksdb+file://.";
throw new ArgumentException(exceptionMessage, nameof(rawUri));
}

if (!_storeConstructors.TryGetValue(
splitScheme[0],
out var constructor))
{
throw new NotSupportedException(
$"No store backend supports the such scheme: {splitScheme[0]}.");
}

// NOTE: Actually, there is only File scheme support and it will work to check only.
if (splitScheme.Length == 2
&& !Enum.TryParse<SchemeType>(splitScheme[1], ignoreCase: true, out _))
{
throw new NotSupportedException(
$"No store backend supports the such scheme: {splitScheme[1]}.");
}

return constructor(uri.AbsolutePath);
}
}
}
1 change: 1 addition & 0 deletions Libplanet.Tools/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace Libplanet.Tools
[HasSubCommands(typeof(KeyCommand), "key", Description = "Manage private keys.")]
[HasSubCommands(typeof(MptCommand), "mpt", Description = "Merkle Patricia Trie utilities.")]
[HasSubCommands(typeof(StoreCommand), "store", Description = "Store utilities.")]
[HasSubCommands(typeof(StatsCommand), "stats", Description = "Stats utilities.")]
public class Program
{
private static readonly string FileConfigurationServiceRoot = Path.Combine(
Expand Down

0 comments on commit 6308ac9

Please sign in to comment.