better maintanance funcion with catch-up

This commit is contained in:
Oliver Kieselbach
2026-02-08 13:58:47 +01:00
parent e1e747d65b
commit 289e5ccda8
4 changed files with 146 additions and 14 deletions
@@ -63,7 +63,7 @@ namespace AutopilotMonitor.Functions.Services
try
{
await MarkStalledSessionsAsTimedOutAsync();
await AggregateYesterdayMetricsAsync();
await AggregateMetricsWithCatchUpAsync();
await CleanupOldDataAsync();
await RecomputePlatformStatsAsync();
@@ -146,10 +146,7 @@ namespace AutopilotMonitor.Functions.Services
var timeoutHours = config?.SessionTimeoutHours ?? 5;
var cutoffTime = DateTime.UtcNow.AddHours(-timeoutHours);
var sessions = await _storageService.GetSessionsAsync(tenantId, maxResults: 1000000);
var stalledSessions = sessions
.Where(s => s.Status == SessionStatus.InProgress && s.StartedAt < cutoffTime)
.ToList();
var stalledSessions = await _storageService.GetStalledSessionsAsync(tenantId, cutoffTime);
if (stalledSessions.Count == 0)
{
@@ -203,12 +200,39 @@ namespace AutopilotMonitor.Functions.Services
}
/// <summary>
/// Aggregates yesterday's metrics and saves them as historical snapshots
/// Aggregates metrics for any missed days in the last 7 days, plus yesterday.
/// Checks the UsageMetrics table for existing snapshots to avoid re-aggregation.
/// </summary>
private async Task AggregateYesterdayMetricsAsync()
private async Task AggregateMetricsWithCatchUpAsync()
{
var yesterday = DateTime.UtcNow.AddDays(-1).Date;
await AggregateMetricsForDateAsync(yesterday);
const int maxCatchUpDays = 7;
var today = DateTime.UtcNow.Date;
var aggregatedCount = 0;
for (int daysBack = maxCatchUpDays; daysBack >= 1; daysBack--)
{
var date = today.AddDays(-daysBack);
var dateStr = date.ToString("yyyy-MM-dd");
try
{
if (await _storageService.HasUsageMetricsSnapshotAsync(dateStr))
continue;
_logger.LogInformation($"Catch-up: Aggregating metrics for missed date {dateStr}");
await AggregateMetricsForDateAsync(date);
aggregatedCount++;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Failed to aggregate metrics for {dateStr} during catch-up");
}
}
if (aggregatedCount > 0)
_logger.LogInformation($"Catch-up completed: aggregated {aggregatedCount} missed day(s)");
else
_logger.LogInformation("No missed days to catch up on");
}
/// <summary>
@@ -223,8 +247,7 @@ namespace AutopilotMonitor.Functions.Services
{
var targetDateStr = targetDate.ToString("yyyy-MM-dd");
var allSessions = await _storageService.GetAllSessionsAsync(maxResults: 1000000);
var targetDateSessions = allSessions.Where(s => s.StartedAt.Date == targetDate).ToList();
var targetDateSessions = await _storageService.GetSessionsByDateRangeAsync(targetDate, targetDate.AddDays(1));
if (targetDateSessions.Count == 0)
{
@@ -798,6 +798,29 @@ namespace AutopilotMonitor.Functions.Services
}
}
/// <summary>
/// Checks if a global usage metrics snapshot exists for a given date.
/// Used by maintenance catch-up to determine which dates need aggregation.
/// </summary>
public async Task<bool> HasUsageMetricsSnapshotAsync(string date)
{
try
{
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.UsageMetrics);
await tableClient.GetEntityAsync<TableEntity>(date, "global");
return true;
}
catch (RequestFailedException ex) when (ex.Status == 404)
{
return false;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Failed to check usage metrics snapshot for {date}");
return false;
}
}
// ===== DATA RETENTION METHODS =====
/// <summary>
@@ -831,6 +854,72 @@ namespace AutopilotMonitor.Functions.Services
}
}
/// <summary>
/// Gets all sessions within a date range, optionally filtered by tenant.
/// Uses server-side filtering to avoid loading all sessions into memory.
/// </summary>
public async Task<List<SessionSummary>> GetSessionsByDateRangeAsync(DateTime startDate, DateTime endDate, string? tenantId = null)
{
if (!string.IsNullOrEmpty(tenantId))
SecurityValidator.EnsureValidGuid(tenantId, nameof(tenantId));
try
{
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.Sessions);
var filter = !string.IsNullOrEmpty(tenantId)
? $"PartitionKey eq '{tenantId}' and StartedAt ge datetime'{startDate:yyyy-MM-ddTHH:mm:ss}Z' and StartedAt lt datetime'{endDate:yyyy-MM-ddTHH:mm:ss}Z'"
: $"StartedAt ge datetime'{startDate:yyyy-MM-ddTHH:mm:ss}Z' and StartedAt lt datetime'{endDate:yyyy-MM-ddTHH:mm:ss}Z'";
var query = tableClient.QueryAsync<TableEntity>(filter: filter);
var sessions = new List<SessionSummary>();
await foreach (var entity in query)
{
sessions.Add(MapToSessionSummary(entity));
}
_logger.LogInformation($"Found {sessions.Count} sessions between {startDate:yyyy-MM-dd} and {endDate:yyyy-MM-dd}" +
(tenantId != null ? $" for tenant {tenantId}" : ""));
return sessions;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Failed to get sessions by date range");
return new List<SessionSummary>();
}
}
/// <summary>
/// Gets stalled sessions (InProgress status, started before cutoff time) for a tenant.
/// Uses server-side filtering to avoid loading all sessions into memory.
/// </summary>
public async Task<List<SessionSummary>> GetStalledSessionsAsync(string tenantId, DateTime cutoffTime)
{
SecurityValidator.EnsureValidGuid(tenantId, nameof(tenantId));
try
{
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.Sessions);
var filter = $"PartitionKey eq '{tenantId}' and Status eq 'InProgress' and StartedAt lt datetime'{cutoffTime:yyyy-MM-ddTHH:mm:ss}Z'";
var query = tableClient.QueryAsync<TableEntity>(filter: filter);
var sessions = new List<SessionSummary>();
await foreach (var entity in query)
{
sessions.Add(MapToSessionSummary(entity));
}
return sessions;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Failed to get stalled sessions for tenant {tenantId}");
return new List<SessionSummary>();
}
}
/// <summary>
/// Gets all unique tenant IDs from sessions table
/// </summary>
@@ -45,6 +45,7 @@ export default function AdminConfigurationPage() {
const { getAccessToken } = useAuth();
const [galacticAdminMode, setGalacticAdminMode] = useState(false);
const [triggeringMaintenance, setTriggeringMaintenance] = useState(false);
const [maintenanceDate, setMaintenanceDate] = useState<string>("");
const [error, setError] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
@@ -172,7 +173,8 @@ export default function AdminConfigurationPage() {
throw new Error('Failed to get access token');
}
const response = await fetch(`${API_BASE_URL}/api/maintenance/trigger`, {
const queryParams = maintenanceDate ? `?date=${maintenanceDate}` : '';
const response = await fetch(`${API_BASE_URL}/api/maintenance/trigger${queryParams}`, {
method: "POST",
headers: {
'Authorization': `Bearer ${token}`
@@ -184,7 +186,8 @@ export default function AdminConfigurationPage() {
}
const result = await response.json();
setSuccessMessage("Daily maintenance job completed successfully!");
const dateInfo = maintenanceDate ? ` for ${maintenanceDate}` : '';
setSuccessMessage(`Maintenance job completed successfully${dateInfo}!`);
// Auto-hide success message after 5 seconds
setTimeout(() => setSuccessMessage(null), 5000);
@@ -665,13 +668,29 @@ export default function AdminConfigurationPage() {
</li>
<li className="flex items-start">
<span className="text-purple-500 mr-2">•</span>
<span>Aggregate yesterday's metrics into historical snapshots</span>
<span>Aggregate metrics into historical snapshots (with automatic catch-up for missed days)</span>
</li>
<li className="flex items-start">
<span className="text-purple-500 mr-2">•</span>
<span>Clean up old data based on retention policies</span>
</li>
</ul>
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4 mb-4">
<label className="block text-sm font-medium text-purple-800 mb-1">
Target Date (optional)
</label>
<input
type="date"
value={maintenanceDate}
onChange={(e) => setMaintenanceDate(e.target.value)}
max={new Date(Date.now() - 86400000).toISOString().split('T')[0]}
className="w-full max-w-xs px-3 py-2 border border-purple-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
/>
<p className="text-xs text-gray-500 mt-2">
Leave empty to run the standard maintenance with automatic catch-up (aggregates any missed days within the last 7 days).
Select a specific date to manually aggregate metrics for that day, e.g. to backfill data older than 7 days.
</p>
</div>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 mb-4">
<div className="flex items-start space-x-2">
<svg className="w-5 h-5 text-yellow-600 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
File diff suppressed because one or more lines are too long