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.
@@ -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
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ gem 'safe_ruby_event_store', path: '../safe_ruby_event_store'
@@ -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
+
@@ -0,0 +1,4 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+ RSpec::Core::RakeTask.new(:spec)
4
+ task default: :spec
@@ -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,3 @@
1
+ module SnapshotAggregateRoot
2
+ VERSION = '0.5.0'
3
+ 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: []