- Create MigrationTool console app for exporting DB config to K8s YAML - Support dry-run mode and validation - Add Npgsql and YamlDotNet dependencies
290 lines
8.5 KiB
Markdown
290 lines
8.5 KiB
Markdown
---
|
||
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> |