Warrior_EA/System/SharedFileCopy.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

120 lines
5.8 KiB
MQL5

//+------------------------------------------------------------------+
//| Warrior_EA |
//| AnimateDread |
//| |
//| Share-aware file copy, retried against a transient lock. Pure |
//| functions - no state, no class needed (same doctrine as |
//| System\TradeChecks.mqh's TC* free functions). |
//+------------------------------------------------------------------+
#ifndef WARRIOR_SHARED_FILE_COPY_MQH
#define WARRIOR_SHARED_FILE_COPY_MQH
#include "RetryWithBackoff.mqh"
//+------------------------------------------------------------------+
//| Share-aware streamed file copy FROM the shared (FILE_COMMON) |
//| folder INTO this program's own sandbox (the tester agent's local |
//| MQL5\Files when running under the Strategy Tester). |
//+------------------------------------------------------------------+
bool CopySharedFile(string srcFileName, string dstFileName, bool quiet)
{
ResetLastError();
int src = FileOpen(srcFileName, FILE_COMMON | FILE_BIN | FILE_READ | FILE_SHARE_READ | FILE_SHARE_WRITE);
if(src == INVALID_HANDLE)
{
//--- Distinguish the two failure stages explicitly - a source failure here (with the share flags
//--- already set) would mean the shared folder itself is unreachable from this sandbox, which is a
//--- completely different problem from a destination/sandbox write failure below.
if(!quiet)
Print(__FUNCTION__ + ": cannot open SOURCE " + srcFileName + " in the shared folder, error " +
IntegerToString(GetLastError()) + " (share flags were set, so this is not a lock).");
return false;
}
ulong size = FileSize(src);
uchar buf[];
//--- 0-byte source would produce a 0-byte model file that CNet::Load rejects later as a stub - refuse
//--- it here instead, so the caller falls back to a fresh topology with an accurate reason logged.
if(size == 0 || ArrayResize(buf, (int)size) != (int)size)
{
FileClose(src);
if(!quiet)
Print(__FUNCTION__ + ": refusing to copy " + srcFileName + " - source is " + IntegerToString((int)size) +
" bytes (empty, or too large to buffer).");
return false;
}
uint read = FileReadArray(src, buf, 0, (int)size);
FileClose(src);
if(read != (uint)size)
{
if(!quiet)
Print(__FUNCTION__ + ": short read on " + srcFileName + " (" + IntegerToString((int)read) + " of " +
IntegerToString((int)size) + " bytes) - not copying a partial model.");
return false;
}
//--- destination is this program's OWN sandbox (no FILE_COMMON) - never the shared production folder.
string tmpName = dstFileName + ".copytmp";
ResetLastError();
int dst = FileOpen(tmpName, FILE_BIN | FILE_WRITE | FILE_SHARE_READ | FILE_SHARE_WRITE);
if(dst == INVALID_HANDLE)
{
if(!quiet)
Print(__FUNCTION__ + ": cannot open DESTINATION " + tmpName + " in this agent's sandbox, error " +
IntegerToString(GetLastError()) + ".");
return false;
}
uint written = FileWriteArray(dst, buf, 0, ArraySize(buf));
FileFlush(dst);
FileClose(dst);
if(written != (uint)size)
{
FileDelete(tmpName);
if(!quiet)
Print(__FUNCTION__ + ": short write to " + tmpName + " (" + IntegerToString((int)written) + " of " +
IntegerToString((int)size) + " bytes) - discarded the partial copy.");
return false;
}
//--- atomic swap into place, so a reader never sees a half-written file
if(!FileMove(tmpName, 0, dstFileName, FILE_REWRITE))
{
if(!quiet)
Print(__FUNCTION__ + ": atomic rename " + tmpName + " -> " + dstFileName + " failed, error " +
IntegerToString(GetLastError()) + ".");
ResetLastError();
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| CopyFileWithRetry's operand - see RetryWithBackoff.mqh's |
//| declaration comment for the shared exponential-backoff shape |
//| this plugs into. quiet is forwarded straight to CopySharedFile, |
//| which is what actually stays silent until the last attempt. |
//+------------------------------------------------------------------+
class CCopySharedFileOp : public IRetryableOp
{
private:
string m_src;
string m_dst;
public:
CCopySharedFileOp(string src, string dst) : m_src(src), m_dst(dst) { }
virtual bool TryOnce(bool quiet) override { return CopySharedFile(m_src, m_dst, quiet); }
};
//+------------------------------------------------------------------+
//| Retries a FileCopy FROM FILE_COMMON that raced a concurrent |
//| writer (typically a live chart's own atomic Save(), mid |
//| write-then-rename on the SAME source file). |
//+------------------------------------------------------------------+
bool CopyFileWithRetry(string srcFileName, string dstFileName)
{
//--- Retries are only a backstop for a genuinely transient hiccup.
const int RETRY_ATTEMPTS = 5;
const int RETRY_DELAY_CAP_MS = 1000;
CCopySharedFileOp op(srcFileName, dstFileName);
bool ok = RetryWithBackoff(GetPointer(op), RETRY_ATTEMPTS, 150, RETRY_DELAY_CAP_MS);
if(!ok)
Print(__FUNCTION__ + ": WARNING - failed to copy " + srcFileName + " (shared folder) -> " + dstFileName +
" after " + IntegerToString(RETRY_ATTEMPTS) + " attempts (see the per-stage reason above)." +
" This run will train from scratch instead of reusing the deployed model.");
return ok;
}
#endif // WARRIOR_SHARED_FILE_COPY_MQH
//+------------------------------------------------------------------+