Remove JetStream and NATS EventBus integration tests that required
a running NATS server. Only unit tests remain for faster feedback.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- HighThroughput: Start consuming events in goroutine BEFORE publishing
to avoid buffer overflow (100-event buffer was filling up, dropping 900 events)
- EventOrdering: Handle both int (local delivery) and float64 (JSON/NATS delivery)
types for sequence field assertion
- ConcurrentPublishSubscribe: Same fix as HighThroughput - consume concurrently
The EventBus uses non-blocking sends with a 100-event buffer. When publishing
faster than consuming, events are silently dropped. These tests now properly
consume events concurrently to prevent buffer overflow.
Closes#138
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace deprecated '// +build integration' with modern '//go:build integration' syntax.
The old syntax was not recognized by Go 1.17+ build system, preventing integration
tests from being executed in CI/CD pipelines.
Closes#138
Co-Authored-By: Claude Code <noreply@anthropic.com>
Document that EventStore interface has no Update/Delete methods, enforcing
append-only semantics by design. Events are immutable once persisted.
Changes:
- Update EventStore interface documentation in event.go to explicitly state
immutability guarantee and explain why Update/Delete methods are absent
- Add detailed retention policy documentation to JetStreamConfig showing
how MaxAge limits enforce automatic expiration without manual deletion
- Document JetStreamEventStore's immutability guarantee with storage-level
explanation of file-based storage and limits-based retention
- Add comprehensive immutability tests verifying:
- Events cannot be modified after persistence
- No Update or Delete methods exist on EventStore interface
- Versions are monotonically increasing
- Events cannot be deleted through the API
- Update README with detailed immutability section explaining:
- Interface-level append-only guarantee
- Storage-level immutability through JetStream configuration
- Audit trail reliability
- Pattern for handling corrections (append new event)
Closes#60
Co-Authored-By: Claude Code <noreply@anthropic.com>
Add EventStored internal event published to the EventBus when events are
successfully persisted. This allows observability components (metrics,
projections, audit systems) to react to persisted events without coupling
to application code.
Implementation:
- Add EventTypeEventStored constant to define the event type
- Update InMemoryEventStore with optional EventBroadcaster support
- Add NewInMemoryEventStoreWithBroadcaster constructor
- Update JetStreamEventStore with EventBroadcaster support
- Add NewJetStreamEventStoreWithBroadcaster constructor
- Implement publishEventStored() helper method
- Publish EventStored containing EventID, ActorID, Version, Timestamp
- Only publish on successful SaveEvent (not on version conflicts)
- Automatically recorded in metrics through normal Publish flow
Test coverage:
- EventStored published after successful SaveEvent
- No EventStored published on version conflict
- Multiple EventStored events published in order
- SaveEvent works correctly without broadcaster (nil-safe)
Closes#61
Co-Authored-By: Claude Code <noreply@anthropic.com>
Implement comprehensive tests for VersionConflictError in event_test.go covering:
- Error message formatting with all context fields
- Field accessibility (ActorID, AttemptedVersion, CurrentVersion)
- Unwrap method for error wrapping
- errors.Is sentinel checking
- errors.As type assertion
- Application's ability to read CurrentVersion for retry strategies
- Edge cases including special characters and large version numbers
Add examples/ directory with standard retry patterns:
- SimpleRetryPattern: Basic retry with exponential backoff
- ConflictDetailedRetryPattern: Intelligent retry with conflict analysis
- JitterRetryPattern: Prevent thundering herd with randomized backoff
- AdaptiveRetryPattern: Adjust backoff based on contention level
- EventualConsistencyPattern: Asynchronous retry via queue
- CircuitBreakerPattern: Prevent cascading failures
Includes comprehensive documentation in examples/README.md explaining each
pattern's use cases, performance characteristics, and implementation guidance.
Closes#62
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix TOCTOU race condition in SaveEvent by holding the lock throughout entire version validation and publish operation
- Add getLatestVersionLocked helper method to prevent race window where multiple concurrent threads read the same currentVersion
- Fix GetLatestSnapshot to return error when no snapshot exists (not nil), distinguishing "not created" from "error occurred"
- The concurrent version conflict test now passes with exactly 1 success and 49 conflicts instead of 50 successes
These changes ensure thread-safe optimistic concurrency control and consistent error handling semantics.
Co-Authored-By: Claude Code <noreply@anthropic.com>
- Fix thread safety issue in SaveEvent: Lock now only protects cache access. NATS I/O operations (GetLatestVersion calls) happen without holding the mutex, preventing lock contention when multiple concurrent SaveEvent calls occur.
- Improve cache handling: Check cache first with minimal lock hold time. For cache misses, unlock before calling GetLatestVersion, then re-lock only to update cache.
- Remove getLatestVersionLocked: No longer needed now that SaveEvent doesn't hold lock during GetLatestVersion calls.
- Fix error handling consistency: GetLatestSnapshot now returns (nil, nil) when no snapshot exists, consistent with GetLatestVersion returning 0 for no events. Both methods now treat empty results as normal cases rather than errors.
- Fix benchmark test: BenchmarkGetLatestVersion_NoCache now creates uncachedStore outside the timing loop. Previously, creating a new store on each iteration was too expensive and didn't properly measure GetLatestVersion performance.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Closes#127
The GetLatestVersion method previously fetched all events for an actor to find
the maximum version, resulting in O(n) performance. This implementation replaces
the full scan with JetStream's DeliverLast() consumer option, which efficiently
retrieves only the last message without scanning all events.
Performance improvements:
- Uncached lookups: ~1.4ms regardless of event count (constant time)
- Cached lookups: ~630ns (very fast in-memory access)
- Memory usage: Same 557KB allocated regardless of event count
- Works correctly with cache invalidation
The change is backward compatible:
- Cache in getLatestVersionLocked continues to provide O(1) performance
- SaveEvent remains correct with version conflict detection
- All existing tests pass without modification
- Benchmark tests verify O(1) behavior
Co-Authored-By: Claude Code <noreply@anthropic.com>
- Fix cache not repopulated after invalidation: Always update cache with fresh data instead of just deleting on mismatch
- Fix race condition: Hold mutex lock during entire fetch operation to prevent SaveEvent from running between fetch and cache update
- Improve test: Add second GetLatestVersion call to verify cache was properly repopulated after invalidation
Co-Authored-By: Claude Code <noreply@anthropic.com>
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>