snapshot_aggregate_root 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/Gemfile +4 -0
- data/README.md +34 -0
- data/Rakefile +4 -0
- data/lib/snapshot_aggregate_root.rb +113 -0
- data/lib/snapshot_aggregate_root/configuration.rb +14 -0
- data/lib/snapshot_aggregate_root/default_apply_strategy.rb +25 -0
- data/lib/snapshot_aggregate_root/version.rb +3 -0
- data/snapshot_aggregate_root.gemspec +31 -0
- metadata +167 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d356e58389d1bb33fdd4462cb9b2df873d94c83e
|
4
|
+
data.tar.gz: 558e2953a0d60cbd5880b9d18748e3f33f7a8fd3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: afbf998b72579cae65db5451109cb5fa8173c0963e2782f2553b1e0adc774ad386f42b665f268344df43a5d4ed59bb150d995738517255807d10028068f9c916
|
7
|
+
data.tar.gz: 4ba0fda9221166df43c470d99065f4283251626bc5a02700577c9536f06341dbaf1e18a8661c46e35c802fc9df6b20a4865a10456f6a9a6bb58bf42b0d384abb
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# SnapshotAggregateRoot
|
2
|
+
|
3
|
+
An extension of https://github.com/arkency/aggregate_root with support for concurrent writers.
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
### Snapshots
|
8
|
+
|
9
|
+
Snapshots are created automatically as required by the `#store` method. Implementations of this class must implement
|
10
|
+
2 methods.
|
11
|
+
|
12
|
+
* `#build_snapshot`
|
13
|
+
* `#apply_snapshot(snapshot)`
|
14
|
+
|
15
|
+
Optionally Implementations may also override
|
16
|
+
|
17
|
+
* `#snapshot_threshold` - Override how often snapshots are taken
|
18
|
+
* `#requires_snapshot?`- Provide a custom implementation for when snapshots a required
|
19
|
+
|
20
|
+
|
21
|
+
### Concurrent Writers
|
22
|
+
|
23
|
+
In order to be safe for current writes snapshot aggregate root exposes a `#with_write_context(stream_name, event_store:)` method. This method loads the aggregate, applies the block, then stores the events within a mutex so that only one concurrent writer can execute the block. Event handlers are triggered after the command is complete
|
24
|
+
|
25
|
+
The entire command handler for a snapshot aggregate root should be executed within this block eg.
|
26
|
+
|
27
|
+
```
|
28
|
+
def apply_command(command)
|
29
|
+
SomeAggregate.new.with_write_context(command.aggregate_id) do |aggregate|
|
30
|
+
aggregate.do_command(command)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
```
|
34
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
require 'snapshot_aggregate_root/version'
|
3
|
+
require 'snapshot_aggregate_root/configuration'
|
4
|
+
require 'snapshot_aggregate_root/default_apply_strategy'
|
5
|
+
|
6
|
+
module SnapshotAggregateRoot
|
7
|
+
attr_accessor :events_since_snapshot
|
8
|
+
|
9
|
+
def apply(event)
|
10
|
+
apply_strategy.(self, event)
|
11
|
+
unpublished_events << event
|
12
|
+
end
|
13
|
+
|
14
|
+
def with_lock(stream_name, event_store: default_event_store, &block)
|
15
|
+
event_store.with_lock(stream_name, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def with_write_context(stream_name, event_store: default_event_store)
|
19
|
+
with_lock(stream_name, event_store: event_store) do
|
20
|
+
load(stream_name, event_store: event_store)
|
21
|
+
yield self
|
22
|
+
store(stream_name, event_store: event_store)
|
23
|
+
end
|
24
|
+
notify(event_store: event_store)
|
25
|
+
end
|
26
|
+
|
27
|
+
def load(stream_name, event_store: default_event_store)
|
28
|
+
@loaded_from_stream_name = stream_name
|
29
|
+
|
30
|
+
snapshot = event_store.last_stream_snapshot(stream_name)
|
31
|
+
if snapshot
|
32
|
+
apply_snapshot(snapshot)
|
33
|
+
events = event_store.read_events_forward(stream_name, start: snapshot.event_id, count: 0)
|
34
|
+
else
|
35
|
+
events = event_store.read_events_forward(stream_name, count: 0)
|
36
|
+
end
|
37
|
+
|
38
|
+
events.each(&method(:apply))
|
39
|
+
self.events_since_snapshot = events.count
|
40
|
+
@unpublished_events = nil
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
def store(stream_name = loaded_from_stream_name, event_store: default_event_store)
|
45
|
+
self.events_since_snapshot += @unpublished_events.count
|
46
|
+
unpublished_events.each do |event|
|
47
|
+
event_store.append_to_stream(event, stream_name: stream_name)
|
48
|
+
unnotified_events.push(event)
|
49
|
+
end
|
50
|
+
@unpublished_events = nil
|
51
|
+
if requires_snapshot?
|
52
|
+
snapshot!(stream_name, event_store: event_store)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def notify(event_store: )
|
57
|
+
unnotified_events.each do |event|
|
58
|
+
event_store.notify_subscribers(event)
|
59
|
+
end
|
60
|
+
@unnotified_events = nil
|
61
|
+
end
|
62
|
+
|
63
|
+
def events_since_snapshot
|
64
|
+
@events_since_snapshot || 0
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
attr_reader :loaded_from_stream_name
|
70
|
+
|
71
|
+
# This method must be implemented by consumers of this module
|
72
|
+
#
|
73
|
+
# Returns an Event
|
74
|
+
def build_snapshot
|
75
|
+
raise "build_snapshot not implemented in #{self}"
|
76
|
+
end
|
77
|
+
|
78
|
+
# This method must be implemented by consumers of this module
|
79
|
+
#
|
80
|
+
# Returns an Event
|
81
|
+
def apply_snapshot(snapshot)
|
82
|
+
raise "apply_snapshot not implemented in #{self}"
|
83
|
+
end
|
84
|
+
|
85
|
+
def requires_snapshot?
|
86
|
+
events_since_snapshot >= snapshot_threshold
|
87
|
+
end
|
88
|
+
|
89
|
+
def snapshot_threshold
|
90
|
+
50
|
91
|
+
end
|
92
|
+
|
93
|
+
def snapshot!(stream_name, event_store:)
|
94
|
+
event = build_snapshot
|
95
|
+
event_store.publish_snapshot(event, stream_name: stream_name)
|
96
|
+
end
|
97
|
+
|
98
|
+
def unpublished_events
|
99
|
+
@unpublished_events ||= []
|
100
|
+
end
|
101
|
+
|
102
|
+
def unnotified_events
|
103
|
+
@unnotified_events ||= []
|
104
|
+
end
|
105
|
+
|
106
|
+
def apply_strategy
|
107
|
+
DefaultApplyStrategy.new
|
108
|
+
end
|
109
|
+
|
110
|
+
def default_event_store
|
111
|
+
SnapshotAggregateRoot.configuration.default_event_store
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module SnapshotAggregateRoot
|
2
|
+
class << self
|
3
|
+
attr_accessor :configuration
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.configure
|
7
|
+
self.configuration ||= Configuration.new
|
8
|
+
yield(configuration)
|
9
|
+
end
|
10
|
+
|
11
|
+
class Configuration
|
12
|
+
attr_accessor :default_event_store
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module SnapshotAggregateRoot
|
2
|
+
MissingHandler = Class.new(StandardError)
|
3
|
+
|
4
|
+
class DefaultApplyStrategy
|
5
|
+
def initialize(strict: true)
|
6
|
+
@strict = strict
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(aggregate, event)
|
10
|
+
name = handler_name(event)
|
11
|
+
if aggregate.respond_to?(name, true)
|
12
|
+
aggregate.method(name).call(event)
|
13
|
+
else
|
14
|
+
raise MissingHandler.new("Missing handler method #{name} on aggregate #{aggregate.class}") if strict
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
attr_reader :strict
|
20
|
+
|
21
|
+
def handler_name(event)
|
22
|
+
"apply_#{event.class.name.demodulize.underscore}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'snapshot_aggregate_root/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'snapshot_aggregate_root'
|
8
|
+
spec.version = SnapshotAggregateRoot::VERSION
|
9
|
+
spec.licenses = ['MIT']
|
10
|
+
spec.authors = ['Gareth Andrew']
|
11
|
+
spec.email = ['gingerhendrix@gmail.com']
|
12
|
+
|
13
|
+
spec.summary = %q{Event sourced aggregate root implementation with concurrent writer and snapshot support}
|
14
|
+
spec.description = %q{Event sourced aggregate root implementation with concurrent writer and snapshot support}
|
15
|
+
spec.homepage = 'https://github.com/gingerhendrix/snapshot_aggregate_root'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = 'exe'
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ['lib']
|
21
|
+
|
22
|
+
spec.add_development_dependency 'bundler', '~> 1.9'
|
23
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
24
|
+
spec.add_development_dependency 'pry'
|
25
|
+
spec.add_development_dependency 'rspec'
|
26
|
+
spec.add_development_dependency 'rails', '~> 4.2.1'
|
27
|
+
spec.add_development_dependency 'transaction_event_store_mongoid', '~> 0.0.1'
|
28
|
+
|
29
|
+
spec.add_dependency 'activesupport', '>= 3.0'
|
30
|
+
spec.add_dependency 'transaction_event_store', '~> 0.0.1'
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: snapshot_aggregate_root
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Gareth Andrew
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-07-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.9'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.9'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pry
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rails
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 4.2.1
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 4.2.1
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: transaction_event_store_mongoid
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.0.1
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.0.1
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: activesupport
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '3.0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '3.0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: transaction_event_store
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 0.0.1
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 0.0.1
|
125
|
+
description: Event sourced aggregate root implementation with concurrent writer and
|
126
|
+
snapshot support
|
127
|
+
email:
|
128
|
+
- gingerhendrix@gmail.com
|
129
|
+
executables: []
|
130
|
+
extensions: []
|
131
|
+
extra_rdoc_files: []
|
132
|
+
files:
|
133
|
+
- ".gitignore"
|
134
|
+
- Gemfile
|
135
|
+
- README.md
|
136
|
+
- Rakefile
|
137
|
+
- lib/snapshot_aggregate_root.rb
|
138
|
+
- lib/snapshot_aggregate_root/configuration.rb
|
139
|
+
- lib/snapshot_aggregate_root/default_apply_strategy.rb
|
140
|
+
- lib/snapshot_aggregate_root/version.rb
|
141
|
+
- snapshot_aggregate_root.gemspec
|
142
|
+
homepage: https://github.com/gingerhendrix/snapshot_aggregate_root
|
143
|
+
licenses:
|
144
|
+
- MIT
|
145
|
+
metadata: {}
|
146
|
+
post_install_message:
|
147
|
+
rdoc_options: []
|
148
|
+
require_paths:
|
149
|
+
- lib
|
150
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
151
|
+
requirements:
|
152
|
+
- - ">="
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: '0'
|
155
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
requirements: []
|
161
|
+
rubyforge_project:
|
162
|
+
rubygems_version: 2.5.1
|
163
|
+
signing_key:
|
164
|
+
specification_version: 4
|
165
|
+
summary: Event sourced aggregate root implementation with concurrent writer and snapshot
|
166
|
+
support
|
167
|
+
test_files: []
|