diff --git a/NineChronicles.DataProvider/GraphTypes/NineChroniclesSummarySchema.cs b/NineChronicles.DataProvider/GraphTypes/NineChroniclesSummarySchema.cs index 782f072f..14dc96c8 100644 --- a/NineChronicles.DataProvider/GraphTypes/NineChroniclesSummarySchema.cs +++ b/NineChronicles.DataProvider/GraphTypes/NineChroniclesSummarySchema.cs @@ -11,6 +11,7 @@ public NineChroniclesSummarySchema(IServiceProvider serviceProvider) : base(serviceProvider) { Query = serviceProvider.GetRequiredService(); + Mutation = serviceProvider.GetRequiredService(); } } } diff --git a/NineChronicles.DataProvider/Queries/NineChroniclesSummaryMutation.cs b/NineChronicles.DataProvider/Queries/NineChroniclesSummaryMutation.cs new file mode 100644 index 00000000..2107ecc4 --- /dev/null +++ b/NineChronicles.DataProvider/Queries/NineChroniclesSummaryMutation.cs @@ -0,0 +1,80 @@ +namespace NineChronicles.DataProvider.Queries +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using GraphQL; + using GraphQL.Types; + using Libplanet.Crypto; + using Nekoyume.Model.State; + using Nekoyume.Module; + using Nekoyume.TableData; + using NineChronicles.DataProvider.Store; + using NineChronicles.DataProvider.Store.Models; + using NineChronicles.Headless.GraphTypes.States; + using Serilog; + + public class NineChroniclesSummaryMutation : ObjectGraphType + { + public NineChroniclesSummaryMutation(MySqlStore store, StateContext stateContext) + { + Store = store; + StateContext = stateContext; + + Field>( + name: "migrateActivateCollections", + arguments: new QueryArguments( + new QueryArgument>> + { + Name = "signers", + } + ), + resolve: context => + { + var signers = context.GetArgument>("signers"); + var collectionSheet = StateContext.WorldState.GetSheet(); + var blockIndex = Store.GetTip(); + var result = new Dictionary(); + foreach (var signer in signers) + { + var avatars = Store.GetAvatarsFromSigner(signer); + foreach (var avatar in avatars) + { + try + { + var collectionState = stateContext.WorldState.GetCollectionState(new Address(avatar.Address!)); + var previous = RenderSubscriber.MigrateActivateCollections(Store, collectionSheet, blockIndex, collectionState, avatar, "Migrate from worker"); + result[avatar.Address!] = (signer, previous, avatar.ActivateCollections.Count); + } + catch (Exception e) + { + Log.Error(e, "[MigrateActivateCollections] Unexpected exception occurred during MocaWorker: {Exc}", e); + } + } + } + + if (result.Any()) + { + StringBuilder sb = new StringBuilder("[MigrateActivateCollections]migration result"); + sb.AppendLine("signer,avatar,previous,migrated"); + foreach (var kv in result) + { + var value = kv.Value; + var log = $"{value.Item1},{kv.Key},{value.Item2},{kv.Value.Item3}"; + sb.AppendLine(log); + } + + return sb.ToString(); + } + + return "[MigrateActivateCollections]no required migrations"; + } + ); + } + + private MySqlStore Store { get; } + + private StateContext StateContext { get; } + } +} diff --git a/NineChronicles.DataProvider/RenderSubscriber.cs b/NineChronicles.DataProvider/RenderSubscriber.cs index 68237b5b..c4f886c1 100644 --- a/NineChronicles.DataProvider/RenderSubscriber.cs +++ b/NineChronicles.DataProvider/RenderSubscriber.cs @@ -147,6 +147,57 @@ public static string GetSignerAndOtherAddressesHex(Address signer, params Addres return sb.ToString(); } + public static int MigrateActivateCollections( + MySqlStore mySqlStore, + CollectionSheet collectionSheet, + long blockIndex, + CollectionState collectionState, + AvatarModel avatar, + string actionId + ) + { + Log.Information("[MigrateActivateCollections] avatar: {Signer}, {Address}, {Collections}", avatar.AgentAddress, avatar.Address, avatar.ActivateCollections.Count); + + // check chain state ids to fill in missing collection data + var existIds = avatar.ActivateCollections.Select(i => i.CollectionId).ToList(); + var targetIds = collectionState.Ids.Except(existIds).ToList(); + Log.Information("[MigrateActivateCollections] migration targets: {Address}, [{ExistIds}]/[{TargetIds}]", avatar.Address, string.Join(",", existIds), string.Join(",", targetIds)); + var previous = avatar.ActivateCollections.Count; + foreach (var collectionId in targetIds) + { + var row = collectionSheet[collectionId]; + var options = new List(); + foreach (var modifier in row.StatModifiers) + { + var option = new CollectionOptionModel + { + StatType = modifier.StatType.ToString(), + OperationType = modifier.Operation.ToString(), + Value = modifier.Value, + }; + options.Add(option); + } + + var collectionModel = new ActivateCollectionModel + { + ActionId = actionId, + Avatar = avatar, + BlockIndex = blockIndex, + CollectionId = collectionId, + Options = options, + }; + avatar.ActivateCollections.Add(collectionModel); + } + + if (targetIds.Any()) + { + mySqlStore.UpdateAvatar(avatar); + Log.Information("[MigrateActivateCollections] Update Avatar: {Address}, [{Previous}]/[{New}]", avatar.Address, previous, avatar.ActivateCollections.Count); + } + + return previous; + } + protected override Task ExecuteAsync(CancellationToken stoppingToken) { _blockRenderer.BlockSubject.Subscribe(b => @@ -1430,38 +1481,27 @@ protected override Task ExecuteAsync(CancellationToken stoppingToken) _actionRenderer.EveryRender().Subscribe(ev => { - if (ev.Exception is null && ev.Action is { } activateCollection) + try { - var outputState = new World(_blockChainStates.GetWorldState(ev.OutputState)); - var collectionSheet = outputState.GetSheet(); - var avatar = MySqlStore.GetAvatar(activateCollection.AvatarAddress, true); - foreach (var (collectionId, materials) in activateCollection.CollectionData) + if (ev.Exception is null && ev.Action is { } activateCollection) { - var row = collectionSheet[collectionId]; - var options = new List(); - foreach (var modifier in row.StatModifiers) - { - var option = new CollectionOptionModel - { - StatType = modifier.StatType.ToString(), - OperationType = modifier.Operation.ToString(), - Value = modifier.Value, - }; - options.Add(option); - } - - var collectionModel = new ActivateCollectionModel - { - ActionId = activateCollection.Id.ToString(), - Avatar = avatar, - BlockIndex = ev.BlockIndex, - CollectionId = collectionId, - Options = options, - }; - avatar.ActivateCollections.Add(collectionModel); + var outputState = new World(_blockChainStates.GetWorldState(ev.OutputState)); + var collectionSheet = outputState.GetSheet(); + var avatar = MySqlStore.GetAvatar(activateCollection.AvatarAddress, true); + var collectionState = outputState.GetCollectionState(activateCollection.AvatarAddress); + MigrateActivateCollections( + MySqlStore, + collectionSheet, + ev.BlockIndex, + collectionState, + avatar, + activateCollection.Id.ToString() + ); } - - MySqlStore.UpdateAvatar(avatar); + } + catch (Exception ex) + { + Log.Error(ex, "RenderSubscriber Error: {ErrorMessage}, StackTrace: {StackTrace}", ex.Message, ex.StackTrace); } }); diff --git a/NineChronicles.DataProvider/Store/MySqlStore.cs b/NineChronicles.DataProvider/Store/MySqlStore.cs index 58e62a46..d6e8b905 100644 --- a/NineChronicles.DataProvider/Store/MySqlStore.cs +++ b/NineChronicles.DataProvider/Store/MySqlStore.cs @@ -2373,5 +2373,12 @@ public void UpdateAvatar(AvatarModel avatar) ctx.Avatars!.Update(avatar); ctx.SaveChanges(); } + + public ICollection GetAvatarsFromSigner(string signer) + { + using NineChroniclesContext ctx = _dbContextFactory.CreateDbContext(); + var avatars = ctx.Avatars!; + return avatars.Include(a => a.ActivateCollections).Where(a => signer == a.AgentAddress).ToList(); + } } }