mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 20:53:06 +00:00
e164eac1eb
The trigger ran the whole maintenance inside the HTTP request (about three minutes against a 230 s gateway limit): the portal reported a timeout while the run went on, a second click started a second run, and nothing serialized those runs against each other or against the 2h timer. Backend - MaintenanceRunGate: the timer and the manual run enter through one blob lease. A held lease writes MaintenanceSkippedLocked and runs nothing; MaintenanceStarted is written after the lease is held, the lease is released last. - POST maintenance/trigger validates the date, probes the lease (409 while a run is active), enqueues on maintenance-trigger (fail-hard) and answers 202 with the typed TriggerMaintenanceResponse. MaintenanceTriggerQueueWorker (self-polling) runs it; a failing or skipped run is not retried. - The run report travels in the details of MaintenanceCompleted. - BlobLeaseLockStore is the shared base of the lease lock stores; SessionDeletionMaintenanceLockStore derives from it, behaviour unchanged. - The probe lets a failed lease release surface as 5xx: a probe lease still held at worker pickup would turn the queued run into a silent skip. Web - useOpsRunStatus: run status from ops events (latest Started newer than the latest Completed/Failed = active), used by the session-cleanup banner (rendering unchanged) and the maintenance trigger section. - Trigger section: queued / already-active messages, status line, polls every 15 s while a run is active, button disabled meanwhile. The 230 s fetch timeout is gone. Shared manifest, generated wire types and the MCP ops-event vocabulary regenerated (two new ops event types, reshaped response). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
90 lines
3.7 KiB
C#
90 lines
3.7 KiB
C#
using System;
|
|
using System.Net;
|
|
using System.Threading.Tasks;
|
|
using AutopilotMonitor.Functions.Helpers;
|
|
using AutopilotMonitor.Functions.Services.Maintenance;
|
|
using AutopilotMonitor.Shared.Models;
|
|
using Microsoft.Azure.Functions.Worker;
|
|
using Microsoft.Azure.Functions.Worker.Http;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace AutopilotMonitor.Functions.Functions.Admin
|
|
{
|
|
/// <summary>
|
|
/// <c>POST /api/maintenance/trigger</c> — manual Global Admin trigger of the platform
|
|
/// maintenance run. The run takes minutes, so the request only queues it:
|
|
/// <c>202 Accepted</c> when queued, <c>409</c> while a run (timer or manual) is active,
|
|
/// <c>500</c> when the enqueue failed — never a hollow success. Progress and the run report
|
|
/// surface as <c>Maintenance*</c> ops events.
|
|
/// </summary>
|
|
public class TriggerMaintenanceFunction
|
|
{
|
|
private readonly ILogger<TriggerMaintenanceFunction> _logger;
|
|
private readonly IMaintenanceTriggerProducer _producer;
|
|
private readonly MaintenanceRunGate _runGate;
|
|
|
|
public TriggerMaintenanceFunction(
|
|
ILogger<TriggerMaintenanceFunction> logger,
|
|
IMaintenanceTriggerProducer producer,
|
|
MaintenanceRunGate runGate)
|
|
{
|
|
_logger = logger;
|
|
_producer = producer;
|
|
_runGate = runGate;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Query parameters:
|
|
/// - date: Optional date to aggregate (yyyy-MM-dd). If not provided, uses yesterday.
|
|
/// - aggregateOnly: If true, only runs aggregation (skips timeout and cleanup)
|
|
/// </summary>
|
|
[Function("TriggerMaintenance")]
|
|
public async Task<HttpResponseData> Run(
|
|
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "maintenance/trigger")] HttpRequestData req,
|
|
FunctionContext context)
|
|
{
|
|
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
|
|
var userEmail = TenantHelper.GetUserIdentifier(req) ?? "GlobalAdmin";
|
|
var ct = context.CancellationToken;
|
|
|
|
try
|
|
{
|
|
if (!QueryParams.TryUtcInstant(req.Query["date"], "date", out var dateInstant, out _))
|
|
{
|
|
return await req.BadRequestAsync("Invalid date format. Use yyyy-MM-dd");
|
|
}
|
|
DateTime? targetDate = dateInstant?.Date;
|
|
bool aggregateOnly = req.Query["aggregateOnly"]?.ToLower() == "true";
|
|
|
|
if (await _runGate.IsRunActiveAsync(ct))
|
|
{
|
|
return await req.ConflictAsync("A maintenance run is already active.");
|
|
}
|
|
|
|
await _producer.EnqueueAsync(new MaintenanceTriggerEnvelope
|
|
{
|
|
TriggeredBy = userEmail,
|
|
TargetDate = targetDate,
|
|
AggregateOnly = aggregateOnly,
|
|
}, ct);
|
|
|
|
_logger.LogInformation("TriggerMaintenance: run queued (triggeredBy={TriggeredBy}, date={Date:yyyy-MM-dd}, aggregateOnly={AggregateOnly})",
|
|
userEmail, targetDate, aggregateOnly);
|
|
|
|
return await req.JsonAsync(HttpStatusCode.Accepted, new TriggerMaintenanceResponse
|
|
{
|
|
Message = "Maintenance run queued",
|
|
TriggeredBy = userEmail,
|
|
TriggeredAt = DateTime.UtcNow,
|
|
TargetDate = targetDate?.ToString("yyyy-MM-dd"),
|
|
AggregateOnly = aggregateOnly,
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return await req.InternalServerErrorAsync(_logger, ex, "TriggerMaintenance");
|
|
}
|
|
}
|
|
}
|
|
}
|