@@ -73,6 +73,13 @@ public class ApiPlcProgramSubscriptionFaker : IDisposable
7373 /// </summary>
7474 public int MaxBackoffMultiplier { get ; set ; } = 10 ;
7575
76+ /// <summary>
77+ /// Maximum duration (in milliseconds) allowed for a single poll before timing out.
78+ /// Default is null (3x PollingInterval is calculated automatically).
79+ /// Set to a specific value to override the default timeout behavior.
80+ /// </summary>
81+ public int ? PollTimeoutMs { get ; set ; } = null ;
82+
7683 /// <summary>
7784 /// Indicates whether the subscription is currently active and polling.
7885 /// </summary>
@@ -175,23 +182,53 @@ private void PollingTimerCallback(object state)
175182 {
176183 var pollMonitoredItemsStarted = DateTime . UtcNow ;
177184 var changedItemsList = new List < ( ApiPlcProgramData item , object oldValue , object newValue ) > ( ) ;
185+ var pollTimedOut = false ;
186+
178187 try
179188 {
180- changedItemsList = PollMonitoredItemsAsync ( ) . GetAwaiter ( ) . GetResult ( ) ;
189+ var timeoutMs = PollTimeoutMs ?? ( PollingInterval * 3 ) ;
190+ using ( var cts = new CancellationTokenSource ( timeoutMs ) )
191+ {
192+ try
193+ {
194+ changedItemsList = PollMonitoredItemsAsync ( cts . Token ) . GetAwaiter ( ) . GetResult ( ) ;
195+ }
196+ catch ( OperationCanceledException )
197+ {
198+ pollTimedOut = true ;
199+ _logger ? . LogError ( $ "{ nameof ( ApiPlcProgramSubscriptionFaker ) } : Poll timeout exceeded { timeoutMs } ms. " +
200+ "Cancelling poll operation. Applying exponential backoff." ) ;
201+ }
202+ }
181203 }
182204 finally
183205 {
184206 var pollDuration = ( DateTime . UtcNow - pollMonitoredItemsStarted ) . TotalMilliseconds ;
207+
185208 if ( changedItemsList . Count > 0 || PollingCycleCompleted != null )
186209 {
187210 OnPollingCycleCompleted ( new PollingCycleCompletedEventArgs ( changedItemsList , pollDuration , pollMonitoredItemsStarted ) ) ;
188211 }
212+
189213 Interlocked . Exchange ( ref _isPolling , 0 ) ;
214+
190215 if ( _pollingTimer != null && ! _isDisposed )
191216 {
192217 try
193218 {
194- if ( pollDuration > PollingInterval )
219+ if ( pollTimedOut )
220+ {
221+ _consecutiveSlowPolls ++ ;
222+ var backoffMultiplier = Math . Min ( _consecutiveSlowPolls , MaxBackoffMultiplier ) ;
223+ var backoffDelay = PollingInterval * backoffMultiplier ;
224+
225+ _logger ? . LogWarning ( $ "{ nameof ( ApiPlcProgramSubscriptionFaker ) } : Poll timeout triggered backoff. " +
226+ $ "Consecutive slow polls: { _consecutiveSlowPolls } . " +
227+ $ "Applying exponential backoff (multiplier: { backoffMultiplier } x, delay: { backoffDelay } ms)") ;
228+
229+ _pollingTimer . Change ( backoffDelay , Timeout . Infinite ) ;
230+ }
231+ else if ( pollDuration > PollingInterval )
195232 {
196233 _consecutiveSlowPolls ++ ;
197234 var backoffMultiplier = Math . Min ( _consecutiveSlowPolls , MaxBackoffMultiplier ) ;
0 commit comments