EPANET Console Core Monitoring Design Discussion
Current Design Analysis
The current system consists of two main components:
- EpanetConsoleCore - A .NET console application that runs EPANET simulations and stores results in a database
- dwd-api-aspnet - An ASP.NET web API application that interacts with the database and triggers EPANET simulations
The main limitation of the current design is that once a simulation is started, there's no way to monitor its progress or status in real-time. This is particularly problematic for long-running simulations that can take tens of minutes to complete.
Important Constraints:
- The system is already under significant database load from bulk inserts of simulation results (2-3 minutes per bulk insert)
- The frontend will only consume progress data via API GET endpoints
- Direct database inserts from EpanetConsoleCore are preferred over API POSTs for performance
Revised Design Options
Option 1: Lightweight Database Monitoring (Recommended)
This approach uses minimal database operations to track progress while avoiding additional load.
Implementation Details:
- Enhance the existing
Scenariotable with progress fields:
public class Scenario
{
// ... existing fields ...
// Progress tracking
public string CurrentOperation { get; set; }
public double ProgressPercentage { get; set; }
public long LastProgressUpdate { get; set; }
public string CurrentMetrics { get; set; } // JSON string for flexible metrics
}
- Modify the console application to update progress:
// In EpanetConsoleCore
private async Task UpdateProgress(string operation, double progress, Dictionary<string, object> metrics)
{
// Update the scenario record directly
var scenario = await enContext.Scenarios.FindAsync(scenarioId);
scenario.CurrentOperation = operation;
scenario.ProgressPercentage = progress;
scenario.LastProgressUpdate = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
scenario.CurrentMetrics = JsonSerializer.Serialize(metrics);
// Use a lightweight update that doesn't trigger full entity tracking
await enContext.Database.ExecuteSqlRawAsync(
@"UPDATE ""Scenarios""
SET ""CurrentOperation"" = {0},
""ProgressPercentage"" = {1},
""LastProgressUpdate"" = {2},
""CurrentMetrics"" = {3}
WHERE ""Id"" = {4}",
operation, progress, scenario.LastProgressUpdate, scenario.CurrentMetrics, scenarioId);
}
- Add API endpoint for monitoring:
[HttpGet("progress/{id}")]
public async Task<ActionResult<ScenarioProgress>> GetProgress(int id)
{
var scenario = await _context.Scenarios
.Where(s => s.Id == id)
.Select(s => new ScenarioProgress
{
CurrentOperation = s.CurrentOperation,
ProgressPercentage = s.ProgressPercentage,
LastUpdate = s.LastProgressUpdate,
Metrics = s.CurrentMetrics
})
.FirstOrDefaultAsync();
if (scenario == null)
return NotFound();
return Ok(scenario);
}
Pros:
- Minimal database impact - single table updates
- No additional tables or complex queries
- Works with existing database transactions
- Simple to implement and maintain
- No additional infrastructure needed
Cons:
- Less detailed historical tracking
- Progress updates might be slightly delayed
- Limited metrics storage
Option 2: Memory-Mapped Progress File
This approach uses a memory-mapped file for progress updates, with the API reading from it.
Implementation Details:
- Create a progress file structure:
public struct SimulationProgress
{
public int ScenarioId;
public long Timestamp;
public double ProgressPercentage;
public fixed char CurrentOperation[100];
public fixed char Metrics[1000];
}
- Console application updates:
// In EpanetConsoleCore
private void UpdateProgress(string operation, double progress, Dictionary<string, object> metrics)
{
using var mmf = MemoryMappedFile.CreateFromFile(
$"progress_{scenarioId}.dat",
FileMode.Create,
null,
Marshal.SizeOf<SimulationProgress>());
using var view = mmf.CreateViewAccessor();
var progress = new SimulationProgress
{
ScenarioId = scenarioId,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
ProgressPercentage = progress,
CurrentOperation = operation,
Metrics = JsonSerializer.Serialize(metrics)
};
view.Write(0, ref progress);
}
- API endpoint:
[HttpGet("progress/{id}")]
public ActionResult<ScenarioProgress> GetProgress(int id)
{
var filePath = $"progress_{id}.dat";
if (!System.IO.File.Exists(filePath))
return NotFound();
using var mmf = MemoryMappedFile.CreateFromFile(filePath, FileMode.Open);
using var view = mmf.CreateViewAccessor();
var progress = new SimulationProgress();
view.Read(0, out progress);
return Ok(new ScenarioProgress
{
CurrentOperation = new string(progress.CurrentOperation).TrimEnd('\0'),
ProgressPercentage = progress.ProgressPercentage,
LastUpdate = progress.Timestamp,
Metrics = new string(progress.Metrics).TrimEnd('\0')
});
}
Pros:
- Zero database impact
- Very fast updates
- Real-time progress tracking
- No additional infrastructure
Cons:
- More complex implementation
- Need to handle file cleanup
- No persistence between runs
- Platform-specific considerations
Recommendation
Given the performance constraints and requirements, I recommend implementing Option 1: Lightweight Database Monitoring with the following considerations:
- Use direct database updates instead of API POSTs
- Keep progress updates minimal and infrequent (e.g., every 5-10 seconds)
- Use lightweight SQL updates that don't trigger full entity tracking
- Store metrics as JSON strings to maintain flexibility
- Add appropriate indexes to the Scenario table for progress queries
This approach provides a good balance between:
- Minimal database impact
- Simple implementation
- Adequate progress tracking
- Easy frontend integration
- No additional infrastructure
Next Steps
- Add progress fields to the Scenario table
- Implement lightweight progress updates in EpanetConsoleCore
- Add progress monitoring endpoint to the API
- Add appropriate indexes to the Scenario table
- Implement progress cleanup on simulation completion
- Add documentation and testing