Skip to content

Commit

Permalink
Merge pull request #60 from Sandthorn/snapshot_support
Browse files Browse the repository at this point in the history
Snapshot support
  • Loading branch information
hallgren authored Jun 14, 2018
2 parents 75aebb6 + 3d9f4fd commit a2f8f15
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 38 deletions.
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,50 @@ end

In this case, the resulting events from the commands `new` and `mark` will have the trace `{ip: :127.0.0.1}` attached to them.

### `Sandthorn::AggregateRoot.unsaved_events?`

Check if there are unsaved events attached to the aggregate.

```ruby
board = Board.new
board.mark :o, 0, 1
board.unsaved_events?
=> true
```

## Snapshot

If there is a lot of events saved to an aggregate it can take some time to reload the current state of the aggregate via the `.find` method. This is because all events belonging to the aggregate has to be fetched and iterated one by one to build its current state. The snapshot functionality makes it possible to store the current aggregate state and re-use it when loading the aggregate. The snapshot is used as a cache where only the events that has occurred after the snapshot has to be fetched and used to build the current state of the aggregate.

There is one global snapshot store where all snapshots are stored independent on aggregate_type. To enable snapshot on a aggregate_type the Class has to be added to the `snapshot_types` Array when configuring Sandthorn. The aggregate will now be stored to the snapshot_store on every `.save` and when using `.find` it will look for a snapshot of the requested aggregate.

Currently its only possible to store the snapshots in memory, so be careful not draining your applications memory space.


```ruby

class Board
include Sandthorn::AggregateRoot
end

Sandthorn.configure do |c|
c.snapshot_types = [Board]
end
```

Its also possible to take manual snapshots without enabling snapshots on the aggregate_type.

```ruby
board = Board.new
board.save

# Save snapshot of the board aggregate
Sandthorn.save_snapshot board

# Get snapshot
snapshot = Sandthorn.find_snapshot board.aggregate_id
```

## Bounded Context

A bounded context is a system divider that split large systems into smaller parts. [Bounded Context by Martin Fowler](http://martinfowler.com/bliki/BoundedContext.html)
Expand Down
25 changes: 21 additions & 4 deletions lib/sandthorn.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
require "sandthorn/errors"
require "sandthorn/aggregate_root"
require "sandthorn/event_stores"
require "sandthorn/snapshot_store"
require 'yaml'
require 'securerandom'

Expand All @@ -10,6 +11,7 @@ class << self
extend Forwardable

def_delegators :configuration, :event_stores
def_delegators :configuration, :snapshot_store

def default_event_store
event_stores.default_store
Expand Down Expand Up @@ -39,12 +41,17 @@ def all aggregate_type
event_store_for(aggregate_type).all(aggregate_type)
end

def find aggregate_id, aggregate_type
event_store_for(aggregate_type).find(aggregate_id, aggregate_type)
def find aggregate_id, aggregate_type, after_aggregate_version = 0
event_store_for(aggregate_type).find(aggregate_id, aggregate_type, after_aggregate_version)
end

def save_snapshot(aggregate)
raise "Not Implemented"
def save_snapshot aggregate
raise Errors::SnapshotError, "Can't take snapshot on object with unsaved events" if aggregate.unsaved_events?
snapshot_store.save aggregate.aggregate_id, aggregate
end

def find_snapshot aggregate_id
return snapshot_store.find aggregate_id
end

def find_event_store(name)
Expand Down Expand Up @@ -84,6 +91,16 @@ def map_types= data
@event_stores.map_types data
end

def snapshot_store
@snapshot_store ||= SnapshotStore.new
end

def snapshot_types= aggregate_types
aggregate_types.each do |aggregate_type|
aggregate_type.snapshot(true)
end
end

alias_method :event_stores=, :event_store=
end
end
Expand Down
28 changes: 22 additions & 6 deletions lib/sandthorn/aggregate_root_base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,19 @@ def save
@aggregate_originating_version = @aggregate_current_event_version
end

Sandthorn.save_snapshot self if self.class.snapshot

self
end

def ==(other)
other.respond_to?(:aggregate_id) && aggregate_id == other.aggregate_id
end

def unsaved_events?
aggregate_events.any?
end

def aggregate_trace args
@aggregate_trace_information = args
yield self if block_given?
Expand Down Expand Up @@ -70,9 +76,17 @@ def event_store(event_store = nil)
end
end

def snapshot(value = nil)
if value
@snapshot = value
else
@snapshot
end
end

def all
Sandthorn.all(self).map { |events|
aggregate_build events
aggregate_build events, nil
}
end

Expand All @@ -83,12 +97,14 @@ def find id

def aggregate_find aggregate_id
begin
events = Sandthorn.find(aggregate_id, self)
unless events && !events.empty?
aggregate_from_snapshot = Sandthorn.find_snapshot(aggregate_id) if self.snapshot
current_aggregate_version = aggregate_from_snapshot.nil? ? 0 : aggregate_from_snapshot.aggregate_current_event_version
events = Sandthorn.find(aggregate_id, self, current_aggregate_version)
if aggregate_from_snapshot.nil? && events.empty?
raise Errors::AggregateNotFound
end

return aggregate_build events
return aggregate_build events, aggregate_from_snapshot
rescue Exception
raise Errors::AggregateNotFound
end
Expand All @@ -111,8 +127,8 @@ def new *args, &block

end

def aggregate_build events
aggregate = create_new_empty_aggregate
def aggregate_build events, aggregate_from_snapshot = nil
aggregate = aggregate_from_snapshot || create_new_empty_aggregate

if events.any?
current_aggregate_version = events.last[:aggregate_version]
Expand Down
22 changes: 0 additions & 22 deletions lib/sandthorn/aggregate_root_snapshot.rb

This file was deleted.

17 changes: 17 additions & 0 deletions lib/sandthorn/snapshot_store.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module Sandthorn
class SnapshotStore
def initialize
@store = Hash.new
end

attr_reader :store

def save key, value
@store[key] = value
end

def find key
@store[key]
end
end
end
11 changes: 11 additions & 0 deletions spec/aggregate_root_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ def no_state_change_only_empty_event
end
end

describe "::snapshot" do
let(:klass) { Class.new { include Sandthorn::AggregateRoot } }
it "is available as a class method" do
expect(klass).to respond_to(:snapshot)
end
it "sets the snapshot to true and returns it" do
klass.snapshot(true)
expect(klass.snapshot).to eq(true)
end
end

describe "when get all aggregates from DirtyClass" do

before(:each) do
Expand Down
68 changes: 68 additions & 0 deletions spec/snapshot_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
require 'spec_helper'

module Sandthorn
module Snapshot
class KlassOne
include Sandthorn::AggregateRoot
snapshot true
end

class KlassTwo
include Sandthorn::AggregateRoot
end

class KlassThree
include Sandthorn::AggregateRoot
end

describe "::snapshot" do
before do
Sandthorn.configure do |c|
c.snapshot_types = [KlassTwo]
end
end
it "snapshot should be enabled on KlassOne and KlassTwo but not KlassThree" do
expect(KlassOne.snapshot).to be_truthy
expect(KlassTwo.snapshot).to be_truthy
expect(KlassThree.snapshot).not_to be_truthy
end
end

describe "find snapshot on snapshot enabled aggregate" do
let(:klass) { KlassOne.new.save }

it "should find on snapshot enabled Class" do
copy = KlassOne.find klass.aggregate_id
expect(copy.aggregate_version).to eql(klass.aggregate_version)
end

it "should get saved snapshot" do
copy = Sandthorn.find_snapshot klass.aggregate_id
expect(copy.aggregate_version).to eql(klass.aggregate_version)
end

end

describe "save and find snapshot on snapshot disabled aggregate" do
let(:klass) { KlassThree.new.save }

it "should not find snapshot" do
snapshot = Sandthorn.find_snapshot klass.aggregate_id
expect(snapshot).to be_nil
end

it "should save and get saved snapshot" do
Sandthorn.save_snapshot klass
snapshot = Sandthorn.find_snapshot klass.aggregate_id
expect(snapshot).not_to be_nil

#Check by key on the snapshot_store hash
expect(Sandthorn.snapshot_store.store.has_key?(klass.aggregate_id)).to be_truthy

end

end

end
end

8 changes: 2 additions & 6 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,13 @@ def class_including(mod)
def url
"sqlite://spec/db/sequel_driver.sqlite3"
end

def sqlite_store_setup

SandthornDriverSequel.migrate_db url: url

driver = SandthornDriverSequel.driver_from_url(url: url) do |conf|
conf.event_serializer = Proc.new { |data| YAML::dump(data) }
conf.event_deserializer = Proc.new { |data| YAML::load(data) }
end

Sandthorn.configure do |c|
c.event_store = driver
c.event_store = SandthornDriverSequel.driver_from_url(url: url)
end

end

0 comments on commit a2f8f15

Please sign in to comment.