368 lines
8.5 KiB
Markdown
368 lines
8.5 KiB
Markdown
# 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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
// 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:
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
```csharp
|
|
await foreach (var @event in replayService.ReplayFromTimeAsync(
|
|
"orders",
|
|
DateTimeOffset.UtcNow.AddHours(-24)))
|
|
{
|
|
await ProcessRecentEventAsync(@event);
|
|
}
|
|
```
|
|
|
|
### Specific Month
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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`:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
// ✅ 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
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
```csharp
|
|
// 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:
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
```csharp
|
|
// 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:
|
|
|
|
```sql
|
|
-- Ensure index exists
|
|
CREATE INDEX IF NOT EXISTS idx_events_stream_timestamp
|
|
ON events(stream_name, timestamp);
|
|
```
|
|
|
|
### Query Performance
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
```csharp
|
|
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
|
|
|
|
- [Replay From Offset](replay-from-offset.md)
|
|
- [Rate Limiting](rate-limiting.md)
|
|
- [Progress Tracking](progress-tracking.md)
|
|
- [Event Replay Overview](README.md)
|
|
- [Projections](../projections/README.md)
|