save progress

This commit is contained in:
master
2026-01-09 18:27:36 +02:00
parent e608752924
commit a21d3dbc1f
361 changed files with 63068 additions and 1192 deletions

View File

@@ -16,8 +16,7 @@ public static class ExportEndpoints
public static void MapExportEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/v1/timeline")
.WithTags("Export")
.WithOpenApi();
.WithTags("Export");
group.MapPost("/{correlationId}/export", ExportTimelineAsync)
.WithName("ExportTimeline")

View File

@@ -1,11 +1,14 @@
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
using Microsoft.AspNetCore.Http.HttpResults;
using StellaOps.HybridLogicalClock;
using StellaOps.Timeline.Core.Replay;
namespace StellaOps.Timeline.WebService.Endpoints;
/// <summary>
/// Replay endpoints for deterministic replay of event sequences.
/// Sprint: SPRINT_20260107_006_005 Task RB-005
/// </summary>
public static class ReplayEndpoints
{
@@ -15,8 +18,7 @@ public static class ReplayEndpoints
public static void MapReplayEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/v1/timeline")
.WithTags("Replay")
.WithOpenApi();
.WithTags("Replay");
group.MapPost("/{correlationId}/replay", InitiateReplayAsync)
.WithName("InitiateReplay")
@@ -29,11 +31,16 @@ public static class ReplayEndpoints
group.MapPost("/replay/{replayId}/cancel", CancelReplayAsync)
.WithName("CancelReplay")
.WithDescription("Cancel an in-progress replay operation");
group.MapDelete("/replay/{replayId}", DeleteReplayAsync)
.WithName("DeleteReplay")
.WithDescription("Delete/cancel a replay operation");
}
private static async Task<Results<Accepted<ReplayInitiatedResponse>, BadRequest<string>>> InitiateReplayAsync(
string correlationId,
ReplayRequest request,
ITimelineReplayOrchestrator orchestrator,
CancellationToken cancellationToken)
{
// Validate request
@@ -42,53 +49,117 @@ public static class ReplayEndpoints
return TypedResults.BadRequest("Correlation ID is required");
}
// TODO: Integrate with StellaOps.Replay.Core
// For now, return a stub response
var replayId = Guid.NewGuid().ToString("N")[..16];
// Convert API request to domain request
var domainRequest = new Core.Replay.ReplayRequest
{
Mode = request.Mode,
FromHlc = ParseHlc(request.FromHlc),
ToHlc = ParseHlc(request.ToHlc)
};
await Task.CompletedTask; // Placeholder for actual implementation
// Initiate replay via orchestrator
var operation = await orchestrator.InitiateReplayAsync(
correlationId,
domainRequest,
cancellationToken).ConfigureAwait(false);
return TypedResults.Accepted(
$"/api/v1/timeline/replay/{replayId}",
$"/api/v1/timeline/replay/{operation.ReplayId}",
new ReplayInitiatedResponse
{
ReplayId = replayId,
CorrelationId = correlationId,
Mode = request.Mode,
Status = "INITIATED",
EstimatedDurationMs = 5000
ReplayId = operation.ReplayId,
CorrelationId = operation.CorrelationId,
Mode = operation.Mode,
Status = MapStatus(operation.Status),
EstimatedDurationMs = EstimateDuration(operation.TotalEvents)
});
}
private static async Task<Results<Ok<ReplayStatusResponse>, NotFound>> GetReplayStatusAsync(
string replayId,
ITimelineReplayOrchestrator orchestrator,
CancellationToken cancellationToken)
{
// TODO: Integrate with replay state store
await Task.CompletedTask;
var operation = await orchestrator.GetReplayStatusAsync(replayId, cancellationToken)
.ConfigureAwait(false);
if (operation is null)
{
return TypedResults.NotFound();
}
return TypedResults.Ok(new ReplayStatusResponse
{
ReplayId = replayId,
Status = "IN_PROGRESS",
Progress = 0.5,
EventsProcessed = 50,
TotalEvents = 100,
StartedAt = DateTimeOffset.UtcNow.AddSeconds(-5)
ReplayId = operation.ReplayId,
CorrelationId = operation.CorrelationId,
Mode = operation.Mode,
Status = MapStatus(operation.Status),
Progress = operation.Progress,
EventsProcessed = operation.EventsProcessed,
TotalEvents = operation.TotalEvents,
StartedAt = operation.StartedAt,
CompletedAt = operation.CompletedAt,
OriginalDigest = operation.OriginalDigest,
ReplayDigest = operation.ReplayDigest,
DeterministicMatch = operation.DeterministicMatch,
Error = operation.Error
});
}
private static async Task<Results<Ok, NotFound>> CancelReplayAsync(
string replayId,
ITimelineReplayOrchestrator orchestrator,
CancellationToken cancellationToken)
{
// TODO: Integrate with replay cancellation
await Task.CompletedTask;
return TypedResults.Ok();
var cancelled = await orchestrator.CancelReplayAsync(replayId, cancellationToken)
.ConfigureAwait(false);
return cancelled ? TypedResults.Ok() : TypedResults.NotFound();
}
private static async Task<Results<Ok, NotFound>> DeleteReplayAsync(
string replayId,
ITimelineReplayOrchestrator orchestrator,
CancellationToken cancellationToken)
{
// Delete is the same as cancel for now
var deleted = await orchestrator.CancelReplayAsync(replayId, cancellationToken)
.ConfigureAwait(false);
return deleted ? TypedResults.Ok() : TypedResults.NotFound();
}
private static HlcTimestamp? ParseHlc(string? hlcString)
{
if (string.IsNullOrWhiteSpace(hlcString))
{
return null;
}
return HlcTimestamp.TryParse(hlcString, out var hlc) ? hlc : null;
}
private static string MapStatus(ReplayStatus status) => status switch
{
ReplayStatus.Initiated => "INITIATED",
ReplayStatus.InProgress => "IN_PROGRESS",
ReplayStatus.Completed => "COMPLETED",
ReplayStatus.Failed => "FAILED",
ReplayStatus.Cancelled => "CANCELLED",
_ => "UNKNOWN"
};
private static long EstimateDuration(int eventCount)
{
// Rough estimate: 10ms per event + 500ms overhead
return (eventCount * 10) + 500;
}
}
// DTOs
/// <summary>
/// Request for initiating a replay operation.
/// </summary>
public sealed record ReplayRequest
{
/// <summary>
@@ -107,23 +178,104 @@ public sealed record ReplayRequest
public string? ToHlc { get; init; }
}
/// <summary>
/// Response when a replay operation is initiated.
/// </summary>
public sealed record ReplayInitiatedResponse
{
/// <summary>
/// The unique replay operation ID.
/// </summary>
public required string ReplayId { get; init; }
/// <summary>
/// The correlation ID being replayed.
/// </summary>
public required string CorrelationId { get; init; }
/// <summary>
/// The replay mode.
/// </summary>
public required string Mode { get; init; }
/// <summary>
/// Current status of the replay.
/// </summary>
public required string Status { get; init; }
/// <summary>
/// Estimated duration in milliseconds.
/// </summary>
public long EstimatedDurationMs { get; init; }
}
/// <summary>
/// Response containing replay operation status.
/// </summary>
public sealed record ReplayStatusResponse
{
/// <summary>
/// The unique replay operation ID.
/// </summary>
public required string ReplayId { get; init; }
/// <summary>
/// The correlation ID being replayed.
/// </summary>
public string? CorrelationId { get; init; }
/// <summary>
/// The replay mode.
/// </summary>
public string? Mode { get; init; }
/// <summary>
/// Current status: INITIATED, IN_PROGRESS, COMPLETED, FAILED, CANCELLED.
/// </summary>
public required string Status { get; init; }
/// <summary>
/// Progress from 0.0 to 1.0.
/// </summary>
public double Progress { get; init; }
/// <summary>
/// Number of events processed so far.
/// </summary>
public int EventsProcessed { get; init; }
/// <summary>
/// Total number of events to process.
/// </summary>
public int TotalEvents { get; init; }
/// <summary>
/// When the replay started.
/// </summary>
public DateTimeOffset StartedAt { get; init; }
/// <summary>
/// When the replay completed (if finished).
/// </summary>
public DateTimeOffset? CompletedAt { get; init; }
/// <summary>
/// SHA-256 digest of the original event chain.
/// </summary>
public string? OriginalDigest { get; init; }
/// <summary>
/// SHA-256 digest of the replayed event chain.
/// </summary>
public string? ReplayDigest { get; init; }
/// <summary>
/// Whether the replay produced deterministic output.
/// </summary>
public bool? DeterministicMatch { get; init; }
/// <summary>
/// Error message if replay failed.
/// </summary>
public string? Error { get; init; }
}

View File

@@ -17,8 +17,7 @@ public static class TimelineEndpoints
public static void MapTimelineEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/v1/timeline")
.WithTags("Timeline")
.WithOpenApi();
.WithTags("Timeline");
group.MapGet("/{correlationId}", GetTimelineAsync)
.WithName("GetTimeline")

View File

@@ -19,6 +19,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
<PackageReference Include="Npgsql" />
</ItemGroup>