aggregate_root 2.6.0 → 2.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c8b5d964bf3266c0353d5ab5f0c3131c4f7ec006a86bf32efa9230b08396d8b
4
- data.tar.gz: 8ca9aa71d57707f8f956a2230eb70369be02558812b77918a81c38648f1a91c8
3
+ metadata.gz: 49a93ca40dc58a4ddfe19ac4836cbc1b77d63a5ed8fd804f55413696bb2a6c5c
4
+ data.tar.gz: 3e3a4614d3b3c4444c21529f17bb106d65391b8d48c193b9ebc39e954125467c
5
5
  SHA512:
6
- metadata.gz: 911d1311973bed3bd58a015c2063917311c5a658593b3c67c0dd82cec5e7e415513ee795dfdd6dc17026570b3cd8b4bb40db49f958987b891a01e1eb209a9d8d
7
- data.tar.gz: 7404ee7fd26f2b39e1dd5df58062bcbc28a650f6c79e68c6d814aad2e8b5fa7aa768cf7dbc5fdc4ac1eaf4669a700c039e768657251450d70857965149a0dc4b
6
+ metadata.gz: d9c9ca99d24ed3523a3c21ccec297bd1ccd3afc2be6836063f53331fe4b59b202dcba2fe3160d0f9b4f2d417d142a0f7a23bffc2342c47a1335d6509224d83b2
7
+ data.tar.gz: 36e601598b6754cb8f4b120044bd45ee91b7729f094b7d2084f5a4075a3c12e2015abe89c1ea6e37961d5c8280691d538ddf926f4c1c4a6501dc8442ffcc2c5b
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AggregateRoot
4
- VERSION = "2.6.0"
4
+ VERSION = "2.7.0"
5
5
  end
@@ -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.6.0
4
+ version: 2.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arkency
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-11-29 00:00:00.000000000 Z
11
+ date: 2022-12-19 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.6.0
19
+ version: 2.7.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.6.0
26
+ version: 2.7.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