clean and last run and count
This commit is contained in:
parent
17d4515961
commit
0d71f39245
|
|
@ -86,8 +86,9 @@ public class WorkflowsController : ControllerBase
|
|||
})
|
||||
.ToListAsync(ct);
|
||||
|
||||
// Latest run per workflow (status + finish time) so the list can surface
|
||||
// the last execution in one column without a round-trip per row.
|
||||
// Latest run per workflow (status, time + duration) and total run count so
|
||||
// the list can surface the last execution and execution count without a
|
||||
// round-trip per row.
|
||||
var ids = workflows.Select(w => w.Id).ToList();
|
||||
var lastRuns = await _db.WorkflowRuns
|
||||
.Where(r => r.TenantId == TenantId && ids.Contains(r.WorkflowId))
|
||||
|
|
@ -95,6 +96,7 @@ public class WorkflowsController : ControllerBase
|
|||
.Select(g => new
|
||||
{
|
||||
WorkflowId = g.Key,
|
||||
RunCount = g.Count(),
|
||||
Status = g
|
||||
.OrderByDescending(r => r.StartedAt ?? r.FinishedAt ?? DateTime.MinValue)
|
||||
.ThenByDescending(r => r.Id)
|
||||
|
|
@ -137,8 +139,12 @@ public class WorkflowsController : ControllerBase
|
|||
w.Version,
|
||||
w.taskCount,
|
||||
w.upToDate,
|
||||
runCount = last?.RunCount ?? 0,
|
||||
lastRunStatus = last?.Status,
|
||||
lastRunAt = last?.FinishedAt ?? last?.StartedAt,
|
||||
lastRunDuration = last is { StartedAt: DateTime s, FinishedAt: DateTime f }
|
||||
? (f - s).TotalSeconds
|
||||
: (double?)null,
|
||||
lastRunError = last?.Error,
|
||||
};
|
||||
});
|
||||
|
|
@ -429,4 +435,48 @@ public class WorkflowsController : ControllerBase
|
|||
return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the run history for one workflow: deletes every run (and, via the
|
||||
/// FK cascade, its task runs). The workflow definition, its tasks and its
|
||||
/// trigger are untouched — only the history is wiped. Returns the number of
|
||||
/// runs removed.
|
||||
/// </summary>
|
||||
[HttpDelete("{id:guid}/runs")]
|
||||
[RequireScope("manage")]
|
||||
public async Task<IActionResult> ClearRuns(Guid id, CancellationToken ct)
|
||||
{
|
||||
var owned = await _db.Workflows.AnyAsync(w => w.Id == id && w.TenantId == TenantId, ct);
|
||||
if (!owned)
|
||||
return NotFound(new { error = "Workflow not found." });
|
||||
|
||||
var runIds = await _db.WorkflowRuns
|
||||
.Where(r => r.WorkflowId == id && r.TenantId == TenantId)
|
||||
.Select(r => r.Id)
|
||||
.ToListAsync(ct);
|
||||
|
||||
if (runIds.Count > 0)
|
||||
{
|
||||
// Remove task runs explicitly first so EF does not depend on the DB
|
||||
// cascade for the (already configured) FK; then remove the runs.
|
||||
var taskRuns = await _db.TaskRuns
|
||||
.Where(t => runIds.Contains(t.RunId))
|
||||
.ToListAsync(ct);
|
||||
_db.TaskRuns.RemoveRange(taskRuns);
|
||||
|
||||
var runs = await _db.WorkflowRuns
|
||||
.Where(r => runIds.Contains(r.Id))
|
||||
.ToListAsync(ct);
|
||||
_db.WorkflowRuns.RemoveRange(runs);
|
||||
|
||||
await _db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Cleared {Count} run(s) for workflow {WorkflowId} (tenant {TenantId})",
|
||||
runIds.Count, id, TenantId);
|
||||
return Ok(new ClearRunsResponse(runIds.Count));
|
||||
}
|
||||
|
||||
public sealed record ClearRunsResponse(int Cleared);
|
||||
}
|
||||
|
|
|
|||
155
w4c-workflows-api.Tests/WorkflowsControllerListAndClearTests.cs
Normal file
155
w4c-workflows-api.Tests/WorkflowsControllerListAndClearTests.cs
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using w4c_workflows.Controllers;
|
||||
using w4c_workflows.Data;
|
||||
using w4c_workflows.Models;
|
||||
using w4c_workflows.Services;
|
||||
using w4c_workflows.Services.Runs;
|
||||
using Xunit;
|
||||
|
||||
using ClearRunsResponse = w4c_workflows.Controllers.WorkflowsController.ClearRunsResponse;
|
||||
|
||||
namespace w4c_workflows.Tests;
|
||||
|
||||
[Collection("WorkflowsPostgres")]
|
||||
public class WorkflowsControllerListAndClearTests
|
||||
{
|
||||
private const string YamlContent = "name: linear\nmode: function\nlanguage: shell\n";
|
||||
|
||||
private sealed class FakeWorkflowSource : IWorkflowSource
|
||||
{
|
||||
public Task<IReadOnlyList<WorkflowFile>> ListAsync(CancellationToken ct)
|
||||
=> Task.FromResult<IReadOnlyList<WorkflowFile>>(Array.Empty<WorkflowFile>());
|
||||
|
||||
public Task<string> ReadAsync(string path, CancellationToken ct)
|
||||
=> Task.FromResult(YamlContent);
|
||||
|
||||
public WorkflowSourceState GetState() => new(null, false);
|
||||
}
|
||||
|
||||
private readonly WorkflowsPostgresFixture _fixture;
|
||||
|
||||
public WorkflowsControllerListAndClearTests(WorkflowsPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
private static WorkflowsController Controller(WorkflowsDbContext db, string tenantId)
|
||||
{
|
||||
var sync = new WorkflowSyncService(
|
||||
db,
|
||||
new WorkflowCompiler(new WorkflowValidator(new LanguageRegistry())),
|
||||
new FakeWorkflowSource(),
|
||||
NullLogger<WorkflowSyncService>.Instance);
|
||||
|
||||
var controller = new WorkflowsController(
|
||||
db,
|
||||
sync,
|
||||
new FakeWorkflowSource(),
|
||||
new MermaidGeneratorService(),
|
||||
new WorkflowHtmlRenderer(NullLogger<WorkflowHtmlRenderer>.Instance),
|
||||
new RenderTokenService(new ConfigurationBuilder().Build(), NullLogger<RenderTokenService>.Instance),
|
||||
new RunLauncher(db, NullLogger<RunLauncher>.Instance),
|
||||
new DurableStateStore(db),
|
||||
NullLogger<WorkflowsController>.Instance)
|
||||
{
|
||||
ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() },
|
||||
};
|
||||
controller.HttpContext.Items["TenantId"] = tenantId;
|
||||
return controller;
|
||||
}
|
||||
|
||||
private static string Tenant() => "t" + Guid.NewGuid().ToString("N")[..12];
|
||||
|
||||
[Fact]
|
||||
public async Task List_returns_run_count_and_last_run_duration()
|
||||
{
|
||||
var tenantId = Tenant();
|
||||
await using var db = _fixture.CreateContext();
|
||||
var workflow = WorkflowDataHelpers.CompileAndSave(db, tenantId, WorkflowDataHelpers.LinearYaml);
|
||||
|
||||
// Two finished runs + one in-flight run (started longest ago so the latest
|
||||
// run by time is the finished one) for the same workflow.
|
||||
WorkflowDataHelpers.AddRun(db, workflow, RunStatus.Succeeded,
|
||||
startedAt: DateTime.UtcNow.AddMinutes(-5), finishedAt: DateTime.UtcNow.AddMinutes(-4));
|
||||
WorkflowDataHelpers.AddRun(db, workflow, RunStatus.Failed,
|
||||
startedAt: DateTime.UtcNow.AddMinutes(-2), finishedAt: DateTime.UtcNow.AddMinutes(-1), error: "boom");
|
||||
WorkflowDataHelpers.AddRun(db, workflow, RunStatus.Running, startedAt: DateTime.UtcNow.AddMinutes(-10));
|
||||
|
||||
// A workflow with no runs must default to 0 / null, not throw.
|
||||
var empty = WorkflowDataHelpers.CompileAndSave(db, tenantId, WorkflowDataHelpers.DurableYaml, "workflows/b.yaml");
|
||||
|
||||
var result = Assert.IsType<OkObjectResult>(await Controller(db, tenantId).List(default));
|
||||
var json = JsonSerializer.Serialize(result.Value, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
var first = root.GetProperty("workflows").EnumerateArray()
|
||||
.Single(w => w.GetProperty("id").GetGuid() == workflow.Id);
|
||||
// The latest run is the failed one (started -2m), so duration = 60s.
|
||||
Assert.Equal(3, first.GetProperty("runCount").GetInt32());
|
||||
Assert.Equal(RunStatus.Failed, first.GetProperty("lastRunStatus").GetString());
|
||||
Assert.Equal(60, first.GetProperty("lastRunDuration").GetDouble(), 1);
|
||||
|
||||
var second = root.GetProperty("workflows").EnumerateArray()
|
||||
.Single(w => w.GetProperty("id").GetGuid() == empty.Id);
|
||||
Assert.Equal(0, second.GetProperty("runCount").GetInt32());
|
||||
Assert.Equal(JsonValueKind.Null, second.GetProperty("lastRunDuration").ValueKind);
|
||||
Assert.Equal(JsonValueKind.Null, second.GetProperty("lastRunStatus").ValueKind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClearRuns_deletes_runs_and_task_runs_for_the_workflow()
|
||||
{
|
||||
var tenantId = Tenant();
|
||||
await using var db = _fixture.CreateContext();
|
||||
var workflow = WorkflowDataHelpers.CompileAndSave(db, tenantId, WorkflowDataHelpers.LinearYaml);
|
||||
var done = WorkflowDataHelpers.AddRun(db, workflow, RunStatus.Succeeded,
|
||||
DateTime.UtcNow.AddMinutes(-5), DateTime.UtcNow.AddMinutes(-4));
|
||||
var failed = WorkflowDataHelpers.AddRun(db, workflow, RunStatus.Failed,
|
||||
DateTime.UtcNow.AddMinutes(-2), DateTime.UtcNow.AddMinutes(-1), error: "boom");
|
||||
|
||||
var root = WorkflowDataHelpers.TaskByKey(workflow, "root");
|
||||
db.TaskRuns.AddRange(
|
||||
new TaskRun { Id = Guid.NewGuid(), RunId = done.Id, TaskId = root.Id, Attempt = 1, Status = TaskRunStatus.Succeeded },
|
||||
new TaskRun { Id = Guid.NewGuid(), RunId = failed.Id, TaskId = root.Id, Attempt = 1, Status = TaskRunStatus.Failed });
|
||||
db.SaveChanges();
|
||||
|
||||
var result = Assert.IsType<OkObjectResult>(await Controller(db, tenantId).ClearRuns(workflow.Id, default));
|
||||
var body = Assert.IsType<ClearRunsResponse>(result.Value);
|
||||
Assert.Equal(2, body.Cleared);
|
||||
|
||||
Assert.Empty(await db.WorkflowRuns.Where(r => r.WorkflowId == workflow.Id).ToListAsync());
|
||||
Assert.Empty(await db.TaskRuns.Where(t => t.RunId == done.Id || t.RunId == failed.Id).ToListAsync());
|
||||
// The workflow definition itself is untouched.
|
||||
Assert.True(await db.Workflows.AnyAsync(w => w.Id == workflow.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClearRuns_404s_for_foreign_workflow()
|
||||
{
|
||||
var tenantId = Tenant();
|
||||
await using var db = _fixture.CreateContext();
|
||||
var other = Tenant();
|
||||
var workflow = WorkflowDataHelpers.CompileAndSave(db, other, WorkflowDataHelpers.LinearYaml);
|
||||
|
||||
var result = await Controller(db, tenantId).ClearRuns(workflow.Id, default);
|
||||
Assert.IsType<NotFoundObjectResult>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClearRuns_is_idempotent_when_no_runs_exist()
|
||||
{
|
||||
var tenantId = Tenant();
|
||||
await using var db = _fixture.CreateContext();
|
||||
var workflow = WorkflowDataHelpers.CompileAndSave(db, tenantId, WorkflowDataHelpers.LinearYaml);
|
||||
|
||||
var result = Assert.IsType<OkObjectResult>(await Controller(db, tenantId).ClearRuns(workflow.Id, default));
|
||||
var body = Assert.IsType<ClearRunsResponse>(result.Value);
|
||||
Assert.Equal(0, body.Cleared);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue