fengling-gateway/.planning/phases/006-gateway-plugin-research/006-01-PLAN.md
movingsam 52eba07097 feat: add MigrationTool for gateway config migration (IMPL-7)
- Create MigrationTool console app for exporting DB config to K8s YAML
- Support dry-run mode and validation
- Add Npgsql and YamlDotNet dependencies
2026-03-08 00:35:04 +08:00

8.5 KiB
Raw Blame History

phase plan type wave depends_on files_modified autonomous requirements must_haves
06-gateway-plugin-research 01 execute 1
true
PLUG-01
PLUG-02
truths artifacts key_links
插件可以从指定目录动态加载
插件在独立的 AssemblyLoadContext 中运行
插件可以被卸载并释放内存
path provides
src/yarpgateway/Plugins/PluginLoadContext.cs ALC 隔离机制
path provides
src/yarpgateway/Plugins/PluginLoader.cs 插件发现和加载
path provides
src/yarpgateway/Plugins/PluginHost.cs 插件生命周期管理
path provides
tests/YarpGateway.Tests/Unit/Plugins/PluginLoadTests.cs 加载/卸载验证
from to via
PluginLoader PluginLoadContext 实例化并加载程序集
from to via
PluginHost PluginLoader 协调插件生命周期

计划 01插件加载基础设施

实现插件动态加载基础设施,包括 AssemblyLoadContext 隔离、插件发现和生命周期管理。

目的: 为网关提供安全的插件加载机制,支持隔离和热重载。 产出: 可工作的插件加载系统,含单元测试。

<execution_context> @/Users/mac/.config/opencode/get-shit-done/workflows/execute-plan.md @/Users/mac/.config/opencode/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/006-gateway-plugin-research/006-RESEARCH.md

现有基础设施

@src/Fengling.Gateway.Plugin.Abstractions/IGatewayPlugin.cs @src/yarpgateway/Program.cs

```csharp // Fengling.Gateway.Plugin.Abstractions public interface IGatewayPlugin { string Name { get; } string Version { get; } string? Description { get; } Task OnLoadAsync(); Task OnUnloadAsync(); } ``` Task 1: 创建 PluginLoadContext 隔离机制 src/yarpgateway/Plugins/PluginLoadContext.cs, tests/YarpGateway.Tests/Unit/Plugins/PluginLoadContextTests.cs - Test 1: 加载插件程序集到独立 ALC - Test 2: 共享契约程序集使用默认 ALC - Test 3: 卸载 ALC 后内存被回收 创建可卸载的 AssemblyLoadContext
  1. 创建 src/yarpgateway/Plugins/PluginLoadContext.cs
  2. 继承 AssemblyLoadContext设置 isCollectible: true
  3. 使用 AssemblyDependencyResolver 解析依赖
  4. 关键:共享 Fengling.Gateway.Plugin.Abstractions 到默认 ALC
public sealed class PluginLoadContext : AssemblyLoadContext
{
    private readonly AssemblyDependencyResolver _resolver;
    private readonly string _sharedAssemblyName = "Fengling.Gateway.Plugin.Abstractions";

    public PluginLoadContext(string pluginPath) : base(isCollectible: true)
    {
        _resolver = new AssemblyDependencyResolver(pluginPath);
    }

    protected override Assembly? Load(AssemblyName assemblyName)
    {
        if (assemblyName.Name == _sharedAssemblyName)
            return null; // 使用默认 ALC
        var path = _resolver.ResolveAssemblyToPath(assemblyName);
        return path != null ? LoadFromAssemblyPath(path) : null;
    }

    protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
    {
        var path = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
        return path != null ? LoadUnmanagedDllFromPath(path) : IntPtr.Zero;
    }
}

注意:先写测试,确保卸载验证通过。 dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginLoadContextTests" --no-build - PluginLoadContext 类存在 - 测试验证隔离和卸载 - 构建通过

Task 2: 创建 PluginLoader 发现和加载逻辑 src/yarpgateway/Plugins/PluginLoader.cs, src/yarpgateway/Plugins/DiscoveredPlugin.cs, tests/YarpGateway.Tests/Unit/Plugins/PluginLoaderTests.cs - Test 1: 从目录发现插件程序集 - Test 2: 加载插件并返回 IGatewayPlugin 实例 - Test 3: 处理无效插件(返回 null 或异常) 创建插件发现和加载器:
  1. 创建 DiscoveredPlugin.cs 记录:

    • Id, Name, Version, AssemblyPath, EntryPoint
  2. 创建 PluginLoader.cs

    • DiscoverPlugins(string directory) - 扫描目录发现插件
    • LoadPlugin(DiscoveredPlugin discovered) - 加载并实例化
    • 使用 System.Reflection.Metadata 或简单扫描
public class PluginLoader
{
    public IEnumerable<DiscoveredPlugin> DiscoverPlugins(string pluginDirectory)
    {
        // 扫描 plugin.json 或程序集属性
    }

    public IGatewayPlugin? LoadPlugin(DiscoveredPlugin discovered)
    {
        var shadowPath = CreateShadowCopy(discovered.AssemblyPath);
        var alc = new PluginLoadContext(shadowPath);
        var assembly = alc.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(discovered.AssemblyPath)));
        var type = assembly.GetType(discovered.EntryPoint);
        return Activator.CreateInstance(type) as IGatewayPlugin;
    }
}

影子复制:创建 CreateShadowCopy 方法,将插件复制到临时目录。 dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginLoaderTests" --no-build - PluginLoader 类存在 - 可发现和加载插件 - 测试通过

Task 3: 创建 PluginHost 生命周期管理 src/yarpgateway/Plugins/PluginHost.cs, src/yarpgateway/Plugins/PluginHandle.cs, tests/YarpGateway.Tests/Unit/Plugins/PluginHostTests.cs - Test 1: LoadAllAsync 加载目录下所有插件 - Test 2: UnloadAsync 卸载指定插件 - Test 3: GetPlugins 返回当前加载的插件 - Test 4: 插件卸载后 WeakReference 显示已回收 创建插件生命周期管理器:
  1. 创建 PluginHandle.cs
    • 封装 PluginLoadContext 和 IGatewayPlugin
    • 实现 IAsyncDisposable
    • 提供 TrackUnloadability() 返回 WeakReference
public sealed class PluginHandle : IAsyncDisposable
{
    private readonly PluginLoadContext _alc;
    private readonly IGatewayPlugin _plugin;
    private readonly string _shadowDirectory;
    private bool _disposed;

    public IGatewayPlugin Plugin => _plugin;
    public WeakReference TrackUnloadability() => new(_alc, trackResurrection: true);

    public async ValueTask DisposeAsync()
    {
        if (_disposed) return;
        _disposed = true;
        await _plugin.OnUnloadAsync();
        _alc.Unload();
    }
}
  1. 创建 PluginHost.cs
    • 注入 PluginLoader
    • 管理插件字典 (name -> PluginHandle)
    • 提供 LoadAllAsync, UnloadAsync, GetPlugins
public class PluginHost
{
    private readonly ConcurrentDictionary<string, PluginHandle> _plugins = new();
    private readonly PluginLoader _loader;

    public async Task LoadAllAsync(string pluginDirectory, CancellationToken ct = default)
    {
        var discovered = _loader.DiscoverPlugins(pluginDirectory);
        foreach (var d in discovered)
        {
            var handle = _loader.LoadPlugin(d);
            if (handle != null)
            {
                await handle.Plugin.OnLoadAsync();
                _plugins[d.Id] = handle;
            }
        }
    }

    public async Task UnloadAsync(string pluginId)
    {
        if (_plugins.TryRemove(pluginId, out var handle))
        {
            await handle.DisposeAsync();
        }
    }
}
dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginHostTests" --no-build - PluginHost 和 PluginHandle 类存在 - 可加载/卸载插件 - 卸载验证测试通过 1. dotnet build src/yarpgateway/YarpGateway.csproj 无错误 2. dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginLoad" 通过 3. 插件加载/卸载功能可用

<success_criteria>

  • 插件可在独立 AssemblyLoadContext 中加载
  • 插件可通过 WeakReference 验证卸载
  • 所有单元测试通过 </success_criteria>
完成后创建 `.planning/phases/006-gateway-plugin-research/006-01-SUMMARY.md`