Generation Tracking
Generation tracking enables optimistic concurrency control and versioning of objects. This is particularly useful for collaborative applications where multiple users might edit the same data.
Overview
Generation tracking allows you to:
- Track multiple versions of the same object
- Detect concurrent modifications
- Implement optimistic locking
- Maintain a complete history of changes
- Recover previous versions when needed
How It Works
ID Format
Objects are stored with IDs in the format: {base_id}.{generation}
base_id: The unique identifier (typically a UUID)generation: Version number starting from 0
Example files on disk:
data/
└── Note/
├── note-123e4567-e89b-12d3-a456-426614174000.0
├── note-123e4567-e89b-12d3-a456-426614174000.1
└── note-123e4567-e89b-12d3-a456-426614174000.2
Atomic Operations
Each save_with_generation creates a new file, ensuring:
- No data corruption during writes
- Previous versions remain intact
- Easy rollback to any version
API Reference
Instance Methods
generation : Int32
Returns the generation number extracted from the object's ID.
note = Note.load("note-xxx.2")
note.generation # => 2
base_id : String
Returns the base ID without the generation suffix.
note = Note.load("note-xxx.2")
note.base_id # => "note-xxx"
save_with_generation : self
Creates a new version with incremented generation number.
note = Note.load("note-xxx.1")
new_note = note.save_with_generation
new_note.sepia_id # => "note-xxx.2"
stale?(expected_generation : Int32) : Bool
Checks if a newer version exists.
note = Note.load("note-xxx.1")
note.stale?(1) # Returns true if note-xxx.2 exists
Class Methods
latest(base_id : String) : self?
Returns the newest version of an object.
latest = Note.latest("note-xxx")
latest.generation # Highest generation number
versions(base_id : String) : Array(self)
Returns all versions sorted by generation.
versions = Note.versions("note-xxx")
versions.map(&.generation) # => [0, 1, 2, ...]
exists?(id : String) : Bool
Checks if an object with the given ID exists.
Note.exists?("note-xxx.1") # => true
Note.exists?("note-xxx.99") # => false
Use Cases
Collaborative Editing
class Document < Sepia::Object
include Sepia::Serializable
property content : String
property last_modified : Time
# ... implement to_sepia/from_sepia
end
# When a user opens a document
doc = Document.load("doc-123.3")
user_generation = doc.generation
# When saving
if doc.stale?(user_generation)
# Show merge dialog
latest = Document.latest(doc.base_id)
# Merge content
else
doc.save_with_generation
end
Version History
# Show document history
versions = Document.versions("doc-123")
versions.each do |version|
puts "Version #{version.generation}: #{version.last_modified}"
end
# Restore specific version
doc = Document.load("doc-123.2")
current = doc.save_with_generation # Creates version 3 from version 2
Audit Trail
class LogEntry < Sepia::Object
include Sepia::Serializable
property action : String
property user_id : String
property timestamp : Time
property data : String
# ... serialization methods
end
# Each log change creates a new version
entry = LogEntry.new("update", "user1", Time.now, data_json)
entry.save_with_generation
# Query all changes
all_changes = LogEntry.versions("log-entry-456")
Performance Considerations
Storage Usage
- Each version consumes additional disk space
- Consider cleanup strategies for old versions
Lookup Performance
latest()scans all files to find highest generation- Consider caching for frequently accessed objects
Cleanup Strategies
# Keep only last N versions
def keep_recent_versions(base_id, max_versions = 10)
versions = MyClass.versions(base_id)
if versions.size > max_versions
versions[0..-(max_versions+1)].each(&:delete)
end
end
# Keep versions newer than date
def keep_recent_by_date(base_id, cutoff_date = Time.now - 30.days)
versions = MyClass.versions(base_id)
versions.select { |v| v.saved_at < cutoff_date }.each(&:delete)
end
Best Practices
- Always check for staleness before saving in collaborative scenarios
- Use meaningful base IDs that reflect the object's identity
- Implement cleanup for long-running applications
- Handle merge conflicts gracefully with user-friendly UI
- Consider using GenerationInfo module to include generation data in JSON
Migration from Non-Generation IDs
Existing objects without generation suffix automatically work:
- Treated as generation 0
- Can be versioned normally
- No migration required
# Old code still works
old_obj = MyClass.load("old-id")
old_obj.generation # => 0
# Start versioning
new_obj = old_obj.save_with_generation
new_obj.generation # => 1