fix(store): Implement version cache invalidation strategy for JetStreamEventStore
Implements cache invalidation on GetLatestVersion when external writers modify the JetStream stream. The strategy ensures consistency in multi-store scenarios while maintaining performance for the single-writer case. Changes: - Add cache invalidation logic to GetLatestVersion() that detects stale cache - Document version cache behavior in JetStreamEventStore struct comment - Add detailed documentation in CLAUDE.md about cache invalidation strategy - Add TestJetStreamEventStore_CacheInvalidationOnExternalWrite integration test - Cache is invalidated by deleting entry, forcing fresh fetch on next check The implementation follows the acceptance criteria by: 1. Documenting the single-writer assumption in code comments 2. Implementing cache invalidation on GetLatestVersion miss 3. Adding comprehensive test for external write scenarios Closes #126 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1322,6 +1322,110 @@ func TestJetStreamEventStore_MultipleStoreInstances(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// === Cache Invalidation Tests ===
|
||||
|
||||
func TestJetStreamEventStore_CacheInvalidationOnExternalWrite(t *testing.T) {
|
||||
nc := getTestNATSConnection(t)
|
||||
defer nc.Close()
|
||||
|
||||
streamName := uniqueStreamName("test-cache-invalidation")
|
||||
defer cleanupStream(nc, streamName)
|
||||
|
||||
// Create two stores for the same stream
|
||||
store1, err := NewJetStreamEventStore(nc, streamName)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create store1: %v", err)
|
||||
}
|
||||
|
||||
store2, err := NewJetStreamEventStore(nc, streamName)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create store2: %v", err)
|
||||
}
|
||||
|
||||
actorID := "actor-cache-test"
|
||||
|
||||
// store1: Save event v1 (caches version 1)
|
||||
event1 := &aether.Event{
|
||||
ID: "evt-1",
|
||||
EventType: "TestEvent",
|
||||
ActorID: actorID,
|
||||
Version: 1,
|
||||
Data: map[string]interface{}{},
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
if err := store1.SaveEvent(event1); err != nil {
|
||||
t.Fatalf("SaveEvent from store1 failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify store1 sees version 1 (uses cache)
|
||||
v1, err := store1.GetLatestVersion(actorID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetLatestVersion from store1 failed: %v", err)
|
||||
}
|
||||
if v1 != 1 {
|
||||
t.Errorf("store1 should see version 1, got %d", v1)
|
||||
}
|
||||
|
||||
// store2: Save event v2 (external write from store1's perspective)
|
||||
event2 := &aether.Event{
|
||||
ID: "evt-2",
|
||||
EventType: "TestEvent",
|
||||
ActorID: actorID,
|
||||
Version: 2,
|
||||
Data: map[string]interface{}{},
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
if err := store2.SaveEvent(event2); err != nil {
|
||||
t.Fatalf("SaveEvent from store2 failed: %v", err)
|
||||
}
|
||||
|
||||
// store1: GetLatestVersion should detect external write and return v2
|
||||
// (This triggers cache invalidation because actual version > cached version)
|
||||
v2, err := store1.GetLatestVersion(actorID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetLatestVersion from store1 (after external write) failed: %v", err)
|
||||
}
|
||||
if v2 != 2 {
|
||||
t.Errorf("store1 should see version 2 after external write, got %d", v2)
|
||||
}
|
||||
|
||||
// store2: Save event v3 (another external write)
|
||||
event3 := &aether.Event{
|
||||
ID: "evt-3",
|
||||
EventType: "TestEvent",
|
||||
ActorID: actorID,
|
||||
Version: 3,
|
||||
Data: map[string]interface{}{},
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
if err := store2.SaveEvent(event3); err != nil {
|
||||
t.Fatalf("SaveEvent from store2 (v3) failed: %v", err)
|
||||
}
|
||||
|
||||
// store1: After cache invalidation, SaveEvent should use fresh data from JetStream
|
||||
event4 := &aether.Event{
|
||||
ID: "evt-4",
|
||||
EventType: "TestEvent",
|
||||
ActorID: actorID,
|
||||
Version: 4,
|
||||
Data: map[string]interface{}{},
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
if err := store1.SaveEvent(event4); err != nil {
|
||||
t.Fatalf("SaveEvent from store1 (after cache invalidation) failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify all 4 events are persisted
|
||||
events, err := store1.GetEvents(actorID, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("GetEvents failed: %v", err)
|
||||
}
|
||||
if len(events) != 4 {
|
||||
t.Errorf("expected 4 events after cache invalidation, got %d", len(events))
|
||||
}
|
||||
}
|
||||
|
||||
// === Interface Compliance Tests ===
|
||||
|
||||
func TestJetStreamEventStore_ImplementsEventStore(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user