BREAKING CHANGES:
**ID-First Storage Paths**
- Direct O(1) entity access without type lookups
- Before: entities/nouns/{TYPE}/metadata/{SHARD}/{ID}.json
- After: entities/nouns/{SHARD}/{ID}/metadata.json
- Migration handled automatically on first init()
**Removed Memory-Unsafe APIs**
- Removed brain.merge() - loaded all entities into memory
- Removed brain.diff() - loaded all entities into memory
- Removed brain.data().backup() - loaded all entities into memory
- Removed brain.data().restore() - depended on backup()
- Removed CLI commands: backup, restore, cow merge
**Migration Paths**
- merge() → Use checkout() or manually copy entities with pagination
- diff() → Use asOf() with manual paginated comparison
- backup() → Use fork() for instant COW snapshots
- restore() → Use checkout() to switch to snapshot branch
Core Improvements:
- ✅ All 8 storage adapters properly call super.init()
- ✅ GraphAdjacencyIndex integration in BaseStorage.init()
- ✅ Fixed ID-first path bugs (vector.json → vectors.json)
- ✅ Fixed MemoryStorage.initializeCounts() for ID-first paths
- ✅ New VFS APIs: du(), access(), find()
- ✅ Comprehensive documentation with migration guides
Storage Adapters Fixed:
- MemoryStorage, FileSystemStorage, AzureBlobStorage
- GCSStorage, R2Storage, S3CompatibleStorage
- OPFSStorage, HistoricalStorageAdapter
Files Changed: 28 files, +1,075/-1,933 lines (net -858)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
16 KiB
Transaction System
Status: ✅ Production Ready (v5.8.0+)
Overview
Brainy's transaction system provides atomic operations with automatic rollback on failure. All operations within a transaction either succeed completely or fail completely - there are no partial failures.
Key Benefits
- Atomicity: All operations succeed or all rollback
- Consistency: Indexes and storage remain consistent
- Automatic: Transparently used by all
brain.add(),brain.update(),brain.delete(), andbrain.relate()operations - Compatible: Works seamlessly with COW, sharding, type-aware storage, and distributed storage
Architecture
User Code (brain.add(), brain.update(), etc.)
↓
Transaction Manager (orchestration)
↓
Operations (SaveNounMetadataOperation, SaveNounOperation, etc.)
↓
Storage Adapter (COW, sharding, type-aware routing)
How It Works
Every write operation in Brainy automatically uses transactions:
// Internally, this uses a transaction
const id = await brain.add({
data: { name: 'Alice', role: 'Engineer' },
type: NounType.Person
})
Transaction Flow:
- Begin Transaction: TransactionManager creates new transaction
- Add Operations: Operations added to transaction (SaveNounMetadataOperation, SaveNounOperation)
- Execute: Each operation executes in sequence
- Commit: All operations succeeded → changes persist
- Rollback: Any operation failed → all changes reverted
Rollback Mechanism
Each operation implements both execute and undo:
class SaveNounMetadataOperation {
async execute(): Promise<void> {
// Save new metadata
await this.storage.saveNounMetadata(this.id, this.metadata)
}
async undo(): Promise<void> {
// Restore previous metadata (or delete if new entity)
if (this.previousMetadata) {
await this.storage.saveNounMetadata(this.id, this.previousMetadata)
} else {
await this.storage.deleteNounMetadata(this.id)
}
}
}
On failure:
- Operations rolled back in reverse order
- Previous state fully restored
- Indexes updated to reflect rollback
Compatibility with Advanced Features
COW (Copy-on-Write)
✅ Fully Compatible
Transactions work transparently with COW branches:
// Create branch
await brain.cow.createBranch('feature-branch')
await brain.cow.checkout('feature-branch')
// Add entity (uses transaction on this branch)
const id = await brain.add({
data: { name: 'Feature Entity' },
type: NounType.Thing
})
// On rollback: Branch remains clean, no partial commits
How It Works:
- Transactions use
StorageAdapterinterface - COW operates at storage layer (refManager, blobStorage, commitLog)
- Branch isolation prevents cross-branch contamination
- Rollback = discard uncommitted changes (COW makes this trivial)
Sharding
✅ Fully Compatible
Transactions work across multiple shards:
// Entities with different UUID prefixes go to different shards
const id1 = 'aaa00000-1111-4111-8111-111111111111' // Shard: aaa
const id2 = 'bbb00000-2222-4222-8222-222222222222' // Shard: bbb
await brain.add({ id: id1, data: { name: 'Entity A' }, type: NounType.Thing })
await brain.relate({ from: id1, to: id2, type: VerbType.RelatesTo })
// Transaction handles cross-shard atomicity automatically
How It Works:
- Sharding is transparent to transactions
analyzeKey()method routes to correct shard based on UUID- Transaction operations don't need to know about shards
- Rollback works across all shards involved
ID-First Storage (v6.0.0+)
✅ Fully Compatible
Transactions work with direct ID-first paths - no type routing needed!
// Entities stored with direct ID-first paths
const personId = await brain.add({
data: { name: 'John Doe' },
type: NounType.Person // → entities/nouns/{shard}/{id}/metadata.json
})
const orgId = await brain.add({
data: { name: 'Acme Corp' },
type: NounType.Organization // → entities/nouns/{shard}/{id}/metadata.json
})
// Type changes handled atomically (type is just metadata)
await brain.update({
id: personId,
type: NounType.Organization, // Type change
data: { name: 'Doe Corp' }
})
How It Works:
- Type information stored in metadata.noun field
- Storage layer uses O(1) ID-first path construction
- No type cache needed (removed in v6.0.0)
- Type counters adjusted on commit/rollback
- 40x faster on cloud storage (eliminates 42-type search)
Distributed Storage
✅ Fully Compatible
Transactions work with distributed/remote storage:
// Works with S3, Azure, GCS, etc.
const brain = new Brainy({
storage: {
type: 's3Compatible',
config: { /* S3 config */ }
}
})
// Transactions ensure atomicity at write coordinator level
await brain.add({ data: { name: 'Remote Entity' }, type: NounType.Thing })
How It Works:
- Transactions operate through
StorageAdapterinterface - Remote storage adapters implement same interface
- Atomicity guaranteed at write-coordinator level
- Read-after-write consistency maintained
Design Philosophy:
- Single-node writes (most common): Fully atomic ✅
- Distributed reads + centralized writes: Transactions on primary ✅
- Multi-primary: Transactions per-instance, cross-instance via coordinator ✅
Examples
Basic Add Operation
import { Brainy } from '@soulcraft/brainy'
import { NounType } from '@soulcraft/brainy/types'
const brain = new Brainy()
await brain.init()
// Automatically uses transaction
const id = await brain.add({
data: { name: 'Alice', role: 'Engineer' },
type: NounType.Person
})
// If add fails, all changes rolled back automatically
Update with Type Change
// Original entity
const id = await brain.add({
data: { name: 'John Smith', category: 'individual' },
type: NounType.Person
})
// Update with type change (atomic)
await brain.update({
id,
type: NounType.Organization, // Type change
data: { name: 'Smith Corp', category: 'business' }
})
// If update fails, original type and data restored
Creating Relationships
const personId = await brain.add({
data: { name: 'Alice' },
type: NounType.Person
})
const projectId = await brain.add({
data: { name: 'Project X' },
type: NounType.Thing
})
// Create relationship (atomic)
await brain.relate({
from: personId,
to: projectId,
type: VerbType.WorksOn
})
// If relate fails, no partial relationship created
Batch Operations
// Multiple operations, all atomic
for (let i = 0; i < 100; i++) {
await brain.add({
data: { name: `Entity ${i}`, index: i },
type: NounType.Thing
})
}
// Each add() is a separate transaction
// If any add fails, only that specific add is rolled back
Delete with Cascade
const personId = await brain.add({
data: { name: 'Bob' },
type: NounType.Person
})
const projectId = await brain.add({
data: { name: 'Project Y' },
type: NounType.Thing
})
await brain.relate({
from: personId,
to: projectId,
type: VerbType.WorksOn
})
// Delete person (atomic - deletes entity + relationships)
await brain.delete(personId)
// If delete fails, both entity and relationships remain
Error Handling
Transactions automatically handle errors and rollback:
try {
await brain.add({
data: { name: 'Test Entity' },
type: NounType.Thing,
vector: [1, 2, 3] // Wrong dimension → error
})
} catch (error) {
// Transaction automatically rolled back
// No partial data in storage or indexes
console.error('Add failed:', error.message)
}
Common Error Scenarios:
- Invalid vector dimension: Automatic rollback
- Type validation failure: Automatic rollback
- Storage write failure: Automatic rollback
- Index update failure: Automatic rollback
Performance Considerations
Transaction Overhead
MEASURED Performance Impact:
- Average overhead: ~2-5ms per transaction (measured:
tests/transaction/transaction.bench.ts) - Operations per transaction: 2-8 (metadata + data + indexes)
- Rollback cost: ~1-3ms (restore previous state)
Optimization:
- Operations executed sequentially (not parallel) for consistency
- Rollback only happens on failure (success path is fast)
- Index updates batched within transaction
Statistics and Monitoring
// Get transaction statistics
const stats = brain.transactionManager?.getStats()
console.log(stats)
// {
// totalTransactions: 1234,
// successfulTransactions: 1200,
// failedTransactions: 34,
// rollbacks: 34,
// averageOperationsPerTransaction: 4.2
// }
Metrics Available:
totalTransactions: Total number of transactions executedsuccessfulTransactions: Number of successful commitsfailedTransactions: Number of rollbacksrollbacks: Total rollback countaverageOperationsPerTransaction: Average operations per transaction
Best Practices
1. Let Brainy Handle Transactions
// ✅ Recommended: Use Brainy's API (transactions automatic)
await brain.add({ data, type })
await brain.update({ id, data })
await brain.delete(id)
// ❌ Avoid: Direct storage access bypasses transactions
await brain.storage.saveNoun(noun) // No transaction protection
2. Handle Errors Gracefully
// ✅ Recommended: Catch errors, transaction rolls back automatically
try {
const id = await brain.add({ data, type })
return id
} catch (error) {
console.error('Add failed, rolled back:', error)
// Decide how to handle (retry, log, alert user)
}
3. Validate Before Operations
// ✅ Recommended: Validate early to avoid unnecessary rollbacks
if (!isValidVector(vector, brain.dimension)) {
throw new Error(`Vector must have ${brain.dimension} dimensions`)
}
await brain.add({ data, type, vector })
4. Monitor Transaction Statistics
// ✅ Recommended: Monitor in production
setInterval(() => {
const stats = brain.transactionManager?.getStats()
if (stats) {
const failureRate = stats.failedTransactions / stats.totalTransactions
if (failureRate > 0.05) { // > 5% failure rate
console.warn('High transaction failure rate:', failureRate)
}
}
}, 60000) // Check every minute
5. Understand Atomicity Guarantees
What Transactions GUARANTEE:
- ✅ Atomicity within single Brainy instance
- ✅ Consistent state across all indexes
- ✅ Automatic rollback on failure
- ✅ Works with all storage adapters (local, remote, COW, sharded)
What Transactions DON'T Provide:
- ❌ Two-phase commit across multiple Brainy instances
- ❌ Distributed locking across nodes
- ❌ Cross-datacenter ACID guarantees
Design: Transactions ensure atomicity at the write coordinator level. For multi-instance scenarios, use DistributedCoordinator.
Testing Transactions
Unit Tests
import { describe, it, expect } from 'vitest'
import { Brainy } from '@soulcraft/brainy'
describe('Transaction Tests', () => {
it('should rollback on failure', async () => {
const brain = new Brainy()
await brain.init()
const id1 = await brain.add({ data: { name: 'Entity 1' }, type: NounType.Thing })
try {
await brain.add({
data: null as any, // Invalid - will fail
type: NounType.Thing
})
} catch (e) {
// Expected failure
}
// First entity should still exist (rollback didn't affect it)
const entity1 = await brain.get(id1)
expect(entity1).toBeTruthy()
})
})
Integration Tests
See tests/transaction/integration/ for comprehensive integration tests covering:
- COW integration (
cow-transactions.test.ts) - Sharding integration (
sharding-transactions.test.ts) - TypeAware integration (
typeaware-transactions.test.ts) - Distributed storage integration (
distributed-transactions.test.ts)
Troubleshooting
High Rollback Rate
Symptom: failedTransactions / totalTransactions > 5%
Possible Causes:
- Invalid vector dimensions
- Type validation errors
- Storage write failures (disk full, network issues)
- Index corruption
Solutions:
- Validate data before operations
- Check storage adapter health
- Monitor disk space and network connectivity
- Review error logs for patterns
Slow Transaction Performance
Symptom: Operations take > 100ms per transaction
Possible Causes:
- Large metadata objects
- Remote storage latency
- Many indexes enabled
- Disk I/O bottleneck
Solutions:
- Optimize metadata size
- Use local caching for remote storage
- Disable unused indexes
- Use SSD storage
Transaction Statistics Missing
Symptom: brain.transactionManager?.getStats() returns undefined
Cause: TransactionManager not initialized
Solution:
// Ensure Brainy is initialized
await brain.init()
// Then access stats
const stats = brain.transactionManager?.getStats()
Architecture Details
Transaction Lifecycle
1. BEGIN
↓
2. ADD OPERATIONS
- SaveNounMetadataOperation
- SaveNounOperation
- UpdateGraphIndexOperation
↓
3. EXECUTE (sequential)
- Execute operation 1 → Success
- Execute operation 2 → Success
- Execute operation 3 → FAILURE
↓
4. ROLLBACK (reverse order)
- Undo operation 2
- Undo operation 1
↓
5. THROW ERROR
Operation Types
| Operation | Description | Undo Behavior |
|---|---|---|
SaveNounMetadataOperation |
Save entity metadata | Restore previous metadata or delete if new |
SaveNounOperation |
Save entity data | Restore previous data or delete if new |
UpdateGraphIndexOperation |
Update graph index | Restore previous index state |
SaveVerbMetadataOperation |
Save relationship metadata | Restore previous metadata or delete if new |
SaveVerbOperation |
Save relationship data | Restore previous data or delete if new |
Storage Adapter Integration
Transactions use the StorageAdapter interface:
interface StorageAdapter {
saveNounMetadata(id: string, metadata: NounMetadata): Promise<void>
saveNoun(noun: Noun): Promise<void>
deleteNounMetadata(id: string): Promise<void>
deleteNoun(id: string): Promise<void>
// ... other methods
}
Key Insight: All storage adapters (filesystem, S3, Azure, GCS, memory) implement this interface. Transactions work with any storage adapter automatically.
Additional Resources
- Unit Tests:
tests/transaction/transaction.test.ts(36 passing tests) - Integration Tests:
tests/transaction/integration/(35 test scenarios) - Compatibility Analysis:
.strategy/TRANSACTION_COMPATIBILITY_ANALYSIS.md(internal) - Performance Benchmarks:
tests/transaction/transaction.bench.ts
Version History
- v5.8.0: Initial transaction system release
- Atomic operations with rollback
- Compatible with COW, sharding, type-aware, distributed
- 36/36 unit tests passing
- 35 integration test scenarios
Summary
Brainy's transaction system provides production-ready atomic operations with automatic rollback. It works transparently with all Brainy APIs and is fully compatible with advanced features like COW, sharding, type-aware storage, and distributed storage.
Key Takeaways:
- ✅ Automatic: No manual transaction management needed
- ✅ Atomic: All operations succeed or all rollback
- ✅ Compatible: Works with all storage adapters and features
- ✅ Production-Ready: Tested with 71 test scenarios (36 unit + 35 integration)
- ✅ Performant: ~2-5ms overhead per transaction (measured)
Start using transactions today - they're already built into brain.add(), brain.update(), brain.delete(), and brain.relate()!