Agent 长任务断点续传演示-Polly超时-重试-熔断
参考:https://www.cnblogs.com/chenwolong/p/22395330
背景:一个长任务,假设由300个任务组成,各个任务之间没有相互依赖,在执行过程中,如果第197个任务失败了,怎么弄?
记录失败日志,后续手动执行第197个任务。
如果任务之间有依赖,则第197个任务失败后,就不能继续向下执行了!
实际应用中长任务执行通常占用时间较长,过程中难免遇到网络波动等影响任务执行的因素,因此,需要一套执行机制确保尽可能的成功执行任务!
本篇采用-Polly超时-重试-熔断机制进行项目展示!
关于Polly超时-重试-熔断机制,可参考:https://www.cnblogs.com/chenwolong/p/22395330
🏗️ 系统架构总览
┌─────────────────────────────────────────────────────────────────┐ │ Program.Main │ │ (程序入口 + 调度中心) │ └─────────────────────────────────────────────────────────────────┘ │ ┌───────────────┼───────────────┐ ▼ ▼ ▼ ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ │ InMemoryProgress │ │ TaskExecutor │ │ HeartbeatMonitor │ │ Store │ │ │ │ │ │ (进度存储服务) │ │ (任务执行器) │ │ (心跳监控器) │ └───────────────────┘ └───────────────────┘ └───────────────────┘ │ │ │ │ ▼ │ │ ┌───────────────────┐ │ │ │ Polly 策略组合 │ │ │ │ (重试 + 熔断 + 超时) │ │ │ └───────────────────┘ │ │ │ │ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ 数据模型层 │ │ ┌─────────────────────┐ ┌─────────────────────────────┐ │ │ │ TaskItem │ │ TaskProgress │ │ │ │ (任务项模型) │ │ (任务进度模型) │ │ │ └─────────────────────┘ └─────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘
📦 类详解
1️⃣ Program - 程序入口
| 项目 | 说明 |
|---|---|
| 职责 | 程序启动、DI 容器配置、任务调度、优雅退出 |
| 依赖 | InMemoryProgressStore, TaskExecutor, HeartbeatMonitor |
| 关键代码 | Host.CreateDefaultBuilder(), Console.CancelKeyPress |
核心流程:
核心目标
1. 配置 DI 容器 (注册服务) ↓ 2. 初始化存储 (InitializeAsync) ↓ 3. 恢复进度 (LoadProgressAsync) ↓ 4. 创建任务列表 ↓ 5. 启动心跳监控 (MonitorAsync) ↓ 6. 执行批量任务 (ExecuteBatchAsync) ↓ 7. 保存最终进度 ↓ 8. 分析失败任务
2️⃣ InMemoryProgressStore - 进度存储服务
| 项目 | 说明 |
|---|---|
| 职责 | 任务进度持久化 (内存模拟)、断点恢复、失败任务查询 |
| 依赖 | ILogger<InMemoryProgressStore> |
| 存储结构 | static List<TaskItem> + static TaskProgress |
| 线程安全 | ✅ lock (_lock) |
核心方法:
| 方法 | 作用 | 调用时机 |
|---|---|---|
InitializeAsync() |
清空历史数据 | 程序启动 |
SaveProgressAsync() |
保存进度 | 每 10 个任务保存一次 |
LoadProgressAsync() |
恢复进度 | 程序启动/重启 |
GetAllTasks() |
获取所有任务 (调试) | 调试时 |
数据流向:
askExecutor ──SaveProgressAsync──> InMemoryProgressStore
│
├── _taskStore (任务详情)
│
└── _summaryProgress (进度摘要)
3️⃣ TaskExecutor - 任务执行器
| 项目 | 说明 |
|---|---|
| 职责 | 执行单个任务、批量执行、容错处理 (重试 + 熔断 + 超时) |
| 依赖 | InMemoryProgressStore, ILogger<TaskExecutor>, HeartbeatMonitor |
| 核心组件 | Polly 策略组合 (AsyncPolicyWrap) |
Polly 策略组合:
┌─────────────────────────────────────────────────────────────┐ │ PolicyWrap │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ TimeoutPolicy (20 秒) │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ │ │ CircuitBreakerPolicy (连续 2 次失败) │ │ │ │ │ │ ┌───────────────────────────────────────────┐ │ │ │ │ │ │ │ RetryPolicy (3 次指数退避) │ │ │ │ │ │ │ │ ┌─────────────────────────────────────┐ │ │ │ │ │ │ │ │ │ SimulateTaskExecutionAsync │ │ │ │ │ │ │ │ │ └─────────────────────────────────────┘ │ │ │ │ │ │ │ └───────────────────────────────────────────┘ │ │ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ └───────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘
执行流程:
ExecuteTaskAsync(任务) ↓ PolicyWrap.ExecuteAsync() ↓ Timeout 检查 (20 秒内必须完成) ↓ CircuitBreaker 检查 (熔断器是否打开) ↓ Retry 执行 (最多 3 次重试) ↓ SimulateTaskExecutionAsync (实际执行) ↓ 成功 → IsCompleted = true 失败 → ErrorMessage = 异常消息
4️⃣ HeartbeatMonitor - 心跳监控器
| 项目 | 说明 |
|---|---|
| 职责 | 后台监控任务执行进度、超时告警 |
| 依赖 | ILogger<HeartbeatMonitor> |
| 监控方式 | 后台独立线程,每分钟检查一次 |
| 超时阈值 | 5 分钟无进展触发告警 |
工作原理:
┌─────────────────────────────────────────────────────────────┐ │ HeartbeatMonitor │ │ │ │ _lastProgressTime (记录最后进度时间) │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ MonitorAsync (后台线程) │ │ │ │ while (!token.IsCancellationRequested) │ │ │ │ ↓ │ │ │ │ Delay 1 分钟 │ │ │ │ ↓ │ │ │ │ elapsed = Now - _lastProgressTime │ │ │ │ ↓ │ │ │ │ if (elapsed > 5 分钟) → 触发告警 ⚠️ │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ │ RecordProgress() ← TaskExecutor 每次保存进度时调用 │ │ ↓ │ │ _lastProgressTime = DateTime.Now │ └─────────────────────────────────────────────────────────────┘
5️⃣ TaskItem - 任务项模型
| 属性 | 类型 | 说明 |
|---|---|---|
Id |
ulong |
任务唯一标识 |
Content |
string |
任务描述 |
IsCompleted |
bool |
是否完成 |
RetryCount |
int |
重试次数 |
LastAttemptTime |
DateTime? |
最后尝试时间 |
ErrorMessage |
string? |
错误消息 |
Embedding |
float[1536] |
向量嵌入 (用于相似度检索) |
Category |
string |
任务分类 |
6️⃣ TaskProgress - 任务进度模型
| 属性 | 类型 | 说明 |
|---|---|---|
ProgressId |
string |
进度标识 |
TotalCount |
long |
总任务数 |
CompletedCount |
long |
已完成数量 |
LastSavedBatch |
long |
上次保存的批次号 |
FailedTaskIds |
List<ulong> |
失败任务 ID 列表 |
StartTime |
DateTime |
开始时间 |
LastUpdateTime |
DateTime? |
最后更新时间 |
Status |
string |
状态 (Running/Completed/Interrupted) |
🎯 每个类的核心作用总结
| 类名 | 一句话总结 | 类比 |
|---|---|---|
| Program | 程序入口 + 调度中心 | 项目经理 (负责启动、协调、收尾) |
| InMemoryProgressStore | 进度数据库 (内存版) | 档案室 (存进度、查历史) |
| TaskExecutor | 任务执行引擎 + 容错处理 | 施工队 (干活 + 重试 + 熔断保护) |
| HeartbeatMonitor | 后台监控器 | 监工 (盯着进度,卡住就报警) |
| TaskItem | 任务数据模型 | 工单 (每个任务的详细信息) |
| TaskProgress | 进度数据模型 | 进度表 (整体完成情况) |
💡 设计亮点
| 设计点 | 体现位置 | 好处 |
|---|---|---|
| 依赖注入 | 所有服务通过 DI 容器注册 | 解耦、易测试、易替换 |
| 异步编程 | 所有 I/O 操作都是 async/await |
不阻塞主线程 |
| 线程安全 | lock (_lock) 保护共享数据 |
多线程并发不冲突 |
| 策略模式 | Polly 策略组合 | 容错逻辑清晰、可配置 |
| 事件驱动 | HeartbeatMonitor.OnTimeoutAlert |
告警可扩展 (钉钉/邮件/短信) |
| 断点续传 | SaveProgressAsync + LoadProgressAsync |
中断后可恢复 |
核心代码
using ConsoleApp1; using ConsoleApp7.Models; using ConsoleApp7.services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; using System.ComponentModel; using System.Diagnostics; using System.Text.Json.Serialization; using System.Timers; namespace ConsoleApp5 { class Program { static async Task Main(string[] args) { Console.WriteLine("╔════════════════════════════════════════╗"); Console.WriteLine("║ 长任务断点续传演示 ║"); Console.WriteLine("╚════════════════════════════════════════╝\n"); var host = Host.CreateDefaultBuilder(args) .ConfigureServices(services => { services.AddSingleton<InMemoryProgressStore>(); services.AddSingleton<TaskExecutor>(); services.AddSingleton<HeartbeatMonitor>(); services.AddLogging(b => b.AddConsole().SetMinimumLevel(LogLevel.Information)); }) .Build(); var store = host.Services.GetRequiredService<InMemoryProgressStore>(); var executor = host.Services.GetRequiredService<TaskExecutor>(); var monitor = host.Services.GetRequiredService<HeartbeatMonitor>(); // ✅ 订阅心跳超时告警事件 monitor.OnTimeoutAlert += (sender, alertMsg) => { // 可扩展为钉钉/邮件/短信告警 Console.WriteLine($"🚨 收到心跳告警:{alertMsg}"); }; await store.InitializeAsync(); var progress = new TaskProgress { TotalCount = 30, StartTime = DateTime.Now, Status = "Running" }; var tasks = Enumerable.Range(1, (int)progress.TotalCount) .Where(i => i > progress.CompletedCount) .Select(i => new TaskItem { Id = (ulong)i, Content = $"任务 #{i} - 数据处理", Category = i % 3 == 0 ? "batch_A" : "batch_B" }) .ToList(); Console.WriteLine($"🚀 开始执行 {tasks.Count} 个任务...\n"); using var cts = new CancellationTokenSource(); var monitorTask = monitor.MonitorAsync(cts.Token); try { await executor.ExecuteBatchAsync(tasks, progress, cts.Token); Console.WriteLine($"\n✅ 完成!成功:{progress.CompletedCount}, 失败:{progress.FailedTaskIds.Count}"); progress.Status = "Completed"; progress.LastUpdateTime = DateTime.Now; } catch (OperationCanceledException) { Console.WriteLine($"\n💾 已中断,进度已保存到内存。下次运行将自动恢复。"); progress.Status = "Interrupted"; } finally { var completedTasks = tasks.Where(t => t.LastAttemptTime != null).ToList(); if (completedTasks.Count > 0) { await store.SaveProgressAsync(progress, completedTasks); } } Console.WriteLine("\n按任意键退出..."); Console.ReadKey(); } } }
Models
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ConsoleApp7.Models { /// <summary> /// 任务项模型 /// </summary> public class TaskItem { /// <summary> /// 任务唯一标识(ulong 类型,可隐式转为 PointId) /// </summary> public ulong Id { get; set; } /// <summary> /// 任务内容描述 /// </summary> public string Content { get; set; } = string.Empty; /// <summary> /// 是否已完成 /// </summary> public bool IsCompleted { get; set; } /// <summary> /// 重试次数(最多 3 次) /// </summary> public int RetryCount { get; set; } /// <summary> /// 最后尝试时间 /// </summary> public DateTime? LastAttemptTime { get; set; } /// <summary> /// 错误消息(失败时记录) /// </summary> public string? ErrorMessage { get; set; } /// <summary> /// 向量嵌入(用于 Qdrant 相似度检索) /// 维度:1536(text-embedding-v2) /// </summary> public float[] Embedding { get; set; } = new float[1536]; /// <summary> /// 任务分类(用于筛选) /// </summary> public string Category { get; set; } = "default"; } /// <summary> /// 任务进度模型 /// 用于断点续传和状态追踪 /// </summary> public class TaskProgress { /// <summary> /// 进度标识(如:batch_001) /// </summary> public string ProgressId { get; set; } = "batch_001"; /// <summary> /// 总任务数 /// </summary> public long TotalCount { get; set; } /// <summary> /// 已完成数量 /// </summary> public long CompletedCount { get; set; } /// <summary> /// 上次保存的批次号(每 10 个任务保存一次) /// </summary> public long LastSavedBatch { get; set; } /// <summary> /// 失败的任务 ID 列表(ulong 类型) /// </summary> public List<ulong> FailedTaskIds { get; set; } = new List<ulong>(); /// <summary> /// 开始时间 /// </summary> public DateTime StartTime { get; set; } /// <summary> /// 最后更新时间 /// </summary> public DateTime? LastUpdateTime { get; set; } /// <summary> /// 状态:Running/Completed/Failed /// </summary> public string Status { get; set; } = "Running"; } }
Services
using ConsoleApp7.Models; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Polly; using Polly.CircuitBreaker; using Polly.Retry; using Polly.Timeout; using Polly.Wrap; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ConsoleApp7.services { /// <summary> /// 内存进度存储服务(模拟 Qdrant,用于演示) /// ✅ 使用 static List<T> 存储 /// </summary> public class InMemoryProgressStore { private readonly ILogger<InMemoryProgressStore> _logger; // ✅ 静态列表,模拟持久化存储 private static readonly List<TaskItem> _taskStore = new List<TaskItem>(); private static TaskProgress? _summaryProgress = null; private static readonly object _lock = new object(); public InMemoryProgressStore(ILogger<InMemoryProgressStore> logger) { _logger = logger; } /// <summary> /// 初始化存储(清空历史数据) /// </summary> public Task InitializeAsync() { lock (_lock) { _taskStore.Clear(); _summaryProgress = null; Console.WriteLine("✅ 内存存储初始化完成"); } return Task.CompletedTask; } /// <summary> /// 保存进度到内存 /// </summary> public Task SaveProgressAsync(TaskProgress progress, List<TaskItem> batch) { lock (_lock) { // 保存任务详情 foreach (var task in batch) { var existing = _taskStore.FirstOrDefault(t => t.Id == task.Id); if (existing != null) { _taskStore.Remove(existing); } _taskStore.Add(task); } // 保存进度摘要 _summaryProgress = new TaskProgress { ProgressId = progress.ProgressId, TotalCount = progress.TotalCount, CompletedCount = progress.CompletedCount, LastSavedBatch = progress.LastSavedBatch, FailedTaskIds = new List<ulong>(progress.FailedTaskIds), StartTime = progress.StartTime, LastUpdateTime = DateTime.Now, Status = progress.Status }; Console.WriteLine($"💾 进度已保存:完成 {progress.CompletedCount}/{progress.TotalCount}"); } return Task.CompletedTask; } /// <summary> /// 获取所有任务(调试用) /// </summary> public List<TaskItem> GetAllTasks() { lock (_lock) { return new List<TaskItem>(_taskStore); } } } /// <summary> /// 任务执行器 /// ✅ 集成 Polly 重试 + 熔断 + 超时 /// </summary> public class TaskExecutor { private readonly InMemoryProgressStore _store; private readonly ILogger<TaskExecutor> _logger; private readonly HeartbeatMonitor _monitor; // ✅ 新增:注入心跳监控 // ✅ 全部使用非泛型 private readonly AsyncRetryPolicy _retryPolicy; private readonly AsyncCircuitBreakerPolicy _circuitBreaker; private readonly AsyncTimeoutPolicy _timeoutPolicy; private readonly AsyncPolicyWrap _policyWrap; // ✅ 非泛型 public TaskExecutor(InMemoryProgressStore store, ILogger<TaskExecutor> logger, HeartbeatMonitor monitor) { _store = store; _logger = logger; _monitor = monitor; // ✅ 重试:最多 3 次,指数退避(非泛型) _retryPolicy = Policy .Handle<Exception>() .WaitAndRetryAsync( retryCount: 3, sleepDurationProvider: retry => TimeSpan.FromSeconds(Math.Pow(1.1, retry)), onRetry: (outcome, timeSpan, retryNumber, context) => { _logger.LogWarning( "任务 {TaskId} 重试 {Retry}/{Max},等待 {Wait}s,原因:{Error}", context["TaskId"], retryNumber, 3, timeSpan.TotalSeconds, outcome.GetBaseException()?.Message); } ); // ✅ 熔断:连续失败 2 次 → 熔断 30 秒(非泛型) _circuitBreaker = Policy .Handle<Exception>() .CircuitBreakerAsync( exceptionsAllowedBeforeBreaking: 2, durationOfBreak: TimeSpan.FromSeconds(30), onBreak: (outcome, timeSpan) => { _logger.LogError("⚠️ 熔断器打开!连续失败 2 次,暂停 {Seconds}s", timeSpan.TotalSeconds); }, onReset: () => { _logger.LogInformation("✅ 熔断器复位,恢复正常"); } ); // ✅ 超时:20 秒(非泛型) _timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromSeconds(20)); // ✅ 组合策略(返回非泛型 AsyncPolicyWrap) _policyWrap = Policy.WrapAsync(_timeoutPolicy, _circuitBreaker, _retryPolicy); } /// <summary> /// 执行单个任务(带重试 + 超时 + 熔断) /// </summary> public async Task<TaskItem> ExecuteTaskAsync(TaskItem task, CancellationToken token) { try { await _policyWrap.ExecuteAsync( async (ctx) => { task.LastAttemptTime = DateTime.Now; await SimulateTaskExecutionAsync(task, token); task.IsCompleted = true; task.ErrorMessage = null; }, new Context { ["TaskId"] = task.Id } ); return task; } catch (BrokenCircuitException) { task.ErrorMessage = "熔断器打开,服务暂时不可用"; task.RetryCount++; return task; } catch (TimeoutRejectedException) { task.ErrorMessage = "任务超时(20 秒)"; task.RetryCount++; return task; } catch (Exception ex) { task.ErrorMessage = ex.Message; task.RetryCount++; return task; } } /// <summary> /// 批量执行任务(带进度保存) /// </summary> public async Task ExecuteBatchAsync(List<TaskItem> tasks, TaskProgress progress, CancellationToken token) { var batch = new List<TaskItem>(); foreach (var task in tasks) { if (token.IsCancellationRequested) { Console.WriteLine("\n⏹️ 任务已取消,保存当前进度..."); break; } var result = await ExecuteTaskAsync(task, token); if (result.IsCompleted) { progress.CompletedCount++; } else { progress.FailedTaskIds.Add(task.Id); } batch.Add(result); if (batch.Count >= 10) { progress.LastSavedBatch++; progress.LastUpdateTime = DateTime.Now; await _store.SaveProgressAsync(progress, batch); _monitor.RecordProgress(); // ✅ 修复:告诉心跳"我还活着" batch.Clear(); } } if (batch.Count > 0) { progress.LastSavedBatch++; await _store.SaveProgressAsync(progress, batch); _monitor.RecordProgress(); // ✅ 修复:最后一次保存也要调用 } } /// <summary> /// 模拟任务执行(随机失败) /// </summary> private async Task SimulateTaskExecutionAsync(TaskItem task, CancellationToken token) { await Task.Delay(Random.Shared.Next(100, 200), token); // 降低失败率方便演示(80% → 30%) if (Random.Shared.NextDouble() < 0.8) { throw new Exception($"任务 {task.Id} 执行失败(模拟网络错误)"); } } } } /// <summary> /// 心跳检测器 /// ✅ 监控任务执行进度 /// ✅ 长时间无进展时告警 /// </summary> public class HeartbeatMonitor { private readonly ILogger<HeartbeatMonitor> _logger; private readonly TimeSpan _timeoutThreshold = TimeSpan.FromMinutes(5); // 5 分钟阈值 private DateTime _lastProgressTime; /// <summary> /// 超时告警事件 /// </summary> public event EventHandler<string>? OnTimeoutAlert; public HeartbeatMonitor(ILogger<HeartbeatMonitor> logger) { _logger = logger; _lastProgressTime = DateTime.Now; } /// <summary> /// 记录进度(每次保存进度时调用) /// </summary> public void RecordProgress() { _lastProgressTime = DateTime.Now; } /// <summary> /// 后台监控任务 /// ✅ 每分钟检查一次进展 /// </summary> public async Task MonitorAsync(CancellationToken token) { while (!token.IsCancellationRequested) { await Task.Delay(TimeSpan.FromMinutes(1), token); var elapsed = DateTime.Now - _lastProgressTime; if (elapsed > _timeoutThreshold) { var alert = $"⚠️ 心跳超时!已 {elapsed.TotalMinutes:F1} 分钟无进展"; _logger.LogWarning(alert); Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine(alert); Console.ResetColor(); OnTimeoutAlert?.Invoke(this, alert); } } } }
项目引用
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <NoWarn>$(NoWarn);NU5104</NoWarn> </PropertyGroup> <ItemGroup> <!-- ✅ Semantic Kernel 核心 --> <PackageReference Include="Microsoft.SemanticKernel" Version="1.78.0" /> <PackageReference Include="Microsoft.SemanticKernel.Connectors.OpenAI" Version="1.78.0" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.5-beta1" /> <!-- ✅ 阿里云 DashScope(通义千问) --> <PackageReference Include="Sdcb.DashScope" Version="2.0.0" /> <!-- ✅ Polly 重试机制 --> <PackageReference Include="Polly" Version="8.4.0" /> <!-- ✅ 依赖注入 --> <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> </ItemGroup> </Project>

浙公网安备 33010602011771号