From a7f39ef5f78fbb01936d773fc54914176862c78c Mon Sep 17 00:00:00 2001 From: Jimmy Date: Wed, 17 Jul 2024 20:25:08 +0800 Subject: [PATCH 01/10] Revert "Revert "Plugin unhandled exception (#3349)" (#3366)" This reverts commit f307a31cee4ba6d7a97c43058b73ba194efab9ac. --- src/Neo/Ledger/Blockchain.cs | 66 +++++++- src/Neo/Plugins/Plugin.cs | 56 ++++++- src/Neo/Plugins/PluginSettings.cs | 33 ++++ src/Neo/Plugins/UnhandledExceptionPolicy.cs | 20 +++ .../ApplicationLogs/ApplicationLogs.json | 3 +- src/Plugins/ApplicationLogs/LogReader.cs | 1 + src/Plugins/ApplicationLogs/Settings.cs | 4 +- src/Plugins/DBFTPlugin/DBFTPlugin.cs | 2 + src/Plugins/DBFTPlugin/DBFTPlugin.json | 3 +- src/Plugins/DBFTPlugin/Settings.cs | 4 +- src/Plugins/OracleService/OracleService.cs | 2 + src/Plugins/OracleService/OracleService.json | 1 + src/Plugins/OracleService/Settings.cs | 4 +- src/Plugins/RpcServer/RpcServer.json | 1 + src/Plugins/RpcServer/RpcServerPlugin.cs | 1 + src/Plugins/RpcServer/Settings.cs | 4 +- src/Plugins/StateService/Settings.cs | 4 +- src/Plugins/StateService/StatePlugin.cs | 2 + src/Plugins/StateService/StateService.json | 3 +- src/Plugins/StorageDumper/Settings.cs | 4 +- src/Plugins/StorageDumper/StorageDumper.cs | 2 +- src/Plugins/StorageDumper/StorageDumper.json | 3 +- src/Plugins/TokensTracker/TokensTracker.cs | 8 + src/Plugins/TokensTracker/TokensTracker.json | 3 +- tests/Neo.UnitTests/Plugins/TestPlugin.cs | 61 ++++++- tests/Neo.UnitTests/Plugins/UT_Plugin.cs | 151 ++++++++++++++++++ 26 files changed, 417 insertions(+), 29 deletions(-) create mode 100644 src/Neo/Plugins/PluginSettings.cs create mode 100644 src/Neo/Plugins/UnhandledExceptionPolicy.cs diff --git a/src/Neo/Ledger/Blockchain.cs b/src/Neo/Ledger/Blockchain.cs index ca27bb2124..e9e3cb0020 100644 --- a/src/Neo/Ledger/Blockchain.cs +++ b/src/Neo/Ledger/Blockchain.cs @@ -12,18 +12,22 @@ using Akka.Actor; using Akka.Configuration; using Akka.IO; +using Akka.Util.Internal; using Neo.IO.Actors; using Neo.Network.P2P; using Neo.Network.P2P.Payloads; using Neo.Persistence; +using Neo.Plugins; using Neo.SmartContract; using Neo.SmartContract.Native; using Neo.VM; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; namespace Neo.Ledger { @@ -468,10 +472,10 @@ private void Persist(Block block) Context.System.EventStream.Publish(application_executed); all_application_executed.Add(application_executed); } - Committing?.Invoke(system, block, snapshot, all_application_executed); + _ = InvokeCommittingAsync(system, block, snapshot, all_application_executed); snapshot.Commit(); } - Committed?.Invoke(system, block); + _ = InvokeCommittedAsync(system, block); system.MemPool.UpdatePoolForBlockPersisted(block, system.StoreView); extensibleWitnessWhiteList = null; block_cache.Remove(block.PrevHash); @@ -480,6 +484,64 @@ private void Persist(Block block) Debug.Assert(header.Index == block.Index); } + internal static async Task InvokeCommittingAsync(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList) + { + await InvokeHandlersAsync(Committing?.GetInvocationList(), h => ((CommittingHandler)h)(system, block, snapshot, applicationExecutedList)); + } + + internal static async Task InvokeCommittedAsync(NeoSystem system, Block block) + { + await InvokeHandlersAsync(Committed?.GetInvocationList(), h => ((CommittedHandler)h)(system, block)); + } + + private static async Task InvokeHandlersAsync(Delegate[] handlers, Action handlerAction) + { + if (handlers == null) return; + + var exceptions = new ConcurrentBag(); + var tasks = handlers.Select(handler => Task.Run(() => + { + try + { + // skip stopped plugin. + if (handler.Target is Plugin { IsStopped: true }) + { + return; + } + + handlerAction(handler); + } + catch (Exception ex) when (handler.Target is Plugin plugin) + { + switch (plugin.ExceptionPolicy) + { + case UnhandledExceptionPolicy.StopNode: + exceptions.Add(ex); + throw; + case UnhandledExceptionPolicy.StopPlugin: + //Stop plugin on exception + plugin.IsStopped = true; + break; + case UnhandledExceptionPolicy.Ignore: + // Log the exception and continue with the next handler + break; + default: + throw new InvalidCastException($"The exception policy {plugin.ExceptionPolicy} is not valid."); + } + + Utility.Log(nameof(plugin), LogLevel.Error, ex); + } + catch (Exception ex) + { + exceptions.Add(ex); + } + })).ToList(); + + await Task.WhenAll(tasks); + + exceptions.ForEach(e => throw e); + } + /// /// Gets a object used for creating the actor. /// diff --git a/src/Neo/Plugins/Plugin.cs b/src/Neo/Plugins/Plugin.cs index 248301af56..9feee25d57 100644 --- a/src/Neo/Plugins/Plugin.cs +++ b/src/Neo/Plugins/Plugin.cs @@ -33,7 +33,8 @@ public abstract class Plugin : IDisposable /// /// The directory containing the plugin folders. Files can be contained in any subdirectory. /// - public static readonly string PluginsDirectory = Combine(GetDirectoryName(System.AppContext.BaseDirectory), "Plugins"); + public static readonly string PluginsDirectory = + Combine(GetDirectoryName(System.AppContext.BaseDirectory), "Plugins"); private static readonly FileSystemWatcher configWatcher; @@ -67,6 +68,18 @@ public abstract class Plugin : IDisposable /// public virtual Version Version => GetType().Assembly.GetName().Version; + /// + /// If the plugin should be stopped when an exception is thrown. + /// Default is . + /// + protected internal virtual UnhandledExceptionPolicy ExceptionPolicy { get; init; } = UnhandledExceptionPolicy.StopNode; + + /// + /// The plugin will be stopped if an exception is thrown. + /// But it also depends on . + /// + internal bool IsStopped { get; set; } + static Plugin() { if (!Directory.Exists(PluginsDirectory)) return; @@ -74,7 +87,8 @@ static Plugin() { EnableRaisingEvents = true, IncludeSubdirectories = true, - NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.CreationTime | NotifyFilters.LastWrite | NotifyFilters.Size, + NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.CreationTime | + NotifyFilters.LastWrite | NotifyFilters.Size, }; configWatcher.Changed += ConfigWatcher_Changed; configWatcher.Created += ConfigWatcher_Changed; @@ -106,7 +120,8 @@ private static void ConfigWatcher_Changed(object sender, FileSystemEventArgs e) { case ".json": case ".dll": - Utility.Log(nameof(Plugin), LogLevel.Warning, $"File {e.Name} is {e.ChangeType}, please restart node."); + Utility.Log(nameof(Plugin), LogLevel.Warning, + $"File {e.Name} is {e.ChangeType}, please restart node."); break; } } @@ -119,7 +134,8 @@ private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEven AssemblyName an = new(args.Name); Assembly assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.FullName == args.Name) ?? - AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().Name == an.Name); + AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.GetName().Name == an.Name); if (assembly != null) return assembly; string filename = an.Name + ".dll"; @@ -150,7 +166,8 @@ public virtual void Dispose() /// The content of the configuration file read. protected IConfigurationSection GetConfiguration() { - return new ConfigurationBuilder().AddJsonFile(ConfigFile, optional: true).Build().GetSection("PluginConfiguration"); + return new ConfigurationBuilder().AddJsonFile(ConfigFile, optional: true).Build() + .GetSection("PluginConfiguration"); } private static void LoadPlugin(Assembly assembly) @@ -187,6 +204,7 @@ internal static void LoadPlugins() catch { } } } + foreach (Assembly assembly in assemblies) { LoadPlugin(assembly); @@ -229,7 +247,33 @@ protected internal virtual void OnSystemLoaded(NeoSystem system) /// if the is handled by a plugin; otherwise, . public static bool SendMessage(object message) { - return Plugins.Any(plugin => plugin.OnMessage(message)); + + return Plugins.Any(plugin => + { + try + { + return !plugin.IsStopped && + plugin.OnMessage(message); + } + catch (Exception ex) + { + switch (plugin.ExceptionPolicy) + { + case UnhandledExceptionPolicy.StopNode: + throw; + case UnhandledExceptionPolicy.StopPlugin: + plugin.IsStopped = true; + break; + case UnhandledExceptionPolicy.Ignore: + break; + default: + throw new InvalidCastException($"The exception policy {plugin.ExceptionPolicy} is not valid."); + } + Utility.Log(nameof(Plugin), LogLevel.Error, ex); + return false; + } + } + ); } } } diff --git a/src/Neo/Plugins/PluginSettings.cs b/src/Neo/Plugins/PluginSettings.cs new file mode 100644 index 0000000000..af33e44eea --- /dev/null +++ b/src/Neo/Plugins/PluginSettings.cs @@ -0,0 +1,33 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// PluginSettings.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Microsoft.Extensions.Configuration; +using Org.BouncyCastle.Security; +using System; + +namespace Neo.Plugins; + +public abstract class PluginSettings(IConfigurationSection section) +{ + public UnhandledExceptionPolicy ExceptionPolicy + { + get + { + var policyString = section.GetValue(nameof(UnhandledExceptionPolicy), nameof(UnhandledExceptionPolicy.StopNode)); + if (Enum.TryParse(policyString, out UnhandledExceptionPolicy policy)) + { + return policy; + } + + throw new InvalidParameterException($"{policyString} is not a valid UnhandledExceptionPolicy"); + } + } +} diff --git a/src/Neo/Plugins/UnhandledExceptionPolicy.cs b/src/Neo/Plugins/UnhandledExceptionPolicy.cs new file mode 100644 index 0000000000..035e173aa3 --- /dev/null +++ b/src/Neo/Plugins/UnhandledExceptionPolicy.cs @@ -0,0 +1,20 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UnhandledExceptionPolicy.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.Plugins +{ + public enum UnhandledExceptionPolicy + { + Ignore = 0, + StopPlugin = 1, + StopNode = 2, + } +} diff --git a/src/Plugins/ApplicationLogs/ApplicationLogs.json b/src/Plugins/ApplicationLogs/ApplicationLogs.json index af601bc81e..2664665dd2 100644 --- a/src/Plugins/ApplicationLogs/ApplicationLogs.json +++ b/src/Plugins/ApplicationLogs/ApplicationLogs.json @@ -3,7 +3,8 @@ "Path": "ApplicationLogs_{0}", "Network": 860833102, "MaxStackSize": 65535, - "Debug": false + "Debug": false, + "UnhandledExceptionPolicy": "StopPlugin" }, "Dependency": [ "RpcServer" diff --git a/src/Plugins/ApplicationLogs/LogReader.cs b/src/Plugins/ApplicationLogs/LogReader.cs index e0e886d93b..b3f767fecb 100644 --- a/src/Plugins/ApplicationLogs/LogReader.cs +++ b/src/Plugins/ApplicationLogs/LogReader.cs @@ -38,6 +38,7 @@ public class LogReader : Plugin, ICommittingHandler, ICommittedHandler, ILogHand public override string Name => "ApplicationLogs"; public override string Description => "Synchronizes smart contract VM executions and notifications (NotifyLog) on blockchain."; + protected override UnhandledExceptionPolicy ExceptionPolicy => Settings.Default.ExceptionPolicy; #region Ctor diff --git a/src/Plugins/ApplicationLogs/Settings.cs b/src/Plugins/ApplicationLogs/Settings.cs index 8f2a0da1e1..6a5f238272 100644 --- a/src/Plugins/ApplicationLogs/Settings.cs +++ b/src/Plugins/ApplicationLogs/Settings.cs @@ -13,7 +13,7 @@ namespace Neo.Plugins.ApplicationLogs { - internal class Settings + internal class Settings : PluginSettings { public string Path { get; } public uint Network { get; } @@ -23,7 +23,7 @@ internal class Settings public static Settings Default { get; private set; } - private Settings(IConfigurationSection section) + private Settings(IConfigurationSection section) : base(section) { Path = section.GetValue("Path", "ApplicationLogs_{0}"); Network = section.GetValue("Network", 5195086u); diff --git a/src/Plugins/DBFTPlugin/DBFTPlugin.cs b/src/Plugins/DBFTPlugin/DBFTPlugin.cs index f09be9d291..65fc5011dc 100644 --- a/src/Plugins/DBFTPlugin/DBFTPlugin.cs +++ b/src/Plugins/DBFTPlugin/DBFTPlugin.cs @@ -31,6 +31,8 @@ public class DBFTPlugin : Plugin, IServiceAddedHandler, IMessageReceivedHandler, public override string ConfigFile => System.IO.Path.Combine(RootPath, "DBFTPlugin.json"); + protected override UnhandledExceptionPolicy ExceptionPolicy => settings.ExceptionPolicy; + public DBFTPlugin() { RemoteNode.MessageReceived += ((IMessageReceivedHandler)this).RemoteNode_MessageReceived_Handler; diff --git a/src/Plugins/DBFTPlugin/DBFTPlugin.json b/src/Plugins/DBFTPlugin/DBFTPlugin.json index 2e2b710ba3..705b2b77cb 100644 --- a/src/Plugins/DBFTPlugin/DBFTPlugin.json +++ b/src/Plugins/DBFTPlugin/DBFTPlugin.json @@ -5,6 +5,7 @@ "AutoStart": false, "Network": 860833102, "MaxBlockSize": 2097152, - "MaxBlockSystemFee": 150000000000 + "MaxBlockSystemFee": 150000000000, + "UnhandledExceptionPolicy": "StopNode" } } diff --git a/src/Plugins/DBFTPlugin/Settings.cs b/src/Plugins/DBFTPlugin/Settings.cs index 28ad21f37a..1f37feaf16 100644 --- a/src/Plugins/DBFTPlugin/Settings.cs +++ b/src/Plugins/DBFTPlugin/Settings.cs @@ -13,7 +13,7 @@ namespace Neo.Plugins.DBFTPlugin { - public class Settings + public class Settings : PluginSettings { public string RecoveryLogs { get; } public bool IgnoreRecoveryLogs { get; } @@ -22,7 +22,7 @@ public class Settings public uint MaxBlockSize { get; } public long MaxBlockSystemFee { get; } - public Settings(IConfigurationSection section) + public Settings(IConfigurationSection section) : base(section) { RecoveryLogs = section.GetValue("RecoveryLogs", "ConsensusState"); IgnoreRecoveryLogs = section.GetValue("IgnoreRecoveryLogs", false); diff --git a/src/Plugins/OracleService/OracleService.cs b/src/Plugins/OracleService/OracleService.cs index d7a54c5c9d..bb5da83654 100644 --- a/src/Plugins/OracleService/OracleService.cs +++ b/src/Plugins/OracleService/OracleService.cs @@ -62,6 +62,8 @@ public class OracleService : Plugin, ICommittingHandler, IServiceAddedHandler, I public override string Description => "Built-in oracle plugin"; + protected override UnhandledExceptionPolicy ExceptionPolicy => Settings.Default.ExceptionPolicy; + public override string ConfigFile => System.IO.Path.Combine(RootPath, "OracleService.json"); public OracleService() diff --git a/src/Plugins/OracleService/OracleService.json b/src/Plugins/OracleService/OracleService.json index 1ab0d93399..49bf1153b3 100644 --- a/src/Plugins/OracleService/OracleService.json +++ b/src/Plugins/OracleService/OracleService.json @@ -6,6 +6,7 @@ "MaxOracleTimeout": 10000, "AllowPrivateHost": false, "AllowedContentTypes": [ "application/json" ], + "UnhandledExceptionPolicy": "Ignore", "Https": { "Timeout": 5000 }, diff --git a/src/Plugins/OracleService/Settings.cs b/src/Plugins/OracleService/Settings.cs index 952ea0c27b..db93c1c400 100644 --- a/src/Plugins/OracleService/Settings.cs +++ b/src/Plugins/OracleService/Settings.cs @@ -37,7 +37,7 @@ public NeoFSSettings(IConfigurationSection section) } } - class Settings + class Settings : PluginSettings { public uint Network { get; } public Uri[] Nodes { get; } @@ -51,7 +51,7 @@ class Settings public static Settings Default { get; private set; } - private Settings(IConfigurationSection section) + private Settings(IConfigurationSection section) : base(section) { Network = section.GetValue("Network", 5195086u); Nodes = section.GetSection("Nodes").GetChildren().Select(p => new Uri(p.Get(), UriKind.Absolute)).ToArray(); diff --git a/src/Plugins/RpcServer/RpcServer.json b/src/Plugins/RpcServer/RpcServer.json index 8f6905dead..dc9c25b8da 100644 --- a/src/Plugins/RpcServer/RpcServer.json +++ b/src/Plugins/RpcServer/RpcServer.json @@ -1,5 +1,6 @@ { "PluginConfiguration": { + "UnhandledExceptionPolicy": "Ignore", "Servers": [ { "Network": 860833102, diff --git a/src/Plugins/RpcServer/RpcServerPlugin.cs b/src/Plugins/RpcServer/RpcServerPlugin.cs index c22462d139..03416c1be5 100644 --- a/src/Plugins/RpcServer/RpcServerPlugin.cs +++ b/src/Plugins/RpcServer/RpcServerPlugin.cs @@ -24,6 +24,7 @@ public class RpcServerPlugin : Plugin private static readonly Dictionary> handlers = new(); public override string ConfigFile => System.IO.Path.Combine(RootPath, "RpcServer.json"); + protected override UnhandledExceptionPolicy ExceptionPolicy => settings.ExceptionPolicy; protected override void Configure() { diff --git a/src/Plugins/RpcServer/Settings.cs b/src/Plugins/RpcServer/Settings.cs index ad624d9082..2cf7b72fb8 100644 --- a/src/Plugins/RpcServer/Settings.cs +++ b/src/Plugins/RpcServer/Settings.cs @@ -18,11 +18,11 @@ namespace Neo.Plugins.RpcServer { - class Settings + class Settings : PluginSettings { public IReadOnlyList Servers { get; init; } - public Settings(IConfigurationSection section) + public Settings(IConfigurationSection section) : base(section) { Servers = section.GetSection(nameof(Servers)).GetChildren().Select(p => RpcServerSettings.Load(p)).ToArray(); } diff --git a/src/Plugins/StateService/Settings.cs b/src/Plugins/StateService/Settings.cs index 8557866bc1..a425b57d7e 100644 --- a/src/Plugins/StateService/Settings.cs +++ b/src/Plugins/StateService/Settings.cs @@ -13,7 +13,7 @@ namespace Neo.Plugins.StateService { - internal class Settings + internal class Settings : PluginSettings { public string Path { get; } public bool FullState { get; } @@ -23,7 +23,7 @@ internal class Settings public static Settings Default { get; private set; } - private Settings(IConfigurationSection section) + private Settings(IConfigurationSection section) : base(section) { Path = section.GetValue("Path", "Data_MPT_{0}"); FullState = section.GetValue("FullState", false); diff --git a/src/Plugins/StateService/StatePlugin.cs b/src/Plugins/StateService/StatePlugin.cs index 5f6e1ef1d8..03dcc55aac 100644 --- a/src/Plugins/StateService/StatePlugin.cs +++ b/src/Plugins/StateService/StatePlugin.cs @@ -41,6 +41,8 @@ public class StatePlugin : Plugin, ICommittingHandler, ICommittedHandler, IWalle public override string Description => "Enables MPT for the node"; public override string ConfigFile => System.IO.Path.Combine(RootPath, "StateService.json"); + protected override UnhandledExceptionPolicy ExceptionPolicy => Settings.Default.ExceptionPolicy; + internal IActorRef Store; internal IActorRef Verifier; diff --git a/src/Plugins/StateService/StateService.json b/src/Plugins/StateService/StateService.json index 265436fc30..cadd2da5fd 100644 --- a/src/Plugins/StateService/StateService.json +++ b/src/Plugins/StateService/StateService.json @@ -4,7 +4,8 @@ "FullState": false, "Network": 860833102, "AutoVerify": false, - "MaxFindResultItems": 100 + "MaxFindResultItems": 100, + "UnhandledExceptionPolicy": "StopPlugin" }, "Dependency": [ "RpcServer" diff --git a/src/Plugins/StorageDumper/Settings.cs b/src/Plugins/StorageDumper/Settings.cs index c2761ce6b9..e645cd7074 100644 --- a/src/Plugins/StorageDumper/Settings.cs +++ b/src/Plugins/StorageDumper/Settings.cs @@ -14,7 +14,7 @@ namespace Neo.Plugins.StorageDumper { - internal class Settings + internal class Settings : PluginSettings { /// /// Amount of storages states (heights) to be dump in a given json file @@ -32,7 +32,7 @@ internal class Settings public static Settings? Default { get; private set; } - private Settings(IConfigurationSection section) + private Settings(IConfigurationSection section) : base(section) { // Geting settings for storage changes state dumper BlockCacheSize = section.GetValue("BlockCacheSize", 1000u); diff --git a/src/Plugins/StorageDumper/StorageDumper.cs b/src/Plugins/StorageDumper/StorageDumper.cs index c47df9ac1d..6f5498b3a2 100644 --- a/src/Plugins/StorageDumper/StorageDumper.cs +++ b/src/Plugins/StorageDumper/StorageDumper.cs @@ -30,7 +30,7 @@ public class StorageDumper : Plugin, ICommittingHandler, ICommittedHandler /// private JObject? _currentBlock; private string? _lastCreateDirectory; - + protected override UnhandledExceptionPolicy ExceptionPolicy => Settings.Default?.ExceptionPolicy ?? UnhandledExceptionPolicy.Ignore; public override string Description => "Exports Neo-CLI status data"; diff --git a/src/Plugins/StorageDumper/StorageDumper.json b/src/Plugins/StorageDumper/StorageDumper.json index b327c37e0c..0c314cf262 100644 --- a/src/Plugins/StorageDumper/StorageDumper.json +++ b/src/Plugins/StorageDumper/StorageDumper.json @@ -3,6 +3,7 @@ "BlockCacheSize": 1000, "HeightToBegin": 0, "StoragePerFolder": 100000, - "Exclude": [ -4 ] + "Exclude": [ -4 ], + "UnhandledExceptionPolicy": "Ignore" } } diff --git a/src/Plugins/TokensTracker/TokensTracker.cs b/src/Plugins/TokensTracker/TokensTracker.cs index e5ffdce3f4..a7ab075245 100644 --- a/src/Plugins/TokensTracker/TokensTracker.cs +++ b/src/Plugins/TokensTracker/TokensTracker.cs @@ -16,6 +16,7 @@ using Neo.Persistence; using Neo.Plugins.RpcServer; using Neo.Plugins.Trackers; +using System; using System.Collections.Generic; using System.Linq; using static System.IO.Path; @@ -30,8 +31,10 @@ public class TokensTracker : Plugin, ICommittingHandler, ICommittedHandler private uint _network; private string[] _enabledTrackers; private IStore _db; + private UnhandledExceptionPolicy _exceptionPolicy; private NeoSystem neoSystem; private readonly List trackers = new(); + protected override UnhandledExceptionPolicy ExceptionPolicy => _exceptionPolicy; public override string Description => "Enquiries balances and transaction history of accounts through RPC"; @@ -57,6 +60,11 @@ protected override void Configure() _maxResults = config.GetValue("MaxResults", 1000u); _network = config.GetValue("Network", 860833102u); _enabledTrackers = config.GetSection("EnabledTrackers").GetChildren().Select(p => p.Value).ToArray(); + var policyString = config.GetValue(nameof(UnhandledExceptionPolicy), nameof(UnhandledExceptionPolicy.StopNode)); + if (Enum.TryParse(policyString, out UnhandledExceptionPolicy policy)) + { + _exceptionPolicy = policy; + } } protected override void OnSystemLoaded(NeoSystem system) diff --git a/src/Plugins/TokensTracker/TokensTracker.json b/src/Plugins/TokensTracker/TokensTracker.json index ca63183b68..dbdbecfd40 100644 --- a/src/Plugins/TokensTracker/TokensTracker.json +++ b/src/Plugins/TokensTracker/TokensTracker.json @@ -4,7 +4,8 @@ "TrackHistory": true, "MaxResults": 1000, "Network": 860833102, - "EnabledTrackers": [ "NEP-11", "NEP-17" ] + "EnabledTrackers": [ "NEP-11", "NEP-17" ], + "UnhandledExceptionPolicy": "StopPlugin" }, "Dependency": [ "RpcServer" diff --git a/tests/Neo.UnitTests/Plugins/TestPlugin.cs b/tests/Neo.UnitTests/Plugins/TestPlugin.cs index dde500b927..5e51220c7f 100644 --- a/tests/Neo.UnitTests/Plugins/TestPlugin.cs +++ b/tests/Neo.UnitTests/Plugins/TestPlugin.cs @@ -10,15 +10,60 @@ // modifications are permitted. using Microsoft.Extensions.Configuration; +using Neo.Ledger; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; using Neo.Plugins; +using System; +using System.Collections.Generic; namespace Neo.UnitTests.Plugins { - public class TestPlugin : Plugin + + internal class TestPluginSettings(IConfigurationSection section) : PluginSettings(section) + { + public static TestPluginSettings Default { get; private set; } + public static void Load(IConfigurationSection section) + { + Default = new TestPluginSettings(section); + } + } + internal class TestNonPlugin + { + public TestNonPlugin() + { + Blockchain.Committing += OnCommitting; + Blockchain.Committed += OnCommitted; + } + + private void OnCommitting(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList) + { + throw new NotImplementedException("Test exception from OnCommitting"); + } + + private void OnCommitted(NeoSystem system, Block block) + { + throw new NotImplementedException("Test exception from OnCommitted"); + } + } + + + internal class TestPlugin : Plugin { - public TestPlugin() : base() { } + private readonly UnhandledExceptionPolicy _exceptionPolicy; + protected internal override UnhandledExceptionPolicy ExceptionPolicy => _exceptionPolicy; - protected override void Configure() { } + public TestPlugin(UnhandledExceptionPolicy exceptionPolicy = UnhandledExceptionPolicy.StopPlugin) : base() + { + Blockchain.Committing += OnCommitting; + Blockchain.Committed += OnCommitted; + _exceptionPolicy = exceptionPolicy; + } + + protected override void Configure() + { + TestPluginSettings.Load(GetConfiguration()); + } public void LogMessage(string message) { @@ -36,5 +81,15 @@ public IConfigurationSection TestGetConfiguration() } protected override bool OnMessage(object message) => true; + + private void OnCommitting(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList) + { + throw new NotImplementedException(); + } + + private void OnCommitted(NeoSystem system, Block block) + { + throw new NotImplementedException(); + } } } diff --git a/tests/Neo.UnitTests/Plugins/UT_Plugin.cs b/tests/Neo.UnitTests/Plugins/UT_Plugin.cs index e5e8cb6e49..c48d32563f 100644 --- a/tests/Neo.UnitTests/Plugins/UT_Plugin.cs +++ b/tests/Neo.UnitTests/Plugins/UT_Plugin.cs @@ -11,8 +11,11 @@ using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Ledger; using Neo.Plugins; using System; +using System.Reflection; +using System.Threading.Tasks; namespace Neo.UnitTests.Plugins { @@ -21,6 +24,50 @@ public class UT_Plugin { private static readonly object locker = new(); + [TestInitialize] + public void TestInitialize() + { + ClearEventHandlers(); + } + + [TestCleanup] + public void TestCleanup() + { + ClearEventHandlers(); + } + + private static void ClearEventHandlers() + { + ClearEventHandler("Committing"); + ClearEventHandler("Committed"); + } + + private static void ClearEventHandler(string eventName) + { + var eventInfo = typeof(Blockchain).GetEvent(eventName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + if (eventInfo == null) + { + return; + } + + var fields = typeof(Blockchain).GetFields(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public); + foreach (var field in fields) + { + if (field.FieldType == typeof(MulticastDelegate) || field.FieldType.BaseType == typeof(MulticastDelegate)) + { + var eventDelegate = (MulticastDelegate)field.GetValue(null); + if (eventDelegate != null && field.Name.Contains(eventName)) + { + foreach (var handler in eventDelegate.GetInvocationList()) + { + eventInfo.RemoveEventHandler(null, handler); + } + break; + } + } + } + } + [TestMethod] public void TestGetConfigFile() { @@ -63,5 +110,109 @@ public void TestGetConfiguration() var pp = new TestPlugin(); pp.TestGetConfiguration().Key.Should().Be("PluginConfiguration"); } + + [TestMethod] + public async Task TestOnException() + { + _ = new TestPlugin(); + // Ensure no exception is thrown + try + { + await Blockchain.InvokeCommittingAsync(null, null, null, null); + await Blockchain.InvokeCommittedAsync(null, null); + } + catch (Exception ex) + { + Assert.Fail($"InvokeCommitting or InvokeCommitted threw an exception: {ex.Message}"); + } + + // Register TestNonPlugin that throws exceptions + _ = new TestNonPlugin(); + + // Ensure exception is thrown + await Assert.ThrowsExceptionAsync(async () => + { + await Blockchain.InvokeCommittingAsync(null, null, null, null); + }); + + await Assert.ThrowsExceptionAsync(async () => + { + await Blockchain.InvokeCommittedAsync(null, null); + }); + } + + [TestMethod] + public async Task TestOnPluginStopped() + { + var pp = new TestPlugin(); + Assert.AreEqual(false, pp.IsStopped); + // Ensure no exception is thrown + try + { + await Blockchain.InvokeCommittingAsync(null, null, null, null); + await Blockchain.InvokeCommittedAsync(null, null); + } + catch (Exception ex) + { + Assert.Fail($"InvokeCommitting or InvokeCommitted threw an exception: {ex.Message}"); + } + + Assert.AreEqual(true, pp.IsStopped); + } + + [TestMethod] + public async Task TestOnPluginStopOnException() + { + // pp will stop on exception. + var pp = new TestPlugin(); + Assert.AreEqual(false, pp.IsStopped); + // Ensure no exception is thrown + try + { + await Blockchain.InvokeCommittingAsync(null, null, null, null); + await Blockchain.InvokeCommittedAsync(null, null); + } + catch (Exception ex) + { + Assert.Fail($"InvokeCommitting or InvokeCommitted threw an exception: {ex.Message}"); + } + + Assert.AreEqual(true, pp.IsStopped); + + // pp2 will not stop on exception. + var pp2 = new TestPlugin(UnhandledExceptionPolicy.Ignore); + Assert.AreEqual(false, pp2.IsStopped); + // Ensure no exception is thrown + try + { + await Blockchain.InvokeCommittingAsync(null, null, null, null); + await Blockchain.InvokeCommittedAsync(null, null); + } + catch (Exception ex) + { + Assert.Fail($"InvokeCommitting or InvokeCommitted threw an exception: {ex.Message}"); + } + + Assert.AreEqual(false, pp2.IsStopped); + } + + [TestMethod] + public async Task TestOnNodeStopOnPluginException() + { + // node will stop on pp exception. + var pp = new TestPlugin(UnhandledExceptionPolicy.StopNode); + Assert.AreEqual(false, pp.IsStopped); + await Assert.ThrowsExceptionAsync(async () => + { + await Blockchain.InvokeCommittingAsync(null, null, null, null); + }); + + await Assert.ThrowsExceptionAsync(async () => + { + await Blockchain.InvokeCommittedAsync(null, null); + }); + + Assert.AreEqual(false, pp.IsStopped); + } } } From ffcd7ce14b7652467fa108427e7b572426c336b1 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Thu, 18 Jul 2024 23:31:17 +0800 Subject: [PATCH 02/10] ensure leveldb is not used in multithread env --- src/Neo/Ledger/Blockchain.cs | 31 ++-- .../Plugins/Storage/LevelDBStore.cs | 5 +- .../LevelDBStore/Plugins/Storage/Snapshot.cs | 20 +++ .../LevelDBStore/Plugins/Storage/Store.cs | 2 +- .../StoreTest.MultiThread.cs | 148 ++++++++++++++++++ tests/Neo.Plugins.Storage.Tests/StoreTest.cs | 2 +- tests/Neo.UnitTests/Plugins/TestPlugin.cs | 2 +- tests/Neo.UnitTests/Plugins/UT_Plugin.cs | 61 ++++---- 8 files changed, 215 insertions(+), 56 deletions(-) create mode 100644 tests/Neo.Plugins.Storage.Tests/StoreTest.MultiThread.cs diff --git a/src/Neo/Ledger/Blockchain.cs b/src/Neo/Ledger/Blockchain.cs index e9e3cb0020..bfea8129e5 100644 --- a/src/Neo/Ledger/Blockchain.cs +++ b/src/Neo/Ledger/Blockchain.cs @@ -472,10 +472,10 @@ private void Persist(Block block) Context.System.EventStream.Publish(application_executed); all_application_executed.Add(application_executed); } - _ = InvokeCommittingAsync(system, block, snapshot, all_application_executed); + InvokeCommitting(system, block, snapshot, all_application_executed); snapshot.Commit(); } - _ = InvokeCommittedAsync(system, block); + InvokeCommitted(system, block); system.MemPool.UpdatePoolForBlockPersisted(block, system.StoreView); extensibleWitnessWhiteList = null; block_cache.Remove(block.PrevHash); @@ -484,22 +484,19 @@ private void Persist(Block block) Debug.Assert(header.Index == block.Index); } - internal static async Task InvokeCommittingAsync(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList) + internal static void InvokeCommitting(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList) { - await InvokeHandlersAsync(Committing?.GetInvocationList(), h => ((CommittingHandler)h)(system, block, snapshot, applicationExecutedList)); + InvokeHandlers(Committing?.GetInvocationList(), h => ((CommittingHandler)h)(system, block, snapshot, applicationExecutedList)); } - internal static async Task InvokeCommittedAsync(NeoSystem system, Block block) + internal static void InvokeCommitted(NeoSystem system, Block block) { - await InvokeHandlersAsync(Committed?.GetInvocationList(), h => ((CommittedHandler)h)(system, block)); + InvokeHandlers(Committed?.GetInvocationList(), h => ((CommittedHandler)h)(system, block)); } - private static async Task InvokeHandlersAsync(Delegate[] handlers, Action handlerAction) + private static void InvokeHandlers(Delegate[] handlers, Action handlerAction) { - if (handlers == null) return; - - var exceptions = new ConcurrentBag(); - var tasks = handlers.Select(handler => Task.Run(() => + handlers?.ForEach(handler => { try { @@ -516,7 +513,6 @@ private static async Task InvokeHandlersAsync(Delegate[] handlers, Action throw e); + }); } /// diff --git a/src/Plugins/LevelDBStore/Plugins/Storage/LevelDBStore.cs b/src/Plugins/LevelDBStore/Plugins/Storage/LevelDBStore.cs index 9c676e8a7f..9207d7f14d 100644 --- a/src/Plugins/LevelDBStore/Plugins/Storage/LevelDBStore.cs +++ b/src/Plugins/LevelDBStore/Plugins/Storage/LevelDBStore.cs @@ -12,7 +12,10 @@ using Neo.IO.Data.LevelDB; using Neo.Persistence; using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; +using System.Threading; namespace Neo.Plugins.Storage { @@ -27,7 +30,7 @@ public LevelDBStore() public IStore GetStore(string path) { - if (Environment.CommandLine.Split(' ').Any(p => p == "/repair" || p == "--repair")) + if (Environment.CommandLine.Split(' ').Any(p => p is "/repair" or "--repair")) DB.Repair(path, Options.Default); return new Store(path); } diff --git a/src/Plugins/LevelDBStore/Plugins/Storage/Snapshot.cs b/src/Plugins/LevelDBStore/Plugins/Storage/Snapshot.cs index 0b0a63b885..3764b379fc 100644 --- a/src/Plugins/LevelDBStore/Plugins/Storage/Snapshot.cs +++ b/src/Plugins/LevelDBStore/Plugins/Storage/Snapshot.cs @@ -11,7 +11,9 @@ using Neo.IO.Data.LevelDB; using Neo.Persistence; +using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using LSnapshot = Neo.IO.Data.LevelDB.Snapshot; namespace Neo.Plugins.Storage @@ -22,6 +24,7 @@ internal class Snapshot : ISnapshot private readonly LSnapshot snapshot; private readonly ReadOptions options; private readonly WriteBatch batch; + private readonly int _threadId; public Snapshot(DB db) { @@ -29,40 +32,57 @@ public Snapshot(DB db) snapshot = db.GetSnapshot(); options = new ReadOptions { FillCache = false, Snapshot = snapshot }; batch = new WriteBatch(); + _threadId = Environment.CurrentManagedThreadId; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureThreadAccess() + { + if (_threadId != Environment.CurrentManagedThreadId) + { + throw new InvalidOperationException("Snapshot cannot be accessed from multiple threads."); + } } public void Commit() { + EnsureThreadAccess(); db.Write(WriteOptions.Default, batch); } public void Delete(byte[] key) { + EnsureThreadAccess(); batch.Delete(key); } public void Dispose() { + EnsureThreadAccess(); snapshot.Dispose(); } public IEnumerable<(byte[] Key, byte[] Value)> Seek(byte[] prefix, SeekDirection direction = SeekDirection.Forward) { + EnsureThreadAccess(); return db.Seek(options, prefix, direction, (k, v) => (k, v)); } public void Put(byte[] key, byte[] value) { + EnsureThreadAccess(); batch.Put(key, value); } public bool Contains(byte[] key) { + EnsureThreadAccess(); return db.Contains(options, key); } public byte[] TryGet(byte[] key) { + EnsureThreadAccess(); return db.Get(options, key); } } diff --git a/src/Plugins/LevelDBStore/Plugins/Storage/Store.cs b/src/Plugins/LevelDBStore/Plugins/Storage/Store.cs index 27b12a8b64..954136b4f2 100644 --- a/src/Plugins/LevelDBStore/Plugins/Storage/Store.cs +++ b/src/Plugins/LevelDBStore/Plugins/Storage/Store.cs @@ -44,7 +44,7 @@ public ISnapshot GetSnapshot() return new Snapshot(db); } - public void Put(byte[] key, byte[] value) + public virtual void Put(byte[] key, byte[] value) { db.Put(WriteOptions.Default, key, value); } diff --git a/tests/Neo.Plugins.Storage.Tests/StoreTest.MultiThread.cs b/tests/Neo.Plugins.Storage.Tests/StoreTest.MultiThread.cs new file mode 100644 index 0000000000..246584108c --- /dev/null +++ b/tests/Neo.Plugins.Storage.Tests/StoreTest.MultiThread.cs @@ -0,0 +1,148 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// StoreSnapshotTest.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Akka.Util.Internal; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Ledger; +using Neo.Network.P2P.Payloads; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace Neo.Plugins.Storage.Tests; + + +partial class StoreTest +{ + + #region Async Tests for OnCommitting and OnCommitted + + // Tests bellow are tests for the async delegate issue + // first reported by Vitor (vncoelho) in https://github.com/neo-project/neo/issues/3356 + + [TestMethod] + public void TestOneThreadLevelDbSnapshotPut() + { + using var store = levelDbStore.GetStore(path_leveldb); + var snapshot = store.GetSnapshot(); + + var testKey = new byte[] { 0x01, 0x02, 0x03 }; + + for (var i = 0; i < 2; i++) + { + var value = new byte[] { 0x04, 0x05, 0x06, (byte)i }; + snapshot.Put(testKey, value); + snapshot.Commit(); + } + snapshot.Dispose(); + } + + [TestMethod] + public void TestSingleExtraThreadLevelDbSnapshotPut() + { + using var store = levelDbStore.GetStore(path_leveldb); + + object locker = new(); + var snapshot = store.GetSnapshot(); + + var testKey = new byte[] { 0x01, 0x02, 0x03 }; + + var tasks = new Task[1]; + for (var i = 0; i < tasks.Length; i++) + { + var value = new byte[] { 0x04, 0x05, 0x06, (byte)i }; + tasks[i] = Task.Run(() => + { + try + { + lock (locker) + { + snapshot.Put(testKey, value); + snapshot.Commit(); + } + } + catch (Exception ex) + { + Console.WriteLine($"Exception in task: {ex.Message}"); + throw; + } + }); + } + + try + { + Task.WaitAll(tasks); + } + catch (AggregateException ae) + { + foreach (var ex in ae.InnerExceptions) + { + Console.WriteLine($"AggregateException: {ex.Message}"); + } + throw; + } + finally + { + snapshot.Dispose(); + } + } + + [TestMethod] + [ExpectedException(typeof(AggregateException))] + public void TestMultiThreadLevelDbSnapshotPut() + { + using var store = levelDbStore.GetStore(path_leveldb); + + object locker = new(); + var snapshot = store.GetSnapshot(); + + var testKey = new byte[] { 0x01, 0x02, 0x03 }; + + var tasks = new Task[2]; + for (var i = 0; i < tasks.Length; i++) + { + var value = new byte[] { 0x04, 0x05, 0x06, (byte)i }; + tasks[i] = Task.Run(() => + { + lock (locker) + { + snapshot.Put(testKey, value); + snapshot.Commit(); + } + }); + } + Task.WaitAll(tasks); + snapshot.Dispose(); + } + + [TestMethod] + public void TestOneSnapshotPerThreadLevelDbSnapshotPut() + { + using var store = levelDbStore.GetStore(path_leveldb); + var testKey = new byte[] { 0x01, 0x02, 0x03 }; + + var tasks = new Task[100]; + for (var i = 0; i < tasks.Length; i++) + { + var value = new byte[] { 0x04, 0x05, 0x06, (byte)i }; + tasks[i] = Task.Run(() => + { + var snapshot = store.GetSnapshot(); + snapshot.Put(testKey, value); + snapshot.Commit(); + snapshot.Dispose(); + }); + } + Task.WaitAll(tasks); + } + + #endregion +} diff --git a/tests/Neo.Plugins.Storage.Tests/StoreTest.cs b/tests/Neo.Plugins.Storage.Tests/StoreTest.cs index 189e212bc2..7d6e914226 100644 --- a/tests/Neo.Plugins.Storage.Tests/StoreTest.cs +++ b/tests/Neo.Plugins.Storage.Tests/StoreTest.cs @@ -17,7 +17,7 @@ namespace Neo.Plugins.Storage.Tests { [TestClass] - public class StoreTest + public partial class StoreTest { private const string path_leveldb = "Data_LevelDB_UT"; private const string path_rocksdb = "Data_RocksDB_UT"; diff --git a/tests/Neo.UnitTests/Plugins/TestPlugin.cs b/tests/Neo.UnitTests/Plugins/TestPlugin.cs index 5e51220c7f..edc0a0a0f3 100644 --- a/tests/Neo.UnitTests/Plugins/TestPlugin.cs +++ b/tests/Neo.UnitTests/Plugins/TestPlugin.cs @@ -53,7 +53,7 @@ internal class TestPlugin : Plugin private readonly UnhandledExceptionPolicy _exceptionPolicy; protected internal override UnhandledExceptionPolicy ExceptionPolicy => _exceptionPolicy; - public TestPlugin(UnhandledExceptionPolicy exceptionPolicy = UnhandledExceptionPolicy.StopPlugin) : base() + public TestPlugin(UnhandledExceptionPolicy exceptionPolicy = UnhandledExceptionPolicy.StopPlugin) { Blockchain.Committing += OnCommitting; Blockchain.Committed += OnCommitted; diff --git a/tests/Neo.UnitTests/Plugins/UT_Plugin.cs b/tests/Neo.UnitTests/Plugins/UT_Plugin.cs index c48d32563f..e3707241e7 100644 --- a/tests/Neo.UnitTests/Plugins/UT_Plugin.cs +++ b/tests/Neo.UnitTests/Plugins/UT_Plugin.cs @@ -12,6 +12,7 @@ using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; using Neo.Ledger; +using Neo.Network.P2P.Payloads; using Neo.Plugins; using System; using System.Reflection; @@ -112,14 +113,14 @@ public void TestGetConfiguration() } [TestMethod] - public async Task TestOnException() + public void TestOnException() { _ = new TestPlugin(); // Ensure no exception is thrown try { - await Blockchain.InvokeCommittingAsync(null, null, null, null); - await Blockchain.InvokeCommittedAsync(null, null); + Blockchain.InvokeCommitting(null, null, null, null); + Blockchain.InvokeCommitted(null, null); } catch (Exception ex) { @@ -130,27 +131,27 @@ public async Task TestOnException() _ = new TestNonPlugin(); // Ensure exception is thrown - await Assert.ThrowsExceptionAsync(async () => - { - await Blockchain.InvokeCommittingAsync(null, null, null, null); - }); - - await Assert.ThrowsExceptionAsync(async () => - { - await Blockchain.InvokeCommittedAsync(null, null); - }); + Assert.ThrowsException(() => + { + Blockchain.InvokeCommitting(null, null, null, null); + }); + + Assert.ThrowsException(() => + { + Blockchain.InvokeCommitted(null, null); + }); } [TestMethod] - public async Task TestOnPluginStopped() + public void TestOnPluginStopped() { var pp = new TestPlugin(); Assert.AreEqual(false, pp.IsStopped); // Ensure no exception is thrown try { - await Blockchain.InvokeCommittingAsync(null, null, null, null); - await Blockchain.InvokeCommittedAsync(null, null); + Blockchain.InvokeCommitting(null, null, null, null); + Blockchain.InvokeCommitted(null, null); } catch (Exception ex) { @@ -161,7 +162,7 @@ public async Task TestOnPluginStopped() } [TestMethod] - public async Task TestOnPluginStopOnException() + public void TestOnPluginStopOnException() { // pp will stop on exception. var pp = new TestPlugin(); @@ -169,8 +170,8 @@ public async Task TestOnPluginStopOnException() // Ensure no exception is thrown try { - await Blockchain.InvokeCommittingAsync(null, null, null, null); - await Blockchain.InvokeCommittedAsync(null, null); + Blockchain.InvokeCommitting(null, null, null, null); + Blockchain.InvokeCommitted(null, null); } catch (Exception ex) { @@ -185,8 +186,8 @@ public async Task TestOnPluginStopOnException() // Ensure no exception is thrown try { - await Blockchain.InvokeCommittingAsync(null, null, null, null); - await Blockchain.InvokeCommittedAsync(null, null); + Blockchain.InvokeCommitting(null, null, null, null); + Blockchain.InvokeCommitted(null, null); } catch (Exception ex) { @@ -197,20 +198,20 @@ public async Task TestOnPluginStopOnException() } [TestMethod] - public async Task TestOnNodeStopOnPluginException() + public void TestOnNodeStopOnPluginException() { // node will stop on pp exception. var pp = new TestPlugin(UnhandledExceptionPolicy.StopNode); Assert.AreEqual(false, pp.IsStopped); - await Assert.ThrowsExceptionAsync(async () => - { - await Blockchain.InvokeCommittingAsync(null, null, null, null); - }); - - await Assert.ThrowsExceptionAsync(async () => - { - await Blockchain.InvokeCommittedAsync(null, null); - }); + Assert.ThrowsException(() => + { + Blockchain.InvokeCommitting(null, null, null, null); + }); + + Assert.ThrowsException(() => + { + Blockchain.InvokeCommitted(null, null); + }); Assert.AreEqual(false, pp.IsStopped); } From a4cade1fa2fe55fbf3ce6bcf6bcb2f89e0b59ede Mon Sep 17 00:00:00 2001 From: Jimmy Date: Thu, 18 Jul 2024 23:33:09 +0800 Subject: [PATCH 03/10] Revert "Revert "Plugin unhandled exception (#3349)" (#3366)" This reverts commit f307a31cee4ba6d7a97c43058b73ba194efab9ac. --- src/Neo/Ledger/Blockchain.cs | 66 +++++++- src/Neo/Plugins/Plugin.cs | 56 ++++++- src/Neo/Plugins/PluginSettings.cs | 33 ++++ src/Neo/Plugins/UnhandledExceptionPolicy.cs | 20 +++ .../ApplicationLogs/ApplicationLogs.json | 3 +- src/Plugins/ApplicationLogs/LogReader.cs | 1 + src/Plugins/ApplicationLogs/Settings.cs | 4 +- src/Plugins/DBFTPlugin/DBFTPlugin.cs | 2 + src/Plugins/DBFTPlugin/DBFTPlugin.json | 3 +- src/Plugins/DBFTPlugin/Settings.cs | 4 +- src/Plugins/OracleService/OracleService.cs | 2 + src/Plugins/OracleService/OracleService.json | 1 + src/Plugins/OracleService/Settings.cs | 4 +- src/Plugins/RpcServer/RpcServer.json | 1 + src/Plugins/RpcServer/RpcServerPlugin.cs | 1 + src/Plugins/RpcServer/Settings.cs | 4 +- src/Plugins/StateService/Settings.cs | 4 +- src/Plugins/StateService/StatePlugin.cs | 2 + src/Plugins/StateService/StateService.json | 3 +- src/Plugins/StorageDumper/Settings.cs | 4 +- src/Plugins/StorageDumper/StorageDumper.cs | 2 +- src/Plugins/StorageDumper/StorageDumper.json | 3 +- src/Plugins/TokensTracker/TokensTracker.cs | 8 + src/Plugins/TokensTracker/TokensTracker.json | 3 +- tests/Neo.UnitTests/Plugins/TestPlugin.cs | 61 ++++++- tests/Neo.UnitTests/Plugins/UT_Plugin.cs | 151 ++++++++++++++++++ 26 files changed, 417 insertions(+), 29 deletions(-) create mode 100644 src/Neo/Plugins/PluginSettings.cs create mode 100644 src/Neo/Plugins/UnhandledExceptionPolicy.cs diff --git a/src/Neo/Ledger/Blockchain.cs b/src/Neo/Ledger/Blockchain.cs index ca27bb2124..e9e3cb0020 100644 --- a/src/Neo/Ledger/Blockchain.cs +++ b/src/Neo/Ledger/Blockchain.cs @@ -12,18 +12,22 @@ using Akka.Actor; using Akka.Configuration; using Akka.IO; +using Akka.Util.Internal; using Neo.IO.Actors; using Neo.Network.P2P; using Neo.Network.P2P.Payloads; using Neo.Persistence; +using Neo.Plugins; using Neo.SmartContract; using Neo.SmartContract.Native; using Neo.VM; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; namespace Neo.Ledger { @@ -468,10 +472,10 @@ private void Persist(Block block) Context.System.EventStream.Publish(application_executed); all_application_executed.Add(application_executed); } - Committing?.Invoke(system, block, snapshot, all_application_executed); + _ = InvokeCommittingAsync(system, block, snapshot, all_application_executed); snapshot.Commit(); } - Committed?.Invoke(system, block); + _ = InvokeCommittedAsync(system, block); system.MemPool.UpdatePoolForBlockPersisted(block, system.StoreView); extensibleWitnessWhiteList = null; block_cache.Remove(block.PrevHash); @@ -480,6 +484,64 @@ private void Persist(Block block) Debug.Assert(header.Index == block.Index); } + internal static async Task InvokeCommittingAsync(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList) + { + await InvokeHandlersAsync(Committing?.GetInvocationList(), h => ((CommittingHandler)h)(system, block, snapshot, applicationExecutedList)); + } + + internal static async Task InvokeCommittedAsync(NeoSystem system, Block block) + { + await InvokeHandlersAsync(Committed?.GetInvocationList(), h => ((CommittedHandler)h)(system, block)); + } + + private static async Task InvokeHandlersAsync(Delegate[] handlers, Action handlerAction) + { + if (handlers == null) return; + + var exceptions = new ConcurrentBag(); + var tasks = handlers.Select(handler => Task.Run(() => + { + try + { + // skip stopped plugin. + if (handler.Target is Plugin { IsStopped: true }) + { + return; + } + + handlerAction(handler); + } + catch (Exception ex) when (handler.Target is Plugin plugin) + { + switch (plugin.ExceptionPolicy) + { + case UnhandledExceptionPolicy.StopNode: + exceptions.Add(ex); + throw; + case UnhandledExceptionPolicy.StopPlugin: + //Stop plugin on exception + plugin.IsStopped = true; + break; + case UnhandledExceptionPolicy.Ignore: + // Log the exception and continue with the next handler + break; + default: + throw new InvalidCastException($"The exception policy {plugin.ExceptionPolicy} is not valid."); + } + + Utility.Log(nameof(plugin), LogLevel.Error, ex); + } + catch (Exception ex) + { + exceptions.Add(ex); + } + })).ToList(); + + await Task.WhenAll(tasks); + + exceptions.ForEach(e => throw e); + } + /// /// Gets a object used for creating the actor. /// diff --git a/src/Neo/Plugins/Plugin.cs b/src/Neo/Plugins/Plugin.cs index 248301af56..9feee25d57 100644 --- a/src/Neo/Plugins/Plugin.cs +++ b/src/Neo/Plugins/Plugin.cs @@ -33,7 +33,8 @@ public abstract class Plugin : IDisposable /// /// The directory containing the plugin folders. Files can be contained in any subdirectory. /// - public static readonly string PluginsDirectory = Combine(GetDirectoryName(System.AppContext.BaseDirectory), "Plugins"); + public static readonly string PluginsDirectory = + Combine(GetDirectoryName(System.AppContext.BaseDirectory), "Plugins"); private static readonly FileSystemWatcher configWatcher; @@ -67,6 +68,18 @@ public abstract class Plugin : IDisposable /// public virtual Version Version => GetType().Assembly.GetName().Version; + /// + /// If the plugin should be stopped when an exception is thrown. + /// Default is . + /// + protected internal virtual UnhandledExceptionPolicy ExceptionPolicy { get; init; } = UnhandledExceptionPolicy.StopNode; + + /// + /// The plugin will be stopped if an exception is thrown. + /// But it also depends on . + /// + internal bool IsStopped { get; set; } + static Plugin() { if (!Directory.Exists(PluginsDirectory)) return; @@ -74,7 +87,8 @@ static Plugin() { EnableRaisingEvents = true, IncludeSubdirectories = true, - NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.CreationTime | NotifyFilters.LastWrite | NotifyFilters.Size, + NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.CreationTime | + NotifyFilters.LastWrite | NotifyFilters.Size, }; configWatcher.Changed += ConfigWatcher_Changed; configWatcher.Created += ConfigWatcher_Changed; @@ -106,7 +120,8 @@ private static void ConfigWatcher_Changed(object sender, FileSystemEventArgs e) { case ".json": case ".dll": - Utility.Log(nameof(Plugin), LogLevel.Warning, $"File {e.Name} is {e.ChangeType}, please restart node."); + Utility.Log(nameof(Plugin), LogLevel.Warning, + $"File {e.Name} is {e.ChangeType}, please restart node."); break; } } @@ -119,7 +134,8 @@ private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEven AssemblyName an = new(args.Name); Assembly assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.FullName == args.Name) ?? - AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().Name == an.Name); + AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.GetName().Name == an.Name); if (assembly != null) return assembly; string filename = an.Name + ".dll"; @@ -150,7 +166,8 @@ public virtual void Dispose() /// The content of the configuration file read. protected IConfigurationSection GetConfiguration() { - return new ConfigurationBuilder().AddJsonFile(ConfigFile, optional: true).Build().GetSection("PluginConfiguration"); + return new ConfigurationBuilder().AddJsonFile(ConfigFile, optional: true).Build() + .GetSection("PluginConfiguration"); } private static void LoadPlugin(Assembly assembly) @@ -187,6 +204,7 @@ internal static void LoadPlugins() catch { } } } + foreach (Assembly assembly in assemblies) { LoadPlugin(assembly); @@ -229,7 +247,33 @@ protected internal virtual void OnSystemLoaded(NeoSystem system) /// if the is handled by a plugin; otherwise, . public static bool SendMessage(object message) { - return Plugins.Any(plugin => plugin.OnMessage(message)); + + return Plugins.Any(plugin => + { + try + { + return !plugin.IsStopped && + plugin.OnMessage(message); + } + catch (Exception ex) + { + switch (plugin.ExceptionPolicy) + { + case UnhandledExceptionPolicy.StopNode: + throw; + case UnhandledExceptionPolicy.StopPlugin: + plugin.IsStopped = true; + break; + case UnhandledExceptionPolicy.Ignore: + break; + default: + throw new InvalidCastException($"The exception policy {plugin.ExceptionPolicy} is not valid."); + } + Utility.Log(nameof(Plugin), LogLevel.Error, ex); + return false; + } + } + ); } } } diff --git a/src/Neo/Plugins/PluginSettings.cs b/src/Neo/Plugins/PluginSettings.cs new file mode 100644 index 0000000000..af33e44eea --- /dev/null +++ b/src/Neo/Plugins/PluginSettings.cs @@ -0,0 +1,33 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// PluginSettings.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Microsoft.Extensions.Configuration; +using Org.BouncyCastle.Security; +using System; + +namespace Neo.Plugins; + +public abstract class PluginSettings(IConfigurationSection section) +{ + public UnhandledExceptionPolicy ExceptionPolicy + { + get + { + var policyString = section.GetValue(nameof(UnhandledExceptionPolicy), nameof(UnhandledExceptionPolicy.StopNode)); + if (Enum.TryParse(policyString, out UnhandledExceptionPolicy policy)) + { + return policy; + } + + throw new InvalidParameterException($"{policyString} is not a valid UnhandledExceptionPolicy"); + } + } +} diff --git a/src/Neo/Plugins/UnhandledExceptionPolicy.cs b/src/Neo/Plugins/UnhandledExceptionPolicy.cs new file mode 100644 index 0000000000..035e173aa3 --- /dev/null +++ b/src/Neo/Plugins/UnhandledExceptionPolicy.cs @@ -0,0 +1,20 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UnhandledExceptionPolicy.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.Plugins +{ + public enum UnhandledExceptionPolicy + { + Ignore = 0, + StopPlugin = 1, + StopNode = 2, + } +} diff --git a/src/Plugins/ApplicationLogs/ApplicationLogs.json b/src/Plugins/ApplicationLogs/ApplicationLogs.json index af601bc81e..2664665dd2 100644 --- a/src/Plugins/ApplicationLogs/ApplicationLogs.json +++ b/src/Plugins/ApplicationLogs/ApplicationLogs.json @@ -3,7 +3,8 @@ "Path": "ApplicationLogs_{0}", "Network": 860833102, "MaxStackSize": 65535, - "Debug": false + "Debug": false, + "UnhandledExceptionPolicy": "StopPlugin" }, "Dependency": [ "RpcServer" diff --git a/src/Plugins/ApplicationLogs/LogReader.cs b/src/Plugins/ApplicationLogs/LogReader.cs index e0e886d93b..b3f767fecb 100644 --- a/src/Plugins/ApplicationLogs/LogReader.cs +++ b/src/Plugins/ApplicationLogs/LogReader.cs @@ -38,6 +38,7 @@ public class LogReader : Plugin, ICommittingHandler, ICommittedHandler, ILogHand public override string Name => "ApplicationLogs"; public override string Description => "Synchronizes smart contract VM executions and notifications (NotifyLog) on blockchain."; + protected override UnhandledExceptionPolicy ExceptionPolicy => Settings.Default.ExceptionPolicy; #region Ctor diff --git a/src/Plugins/ApplicationLogs/Settings.cs b/src/Plugins/ApplicationLogs/Settings.cs index 8f2a0da1e1..6a5f238272 100644 --- a/src/Plugins/ApplicationLogs/Settings.cs +++ b/src/Plugins/ApplicationLogs/Settings.cs @@ -13,7 +13,7 @@ namespace Neo.Plugins.ApplicationLogs { - internal class Settings + internal class Settings : PluginSettings { public string Path { get; } public uint Network { get; } @@ -23,7 +23,7 @@ internal class Settings public static Settings Default { get; private set; } - private Settings(IConfigurationSection section) + private Settings(IConfigurationSection section) : base(section) { Path = section.GetValue("Path", "ApplicationLogs_{0}"); Network = section.GetValue("Network", 5195086u); diff --git a/src/Plugins/DBFTPlugin/DBFTPlugin.cs b/src/Plugins/DBFTPlugin/DBFTPlugin.cs index f09be9d291..65fc5011dc 100644 --- a/src/Plugins/DBFTPlugin/DBFTPlugin.cs +++ b/src/Plugins/DBFTPlugin/DBFTPlugin.cs @@ -31,6 +31,8 @@ public class DBFTPlugin : Plugin, IServiceAddedHandler, IMessageReceivedHandler, public override string ConfigFile => System.IO.Path.Combine(RootPath, "DBFTPlugin.json"); + protected override UnhandledExceptionPolicy ExceptionPolicy => settings.ExceptionPolicy; + public DBFTPlugin() { RemoteNode.MessageReceived += ((IMessageReceivedHandler)this).RemoteNode_MessageReceived_Handler; diff --git a/src/Plugins/DBFTPlugin/DBFTPlugin.json b/src/Plugins/DBFTPlugin/DBFTPlugin.json index 2e2b710ba3..705b2b77cb 100644 --- a/src/Plugins/DBFTPlugin/DBFTPlugin.json +++ b/src/Plugins/DBFTPlugin/DBFTPlugin.json @@ -5,6 +5,7 @@ "AutoStart": false, "Network": 860833102, "MaxBlockSize": 2097152, - "MaxBlockSystemFee": 150000000000 + "MaxBlockSystemFee": 150000000000, + "UnhandledExceptionPolicy": "StopNode" } } diff --git a/src/Plugins/DBFTPlugin/Settings.cs b/src/Plugins/DBFTPlugin/Settings.cs index 28ad21f37a..1f37feaf16 100644 --- a/src/Plugins/DBFTPlugin/Settings.cs +++ b/src/Plugins/DBFTPlugin/Settings.cs @@ -13,7 +13,7 @@ namespace Neo.Plugins.DBFTPlugin { - public class Settings + public class Settings : PluginSettings { public string RecoveryLogs { get; } public bool IgnoreRecoveryLogs { get; } @@ -22,7 +22,7 @@ public class Settings public uint MaxBlockSize { get; } public long MaxBlockSystemFee { get; } - public Settings(IConfigurationSection section) + public Settings(IConfigurationSection section) : base(section) { RecoveryLogs = section.GetValue("RecoveryLogs", "ConsensusState"); IgnoreRecoveryLogs = section.GetValue("IgnoreRecoveryLogs", false); diff --git a/src/Plugins/OracleService/OracleService.cs b/src/Plugins/OracleService/OracleService.cs index d7a54c5c9d..bb5da83654 100644 --- a/src/Plugins/OracleService/OracleService.cs +++ b/src/Plugins/OracleService/OracleService.cs @@ -62,6 +62,8 @@ public class OracleService : Plugin, ICommittingHandler, IServiceAddedHandler, I public override string Description => "Built-in oracle plugin"; + protected override UnhandledExceptionPolicy ExceptionPolicy => Settings.Default.ExceptionPolicy; + public override string ConfigFile => System.IO.Path.Combine(RootPath, "OracleService.json"); public OracleService() diff --git a/src/Plugins/OracleService/OracleService.json b/src/Plugins/OracleService/OracleService.json index 1ab0d93399..49bf1153b3 100644 --- a/src/Plugins/OracleService/OracleService.json +++ b/src/Plugins/OracleService/OracleService.json @@ -6,6 +6,7 @@ "MaxOracleTimeout": 10000, "AllowPrivateHost": false, "AllowedContentTypes": [ "application/json" ], + "UnhandledExceptionPolicy": "Ignore", "Https": { "Timeout": 5000 }, diff --git a/src/Plugins/OracleService/Settings.cs b/src/Plugins/OracleService/Settings.cs index 952ea0c27b..db93c1c400 100644 --- a/src/Plugins/OracleService/Settings.cs +++ b/src/Plugins/OracleService/Settings.cs @@ -37,7 +37,7 @@ public NeoFSSettings(IConfigurationSection section) } } - class Settings + class Settings : PluginSettings { public uint Network { get; } public Uri[] Nodes { get; } @@ -51,7 +51,7 @@ class Settings public static Settings Default { get; private set; } - private Settings(IConfigurationSection section) + private Settings(IConfigurationSection section) : base(section) { Network = section.GetValue("Network", 5195086u); Nodes = section.GetSection("Nodes").GetChildren().Select(p => new Uri(p.Get(), UriKind.Absolute)).ToArray(); diff --git a/src/Plugins/RpcServer/RpcServer.json b/src/Plugins/RpcServer/RpcServer.json index 8f6905dead..dc9c25b8da 100644 --- a/src/Plugins/RpcServer/RpcServer.json +++ b/src/Plugins/RpcServer/RpcServer.json @@ -1,5 +1,6 @@ { "PluginConfiguration": { + "UnhandledExceptionPolicy": "Ignore", "Servers": [ { "Network": 860833102, diff --git a/src/Plugins/RpcServer/RpcServerPlugin.cs b/src/Plugins/RpcServer/RpcServerPlugin.cs index c22462d139..03416c1be5 100644 --- a/src/Plugins/RpcServer/RpcServerPlugin.cs +++ b/src/Plugins/RpcServer/RpcServerPlugin.cs @@ -24,6 +24,7 @@ public class RpcServerPlugin : Plugin private static readonly Dictionary> handlers = new(); public override string ConfigFile => System.IO.Path.Combine(RootPath, "RpcServer.json"); + protected override UnhandledExceptionPolicy ExceptionPolicy => settings.ExceptionPolicy; protected override void Configure() { diff --git a/src/Plugins/RpcServer/Settings.cs b/src/Plugins/RpcServer/Settings.cs index ad624d9082..2cf7b72fb8 100644 --- a/src/Plugins/RpcServer/Settings.cs +++ b/src/Plugins/RpcServer/Settings.cs @@ -18,11 +18,11 @@ namespace Neo.Plugins.RpcServer { - class Settings + class Settings : PluginSettings { public IReadOnlyList Servers { get; init; } - public Settings(IConfigurationSection section) + public Settings(IConfigurationSection section) : base(section) { Servers = section.GetSection(nameof(Servers)).GetChildren().Select(p => RpcServerSettings.Load(p)).ToArray(); } diff --git a/src/Plugins/StateService/Settings.cs b/src/Plugins/StateService/Settings.cs index 8557866bc1..a425b57d7e 100644 --- a/src/Plugins/StateService/Settings.cs +++ b/src/Plugins/StateService/Settings.cs @@ -13,7 +13,7 @@ namespace Neo.Plugins.StateService { - internal class Settings + internal class Settings : PluginSettings { public string Path { get; } public bool FullState { get; } @@ -23,7 +23,7 @@ internal class Settings public static Settings Default { get; private set; } - private Settings(IConfigurationSection section) + private Settings(IConfigurationSection section) : base(section) { Path = section.GetValue("Path", "Data_MPT_{0}"); FullState = section.GetValue("FullState", false); diff --git a/src/Plugins/StateService/StatePlugin.cs b/src/Plugins/StateService/StatePlugin.cs index 5f6e1ef1d8..03dcc55aac 100644 --- a/src/Plugins/StateService/StatePlugin.cs +++ b/src/Plugins/StateService/StatePlugin.cs @@ -41,6 +41,8 @@ public class StatePlugin : Plugin, ICommittingHandler, ICommittedHandler, IWalle public override string Description => "Enables MPT for the node"; public override string ConfigFile => System.IO.Path.Combine(RootPath, "StateService.json"); + protected override UnhandledExceptionPolicy ExceptionPolicy => Settings.Default.ExceptionPolicy; + internal IActorRef Store; internal IActorRef Verifier; diff --git a/src/Plugins/StateService/StateService.json b/src/Plugins/StateService/StateService.json index 265436fc30..cadd2da5fd 100644 --- a/src/Plugins/StateService/StateService.json +++ b/src/Plugins/StateService/StateService.json @@ -4,7 +4,8 @@ "FullState": false, "Network": 860833102, "AutoVerify": false, - "MaxFindResultItems": 100 + "MaxFindResultItems": 100, + "UnhandledExceptionPolicy": "StopPlugin" }, "Dependency": [ "RpcServer" diff --git a/src/Plugins/StorageDumper/Settings.cs b/src/Plugins/StorageDumper/Settings.cs index c2761ce6b9..e645cd7074 100644 --- a/src/Plugins/StorageDumper/Settings.cs +++ b/src/Plugins/StorageDumper/Settings.cs @@ -14,7 +14,7 @@ namespace Neo.Plugins.StorageDumper { - internal class Settings + internal class Settings : PluginSettings { /// /// Amount of storages states (heights) to be dump in a given json file @@ -32,7 +32,7 @@ internal class Settings public static Settings? Default { get; private set; } - private Settings(IConfigurationSection section) + private Settings(IConfigurationSection section) : base(section) { // Geting settings for storage changes state dumper BlockCacheSize = section.GetValue("BlockCacheSize", 1000u); diff --git a/src/Plugins/StorageDumper/StorageDumper.cs b/src/Plugins/StorageDumper/StorageDumper.cs index c47df9ac1d..6f5498b3a2 100644 --- a/src/Plugins/StorageDumper/StorageDumper.cs +++ b/src/Plugins/StorageDumper/StorageDumper.cs @@ -30,7 +30,7 @@ public class StorageDumper : Plugin, ICommittingHandler, ICommittedHandler /// private JObject? _currentBlock; private string? _lastCreateDirectory; - + protected override UnhandledExceptionPolicy ExceptionPolicy => Settings.Default?.ExceptionPolicy ?? UnhandledExceptionPolicy.Ignore; public override string Description => "Exports Neo-CLI status data"; diff --git a/src/Plugins/StorageDumper/StorageDumper.json b/src/Plugins/StorageDumper/StorageDumper.json index b327c37e0c..0c314cf262 100644 --- a/src/Plugins/StorageDumper/StorageDumper.json +++ b/src/Plugins/StorageDumper/StorageDumper.json @@ -3,6 +3,7 @@ "BlockCacheSize": 1000, "HeightToBegin": 0, "StoragePerFolder": 100000, - "Exclude": [ -4 ] + "Exclude": [ -4 ], + "UnhandledExceptionPolicy": "Ignore" } } diff --git a/src/Plugins/TokensTracker/TokensTracker.cs b/src/Plugins/TokensTracker/TokensTracker.cs index e5ffdce3f4..a7ab075245 100644 --- a/src/Plugins/TokensTracker/TokensTracker.cs +++ b/src/Plugins/TokensTracker/TokensTracker.cs @@ -16,6 +16,7 @@ using Neo.Persistence; using Neo.Plugins.RpcServer; using Neo.Plugins.Trackers; +using System; using System.Collections.Generic; using System.Linq; using static System.IO.Path; @@ -30,8 +31,10 @@ public class TokensTracker : Plugin, ICommittingHandler, ICommittedHandler private uint _network; private string[] _enabledTrackers; private IStore _db; + private UnhandledExceptionPolicy _exceptionPolicy; private NeoSystem neoSystem; private readonly List trackers = new(); + protected override UnhandledExceptionPolicy ExceptionPolicy => _exceptionPolicy; public override string Description => "Enquiries balances and transaction history of accounts through RPC"; @@ -57,6 +60,11 @@ protected override void Configure() _maxResults = config.GetValue("MaxResults", 1000u); _network = config.GetValue("Network", 860833102u); _enabledTrackers = config.GetSection("EnabledTrackers").GetChildren().Select(p => p.Value).ToArray(); + var policyString = config.GetValue(nameof(UnhandledExceptionPolicy), nameof(UnhandledExceptionPolicy.StopNode)); + if (Enum.TryParse(policyString, out UnhandledExceptionPolicy policy)) + { + _exceptionPolicy = policy; + } } protected override void OnSystemLoaded(NeoSystem system) diff --git a/src/Plugins/TokensTracker/TokensTracker.json b/src/Plugins/TokensTracker/TokensTracker.json index ca63183b68..dbdbecfd40 100644 --- a/src/Plugins/TokensTracker/TokensTracker.json +++ b/src/Plugins/TokensTracker/TokensTracker.json @@ -4,7 +4,8 @@ "TrackHistory": true, "MaxResults": 1000, "Network": 860833102, - "EnabledTrackers": [ "NEP-11", "NEP-17" ] + "EnabledTrackers": [ "NEP-11", "NEP-17" ], + "UnhandledExceptionPolicy": "StopPlugin" }, "Dependency": [ "RpcServer" diff --git a/tests/Neo.UnitTests/Plugins/TestPlugin.cs b/tests/Neo.UnitTests/Plugins/TestPlugin.cs index dde500b927..5e51220c7f 100644 --- a/tests/Neo.UnitTests/Plugins/TestPlugin.cs +++ b/tests/Neo.UnitTests/Plugins/TestPlugin.cs @@ -10,15 +10,60 @@ // modifications are permitted. using Microsoft.Extensions.Configuration; +using Neo.Ledger; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; using Neo.Plugins; +using System; +using System.Collections.Generic; namespace Neo.UnitTests.Plugins { - public class TestPlugin : Plugin + + internal class TestPluginSettings(IConfigurationSection section) : PluginSettings(section) + { + public static TestPluginSettings Default { get; private set; } + public static void Load(IConfigurationSection section) + { + Default = new TestPluginSettings(section); + } + } + internal class TestNonPlugin + { + public TestNonPlugin() + { + Blockchain.Committing += OnCommitting; + Blockchain.Committed += OnCommitted; + } + + private void OnCommitting(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList) + { + throw new NotImplementedException("Test exception from OnCommitting"); + } + + private void OnCommitted(NeoSystem system, Block block) + { + throw new NotImplementedException("Test exception from OnCommitted"); + } + } + + + internal class TestPlugin : Plugin { - public TestPlugin() : base() { } + private readonly UnhandledExceptionPolicy _exceptionPolicy; + protected internal override UnhandledExceptionPolicy ExceptionPolicy => _exceptionPolicy; - protected override void Configure() { } + public TestPlugin(UnhandledExceptionPolicy exceptionPolicy = UnhandledExceptionPolicy.StopPlugin) : base() + { + Blockchain.Committing += OnCommitting; + Blockchain.Committed += OnCommitted; + _exceptionPolicy = exceptionPolicy; + } + + protected override void Configure() + { + TestPluginSettings.Load(GetConfiguration()); + } public void LogMessage(string message) { @@ -36,5 +81,15 @@ public IConfigurationSection TestGetConfiguration() } protected override bool OnMessage(object message) => true; + + private void OnCommitting(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList) + { + throw new NotImplementedException(); + } + + private void OnCommitted(NeoSystem system, Block block) + { + throw new NotImplementedException(); + } } } diff --git a/tests/Neo.UnitTests/Plugins/UT_Plugin.cs b/tests/Neo.UnitTests/Plugins/UT_Plugin.cs index e5e8cb6e49..c48d32563f 100644 --- a/tests/Neo.UnitTests/Plugins/UT_Plugin.cs +++ b/tests/Neo.UnitTests/Plugins/UT_Plugin.cs @@ -11,8 +11,11 @@ using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Ledger; using Neo.Plugins; using System; +using System.Reflection; +using System.Threading.Tasks; namespace Neo.UnitTests.Plugins { @@ -21,6 +24,50 @@ public class UT_Plugin { private static readonly object locker = new(); + [TestInitialize] + public void TestInitialize() + { + ClearEventHandlers(); + } + + [TestCleanup] + public void TestCleanup() + { + ClearEventHandlers(); + } + + private static void ClearEventHandlers() + { + ClearEventHandler("Committing"); + ClearEventHandler("Committed"); + } + + private static void ClearEventHandler(string eventName) + { + var eventInfo = typeof(Blockchain).GetEvent(eventName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + if (eventInfo == null) + { + return; + } + + var fields = typeof(Blockchain).GetFields(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public); + foreach (var field in fields) + { + if (field.FieldType == typeof(MulticastDelegate) || field.FieldType.BaseType == typeof(MulticastDelegate)) + { + var eventDelegate = (MulticastDelegate)field.GetValue(null); + if (eventDelegate != null && field.Name.Contains(eventName)) + { + foreach (var handler in eventDelegate.GetInvocationList()) + { + eventInfo.RemoveEventHandler(null, handler); + } + break; + } + } + } + } + [TestMethod] public void TestGetConfigFile() { @@ -63,5 +110,109 @@ public void TestGetConfiguration() var pp = new TestPlugin(); pp.TestGetConfiguration().Key.Should().Be("PluginConfiguration"); } + + [TestMethod] + public async Task TestOnException() + { + _ = new TestPlugin(); + // Ensure no exception is thrown + try + { + await Blockchain.InvokeCommittingAsync(null, null, null, null); + await Blockchain.InvokeCommittedAsync(null, null); + } + catch (Exception ex) + { + Assert.Fail($"InvokeCommitting or InvokeCommitted threw an exception: {ex.Message}"); + } + + // Register TestNonPlugin that throws exceptions + _ = new TestNonPlugin(); + + // Ensure exception is thrown + await Assert.ThrowsExceptionAsync(async () => + { + await Blockchain.InvokeCommittingAsync(null, null, null, null); + }); + + await Assert.ThrowsExceptionAsync(async () => + { + await Blockchain.InvokeCommittedAsync(null, null); + }); + } + + [TestMethod] + public async Task TestOnPluginStopped() + { + var pp = new TestPlugin(); + Assert.AreEqual(false, pp.IsStopped); + // Ensure no exception is thrown + try + { + await Blockchain.InvokeCommittingAsync(null, null, null, null); + await Blockchain.InvokeCommittedAsync(null, null); + } + catch (Exception ex) + { + Assert.Fail($"InvokeCommitting or InvokeCommitted threw an exception: {ex.Message}"); + } + + Assert.AreEqual(true, pp.IsStopped); + } + + [TestMethod] + public async Task TestOnPluginStopOnException() + { + // pp will stop on exception. + var pp = new TestPlugin(); + Assert.AreEqual(false, pp.IsStopped); + // Ensure no exception is thrown + try + { + await Blockchain.InvokeCommittingAsync(null, null, null, null); + await Blockchain.InvokeCommittedAsync(null, null); + } + catch (Exception ex) + { + Assert.Fail($"InvokeCommitting or InvokeCommitted threw an exception: {ex.Message}"); + } + + Assert.AreEqual(true, pp.IsStopped); + + // pp2 will not stop on exception. + var pp2 = new TestPlugin(UnhandledExceptionPolicy.Ignore); + Assert.AreEqual(false, pp2.IsStopped); + // Ensure no exception is thrown + try + { + await Blockchain.InvokeCommittingAsync(null, null, null, null); + await Blockchain.InvokeCommittedAsync(null, null); + } + catch (Exception ex) + { + Assert.Fail($"InvokeCommitting or InvokeCommitted threw an exception: {ex.Message}"); + } + + Assert.AreEqual(false, pp2.IsStopped); + } + + [TestMethod] + public async Task TestOnNodeStopOnPluginException() + { + // node will stop on pp exception. + var pp = new TestPlugin(UnhandledExceptionPolicy.StopNode); + Assert.AreEqual(false, pp.IsStopped); + await Assert.ThrowsExceptionAsync(async () => + { + await Blockchain.InvokeCommittingAsync(null, null, null, null); + }); + + await Assert.ThrowsExceptionAsync(async () => + { + await Blockchain.InvokeCommittedAsync(null, null); + }); + + Assert.AreEqual(false, pp.IsStopped); + } } } From 5ebdfd4ce383e7b0e247de7c1a171983ea791072 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Fri, 19 Jul 2024 00:11:57 +0800 Subject: [PATCH 04/10] remove async. --- src/Neo/Ledger/Blockchain.cs | 42 +++++++---------- tests/Neo.UnitTests/Plugins/TestPlugin.cs | 6 +-- tests/Neo.UnitTests/Plugins/UT_Plugin.cs | 55 +++++++++++------------ 3 files changed, 46 insertions(+), 57 deletions(-) diff --git a/src/Neo/Ledger/Blockchain.cs b/src/Neo/Ledger/Blockchain.cs index e9e3cb0020..f675226303 100644 --- a/src/Neo/Ledger/Blockchain.cs +++ b/src/Neo/Ledger/Blockchain.cs @@ -12,7 +12,6 @@ using Akka.Actor; using Akka.Configuration; using Akka.IO; -using Akka.Util.Internal; using Neo.IO.Actors; using Neo.Network.P2P; using Neo.Network.P2P.Payloads; @@ -22,12 +21,11 @@ using Neo.SmartContract.Native; using Neo.VM; using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; -using System.Threading.Tasks; +using System.Runtime.CompilerServices; namespace Neo.Ledger { @@ -472,10 +470,10 @@ private void Persist(Block block) Context.System.EventStream.Publish(application_executed); all_application_executed.Add(application_executed); } - _ = InvokeCommittingAsync(system, block, snapshot, all_application_executed); + InvokeCommitting(system, block, snapshot, all_application_executed); snapshot.Commit(); } - _ = InvokeCommittedAsync(system, block); + InvokeCommitted(system, block); system.MemPool.UpdatePoolForBlockPersisted(block, system.StoreView); extensibleWitnessWhiteList = null; block_cache.Remove(block.PrevHash); @@ -484,39 +482,40 @@ private void Persist(Block block) Debug.Assert(header.Index == block.Index); } - internal static async Task InvokeCommittingAsync(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void InvokeCommitting(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList) { - await InvokeHandlersAsync(Committing?.GetInvocationList(), h => ((CommittingHandler)h)(system, block, snapshot, applicationExecutedList)); + InvokeHandlers(Committing?.GetInvocationList(), h => ((CommittingHandler)h)(system, block, snapshot, applicationExecutedList)); } - internal static async Task InvokeCommittedAsync(NeoSystem system, Block block) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void InvokeCommitted(NeoSystem system, Block block) { - await InvokeHandlersAsync(Committed?.GetInvocationList(), h => ((CommittedHandler)h)(system, block)); + InvokeHandlers(Committed?.GetInvocationList(), h => ((CommittedHandler)h)(system, block)); } - private static async Task InvokeHandlersAsync(Delegate[] handlers, Action handlerAction) + private static void InvokeHandlers(Delegate[] handlers, Action handlerAction) { if (handlers == null) return; - var exceptions = new ConcurrentBag(); - var tasks = handlers.Select(handler => Task.Run(() => + foreach (var handler in handlers) { try { // skip stopped plugin. if (handler.Target is Plugin { IsStopped: true }) { - return; + continue; } handlerAction(handler); } catch (Exception ex) when (handler.Target is Plugin plugin) { + Utility.Log(nameof(plugin), LogLevel.Error, ex); switch (plugin.ExceptionPolicy) { case UnhandledExceptionPolicy.StopNode: - exceptions.Add(ex); throw; case UnhandledExceptionPolicy.StopPlugin: //Stop plugin on exception @@ -526,20 +525,11 @@ private static async Task InvokeHandlersAsync(Delegate[] handlers, Action throw e); + } } /// diff --git a/tests/Neo.UnitTests/Plugins/TestPlugin.cs b/tests/Neo.UnitTests/Plugins/TestPlugin.cs index 5e51220c7f..d25d1ca3f3 100644 --- a/tests/Neo.UnitTests/Plugins/TestPlugin.cs +++ b/tests/Neo.UnitTests/Plugins/TestPlugin.cs @@ -36,12 +36,12 @@ public TestNonPlugin() Blockchain.Committed += OnCommitted; } - private void OnCommitting(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList) + private static void OnCommitting(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList) { throw new NotImplementedException("Test exception from OnCommitting"); } - private void OnCommitted(NeoSystem system, Block block) + private static void OnCommitted(NeoSystem system, Block block) { throw new NotImplementedException("Test exception from OnCommitted"); } @@ -53,7 +53,7 @@ internal class TestPlugin : Plugin private readonly UnhandledExceptionPolicy _exceptionPolicy; protected internal override UnhandledExceptionPolicy ExceptionPolicy => _exceptionPolicy; - public TestPlugin(UnhandledExceptionPolicy exceptionPolicy = UnhandledExceptionPolicy.StopPlugin) : base() + public TestPlugin(UnhandledExceptionPolicy exceptionPolicy = UnhandledExceptionPolicy.StopPlugin) { Blockchain.Committing += OnCommitting; Blockchain.Committed += OnCommitted; diff --git a/tests/Neo.UnitTests/Plugins/UT_Plugin.cs b/tests/Neo.UnitTests/Plugins/UT_Plugin.cs index c48d32563f..a76119f11b 100644 --- a/tests/Neo.UnitTests/Plugins/UT_Plugin.cs +++ b/tests/Neo.UnitTests/Plugins/UT_Plugin.cs @@ -15,14 +15,13 @@ using Neo.Plugins; using System; using System.Reflection; -using System.Threading.Tasks; namespace Neo.UnitTests.Plugins { [TestClass] public class UT_Plugin { - private static readonly object locker = new(); + private static readonly object s_locker = new(); [TestInitialize] public void TestInitialize() @@ -94,7 +93,7 @@ public void TestGetVersion() [TestMethod] public void TestSendMessage() { - lock (locker) + lock (s_locker) { Plugin.Plugins.Clear(); Plugin.SendMessage("hey1").Should().BeFalse(); @@ -112,14 +111,14 @@ public void TestGetConfiguration() } [TestMethod] - public async Task TestOnException() + public void TestOnException() { _ = new TestPlugin(); // Ensure no exception is thrown try { - await Blockchain.InvokeCommittingAsync(null, null, null, null); - await Blockchain.InvokeCommittedAsync(null, null); + Blockchain.InvokeCommitting(null, null, null, null); + Blockchain.InvokeCommitted(null, null); } catch (Exception ex) { @@ -130,27 +129,27 @@ public async Task TestOnException() _ = new TestNonPlugin(); // Ensure exception is thrown - await Assert.ThrowsExceptionAsync(async () => - { - await Blockchain.InvokeCommittingAsync(null, null, null, null); - }); - - await Assert.ThrowsExceptionAsync(async () => - { - await Blockchain.InvokeCommittedAsync(null, null); - }); + Assert.ThrowsException(() => + { + Blockchain.InvokeCommitting(null, null, null, null); + }); + + Assert.ThrowsException(() => + { + Blockchain.InvokeCommitted(null, null); + }); } [TestMethod] - public async Task TestOnPluginStopped() + public void TestOnPluginStopped() { var pp = new TestPlugin(); Assert.AreEqual(false, pp.IsStopped); // Ensure no exception is thrown try { - await Blockchain.InvokeCommittingAsync(null, null, null, null); - await Blockchain.InvokeCommittedAsync(null, null); + Blockchain.InvokeCommitting(null, null, null, null); + Blockchain.InvokeCommitted(null, null); } catch (Exception ex) { @@ -161,7 +160,7 @@ public async Task TestOnPluginStopped() } [TestMethod] - public async Task TestOnPluginStopOnException() + public void TestOnPluginStopOnException() { // pp will stop on exception. var pp = new TestPlugin(); @@ -169,8 +168,8 @@ public async Task TestOnPluginStopOnException() // Ensure no exception is thrown try { - await Blockchain.InvokeCommittingAsync(null, null, null, null); - await Blockchain.InvokeCommittedAsync(null, null); + Blockchain.InvokeCommitting(null, null, null, null); + Blockchain.InvokeCommitted(null, null); } catch (Exception ex) { @@ -185,8 +184,8 @@ public async Task TestOnPluginStopOnException() // Ensure no exception is thrown try { - await Blockchain.InvokeCommittingAsync(null, null, null, null); - await Blockchain.InvokeCommittedAsync(null, null); + Blockchain.InvokeCommitting(null, null, null, null); + Blockchain.InvokeCommitted(null, null); } catch (Exception ex) { @@ -197,19 +196,19 @@ public async Task TestOnPluginStopOnException() } [TestMethod] - public async Task TestOnNodeStopOnPluginException() + public void TestOnNodeStopOnPluginException() { // node will stop on pp exception. var pp = new TestPlugin(UnhandledExceptionPolicy.StopNode); Assert.AreEqual(false, pp.IsStopped); - await Assert.ThrowsExceptionAsync(async () => + Assert.ThrowsException(() => { - await Blockchain.InvokeCommittingAsync(null, null, null, null); + Blockchain.InvokeCommitting(null, null, null, null); }); - await Assert.ThrowsExceptionAsync(async () => + Assert.ThrowsException(() => { - await Blockchain.InvokeCommittedAsync(null, null); + Blockchain.InvokeCommitted(null, null); }); Assert.AreEqual(false, pp.IsStopped); From 82fcfb663205a964ee4dcdc353d9e3fbbd6c3e28 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Fri, 19 Jul 2024 00:32:36 +0800 Subject: [PATCH 05/10] Update src/Neo/Plugins/UnhandledExceptionPolicy.cs Co-authored-by: Christopher Schuchardt --- src/Neo/Plugins/UnhandledExceptionPolicy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Neo/Plugins/UnhandledExceptionPolicy.cs b/src/Neo/Plugins/UnhandledExceptionPolicy.cs index 035e173aa3..0422f2c5e7 100644 --- a/src/Neo/Plugins/UnhandledExceptionPolicy.cs +++ b/src/Neo/Plugins/UnhandledExceptionPolicy.cs @@ -11,7 +11,7 @@ namespace Neo.Plugins { - public enum UnhandledExceptionPolicy + public enum UnhandledExceptionPolicy : byte { Ignore = 0, StopPlugin = 1, From ba3e74880f77edb39e19b87cf56d6dc7aa043920 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Fri, 19 Jul 2024 00:50:47 +0800 Subject: [PATCH 06/10] not use linq --- src/Neo/Plugins/Plugin.cs | 57 ++++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/src/Neo/Plugins/Plugin.cs b/src/Neo/Plugins/Plugin.cs index 9feee25d57..cf5b798a6d 100644 --- a/src/Neo/Plugins/Plugin.cs +++ b/src/Neo/Plugins/Plugin.cs @@ -247,33 +247,46 @@ protected internal virtual void OnSystemLoaded(NeoSystem system) /// if the is handled by a plugin; otherwise, . public static bool SendMessage(object message) { + foreach (var plugin in Plugins) + { + if (plugin.IsStopped) + { + continue; + } - return Plugins.Any(plugin => + bool result; + try { - try - { - return !plugin.IsStopped && - plugin.OnMessage(message); - } - catch (Exception ex) + result = plugin.OnMessage(message); + } + catch (Exception ex) + { + Utility.Log(nameof(Plugin), LogLevel.Error, ex); + + switch (plugin.ExceptionPolicy) { - switch (plugin.ExceptionPolicy) - { - case UnhandledExceptionPolicy.StopNode: - throw; - case UnhandledExceptionPolicy.StopPlugin: - plugin.IsStopped = true; - break; - case UnhandledExceptionPolicy.Ignore: - break; - default: - throw new InvalidCastException($"The exception policy {plugin.ExceptionPolicy} is not valid."); - } - Utility.Log(nameof(Plugin), LogLevel.Error, ex); - return false; + case UnhandledExceptionPolicy.StopNode: + throw; + case UnhandledExceptionPolicy.StopPlugin: + plugin.IsStopped = true; + break; + case UnhandledExceptionPolicy.Ignore: + break; + default: + throw new InvalidCastException($"The exception policy {plugin.ExceptionPolicy} is not valid."); } + + continue; // Skip to the next plugin if an exception is handled + } + + if (result) + { + return true; } - ); + } + + return false; } + } } From 5a7443f678777a032cfcab7a57d8a56d0aa9f44d Mon Sep 17 00:00:00 2001 From: Shargon Date: Tue, 23 Jul 2024 01:19:21 -0700 Subject: [PATCH 07/10] Update src/Neo/Plugins/UnhandledExceptionPolicy.cs Co-authored-by: Christopher Schuchardt --- src/Neo/Plugins/UnhandledExceptionPolicy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Neo/Plugins/UnhandledExceptionPolicy.cs b/src/Neo/Plugins/UnhandledExceptionPolicy.cs index 035e173aa3..0422f2c5e7 100644 --- a/src/Neo/Plugins/UnhandledExceptionPolicy.cs +++ b/src/Neo/Plugins/UnhandledExceptionPolicy.cs @@ -11,7 +11,7 @@ namespace Neo.Plugins { - public enum UnhandledExceptionPolicy + public enum UnhandledExceptionPolicy : byte { Ignore = 0, StopPlugin = 1, From c9e6dd457c8e6b999f68a911040031667a5b8f15 Mon Sep 17 00:00:00 2001 From: Shargon Date: Wed, 24 Jul 2024 01:22:34 -0700 Subject: [PATCH 08/10] use ignore case --- src/Neo/Plugins/PluginSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Neo/Plugins/PluginSettings.cs b/src/Neo/Plugins/PluginSettings.cs index af33e44eea..66549a390e 100644 --- a/src/Neo/Plugins/PluginSettings.cs +++ b/src/Neo/Plugins/PluginSettings.cs @@ -22,7 +22,7 @@ public UnhandledExceptionPolicy ExceptionPolicy get { var policyString = section.GetValue(nameof(UnhandledExceptionPolicy), nameof(UnhandledExceptionPolicy.StopNode)); - if (Enum.TryParse(policyString, out UnhandledExceptionPolicy policy)) + if (Enum.TryParse(policyString, true, out UnhandledExceptionPolicy policy)) { return policy; } From 3944f42127eb0dd92c40afcec076c709dd636d19 Mon Sep 17 00:00:00 2001 From: Shargon Date: Wed, 24 Jul 2024 01:23:30 -0700 Subject: [PATCH 09/10] Update src/Plugins/TokensTracker/TokensTracker.cs --- src/Plugins/TokensTracker/TokensTracker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugins/TokensTracker/TokensTracker.cs b/src/Plugins/TokensTracker/TokensTracker.cs index a7ab075245..34fe0a2c95 100644 --- a/src/Plugins/TokensTracker/TokensTracker.cs +++ b/src/Plugins/TokensTracker/TokensTracker.cs @@ -61,7 +61,7 @@ protected override void Configure() _network = config.GetValue("Network", 860833102u); _enabledTrackers = config.GetSection("EnabledTrackers").GetChildren().Select(p => p.Value).ToArray(); var policyString = config.GetValue(nameof(UnhandledExceptionPolicy), nameof(UnhandledExceptionPolicy.StopNode)); - if (Enum.TryParse(policyString, out UnhandledExceptionPolicy policy)) + if (Enum.TryParse(policyString, true, out UnhandledExceptionPolicy policy)) { _exceptionPolicy = policy; } From 5034e7f19f4989036d2a64bf60e650b26b267a21 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Mon, 29 Jul 2024 20:08:06 +0800 Subject: [PATCH 10/10] Update src/Neo/Plugins/Plugin.cs Co-authored-by: Shargon --- src/Neo/Plugins/Plugin.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Neo/Plugins/Plugin.cs b/src/Neo/Plugins/Plugin.cs index cf5b798a6d..7b85d85d92 100644 --- a/src/Neo/Plugins/Plugin.cs +++ b/src/Neo/Plugins/Plugin.cs @@ -70,7 +70,7 @@ public abstract class Plugin : IDisposable /// /// If the plugin should be stopped when an exception is thrown. - /// Default is . + /// Default is StopNode. /// protected internal virtual UnhandledExceptionPolicy ExceptionPolicy { get; init; } = UnhandledExceptionPolicy.StopNode;