snapshot_aggregate_root 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []