aggregate_root 2.5.1 → 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: f75cec09db6eaf2240eb085574f991b1e2c64d66fcb295bb86edfc23bebcd466
4
- data.tar.gz: e624359318264e4bc1a37f5ddaeb0340d4cf8af2a347305cdd414e18ac6e10ba
3
+ metadata.gz: 49a93ca40dc58a4ddfe19ac4836cbc1b77d63a5ed8fd804f55413696bb2a6c5c
4
+ data.tar.gz: 3e3a4614d3b3c4444c21529f17bb106d65391b8d48c193b9ebc39e954125467c
5
5
  SHA512:
6
- metadata.gz: 97a4f3220f21427b373427340e7ed1d7fae73f8bb559fc34bef386f6a82b0ab15486dc2515ad84b99b34892bf5972ca4c474aeb2dcb4a3863f4da18a070135a2
7
- data.tar.gz: 03c61cd4a921c235dd216aae38bb937a4c80be1ed34342f558a8dec78c2d879718c626154aa202e132b57f3c269445c1110972af8ca5f10ce17d7088d8e53cef
6
+ metadata.gz: d9c9ca99d24ed3523a3c21ccec297bd1ccd3afc2be6836063f53331fe4b59b202dcba2fe3160d0f9b4f2d417d142a0f7a23bffc2342c47a1335d6509224d83b2
7
+ data.tar.gz: 36e601598b6754cb8f4b120044bd45ee91b7729f094b7d2084f5a4075a3c12e2015abe89c1ea6e37961d5c8280691d538ddf926f4c1c4a6501dc8442ffcc2c5b
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AggregateRoot
4
+ class InstrumentedApplyStrategy
5
+ def initialize(strategy, instrumentation)
6
+ @strategy = strategy
7
+ @instrumentation = instrumentation
8
+ end
9
+
10
+ def call(aggregate, event)
11
+ instrumentation.instrument("apply.aggregate_root",
12
+ aggregate: aggregate,
13
+ event: event) do
14
+ strategy.call(aggregate, event)
15
+ end
16
+ end
17
+
18
+ def method_missing(method_name, *arguments, &block)
19
+ if respond_to?(method_name)
20
+ strategy.public_send(method_name, *arguments, &block)
21
+ else
22
+ super
23
+ end
24
+ end
25
+
26
+ def respond_to_missing?(method_name, _include_private)
27
+ strategy.respond_to?(method_name)
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :instrumentation, :strategy
33
+ end
34
+ end
@@ -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.5.1"
4
+ VERSION = "2.7.0"
5
5
  end
@@ -6,6 +6,8 @@ require_relative "aggregate_root/transform"
6
6
  require_relative "aggregate_root/default_apply_strategy"
7
7
  require_relative "aggregate_root/repository"
8
8
  require_relative "aggregate_root/instrumented_repository"
9
+ require_relative "aggregate_root/instrumented_apply_strategy"
10
+ require_relative 'aggregate_root/snapshot_repository'
9
11
 
10
12
  module AggregateRoot
11
13
  module OnDSL
@@ -59,6 +61,21 @@ module AggregateRoot
59
61
  def unpublished_events
60
62
  @unpublished_events.each
61
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
62
79
  end
63
80
 
64
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.5.1
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-06-24 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.5.1
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.5.1
26
+ version: 2.7.0
27
27
  description:
28
28
  email: dev@arkency.com
29
29
  executables: []
@@ -35,8 +35,10 @@ files:
35
35
  - lib/aggregate_root.rb
36
36
  - lib/aggregate_root/configuration.rb
37
37
  - lib/aggregate_root/default_apply_strategy.rb
38
+ - lib/aggregate_root/instrumented_apply_strategy.rb
38
39
  - lib/aggregate_root/instrumented_repository.rb
39
40
  - lib/aggregate_root/repository.rb
41
+ - lib/aggregate_root/snapshot_repository.rb
40
42
  - lib/aggregate_root/transform.rb
41
43
  - lib/aggregate_root/version.rb
42
44
  homepage: https://railseventstore.org
@@ -63,7 +65,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
63
65
  - !ruby/object:Gem::Version
64
66
  version: '0'
65
67
  requirements: []
66
- rubygems_version: 3.3.7
68
+ rubygems_version: 3.3.26
67
69
  signing_key:
68
70
  specification_version: 4
69
71
  summary: Event sourced aggregate root implementation for RubyEventStore