公司場景 Pattern
本頁收集公司實際需求中最常用的 Polly 韌性 Pattern。
Pattern 1:PLC 通訊韌性管道
場景
透過 Modbus TCP 讀取 PLC 暫存器。網路偶爾不穩、PLC 忙碌時不回應、設備重啟期間完全斷線。需要一個完整的韌性管道來處理所有情況。
策略組合(Pipeline 結構)
ResiliencePipelineBuilder 鏈式註冊的策略會形成「洋蔥式包裹」:先註冊的策略在外層,後註冊的在內層。下圖展示 Pattern 1 完整的 5 層管道與失敗 / 成功如何在層間傳遞:
讀圖重點:
| 層 | 策略 | 吸收的失敗 | 對外丟出的失敗 |
|---|---|---|---|
| 1 | TotalTimeout | 整段超過 30s | TimeoutRejectedException |
| 2 | Fallback | 內層任何例外 | 一律 swallow,回傳安全預設值 |
| 3 | Retry | 單次失敗(重試 3 次) | 重試耗盡後原 exception |
| 4 | CircuitBreaker | 短時間內大量失敗 | BrokenCircuitException(暫停打下游) |
| 5 | AttemptTimeout | 單次 Modbus 呼叫過久 | TimeoutRejectedException |
| — | Downstream | — | 原始 ModbusException / IOException |
- 實線箭頭:請求往內層傳遞的呼叫鏈。
- 虛線箭頭:結果(成功值或例外)往外層回傳的路徑。
- Fallback 之外才是「對呼叫端可見的失敗」:所有從 layer 3–5 拋出的例外,只要 Fallback 還沒被 outer Timeout 砍掉,最終都會被吃掉成空陣列。
完整實作
public class PlcCommunicationPipeline
{
private readonly ResiliencePipeline<ushort[]> _pipeline;
public PlcCommunicationPipeline(ILogger logger)
{
_pipeline = new ResiliencePipelineBuilder<ushort[]>()
// 1. 總逾時:整個操作(含所有重試)不超過 30 秒
.AddTimeout(new TimeoutStrategyOptions
{
Timeout = TimeSpan.FromSeconds(30),
Name = "TotalTimeout"
})
// 2. Fallback:所有策略都失敗時回傳空陣列
.AddFallback(new FallbackStrategyOptions<ushort[]>
{
ShouldHandle = new PredicateBuilder<ushort[]>()
.Handle<Exception>(),
FallbackAction = args =>
{
logger.LogWarning("PLC 通訊完全失敗,回傳空值");
return Outcome.FromResultAsValueTask(Array.Empty<ushort>());
}
})
// 3. 重試:最多 3 次,指數退避
.AddRetry(new RetryStrategyOptions<ushort[]>
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
Delay = TimeSpan.FromMilliseconds(500),
ShouldHandle = new PredicateBuilder<ushort[]>()
.Handle<ModbusException>()
.Handle<TimeoutRejectedException>()
.Handle<IOException>(),
OnRetry = args =>
{
logger.LogWarning(
"PLC 讀取失敗 [{Error}],第 {Attempt} 次重試",
args.Outcome.Exception?.Message,
args.AttemptNumber + 1);
return ValueTask.CompletedTask;
}
})
// 4. 斷路器:連續失敗太多就暫停嘗試
.AddCircuitBreaker(new CircuitBreakerStrategyOptions<ushort[]>
{
FailureRatio = 0.8,
SamplingDuration = TimeSpan.FromSeconds(60),
MinimumThroughput = 10,
BreakDuration = TimeSpan.FromSeconds(30),
ShouldHandle = new PredicateBuilder<ushort[]>()
.Handle<ModbusException>()
.Handle<IOException>(),
OnOpened = args =>
{
logger.LogError("PLC 斷路器開啟,暫停通訊 {Duration}s",
args.BreakDuration.TotalSeconds);
return ValueTask.CompletedTask;
}
})
// 5. 單次逾時:每次讀取不超過 3 秒
.AddTimeout(new TimeoutStrategyOptions
{
Timeout = TimeSpan.FromSeconds(3),
Name = "AttemptTimeout"
})
.Build();
}
public async Task<ushort[]> ReadRegistersAsync(
IModbusClient client, int startAddress, int count,
CancellationToken ct = default)
{
return await _pipeline.ExecuteAsync(
async token => await client.ReadHoldingRegistersAsync(
1, startAddress, count, token),
ct);
}
}
Pattern 2:SECS/GEM Transaction 重試
場景
SECS/GEM 通訊中,Transaction 可能因為設備忙碌(S1F0 ABORT)或通訊超時而失敗。不同類型的 Transaction 需要不同的重試策略。
實作
public static class SecsGemPipelines
{
/// <summary>
/// 一般查詢用(S1F1、S1F13 等),允許較多重試。
/// </summary>
public static ResiliencePipeline<SecsMessage> CreateQueryPipeline(ILogger logger)
{
return new ResiliencePipelineBuilder<SecsMessage>()
.AddRetry(new RetryStrategyOptions<SecsMessage>
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Constant,
Delay = TimeSpan.FromSeconds(1),
ShouldHandle = new PredicateBuilder<SecsMessage>()
.Handle<SecsTransactionException>(
ex => ex.ErrorCode == SecsErrorCode.T3Timeout)
.Handle<SecsTransactionException>(
ex => ex.ErrorCode == SecsErrorCode.DeviceBusy),
OnRetry = args =>
{
logger.LogWarning("SECS Transaction 失敗 [{Error}],重試 {N}",
args.Outcome.Exception?.Message, args.AttemptNumber + 1);
return ValueTask.CompletedTask;
}
})
.AddTimeout(TimeSpan.FromSeconds(10))
.Build();
}
/// <summary>
/// 控制命令用(S2F41 Remote Command),更保守的重試。
/// </summary>
public static ResiliencePipeline<SecsMessage> CreateCommandPipeline(ILogger logger)
{
return new ResiliencePipelineBuilder<SecsMessage>()
.AddRetry(new RetryStrategyOptions<SecsMessage>
{
MaxRetryAttempts = 1, // 控制命令只重試一次
Delay = TimeSpan.FromSeconds(2),
ShouldHandle = new PredicateBuilder<SecsMessage>()
.Handle<SecsTransactionException>(
ex => ex.ErrorCode == SecsErrorCode.T3Timeout),
// DeviceBusy 不重試控制命令(避免重複執行)
})
.AddTimeout(TimeSpan.FromSeconds(30))
.Build();
}
}
Pattern 3:HTTP API 韌性(MES/ERP 呼叫)
場景
設備控制程式需要呼叫 MES/ERP 的 REST API 回報生產資料。API 伺服器可能暫時無回應或回傳 5xx 錯誤。
使用 Microsoft.Extensions.Http.Resilience
// Program.cs / DI 設定
services.AddHttpClient("MES", client =>
{
client.BaseAddress = new Uri("https://mes.internal/api/");
client.Timeout = TimeSpan.FromSeconds(30);
})
.AddStandardResilienceHandler(); // 微軟預設的韌性策略組合
AddStandardResilienceHandler 內建的策略組合:
- Rate Limiter → Total Timeout → Retry → Circuit Breaker → Attempt Timeout
自訂 HTTP 韌性策略
services.AddHttpClient("ERP")
.AddResilienceHandler("erp-pipeline", builder =>
{
builder
.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
Delay = TimeSpan.FromSeconds(1),
// 只對 5xx 和網路錯誤重試
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.Handle<HttpRequestException>()
.HandleResult(r => r.StatusCode >= System.Net.HttpStatusCode.InternalServerError)
})
.AddTimeout(TimeSpan.FromSeconds(10));
});
使用:
public class MesReportService
{
private readonly HttpClient _httpClient;
public MesReportService(IHttpClientFactory factory)
{
_httpClient = factory.CreateClient("MES");
}
public async Task ReportProductionAsync(ProductionData data)
{
// 韌性策略自動套用
var response = await _httpClient.PostAsJsonAsync("production", data);
response.EnsureSuccessStatusCode();
}
}
Pattern 4:Polly Retry vs Rx.NET RetryWhen
何時用哪個?
| 場景 | 推薦 | 理由 |
|---|---|---|
| 單次 request-response | Polly | 天生為此設計 |
| 持續的資料流 | Rx.NET RetryWhen | Observable 管道內處理 |
| HTTP 呼叫 | Polly | 有 HttpClient 整合 |
| 設備輪詢 | Rx.NET | 整個輪詢流就是 Observable |
| DI 管理策略 | Polly | AddResiliencePipeline |
| 組合時間操作 | Rx.NET | Throttle、Buffer 等 |
在 Rx.NET 管道中使用 Polly
當你的 Observable 管道中有單次非同步操作需要韌性時,可以在 SelectMany 內使用 Polly:
var retryPipeline = new ResiliencePipelineBuilder<DeviceStatus>()
.AddRetry(new RetryStrategyOptions<DeviceStatus>
{
MaxRetryAttempts = 2,
Delay = TimeSpan.FromMilliseconds(500),
})
.AddTimeout(TimeSpan.FromSeconds(3))
.Build();
// Rx 管道 + Polly 單次操作韌性
Observable.Interval(TimeSpan.FromSeconds(1))
.SelectMany(_ => Observable.FromAsync(ct =>
retryPipeline.ExecuteAsync(
async token => await device.ReadStatusAsync(token), ct)))
.DistinctUntilChanged()
.Subscribe(status => UpdateDashboard(status));
Pattern 5:DI 整合 — AddResiliencePipeline
集中註冊韌性策略
// Program.cs
services.AddResiliencePipeline("plc-communication", builder =>
{
builder
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
Delay = TimeSpan.FromMilliseconds(500),
})
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(30),
MinimumThroughput = 5,
BreakDuration = TimeSpan.FromSeconds(30),
})
.AddTimeout(TimeSpan.FromSeconds(5));
});
services.AddResiliencePipeline("mes-api", builder =>
{
builder
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 5,
BackoffType = DelayBackoffType.ExponentialWithJitter,
Delay = TimeSpan.FromSeconds(1),
})
.AddTimeout(TimeSpan.FromSeconds(30));
});
注入使用
public class DeviceService
{
private readonly ResiliencePipeline _pipeline;
public DeviceService(
ResiliencePipelineProvider<string> pipelineProvider)
{
_pipeline = pipelineProvider.GetPipeline("plc-communication");
}
public async Task<int> ReadRegisterAsync(string address)
{
return await _pipeline.ExecuteAsync(async ct =>
await _plcClient.ReadAsync(address, ct));
}
}
泛型 Pipeline 註冊
services.AddResiliencePipeline<string, DeviceData>("device-read", builder =>
{
builder
.AddFallback(new FallbackStrategyOptions<DeviceData>
{
FallbackAction = _ =>
Outcome.FromResultAsValueTask(DeviceData.Empty)
})
.AddRetry(new RetryStrategyOptions<DeviceData>
{
MaxRetryAttempts = 3,
})
.AddTimeout(TimeSpan.FromSeconds(5));
});
延伸閱讀: