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

290 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
phase: 06-gateway-plugin-research
plan: 01
type: execute
wave: 1
depends_on: []
files_modified: []
autonomous: true
requirements: [PLUG-01, PLUG-02]
must_haves:
truths:
- "插件可以从指定目录动态加载"
- "插件在独立的 AssemblyLoadContext 中运行"
- "插件可以被卸载并释放内存"
artifacts:
- path: "src/yarpgateway/Plugins/PluginLoadContext.cs"
provides: "ALC 隔离机制"
- path: "src/yarpgateway/Plugins/PluginLoader.cs"
provides: "插件发现和加载"
- path: "src/yarpgateway/Plugins/PluginHost.cs"
provides: "插件生命周期管理"
- path: "tests/YarpGateway.Tests/Unit/Plugins/PluginLoadTests.cs"
provides: "加载/卸载验证"
key_links:
- from: "PluginLoader"
to: "PluginLoadContext"
via: "实例化并加载程序集"
- from: "PluginHost"
to: "PluginLoader"
via: "协调插件生命周期"
---
# 计划 01插件加载基础设施
<objective>
实现插件动态加载基础设施,包括 AssemblyLoadContext 隔离、插件发现和生命周期管理。
**目的:** 为网关提供安全的插件加载机制,支持隔离和热重载。
**产出:** 可工作的插件加载系统,含单元测试。
</objective>
<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>
<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
</context>
<interfaces>
<!-- 现有插件接口 -->
```csharp
// Fengling.Gateway.Plugin.Abstractions
public interface IGatewayPlugin
{
string Name { get; }
string Version { get; }
string? Description { get; }
Task OnLoadAsync();
Task OnUnloadAsync();
}
```
</interfaces>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: 创建 PluginLoadContext 隔离机制</name>
<files>
src/yarpgateway/Plugins/PluginLoadContext.cs,
tests/YarpGateway.Tests/Unit/Plugins/PluginLoadContextTests.cs
</files>
<behavior>
- Test 1: 加载插件程序集到独立 ALC
- Test 2: 共享契约程序集使用默认 ALC
- Test 3: 卸载 ALC 后内存被回收
</behavior>
<action>
创建可卸载的 AssemblyLoadContext
1. 创建 `src/yarpgateway/Plugins/PluginLoadContext.cs`
2. 继承 AssemblyLoadContext设置 isCollectible: true
3. 使用 AssemblyDependencyResolver 解析依赖
4. 关键:共享 `Fengling.Gateway.Plugin.Abstractions` 到默认 ALC
```csharp
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;
}
}
```
**注意**:先写测试,确保卸载验证通过。
</action>
<verify>
<automated>dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginLoadContextTests" --no-build</automated>
</verify>
<done>
- PluginLoadContext 类存在
- 测试验证隔离和卸载
- 构建通过
</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: 创建 PluginLoader 发现和加载逻辑</name>
<files>
src/yarpgateway/Plugins/PluginLoader.cs,
src/yarpgateway/Plugins/DiscoveredPlugin.cs,
tests/YarpGateway.Tests/Unit/Plugins/PluginLoaderTests.cs
</files>
<behavior>
- Test 1: 从目录发现插件程序集
- Test 2: 加载插件并返回 IGatewayPlugin 实例
- Test 3: 处理无效插件(返回 null 或异常)
</behavior>
<action>
创建插件发现和加载器:
1. 创建 `DiscoveredPlugin.cs` 记录:
- Id, Name, Version, AssemblyPath, EntryPoint
2. 创建 `PluginLoader.cs`
- `DiscoverPlugins(string directory)` - 扫描目录发现插件
- `LoadPlugin(DiscoveredPlugin discovered)` - 加载并实例化
- 使用 System.Reflection.Metadata 或简单扫描
```csharp
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` 方法,将插件复制到临时目录。
</action>
<verify>
<automated>dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginLoaderTests" --no-build</automated>
</verify>
<done>
- PluginLoader 类存在
- 可发现和加载插件
- 测试通过
</done>
</task>
<task type="auto" tdd="true">
<name>Task 3: 创建 PluginHost 生命周期管理</name>
<files>
src/yarpgateway/Plugins/PluginHost.cs,
src/yarpgateway/Plugins/PluginHandle.cs,
tests/YarpGateway.Tests/Unit/Plugins/PluginHostTests.cs
</files>
<behavior>
- Test 1: LoadAllAsync 加载目录下所有插件
- Test 2: UnloadAsync 卸载指定插件
- Test 3: GetPlugins 返回当前加载的插件
- Test 4: 插件卸载后 WeakReference 显示已回收
</behavior>
<action>
创建插件生命周期管理器:
1. 创建 `PluginHandle.cs`
- 封装 PluginLoadContext 和 IGatewayPlugin
- 实现 IAsyncDisposable
- 提供 TrackUnloadability() 返回 WeakReference
```csharp
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();
}
}
```
2. 创建 `PluginHost.cs`
- 注入 PluginLoader
- 管理插件字典 (name -> PluginHandle)
- 提供 LoadAllAsync, UnloadAsync, GetPlugins
```csharp
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();
}
}
}
```
</action>
<verify>
<automated>dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginHostTests" --no-build</automated>
</verify>
<done>
- PluginHost 和 PluginHandle 类存在
- 可加载/卸载插件
- 卸载验证测试通过
</done>
</task>
</tasks>
<verification>
1. dotnet build src/yarpgateway/YarpGateway.csproj 无错误
2. dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginLoad" 通过
3. 插件加载/卸载功能可用
</verification>
<success_criteria>
- 插件可在独立 AssemblyLoadContext 中加载
- 插件可通过 WeakReference 验证卸载
- 所有单元测试通过
</success_criteria>
<output>
完成后创建 `.planning/phases/006-gateway-plugin-research/006-01-SUMMARY.md`
</output>