跳到主要内容

公司場景 Pattern

本頁收集公司實際需求中最常用的 Polly 韌性 Pattern。


Pattern 1:PLC 通訊韌性管道

場景

透過 Modbus TCP 讀取 PLC 暫存器。網路偶爾不穩、PLC 忙碌時不回應、設備重啟期間完全斷線。需要一個完整的韌性管道來處理所有情況。

策略組合(Pipeline 結構)

ResiliencePipelineBuilder 鏈式註冊的策略會形成「洋蔥式包裹」:先註冊的策略在外層,後註冊的在內層。下圖展示 Pattern 1 完整的 5 層管道與失敗 / 成功如何在層間傳遞:

讀圖重點:

策略吸收的失敗對外丟出的失敗
1TotalTimeout整段超過 30sTimeoutRejectedException
2Fallback內層任何例外一律 swallow,回傳安全預設值
3Retry單次失敗(重試 3 次)重試耗盡後原 exception
4CircuitBreaker短時間內大量失敗BrokenCircuitException(暫停打下游)
5AttemptTimeout單次 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-responsePolly天生為此設計
持續的資料流Rx.NET RetryWhenObservable 管道內處理
HTTP 呼叫Polly有 HttpClient 整合
設備輪詢Rx.NET整個輪詢流就是 Observable
DI 管理策略PollyAddResiliencePipeline
組合時間操作Rx.NETThrottle、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));
});

延伸閱讀

  • 核心策略詳解 — 每個策略的完整參數說明
  • 最佳實踐 — 策略選擇與測試
  • Rx.NET 工業場景 Pattern — Rx.NET 的重連模式(參見 Rx.NET 技術指南)