552 lines
16 KiB
MQL5
552 lines
16 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| CUnifiedFileIO.mqh - Thread-Safe File Operations with Retry Logic |
| |||
//| Provides robust file I/O with automatic retry, buffering, and |
| |||
//| atomic write operations |
| |||
//+------------------------------------------------------------------+
| |||
#ifndef CUNIFIEDFILEIO_MQH
| |||
#define CUNIFIEDFILEIO_MQH
| |||
| |||
#include "CErrorRecovery.mqh"
| |||
| |||
// File operation types
| |||
enum ENUM_FILE_OPERATION
| |||
{
| |||
FILE_OP_READ = 0,
| |||
FILE_OP_WRITE = 1,
| |||
FILE_OP_APPEND = 2,
| |||
FILE_OP_DELETE = 3
| |||
};
| |||
| |||
// Buffer strategy
| |||
enum ENUM_BUFFER_STRATEGY
| |||
{
| |||
BUFFER_NONE = 0, // Direct I/O
| |||
BUFFER_MEMORY = 1, // Memory buffer with async flush
| |||
BUFFER_DISK = 2 // Disk buffer for critical data
| |||
};
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| File Buffer Entry |
| |||
//+------------------------------------------------------------------+
| |||
struct SFileBufferEntry
| |||
{
| |||
string data;
| |||
datetime timestamp;
| |||
int priority;
| |||
int retry_count;
| |||
| |||
SFileBufferEntry()
| |||
{
| |||
data = "";
| |||
timestamp = 0;
| |||
priority = 0;
| |||
retry_count = 0;
| |||
}
| |||
};
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Unified File I/O Manager |
| |||
//+------------------------------------------------------------------+
| |||
class CUnifiedFileIO
| |||
{
| |||
private:
| |||
static CUnifiedFileIO* s_instance;
| |||
| |||
// Memory buffer
| |||
SFileBufferEntry m_buffer[];
| |||
int m_buffer_count;
| |||
int m_buffer_max;
| |||
int m_buffer_critical_threshold;
| |||
| |||
// Statistics
| |||
int m_total_operations;
| |||
int m_successful_operations;
| |||
int m_failed_operations;
| |||
int m_buffered_operations;
| |||
| |||
// Configuration
| |||
int m_max_retries;
| |||
int m_retry_delay_ms;
| |||
bool m_use_atomic_writes;
| |||
bool m_verify_writes;
| |||
int m_file_share_flags;
| |||
| |||
// Error recovery
| |||
CErrorRecovery* m_error_recovery;
| |||
| |||
// Folder creation tracking
| |||
string m_created_folders[];
| |||
int m_created_count;
| |||
| |||
public:
| |||
CUnifiedFileIO()
| |||
{
| |||
m_buffer_count = 0;
| |||
m_buffer_max = 1000;
| |||
m_buffer_critical_threshold = 800;
| |||
ArrayResize(m_buffer, m_buffer_max);
| |||
| |||
m_total_operations = 0;
| |||
m_successful_operations = 0;
| |||
m_failed_operations = 0;
| |||
m_buffered_operations = 0;
| |||
| |||
m_max_retries = 3;
| |||
m_retry_delay_ms = 50;
| |||
m_use_atomic_writes = true;
| |||
m_verify_writes = true;
| |||
m_file_share_flags = FILE_SHARE_READ | FILE_SHARE_WRITE;
| |||
| |||
m_error_recovery = CErrorRecovery::Instance();
| |||
| |||
m_created_count = 0;
| |||
ArrayResize(m_created_folders, 100);
| |||
}
| |||
| |||
~CUnifiedFileIO()
| |||
{
| |||
// Flush any remaining buffered data
| |||
FlushBuffer(true); // Force flush
| |||
}
| |||
| |||
static CUnifiedFileIO* Instance()
| |||
{
| |||
if(s_instance == NULL)
| |||
s_instance = new CUnifiedFileIO();
| |||
return s_instance;
| |||
}
| |||
| |||
static void Destroy()
| |||
{
| |||
if(s_instance != NULL)
| |||
{
| |||
delete s_instance;
| |||
s_instance = NULL;
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Configuration |
| |||
//+------------------------------------------------------------------+
| |||
void SetMaxRetries(int max_retries) { m_max_retries = max_retries; }
| |||
void SetRetryDelay(int delay_ms) { m_retry_delay_ms = delay_ms; }
| |||
void SetBufferSize(int size) { m_buffer_max = size; ArrayResize(m_buffer, size); }
| |||
void SetAtomicWrites(bool atomic) { m_use_atomic_writes = atomic; }
| |||
void SetVerifyWrites(bool verify) { m_verify_writes = verify; }
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Safe file read with retry logic |
| |||
//+------------------------------------------------------------------+
| |||
bool ReadFile(const string filename, string &content, bool use_common = true)
| |||
{
| |||
m_total_operations++;
| |||
| |||
// Clear any pending errors
| |||
ClearError();
| |||
| |||
// Ensure folder exists
| |||
EnsureFolderExists(filename, use_common);
| |||
| |||
int flags = FILE_READ | FILE_TXT | FILE_ANSI;
| |||
if(use_common) flags |= FILE_COMMON;
| |||
flags |= m_file_share_flags;
| |||
| |||
int handle = INVALID_HANDLE;
| |||
| |||
// Retry loop
| |||
for(int attempt = 1; attempt <= m_max_retries; attempt++)
| |||
{
| |||
handle = FileOpen(filename, flags);
| |||
| |||
if(handle != INVALID_HANDLE)
| |||
break;
| |||
| |||
int error_code = GetLastError();
| |||
if(error_code != ERR_FILE_NOT_FOUND && attempt < m_max_retries)
| |||
{
| |||
Log("File read attempt " + IntegerToString(attempt) + "/" + IntegerToString(m_max_retries) +
| |||
" failed for " + filename + ": " + IntegerToString(error_code));
| |||
Sleep(m_retry_delay_ms * attempt);
| |||
ClearError();
| |||
}
| |||
else
| |||
{
| |||
break;
| |||
}
| |||
}
| |||
| |||
if(handle == INVALID_HANDLE)
| |||
{
| |||
m_failed_operations++;
| |||
int err = GetLastError();
| |||
if(err != 0)
| |||
m_error_recovery.HandleError(err, "CUnifiedFileIO::ReadFile", filename);
| |||
return false;
| |||
}
| |||
| |||
// Read content
| |||
content = "";
| |||
while(!FileIsEnding(handle))
| |||
{
| |||
content += FileReadString(handle) + "\n";
| |||
}
| |||
| |||
FileClose(handle);
| |||
| |||
m_successful_operations++;
| |||
return true;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Safe file write with atomic operation option |
| |||
//+------------------------------------------------------------------+
| |||
bool WriteFile(const string filename, const string content, bool use_common = true, bool append = false)
| |||
{
| |||
m_total_operations++;
| |||
| |||
// Clear any pending errors
| |||
ClearError();
| |||
| |||
// Ensure folder exists
| |||
EnsureFolderExists(filename, use_common);
| |||
| |||
if(m_use_atomic_writes && !append)
| |||
{
| |||
return WriteFileAtomic(filename, content, use_common);
| |||
}
| |||
| |||
int flags = FILE_WRITE | FILE_TXT | FILE_ANSI;
| |||
if(use_common) flags |= FILE_COMMON;
| |||
if(append) flags |= FILE_READ; // Required for seek
| |||
flags |= m_file_share_flags;
| |||
| |||
int handle = INVALID_HANDLE;
| |||
| |||
// Retry loop
| |||
for(int attempt = 1; attempt <= m_max_retries; attempt++)
| |||
{
| |||
if(append)
| |||
{
| |||
handle = FileOpen(filename, flags, ',');
| |||
if(handle != INVALID_HANDLE)
| |||
FileSeek(handle, 0, SEEK_END);
| |||
}
| |||
else
| |||
{
| |||
handle = FileOpen(filename, flags);
| |||
}
| |||
| |||
if(handle != INVALID_HANDLE)
| |||
break;
| |||
| |||
int error_code = GetLastError();
| |||
if(attempt < m_max_retries)
| |||
{
| |||
Log("File write attempt " + IntegerToString(attempt) + "/" + IntegerToString(m_max_retries) +
| |||
" failed for " + filename + ": " + IntegerToString(error_code));
| |||
Sleep(m_retry_delay_ms * attempt);
| |||
ClearError();
| |||
}
| |||
else
| |||
{
| |||
break;
| |||
}
| |||
}
| |||
| |||
if(handle == INVALID_HANDLE)
| |||
{
| |||
m_failed_operations++;
| |||
int err = GetLastError();
| |||
if(err != 0)
| |||
m_error_recovery.HandleError(err, "CUnifiedFileIO::WriteFile", filename);
| |||
| |||
// Buffer to memory as fallback
| |||
BufferWrite(filename, content);
| |||
return false;
| |||
}
| |||
| |||
// Write content
| |||
bool write_ok = (FileWriteString(handle, content) > 0);
| |||
FileFlush(handle);
| |||
| |||
// Verify write if requested
| |||
if(m_verify_writes && write_ok && !append)
| |||
{
| |||
long size = FileSize(handle);
| |||
if(size <= 0)
| |||
{
| |||
Log("Write verification failed for " + filename);
| |||
write_ok = false;
| |||
}
| |||
}
| |||
| |||
FileClose(handle);
| |||
| |||
if(write_ok)
| |||
{
| |||
m_successful_operations++;
| |||
return true;
| |||
}
| |||
else
| |||
{
| |||
m_failed_operations++;
| |||
BufferWrite(filename, content);
| |||
return false;
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Atomic file write (write to temp, then rename) |
| |||
//+------------------------------------------------------------------+
| |||
bool WriteFileAtomic(const string filename, const string content, bool use_common = true)
| |||
{
| |||
string temp_filename = filename + ".tmp";
| |||
| |||
// Write to temp file
| |||
int flags = FILE_WRITE | FILE_TXT | FILE_ANSI;
| |||
if(use_common) flags |= FILE_COMMON;
| |||
flags |= m_file_share_flags;
| |||
| |||
int handle = FileOpen(temp_filename, flags);
| |||
if(handle == INVALID_HANDLE)
| |||
{
| |||
// Fallback to non-atomic write
| |||
return WriteFile(filename, content, use_common, false);
| |||
}
| |||
| |||
bool write_ok = (FileWriteString(handle, content) > 0);
| |||
FileFlush(handle);
| |||
long temp_size = FileSize(handle);
| |||
FileClose(handle);
| |||
| |||
if(!write_ok || temp_size <= 0)
| |||
{
| |||
// Clean up temp file
| |||
FileDelete(temp_filename, use_common ? FILE_COMMON : 0);
| |||
return false;
| |||
}
| |||
| |||
// Delete old file if exists
| |||
if(FileIsExist(filename, use_common ? FILE_COMMON : 0))
| |||
{
| |||
FileDelete(filename, use_common ? FILE_COMMON : 0);
| |||
}
| |||
| |||
// Rename temp to final
| |||
// Note: MQL5 doesn't have direct FileRename, so we copy and delete
| |||
bool renamed = FileCopy(temp_filename, use_common ? FILE_COMMON : 0,
| |||
filename, use_common ? FILE_COMMON : 0,
| |||
FILE_REWRITE);
| |||
| |||
if(renamed)
| |||
{
| |||
FileDelete(temp_filename, use_common ? FILE_COMMON : 0);
| |||
m_successful_operations++;
| |||
return true;
| |||
}
| |||
else
| |||
{
| |||
// Cleanup
| |||
FileDelete(temp_filename, use_common ? FILE_COMMON : 0);
| |||
m_failed_operations++;
| |||
BufferWrite(filename, content);
| |||
return false;
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Buffer write for failed operations |
| |||
//+------------------------------------------------------------------+
| |||
void BufferWrite(const string filename, const string content)
| |||
{
| |||
if(m_buffer_count >= m_buffer_max)
| |||
{
| |||
// Remove oldest entry
| |||
for(int i = 0; i < m_buffer_count - 1; i++)
| |||
{
| |||
m_buffer[i] = m_buffer[i + 1];
| |||
}
| |||
m_buffer_count--;
| |||
}
| |||
| |||
SFileBufferEntry entry;
| |||
entry.data = filename + "|" + content;
| |||
entry.timestamp = TimeCurrent();
| |||
entry.priority = 1;
| |||
entry.retry_count = 0;
| |||
| |||
m_buffer[m_buffer_count] = entry;
| |||
m_buffer_count++;
| |||
m_buffered_operations++;
| |||
| |||
if(m_buffer_count >= m_buffer_critical_threshold)
| |||
{
| |||
Log("WARNING: File buffer at " + IntegerToString(m_buffer_count) + "/" + IntegerToString(m_buffer_max));
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Flush buffer to disk |
| |||
//+------------------------------------------------------------------+
| |||
int FlushBuffer(bool force = false)
| |||
{
| |||
if(m_buffer_count == 0) return 0;
| |||
| |||
int flushed = 0;
| |||
int max_flush = force ? m_buffer_count : MathMin(100, m_buffer_count);
| |||
| |||
for(int i = 0; i < max_flush; i++)
| |||
{
| |||
SFileBufferEntry &entry = m_buffer[i];
| |||
| |||
// Parse filename and content
| |||
int sep_pos = StringFind(entry.data, "|");
| |||
if(sep_pos < 0) continue;
| |||
| |||
string filename = StringSubstr(entry.data, 0, sep_pos);
| |||
string content = StringSubstr(entry.data, sep_pos + 1);
| |||
| |||
if(WriteFile(filename, content, true, true)) // Append mode
| |||
{
| |||
flushed++;
| |||
}
| |||
else
| |||
{
| |||
entry.retry_count++;
| |||
if(entry.retry_count >= m_max_retries)
| |||
{
| |||
Log("Failed to flush buffer entry after " + IntegerToString(m_max_retries) + " retries");
| |||
// Keep in buffer for next attempt
| |||
}
| |||
}
| |||
}
| |||
| |||
// Remove flushed entries
| |||
int remaining = 0;
| |||
for(int i = 0; i < m_buffer_count; i++)
| |||
{
| |||
if(i < flushed)
| |||
continue; // Skip flushed entries
| |||
| |||
m_buffer[remaining] = m_buffer[i];
| |||
remaining++;
| |||
}
| |||
| |||
m_buffer_count = remaining;
| |||
ArrayResize(m_buffer, m_buffer_max); // Ensure capacity
| |||
| |||
if(flushed > 0)
| |||
{
| |||
Log("Flushed " + IntegerToString(flushed) + " buffer entries, " + IntegerToString(m_buffer_count) + " remaining");
| |||
}
| |||
| |||
return flushed;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Ensure folder exists |
| |||
//+------------------------------------------------------------------+
| |||
void EnsureFolderExists(const string path, bool use_common)
| |||
{
| |||
// Extract folder path
| |||
int last_sep = -1;
| |||
for(int i = StringLen(path) - 1; i >= 0; i--)
| |||
{
| |||
string c = StringSubstr(path, i, 1);
| |||
if(c == "\\" || c == "/")
| |||
{
| |||
last_sep = i;
| |||
break;
| |||
}
| |||
}
| |||
| |||
if(last_sep < 0) return; // No folder component
| |||
| |||
string folder = StringSubstr(path, 0, last_sep);
| |||
if(folder == "") return;
| |||
| |||
// Check if already created
| |||
for(int i = 0; i < m_created_count; i++)
| |||
{
| |||
if(m_created_folders[i] == folder)
| |||
return;
| |||
}
| |||
| |||
// Create folder
| |||
int flags = use_common ? FILE_COMMON : 0;
| |||
bool created = FolderCreate(folder, flags);
| |||
| |||
if(created || GetLastError() == 0)
| |||
{
| |||
// Track created folder
| |||
if(m_created_count < ArraySize(m_created_folders))
| |||
{
| |||
m_created_folders[m_created_count] = folder;
| |||
m_created_count++;
| |||
}
| |||
}
| |||
| |||
ClearError();
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Statistics and reporting |
| |||
//+------------------------------------------------------------------+
| |||
string GetStatistics()
| |||
{
| |||
string stats = StringFormat(
| |||
"=== Unified File I/O Statistics ===\n" +
| |||
"Total Operations: %d\n" +
| |||
"Successful: %d (%.1f%%)\n" +
| |||
"Failed: %d (%.1f%%)\n" +
| |||
"Buffered: %d\n" +
| |||
"Buffer: %d/%d entries",
| |||
m_total_operations,
| |||
m_successful_operations,
| |||
(m_total_operations > 0 ? 100.0 * m_successful_operations / m_total_operations : 0),
| |||
m_failed_operations,
| |||
(m_total_operations > 0 ? 100.0 * m_failed_operations / m_total_operations : 0),
| |||
m_buffered_operations,
| |||
m_buffer_count,
| |||
m_buffer_max
| |||
);
| |||
| |||
return stats;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Utility functions |
| |||
//+------------------------------------------------------------------+
| |||
void Log(const string message)
| |||
{
| |||
Print("[UnifiedFileIO] " + message);
| |||
}
| |||
| |||
void ClearError()
| |||
{
| |||
int _ = GetLastError();
| |||
}
| |||
| |||
// Getters
| |||
int GetBufferedCount() const { return m_buffer_count; }
| |||
bool IsBufferCritical() const { return m_buffer_count >= m_buffer_critical_threshold; }
| |||
int GetBufferUtilizationPercent() const { return (m_buffer_count * 100) / m_buffer_max; }
| |||
};
| |||
| |||
// Static instance initialization
| |||
CUnifiedFileIO* CUnifiedFileIO::s_instance = NULL;
| |||
| |||
// Convenience macros
| |||
#define SAFE_FILE_READ(filename, content, use_common) \
| |||
CUnifiedFileIO::Instance().ReadFile(filename, content, use_common)
| |||
| |||
#define SAFE_FILE_WRITE(filename, content, use_common) \
| |||
CUnifiedFileIO::Instance().WriteFile(filename, content, use_common, false)
| |||
| |||
#define SAFE_FILE_APPEND(filename, content, use_common) \
| |||
CUnifiedFileIO::Instance().WriteFile(filename, content, use_common, true)
| |||
| |||
#define FLUSH_FILE_BUFFER(force) \
| |||
CUnifiedFileIO::Instance().FlushBuffer(force)
| |||
| |||
#endif // CUNIFIEDFILEIO_MQH
|