aggregate_root 2.6.0 → 2.8.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
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d826b6ed4e196ffabc267132bae45d6a42040f16006c9c0feb3eed559f1046b9
|
4
|
+
data.tar.gz: d89c1cbd5a35e0c7d2f1b8921671de56d428e0af8a5fae42a144dd444b0e5fc6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e758436e49345ea06fdb8236bcfbcf0a644878113c74cc3a2adf9284b4d1d8c8f2492a7a43cd83ad02646b78cb4b8623c6df67e1440c2f7291afc546e601c472
|
7
|
+
data.tar.gz: 127b7d1fdb8966b027d5c55897de6d96d915d94a9307f56b664a80a6044e03495eae19fd1e869fe94a33bbe9bc84be7da013a39424b4bbca4164b1375913631e
|
@@ -1,10 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
2
|
+
require 'delegate'
|
3
3
|
module AggregateRoot
|
4
4
|
class InstrumentedRepository
|
5
5
|
def initialize(repository, instrumentation)
|
6
6
|
@repository = repository
|
7
7
|
@instrumentation = instrumentation
|
8
|
+
self.error_handler = method(:handle_error) if respond_to?(:error_handler=)
|
8
9
|
end
|
9
10
|
|
10
11
|
def load(aggregate, stream_name)
|
@@ -42,6 +43,14 @@ module AggregateRoot
|
|
42
43
|
|
43
44
|
private
|
44
45
|
|
46
|
+
def handle_error(error)
|
47
|
+
instrumentation.instrument(
|
48
|
+
"error_occured.repository.aggregate_root",
|
49
|
+
exception: [error.class.name, error.message],
|
50
|
+
exception_object: error,
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
45
54
|
attr_reader :instrumentation, :repository
|
46
55
|
end
|
47
56
|
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'base64'
|
3
|
+
require 'ruby_event_store/event'
|
4
|
+
|
5
|
+
module AggregateRoot
|
6
|
+
class SnapshotRepository
|
7
|
+
DEFAULT_SNAPSHOT_INTERVAL = 100.freeze
|
8
|
+
SNAPSHOT_STREAM_PATTERN = ->(base_stream_name) { "#{base_stream_name}_snapshots" }
|
9
|
+
NotRestorableSnapshot = Class.new(StandardError)
|
10
|
+
NotDumpableAggregateRoot = Class.new(StandardError)
|
11
|
+
|
12
|
+
def initialize(event_store, interval = DEFAULT_SNAPSHOT_INTERVAL)
|
13
|
+
raise ArgumentError, 'interval must be an Integer' unless interval.instance_of?(Integer)
|
14
|
+
raise ArgumentError, 'interval must be greater than 0' unless interval > 0
|
15
|
+
@event_store = event_store
|
16
|
+
@interval = interval
|
17
|
+
@error_handler = ->(_) { }
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_writer :error_handler
|
21
|
+
|
22
|
+
Snapshot = Class.new(RubyEventStore::Event)
|
23
|
+
|
24
|
+
def load(aggregate, stream_name)
|
25
|
+
last_snapshot = load_snapshot_event(stream_name)
|
26
|
+
query = event_store.read.stream(stream_name)
|
27
|
+
if last_snapshot
|
28
|
+
begin
|
29
|
+
aggregate = load_marshal(last_snapshot)
|
30
|
+
rescue NotRestorableSnapshot => e
|
31
|
+
error_handler.(e)
|
32
|
+
else
|
33
|
+
aggregate.version = last_snapshot.data.fetch(:version)
|
34
|
+
query = query.from(last_snapshot.data.fetch(:last_event_id))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
query.reduce { |_, ev| aggregate.apply(ev) }
|
38
|
+
aggregate.version = aggregate.version + aggregate.unpublished_events.count
|
39
|
+
aggregate
|
40
|
+
end
|
41
|
+
|
42
|
+
def store(aggregate, stream_name)
|
43
|
+
events = aggregate.unpublished_events.to_a
|
44
|
+
event_store.publish(events,
|
45
|
+
stream_name: stream_name,
|
46
|
+
expected_version: aggregate.version)
|
47
|
+
|
48
|
+
aggregate.version = aggregate.version + events.count
|
49
|
+
|
50
|
+
if time_for_snapshot?(aggregate.version, events.size)
|
51
|
+
begin
|
52
|
+
publish_snapshot_event(aggregate, stream_name, events.last.event_id)
|
53
|
+
rescue NotDumpableAggregateRoot => e
|
54
|
+
error_handler.(e)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
attr_reader :event_store, :interval, :error_handler
|
62
|
+
|
63
|
+
def publish_snapshot_event(aggregate, stream_name, last_event_id)
|
64
|
+
event_store.publish(
|
65
|
+
Snapshot.new(
|
66
|
+
data: { marshal: build_marshal(aggregate), last_event_id: last_event_id, version: aggregate.version }
|
67
|
+
),
|
68
|
+
stream_name: SNAPSHOT_STREAM_PATTERN.(stream_name)
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
def build_marshal(aggregate)
|
73
|
+
Marshal.dump(aggregate)
|
74
|
+
rescue TypeError
|
75
|
+
raise NotDumpableAggregateRoot, "#{aggregate.class} cannot be dumped.
|
76
|
+
It may be caused by instance variables being: bindings, procedure or method objects, instances of class IO, or singleton objects.
|
77
|
+
Snapshot skipped."
|
78
|
+
end
|
79
|
+
|
80
|
+
def load_snapshot_event(stream_name)
|
81
|
+
event_store.read.stream(SNAPSHOT_STREAM_PATTERN.(stream_name)).last
|
82
|
+
end
|
83
|
+
|
84
|
+
def load_marshal(snpashot_event)
|
85
|
+
Marshal.load(snpashot_event.data.fetch(:marshal))
|
86
|
+
rescue TypeError, ArgumentError
|
87
|
+
raise NotRestorableSnapshot, "Aggregate root cannot be restored from the last snapshot (event id: #{snpashot_event.event_id}).
|
88
|
+
It may be caused by aggregate class rename or Marshal version mismatch.
|
89
|
+
Loading aggregate based on the whole stream."
|
90
|
+
end
|
91
|
+
|
92
|
+
def time_for_snapshot?(aggregate_version, just_published_events)
|
93
|
+
events_in_stream = aggregate_version + 1
|
94
|
+
events_since_time_for_snapshot = events_in_stream % interval
|
95
|
+
just_published_events > events_since_time_for_snapshot
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
data/lib/aggregate_root.rb
CHANGED
@@ -7,6 +7,7 @@ require_relative "aggregate_root/default_apply_strategy"
|
|
7
7
|
require_relative "aggregate_root/repository"
|
8
8
|
require_relative "aggregate_root/instrumented_repository"
|
9
9
|
require_relative "aggregate_root/instrumented_apply_strategy"
|
10
|
+
require_relative 'aggregate_root/snapshot_repository'
|
10
11
|
|
11
12
|
module AggregateRoot
|
12
13
|
module OnDSL
|
@@ -60,6 +61,21 @@ module AggregateRoot
|
|
60
61
|
def unpublished_events
|
61
62
|
@unpublished_events.each
|
62
63
|
end
|
64
|
+
|
65
|
+
UNMARSHALED_VARIABLES = [:@version, :@unpublished_events]
|
66
|
+
|
67
|
+
def marshal_dump
|
68
|
+
instance_variables.reject{|m| UNMARSHALED_VARIABLES.include? m}.inject({}) do |vars, attr|
|
69
|
+
vars[attr] = instance_variable_get(attr)
|
70
|
+
vars
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def marshal_load(vars)
|
75
|
+
vars.each do |attr, value|
|
76
|
+
instance_variable_set(attr, value) unless UNMARSHALED_VARIABLES.include?(attr)
|
77
|
+
end
|
78
|
+
end
|
63
79
|
end
|
64
80
|
|
65
81
|
def self.with_default_apply_strategy
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: aggregate_root
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Arkency
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-01-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ruby_event_store
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - '='
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 2.
|
19
|
+
version: 2.8.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - '='
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 2.
|
26
|
+
version: 2.8.0
|
27
27
|
description:
|
28
28
|
email: dev@arkency.com
|
29
29
|
executables: []
|
@@ -38,6 +38,7 @@ files:
|
|
38
38
|
- lib/aggregate_root/instrumented_apply_strategy.rb
|
39
39
|
- lib/aggregate_root/instrumented_repository.rb
|
40
40
|
- lib/aggregate_root/repository.rb
|
41
|
+
- lib/aggregate_root/snapshot_repository.rb
|
41
42
|
- lib/aggregate_root/transform.rb
|
42
43
|
- lib/aggregate_root/version.rb
|
43
44
|
homepage: https://railseventstore.org
|