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: 3c8b5d964bf3266c0353d5ab5f0c3131c4f7ec006a86bf32efa9230b08396d8b
4
- data.tar.gz: 8ca9aa71d57707f8f956a2230eb70369be02558812b77918a81c38648f1a91c8
3
+ metadata.gz: d826b6ed4e196ffabc267132bae45d6a42040f16006c9c0feb3eed559f1046b9
4
+ data.tar.gz: d89c1cbd5a35e0c7d2f1b8921671de56d428e0af8a5fae42a144dd444b0e5fc6
5
5
  SHA512:
6
- metadata.gz: 911d1311973bed3bd58a015c2063917311c5a658593b3c67c0dd82cec5e7e415513ee795dfdd6dc17026570b3cd8b4bb40db49f958987b891a01e1eb209a9d8d
7
- data.tar.gz: 7404ee7fd26f2b39e1dd5df58062bcbc28a650f6c79e68c6d814aad2e8b5fa7aa768cf7dbc5fdc4ac1eaf4669a700c039e768657251450d70857965149a0dc4b
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AggregateRoot
4
- VERSION = "2.6.0"
4
+ VERSION = "2.8.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.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: 2022-11-29 00:00:00.000000000 Z
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.6.0
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.6.0
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