save progress
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user