Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Neo Plugin New feature] UnhandledExceptionPolicy on Plugin Unhandled Exception #3311

Merged
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 65 additions & 2 deletions src/Neo/Ledger/Blockchain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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);
Expand All @@ -480,6 +484,65 @@ private void Persist(Block block)
Debug.Assert(header.Index == block.Index);
}

internal static async Task InvokeCommittingAsync(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList<ApplicationExecuted> 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<Delegate> handlerAction)
{
if (handlers == null) return;

var exceptions = new ConcurrentBag<Exception>();
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 ArgumentOutOfRangeException();
}

Utility.Log(nameof(plugin), LogLevel.Error, ex);
}
catch (Exception ex)
{
exceptions.Add(ex);
}
})).ToList();

await Task.WhenAll(tasks);

exceptions.ForEach(e => throw e);
}


shargon marked this conversation as resolved.
Show resolved Hide resolved
/// <summary>
/// Gets a <see cref="Akka.Actor.Props"/> object used for creating the <see cref="Blockchain"/> actor.
/// </summary>
Expand Down
56 changes: 50 additions & 6 deletions src/Neo/Plugins/Plugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ public abstract class Plugin : IDisposable
/// <summary>
/// The directory containing the plugin folders. Files can be contained in any subdirectory.
/// </summary>
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;

Expand Down Expand Up @@ -67,14 +68,27 @@ public abstract class Plugin : IDisposable
/// </summary>
public virtual Version Version => GetType().Assembly.GetName().Version;

/// <summary>
/// If the plugin should be stopped when an exception is thrown.
/// Default is <see langword="true"/>.
/// </summary>
protected internal virtual UnhandledExceptionPolicy ExceptionPolicy { get; init; } = UnhandledExceptionPolicy.StopNode;

/// <summary>
/// The plugin will be stopped if an exception is thrown.
/// But it also depends on <see cref="UnhandledExceptionPolicy"/>.
/// </summary>
internal bool IsStopped { get; set; }

static Plugin()
{
if (!Directory.Exists(PluginsDirectory)) return;
configWatcher = new FileSystemWatcher(PluginsDirectory)
{
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;
Expand Down Expand Up @@ -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;
}
}
Expand All @@ -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";
Expand Down Expand Up @@ -150,7 +166,8 @@ public virtual void Dispose()
/// <returns>The content of the configuration file read.</returns>
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)
Expand Down Expand Up @@ -187,6 +204,7 @@ internal static void LoadPlugins()
catch { }
}
}

foreach (Assembly assembly in assemblies)
{
LoadPlugin(assembly);
Expand Down Expand Up @@ -229,7 +247,33 @@ protected internal virtual void OnSystemLoaded(NeoSystem system)
/// <returns><see langword="true"/> if the <paramref name="message"/> is handled by a plugin; otherwise, <see langword="false"/>.</returns>
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 ArgumentOutOfRangeException();
}
Utility.Log(nameof(Plugin), LogLevel.Error, ex);
return false;
}
}
);
}
}
}
34 changes: 34 additions & 0 deletions src/Neo/Plugins/PluginSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// 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("UnhandledExceptionPolicy", "StopNode");
shargon marked this conversation as resolved.
Show resolved Hide resolved
if (Enum.TryParse(policyString, out UnhandledExceptionPolicy policy))
{

return policy;
shargon marked this conversation as resolved.
Show resolved Hide resolved
}

throw new InvalidParameterException($"{policyString} is not a valid UnhandledExceptionPolicy");
}
}
}
20 changes: 20 additions & 0 deletions src/Neo/Plugins/UnhandledExceptionPolicy.cs
Original file line number Diff line number Diff line change
@@ -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,
}
}
3 changes: 2 additions & 1 deletion src/Plugins/ApplicationLogs/ApplicationLogs.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"Path": "ApplicationLogs_{0}",
"Network": 860833102,
"MaxStackSize": 65535,
"Debug": false
"Debug": false,
"UnhandledExceptionPolicy": "StopPlugin"
},
"Dependency": [
"RpcServer"
Expand Down
1 change: 1 addition & 0 deletions src/Plugins/ApplicationLogs/LogReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public class LogReader : Plugin

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

Expand Down
4 changes: 2 additions & 2 deletions src/Plugins/ApplicationLogs/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

namespace Neo.Plugins.ApplicationLogs
{
internal class Settings
internal class Settings : PluginSettings
{
public string Path { get; }
public uint Network { get; }
Expand All @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/Plugins/DBFTPlugin/DBFTPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public class DBFTPlugin : Plugin

public override string ConfigFile => System.IO.Path.Combine(RootPath, "DBFTPlugin.json");

protected override UnhandledExceptionPolicy ExceptionPolicy => settings.ExceptionPolicy;

public DBFTPlugin()
{
RemoteNode.MessageReceived += RemoteNode_MessageReceived;
Expand Down
3 changes: 2 additions & 1 deletion src/Plugins/DBFTPlugin/DBFTPlugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"AutoStart": false,
"Network": 860833102,
"MaxBlockSize": 2097152,
"MaxBlockSystemFee": 150000000000
"MaxBlockSystemFee": 150000000000,
"UnhandledExceptionPolicy": "StopNode"
}
}
4 changes: 2 additions & 2 deletions src/Plugins/DBFTPlugin/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

namespace Neo.Plugins.DBFTPlugin
{
public class Settings
public class Settings : PluginSettings
{
public string RecoveryLogs { get; }
public bool IgnoreRecoveryLogs { get; }
Expand All @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/Plugins/OracleService/OracleService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ public class OracleService : Plugin

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()
Expand Down
1 change: 1 addition & 0 deletions src/Plugins/OracleService/OracleService.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"MaxOracleTimeout": 10000,
"AllowPrivateHost": false,
"AllowedContentTypes": [ "application/json" ],
"UnhandledExceptionPolicy": "Ignore",
"Https": {
"Timeout": 5000
},
Expand Down
4 changes: 2 additions & 2 deletions src/Plugins/OracleService/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public NeoFSSettings(IConfigurationSection section)
}
}

class Settings
class Settings : PluginSettings
{
public uint Network { get; }
public Uri[] Nodes { get; }
Expand All @@ -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<string>(), UriKind.Absolute)).ToArray();
Expand Down
1 change: 1 addition & 0 deletions src/Plugins/RpcServer/RpcServer.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"PluginConfiguration": {
"UnhandledExceptionPolicy": "Ignore",
"Servers": [
{
"Network": 860833102,
Expand Down
1 change: 1 addition & 0 deletions src/Plugins/RpcServer/RpcServerPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class RpcServerPlugin : Plugin
private static readonly Dictionary<uint, List<object>> handlers = new();

public override string ConfigFile => System.IO.Path.Combine(RootPath, "RpcServer.json");
protected override UnhandledExceptionPolicy ExceptionPolicy => settings.ExceptionPolicy;

protected override void Configure()
{
Expand Down
Loading