Warrior_EA/System/RetryWithBackoff.mqh
AnimateDread 1b077eeee4 refactor(persistence): dedupe the exponential-backoff retry loop into RetryWithBackoff
CopyFileWithRetry (System/SharedFileCopy.mqh) and CModelPersistence::
LoadNetWithRetry independently implemented the identical 5-attempt
Sleep-doubled-and-capped retry shape around a different single
operation, with a comment on the latter pointing at the former as the
"same reasoning" instead of sharing code. Added System/RetryWithBackoff.mqh:
an IRetryableOp interface (one bool TryOnce(bool quiet) method, MQL5 has
no closures/function pointers that bind per-call-site arguments) plus the
RetryWithBackoff(op, attempts, initialDelayMs, delayCapMs) loop. Each call
site now defines a tiny local operand class (CCopySharedFileOp,
CLoadNetOnceOp) and keeps its own tuning constants (150ms/1000ms cap vs
200ms/2000ms cap) unchanged - pure mechanical relocation, no behavior
change. CModelPersistence stays stateless (grep-verified in the prior
Persistence extraction): CLoadNetOnceOp is a separate local class, not a
new member on CModelPersistence itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 02:55:04 -04:00

49 lines
2.4 KiB
MQL5

//+------------------------------------------------------------------+
//| Warrior_EA |
//| AnimateDread |
//| |
//| The exponential-backoff retry SHAPE, shared by every call site |
//| that retries one transient-lock-prone operation a fixed number |
//| of times with an increasing, capped sleep between attempts. Each |
//| operation's own arguments differ (a file copy vs a model load), |
//| so the shape is factored out behind a tiny callback interface |
//| instead of a raw function pointer - MQL5 has no closures, and |
//| a function pointer cannot bind the per-call-site arguments. |
//+------------------------------------------------------------------+
#ifndef WARRIOR_RETRY_WITH_BACKOFF_MQH
#define WARRIOR_RETRY_WITH_BACKOFF_MQH
//+------------------------------------------------------------------+
//| One retryable attempt. quiet is true on every attempt but the |
//| last, so an implementation that logs its own per-attempt reason |
//| can stay silent until the failure actually matters. |
//+------------------------------------------------------------------+
class IRetryableOp
{
public:
virtual bool TryOnce(bool quiet) = 0;
};
//+------------------------------------------------------------------+
//| Runs op.TryOnce() up to retryAttempts times, sleeping delayMs |
//| (doubled each attempt, capped at delayCapMs) between attempts. |
//| Returns the first successful attempt's result, or false if every |
//| attempt failed. |
//+------------------------------------------------------------------+
bool RetryWithBackoff(IRetryableOp *op, const int retryAttempts, const int initialDelayMs, const int delayCapMs)
{
if(CheckPointer(op) == POINTER_INVALID)
return false;
int delayMs = initialDelayMs;
bool ok = false;
for(int attempt = 0; attempt < retryAttempts && !ok; attempt++)
{
if(attempt > 0)
{
Sleep(delayMs);
delayMs = (int)MathMin(delayMs * 2, delayCapMs);
}
ok = op.TryOnce(attempt < retryAttempts - 1);
}
return ok;
}
#endif // WARRIOR_RETRY_WITH_BACKOFF_MQH
//+------------------------------------------------------------------+