dotnet-cqrs/docs/event-streaming/event-replay/replay-from-time.md

8.5 KiB

Time-Based Event Replay

Replay events from specific timestamps for time-travel debugging and historical analysis.

Overview

Time-based replay allows you to process events that occurred during specific time periods, enabling:

  • Time-travel debugging to specific moments
  • Historical data reprocessing after bug fixes
  • Creating new projections from specific dates
  • Auditing and compliance queries

Quick Start

using Svrnty.CQRS.Events.Abstractions;

var replayService = serviceProvider.GetRequiredService<IEventReplayService>();

// Replay from 7 days ago
await foreach (var @event in replayService.ReplayFromTimeAsync(
    streamName: "orders",
    startTime: DateTimeOffset.UtcNow.AddDays(-7)))
{
    await ProcessEventAsync(@event);
}

Replay From Specific Time

// Replay from specific UTC time
var startTime = new DateTimeOffset(2025, 12, 1, 0, 0, 0, TimeSpan.Zero);

await foreach (var @event in replayService.ReplayFromTimeAsync(
    "orders",
    startTime,
    options: new ReplayOptions
    {
        BatchSize = 100,
        MaxEventsPerSecond = 1000
    }))
{
    await RebuildProjectionAsync(@event);
}

Replay Time Range

Process events within a specific time window:

// Replay single day
var startTime = new DateTimeOffset(2025, 12, 1, 0, 0, 0, TimeSpan.Zero);
var endTime = new DateTimeOffset(2025, 12, 2, 0, 0, 0, TimeSpan.Zero);

await foreach (var @event in replayService.ReplayTimeRangeAsync(
    streamName: "analytics",
    startTime: startTime,
    endTime: endTime))
{
    await ProcessAnalyticsEventAsync(@event);
}

Common Time-Based Scenarios

Last 24 Hours

await foreach (var @event in replayService.ReplayFromTimeAsync(
    "orders",
    DateTimeOffset.UtcNow.AddHours(-24)))
{
    await ProcessRecentEventAsync(@event);
}

Specific Month

var startOfMonth = new DateTimeOffset(2025, 11, 1, 0, 0, 0, TimeSpan.Zero);
var endOfMonth = new DateTimeOffset(2025, 12, 1, 0, 0, 0, TimeSpan.Zero);

await foreach (var @event in replayService.ReplayTimeRangeAsync(
    "sales",
    startOfMonth,
    endOfMonth))
{
    await ProcessMonthlyReportAsync(@event);
}

Business Hours Only

var businessStart = new DateTimeOffset(2025, 12, 1, 9, 0, 0, TimeSpan.Zero);  // 9 AM
var businessEnd = new DateTimeOffset(2025, 12, 1, 17, 0, 0, TimeSpan.Zero);  // 5 PM

await foreach (var @event in replayService.ReplayTimeRangeAsync(
    "customer-interactions",
    businessStart,
    businessEnd))
{
    await AnalyzeBusinessHoursActivityAsync(@event);
}

Replay Options

Configure replay behavior with ReplayOptions:

var options = new ReplayOptions
{
    BatchSize = 100,                          // Events per database query
    MaxEventsPerSecond = 1000,                // Rate limit
    EventTypeFilter = new[] { "OrderPlaced", "OrderShipped" },
    MaxEvents = 10000,                        // Stop after 10k events
    ProgressCallback = progress =>
    {
        Console.WriteLine($"Progress: {progress.EventsProcessed} events");
        Console.WriteLine($"Rate: {progress.EventsPerSecond:F0} events/sec");
        Console.WriteLine($"ETA: {progress.EstimatedCompletion}");
    },
    ProgressInterval = 1000                   // Callback every 1000 events
};

await foreach (var @event in replayService.ReplayFromTimeAsync(
    "orders",
    DateTimeOffset.UtcNow.AddDays(-30),
    options))
{
    await ProcessEventAsync(@event);
}

Time Zone Considerations

All timestamps are in UTC:

// ✅ Good - UTC
var startTime = new DateTimeOffset(2025, 12, 1, 0, 0, 0, TimeSpan.Zero);

// ❌ Bad - Local time (will be converted to UTC)
var startTime = DateTime.Now.AddDays(-7);  // Avoid local time

// ✅ Better - Explicit UTC
var startTime = DateTime.UtcNow.AddDays(-7);

// ✅ Best - DateTimeOffset with explicit offset
var startTime = DateTimeOffset.UtcNow.AddDays(-7);

Performance Optimization

Batch Size Tuning

// Large time range - use larger batches
var options = new ReplayOptions
{
    BatchSize = 1000  // Reduce database round-trips
};

await foreach (var @event in replayService.ReplayFromTimeAsync(
    "orders",
    DateTimeOffset.UtcNow.AddMonths(-6),
    options))
{
    await ProcessEventAsync(@event);
}

Rate Limiting

// Production replay - limit impact
var options = new ReplayOptions
{
    MaxEventsPerSecond = 100  // Gentle on system
};

await foreach (var @event in replayService.ReplayFromTimeAsync(
    "orders",
    DateTimeOffset.UtcNow.AddDays(-30),
    options))
{
    await ProcessEventAsync(@event);
}

Combining with Offset-Based Replay

You can combine time-based and offset-based replay:

// Find offset for specific time
var events = replayService.ReplayFromTimeAsync("orders", specificTime);
var firstEvent = await events.FirstOrDefaultAsync();

if (firstEvent != null)
{
    // Now replay from that offset
    await foreach (var @event in replayService.ReplayFromOffsetAsync(
        "orders",
        startOffset: firstEvent.Offset))
    {
        await ProcessEventAsync(@event);
    }
}

Use Cases

Time-Travel Debugging

// Reproduce bug that occurred at specific time
var bugTime = new DateTimeOffset(2025, 12, 1, 14, 30, 0, TimeSpan.Zero);
var windowStart = bugTime.AddMinutes(-5);
var windowEnd = bugTime.AddMinutes(5);

await foreach (var @event in replayService.ReplayTimeRangeAsync(
    "orders",
    windowStart,
    windowEnd))
{
    Console.WriteLine($"{@event.Timestamp}: {@event.EventType} - {@event.EventId}");
    // Debug state at each event
}

Historical Reporting

// Generate monthly reports from historical data
for (int month = 1; month <= 12; month++)
{
    var startOfMonth = new DateTimeOffset(2025, month, 1, 0, 0, 0, TimeSpan.Zero);
    var endOfMonth = startOfMonth.AddMonths(1);

    var report = new MonthlyReport { Month = month };

    await foreach (var @event in replayService.ReplayTimeRangeAsync(
        "sales",
        startOfMonth,
        endOfMonth))
    {
        report.ProcessEvent(@event);
    }

    await SaveReportAsync(report);
}

Compliance Auditing

// Audit all user actions during compliance period
var auditStart = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
var auditEnd = new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);

await foreach (var @event in replayService.ReplayTimeRangeAsync(
    "user-actions",
    auditStart,
    auditEnd,
    options: new ReplayOptions
    {
        EventTypeFilter = new[] { "UserLogin", "DataAccess", "DataModification" }
    }))
{
    await LogAuditEventAsync(@event);
}

Database Considerations

Index Requirements

Time-based replay relies on timestamp indexes:

-- Ensure index exists
CREATE INDEX IF NOT EXISTS idx_events_stream_timestamp
ON events(stream_name, timestamp);

Query Performance

// Efficient - narrow time range
await foreach (var @event in replayService.ReplayTimeRangeAsync(
    "orders",
    DateTimeOffset.UtcNow.AddHours(-1),
    DateTimeOffset.UtcNow))
{
    // Fast query
}

// Less efficient - wide time range
await foreach (var @event in replayService.ReplayFromTimeAsync(
    "orders",
    DateTimeOffset.UtcNow.AddYears(-1)))
{
    // May scan many events
}

Error Handling

try
{
    await foreach (var @event in replayService.ReplayFromTimeAsync(
        "orders",
        DateTimeOffset.UtcNow.AddDays(-7)))
    {
        try
        {
            await ProcessEventAsync(@event);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to process event {EventId}", @event.EventId);
            // Continue processing other events
        }
    }
}
catch (OperationCanceledException)
{
    _logger.LogInformation("Replay cancelled by user");
}
catch (Exception ex)
{
    _logger.LogError(ex, "Replay failed");
    throw;
}

Best Practices

DO

  • Use UTC timestamps consistently
  • Add progress callbacks for long replays
  • Use rate limiting for production replays
  • Filter by event type when possible
  • Handle individual event errors gracefully
  • Log replay start and completion

DON'T

  • Don't use local time without explicit conversion
  • Don't replay large time ranges without rate limiting
  • Don't ignore progress tracking
  • Don't replay in production without testing first
  • Don't forget to handle cancellation

See Also