entity_store 1.2.0 → 1.5.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: 02a69a4134e4ef998a3258424ed1ff35e171babf83d72665ab37464aaba1abbe
4
- data.tar.gz: 2b9af465f21d0131c71b80cd3a9e5fb08579ebfbe27e9c2595e45b9ccffba622
3
+ metadata.gz: d4575519de291e09a6f02085a07daeb09818edc01a5d8895204d443fc03db6b9
4
+ data.tar.gz: ce27f46beb087f7bcd48b6d5e690db1239e75adaffdcb524b1c24df0cbeea49f
5
5
  SHA512:
6
- metadata.gz: 893058a17b8cef182d8a7349ad03107b00361d66af25837f44846aa8daed83cf9bbc38cef57874a753e3d79e7bf47e8c66a4612a25564a893f5188fde7e7119a
7
- data.tar.gz: 8b10d320996be80cfc718fa709339132261104b0edb7b6fe3b286757dd5ec9f3535400d1214a6d7af4ef21abc4fb612e47ef0bbdfde9c99a290c34bee226cc48
6
+ metadata.gz: de737107e012f7a07b5aea26280b425a6db374e13325054344e7c9fac196ee98bde2dbcc7d231fc0cd28b26992d805188a06d727373ef7c46e3552598c9c6e52
7
+ data.tar.gz: a624bb070c990b68b0a436d07c940f656238a244a80bfe2c789f3bb7feb4f8de0e66c65696496d75b936557e5bb4465466cd1e5f05f1b4583e8cf3bab3cc31ee
@@ -4,9 +4,11 @@ module EntityStore
4
4
  # Stores
5
5
  attr_accessor :store, :feed_store
6
6
 
7
+ attr_accessor :cache_event_subscribers
8
+
7
9
  # Allows config to pass in a lambda or Proc to use as the type loader in place
8
- # of the default.
9
- # Original use case was migration of entity classes to new module namespace when
10
+ # of the default.
11
+ # Original use case was migration of entity classes to new module namespace when
10
12
  # extracting to a shared library
11
13
  attr_accessor :type_loader
12
14
 
@@ -17,15 +19,15 @@ module EntityStore
17
19
  yield self
18
20
 
19
21
  raise StandardError.new("store not assigned") unless store
20
- store.open
22
+ store.open
21
23
  feed_store.open if feed_store
22
24
  end
23
-
25
+
24
26
  def event_subscribers
25
27
  @_event_subscribers ||=[]
26
28
  end
27
-
28
- # Public - indicates the version increment that is used to
29
+
30
+ # Public - indicates the version increment that is used to
29
31
  # decided whether a snapshot of an entity should be created when it's saved
30
32
  def snapshot_threshold
31
33
  @_snapshot_threshold ||= 10
@@ -35,7 +37,6 @@ module EntityStore
35
37
  @_snapshot_threshold = value
36
38
  end
37
39
 
38
-
39
40
  def load_type(type_name)
40
41
  if EntityStore::Config.type_loader
41
42
  EntityStore::Config.type_loader.call(type_name)
@@ -50,4 +51,4 @@ module EntityStore
50
51
  end
51
52
 
52
53
  end
53
- end
54
+ end
@@ -4,11 +4,19 @@ module EntityStore
4
4
 
5
5
  ALL_METHOD = :all_events
6
6
 
7
+ def initialize(event_subscribers = nil)
8
+ @_event_subscribers = event_subscribers if event_subscribers
9
+ end
10
+
11
+ def event_subscribers
12
+ @_event_subscribers || EntityStore::Config.event_subscribers
13
+ end
14
+
7
15
  def publish(entity_type, event)
8
- publish_to_feed entity_type, event
16
+ publish_to_feed(entity_type, event)
9
17
 
10
- subscribers_to(event.receiver_name).each do |s| send_to_subscriber s, event.receiver_name, event end
11
- subscribers_to_all.each do |s| send_to_subscriber s, ALL_METHOD, event end
18
+ subscribers_to(event.receiver_name).each { |s| send_to_subscriber(s, event.receiver_name, event) }
19
+ subscribers_to_all.each { |s| send_to_subscriber(s, ALL_METHOD, event) }
12
20
  end
13
21
 
14
22
  def send_to_subscriber subscriber, receiver_name, event
@@ -19,18 +27,48 @@ module EntityStore
19
27
  end
20
28
 
21
29
  def subscribers_to(event_name)
22
- subscribers.select { |s| s.instance_methods.include?(event_name.to_sym) }
30
+ subscriber_lookup[event_name.to_sym].dup
23
31
  end
24
32
 
25
33
  def subscribers_to_all
26
- subscribers.select { |s| s.instance_methods.include?(ALL_METHOD) }
34
+ subscribers_to(ALL_METHOD)
35
+ end
36
+
37
+ def subscriber_lookup_cache
38
+ @@lookup_cache ||= Hash.new
39
+ end
40
+
41
+ def subscriber_lookup
42
+ return generate_subscriber_lookup unless EntityStore::Config.cache_event_subscribers
43
+
44
+ @lookup ||= begin
45
+ lookup_cache_key = event_subscribers.map(&:to_s).join
46
+
47
+ if subscriber_lookup_cache[lookup_cache_key]
48
+ subscriber_lookup_cache[lookup_cache_key]
49
+ else
50
+ subscriber_lookup_cache[lookup_cache_key] = generate_subscriber_lookup
51
+ end
52
+ end
53
+ end
54
+
55
+ def generate_subscriber_lookup
56
+ lookup = Hash.new { |h, k| h[k] = Array.new }
57
+
58
+ subscribers.each do |s|
59
+ s.instance_methods.each do |m|
60
+ lookup[m] << s
61
+ end
62
+ end
63
+
64
+ lookup
27
65
  end
28
66
 
29
67
  def subscribers
30
- EntityStore::Config.event_subscribers.map do |subscriber|
68
+ event_subscribers.map do |subscriber|
31
69
  case subscriber
32
70
  when String
33
- Utils.get_type_constant(subscriber)
71
+ EntityStore::Config.load_type(subscriber)
34
72
  else
35
73
  subscriber
36
74
  end
@@ -46,17 +84,17 @@ module EntityStore
46
84
  end
47
85
 
48
86
  # Public - replay events of a given type to a given subscriber
49
- #
87
+ #
50
88
  # since - Time reference point
51
89
  # type - String type name of event
52
- # subscriber - Class of the subscriber to replay events to
53
- #
90
+ # subscriber - Class of the subscriber to replay events to
91
+ #
54
92
  # Returns nothing
55
93
  def replay(since, type, subscriber)
56
94
  max_items = 100
57
95
  event_data_objects = feed_store.get_events(since, type, max_items)
58
96
 
59
- while event_data_objects.count > 0 do
97
+ while event_data_objects.count > 0 do
60
98
  event_data_objects.each do |event_data_object|
61
99
  begin
62
100
  event = EntityStore::Config.load_type(event_data_object.type).new(event_data_object.attrs)
@@ -1,7 +1,7 @@
1
1
  module EntityStore
2
2
  class NotFound < StandardError
3
- def initialise(id)
3
+ def initialize(id)
4
4
  super("no item with id #{id} could be found")
5
5
  end
6
6
  end
7
- end
7
+ end
@@ -2,10 +2,19 @@ module EntityStore
2
2
  class Store
3
3
  include Logging
4
4
 
5
+ def initialize(storage_client = nil, event_bus = nil)
6
+ @_storage_client = storage_client if storage_client
7
+ @_event_bus = event_bus if event_bus
8
+ end
9
+
5
10
  def storage_client
6
11
  @_storage_client ||= EntityStore::Config.store
7
12
  end
8
13
 
14
+ def event_bus
15
+ @_event_bus ||= EventBus.new
16
+ end
17
+
9
18
  def add(entity)
10
19
  entity.id = storage_client.add_entity(entity)
11
20
  add_events(entity)
@@ -31,8 +40,10 @@ module EntityStore
31
40
  entity.id = storage_client.add_entity(entity)
32
41
  end
33
42
 
34
- add_events(entity) do
35
- snapshot_entity(entity) if entity.snapshot_due?
43
+ added_events = add_events(entity)
44
+
45
+ if entity.snapshot_due? || added_events >= Config.snapshot_threshold
46
+ snapshot_entity(entity)
36
47
  end
37
48
 
38
49
  # publish version increment signal event to the bus
@@ -44,6 +55,33 @@ module EntityStore
44
55
  raise e
45
56
  end
46
57
 
58
+ # Upsert an entity where events have existed previously
59
+ # for example when migrating data
60
+ #
61
+ # Please note this method requires that the events expose their id property
62
+ # as a method named _id.
63
+ #
64
+ def upsert(entity)
65
+ unless entity.pending_events.empty?
66
+ entity.version = entity.pending_events.map(&:entity_version).max || 1
67
+
68
+ if entity.id
69
+ storage_client.save_entity(entity)
70
+ else
71
+ entity.id = storage_client.add_entity(entity)
72
+ end
73
+
74
+ upsert_events(entity)
75
+
76
+ # publish version increment signal event to the bus
77
+ event_bus.publish(entity.type, entity.generate_version_incremented_event)
78
+ end
79
+ entity
80
+ rescue => e
81
+ log_error "Store#upsert error: #{e.inspect} - #{entity.inspect}", e
82
+ raise e
83
+ end
84
+
47
85
  def snapshot_entity(entity)
48
86
  log_info { "Store#snapshot_entity : Snapshotting #{entity.id}"}
49
87
  storage_client.snapshot_entity(entity)
@@ -69,9 +107,22 @@ module EntityStore
69
107
  end
70
108
  storage_client.add_events(items)
71
109
 
72
- yield if block_given?
110
+ items.each { |e| event_bus.publish(entity.type, e) }
111
+
112
+ entity.clear_pending_events
113
+ items.count
114
+ end
115
+
116
+ def upsert_events(entity)
117
+ items = entity.pending_events.map do |event|
118
+ event.entity_id ||= entity.id.to_s
119
+ event.entity_version ||= entity.version
120
+ event
121
+ end
122
+
123
+ filtered_items = storage_client.upsert_events(items)
73
124
 
74
- items.each {|e| event_bus.publish(entity.type, e) }
125
+ filtered_items.each { |e| event_bus.publish(entity.type, e) }
75
126
 
76
127
  entity.clear_pending_events
77
128
  end
@@ -146,10 +197,6 @@ module EntityStore
146
197
  @_storage_client = nil
147
198
  end
148
199
 
149
- def event_bus
150
- @_event_bus ||= EventBus.new
151
- end
152
-
153
200
  # Public: returns an array representing a full audit trail for the entity.
154
201
  # After each event is applied the state of the entity is rendered.
155
202
  # Optionally accepts a block which should return true or false to indicate
@@ -1,3 +1,3 @@
1
1
  module EntityStore
2
- VERSION = "1.2.0".freeze
2
+ VERSION = "1.5.0".freeze
3
3
  end
data/lib/entity_store.rb CHANGED
@@ -1,6 +1,4 @@
1
1
  module EntityStore
2
-
3
- require_relative 'entity_store/utils'
4
2
  require_relative 'entity_store/logging'
5
3
  require_relative 'entity_store/config'
6
4
  require_relative 'entity_store/time_factory'
@@ -84,7 +84,7 @@ describe Entity do
84
84
  subject { DummyEntity.new(1) }
85
85
 
86
86
  it "should raise a readable error" do
87
- expect { subject }.to raise_error(RuntimeError, "Do not know how to create DummyEntity from Fixnum")
87
+ expect { subject }.to raise_error(RuntimeError, /\ADo not know how to create DummyEntity from (Integer|Fixnum)\z/)
88
88
  end
89
89
  end
90
90
  end
@@ -74,6 +74,39 @@ describe Store do
74
74
 
75
75
  end
76
76
 
77
+ describe "#upsert_events" do
78
+ before(:each) do
79
+ @entity = DummyEntityForStore.new(:name => random_string)
80
+ @entity.id = random_string
81
+ @entity.version = random_integer
82
+ @entity.pending_events << double(Event, :entity_id => @entity.id, :entity_version => @entity.version)
83
+ @entity.pending_events << double(Event, :entity_id => @entity.id, :entity_version => @entity.version)
84
+ @entity.pending_events << double(Event, :entity_id => @entity.id, :entity_version => @entity.version)
85
+ @storage_client = double("StorageClient", :upsert_events => filtered_events)
86
+ @store = Store.new
87
+ @store.stub(:storage_client) { @storage_client }
88
+ @event_bus = double(EventBus, :publish => true)
89
+ @store.stub(:event_bus) { @event_bus}
90
+ end
91
+
92
+ subject { @store.upsert_events(@entity) }
93
+
94
+ let(:filtered_events) { @entity.pending_events.take(2) }
95
+
96
+ it "adds each of the events" do
97
+ @storage_client.should_receive(:upsert_events).with(@entity.pending_events)
98
+ subject
99
+ end
100
+
101
+ it "publishes each event to the EventBus" do
102
+ filtered_events.each do |e|
103
+ @event_bus.should_receive(:publish).with(@entity.type, e)
104
+ end
105
+ subject
106
+ end
107
+
108
+ end
109
+
77
110
  describe "#save" do
78
111
 
79
112
  before(:each) do
@@ -81,7 +114,7 @@ describe Store do
81
114
  @entity = DummyEntityForStore.new(:id => random_string)
82
115
  @storage_client = double("StorageClient", :save_entity => true)
83
116
  @store = Store.new
84
- @store.stub(:add_events).and_yield
117
+ @store.stub(:add_events) { |entity| entity.pending_events.count }
85
118
  @store.stub(:storage_client) { @storage_client }
86
119
  @entity.stub(:pending_events) { [ double('Event') ] }
87
120
  end
@@ -161,8 +194,23 @@ describe Store do
161
194
  subject
162
195
  end
163
196
  end
164
- end
165
197
 
198
+ context "when flushed events exceeds snapshotting threshold" do
199
+ before(:each) do
200
+ @entity.version = 1
201
+ @entity.stub(:pending_events) do
202
+ (1..EntityStore::Config.snapshot_threshold).map do
203
+ double('Event')
204
+ end
205
+ end
206
+ end
207
+
208
+ it "should snapshot the entity" do
209
+ @storage_client.should_receive(:snapshot_entity).with(@entity)
210
+ subject
211
+ end
212
+ end
213
+ end
166
214
  end
167
215
 
168
216
  describe "getters" do
@@ -26,7 +26,17 @@ class DummyEntitySubscriber
26
26
  end
27
27
 
28
28
  def dummy_entity_name_set(event)
29
- DummyEntitySubscriber.event_name = event.name
29
+ self.class.event_name = event.name
30
+ end
31
+ end
32
+
33
+ class AnotherEntitySubscriber
34
+ class << self
35
+ attr_accessor :event_name
36
+ end
37
+
38
+ def dummy_entity_name_set(event)
39
+ self.class.event_name = event.name
30
40
  end
31
41
  end
32
42
 
@@ -80,6 +90,49 @@ class DummyStore
80
90
  end
81
91
  end
82
92
 
93
+ describe "creation without static instances" do
94
+ let(:store) do
95
+ storage_client = DummyStore.new
96
+ Store.new(storage_client, event_bus)
97
+ end
98
+
99
+ let(:event_bus) do
100
+ event_subscribers = []
101
+ event_subscribers << AnotherEntitySubscriber
102
+
103
+ EventBus.new(event_subscribers)
104
+ end
105
+
106
+ before do
107
+ EntityStore::Config.setup do |config|
108
+ config.store = DummyStore.new
109
+ config.event_subscribers << DummyEntitySubscriber
110
+ end
111
+ end
112
+
113
+ context "when save entity" do
114
+ let(:name) { random_string }
115
+ before(:each) do
116
+ @entity = DummyEntity.new
117
+ @entity.set_name name
118
+ @id = store.save @entity
119
+ end
120
+
121
+ it "does not publish event to the non configured subscriber" do
122
+ DummyEntitySubscriber.event_name.should_not eq(name)
123
+ end
124
+ it "publishes event to the subscriber" do
125
+ AnotherEntitySubscriber.event_name.should eq(name)
126
+ end
127
+ it "is retrievable with the events applied" do
128
+ store.get(@entity.id).name.should eq(name)
129
+ end
130
+ it "is not retrievable from the static store instance" do
131
+ EntityStore::Store.new.get(@entity.id).should eq(nil)
132
+ end
133
+ end
134
+ end
135
+
83
136
  describe "end to end" do
84
137
  before(:each) do
85
138
  EntityStore::Config.setup do |config|
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: entity_store
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Bird
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-03-12 00:00:00.000000000 Z
11
+ date: 2022-06-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bson
@@ -57,7 +57,6 @@ files:
57
57
  - lib/entity_store/not_found.rb
58
58
  - lib/entity_store/store.rb
59
59
  - lib/entity_store/time_factory.rb
60
- - lib/entity_store/utils.rb
61
60
  - lib/entity_store/version.rb
62
61
  - lib/tasks/entity_store.rake
63
62
  - spec/entity_store/config_spec.rb
@@ -87,8 +86,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
87
86
  - !ruby/object:Gem::Version
88
87
  version: '0'
89
88
  requirements: []
90
- rubyforge_project:
91
- rubygems_version: 2.7.4
89
+ rubygems_version: 3.2.33
92
90
  signing_key:
93
91
  specification_version: 4
94
92
  summary: Event sourced entity store with a replaceable body
@@ -1,8 +0,0 @@
1
- module EntityStore
2
- module Utils
3
- def self.get_type_constant(type_name)
4
- type_name.split('::').inject(Object) { |obj, name| obj.const_get(name) }
5
- end
6
- end
7
- end
8
-