event_store 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,16 @@
1
+ module EventStore
2
+ class TimeHacker
3
+ class << self
4
+ #Hack around various DB adapters that hydrate dates from the db into the local ruby timezone
5
+ def translate_occurred_at_from_local_to_gmt(occurred_at)
6
+ if occurred_at.class == Time
7
+ #expecting "2001-02-03 01:26:40 -0700"
8
+ Time.parse(occurred_at.to_s.gsub(/\s[+-]\d+$/, ' UTC'))
9
+ elsif occurred_at.class == DateTime
10
+ #expecting "2001-02-03T01:26:40+00:00"
11
+ Time.parse(occurred_at.iso8601.gsub('T', ' ').gsub(/[+-]\d{2}\:\d{2}/, ' UTC'))
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ module EventStore
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,34 @@
1
+ require 'event_store'
2
+ require 'benchmark'
3
+
4
+ # db_config = Hash[
5
+ # :username => 'nexia',
6
+ # :password => 'Password1',
7
+ # host: 'ec2-54-221-80-232.compute-1.amazonaws.com',
8
+ # encoding: 'utf8',
9
+ # pool: 1000,
10
+ # adapter: :postgres,
11
+ # database: 'event_store_performance'
12
+ # ]
13
+ # EventStore.connect ( db_config )
14
+ EventStore.connect :adapter => :postgres, :database => 'event_store_performance', host: 'localhost'
15
+ EventStore.redis_connect host: 'localhost'
16
+
17
+ ITERATIONS = 1000
18
+
19
+ Benchmark.bmbm do |x|
20
+ x.report "Time to read #{ITERATIONS} Event Snapshots" do
21
+ ITERATIONS.times do
22
+ EventStore::Client.new(rand(200) + 1, :device).snapshot
23
+ end
24
+ end
25
+ end
26
+
27
+ Benchmark.bmbm do |x|
28
+ x.report "Time to read #{ITERATIONS} Event Streams" do
29
+ ITERATIONS.times do
30
+ EventStore::Client.new(rand(200) + 1, :device).event_stream
31
+ end
32
+ end
33
+ end
34
+
@@ -0,0 +1,48 @@
1
+ lib = File.expand_path('../../../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ require 'event_store'
5
+
6
+ EventStore.connect :adapter => :postgres, :database => 'event_store_performance', :host => 'localhost'
7
+
8
+ iterations = 100
9
+ loads_per_iteration = 100
10
+
11
+ # GC.stat output explanation
12
+ # COUNT: the number of times a GC ran (both full GC and lazy sweep are
13
+ # included)
14
+ # HEAP_USED: the number of heaps that have more than 0 slots used in them. The
15
+ # larger this number, the slower your GC will be.
16
+ # HEAP_LENGTH: the total number of heaps allocated in memory. For example 1648
17
+ # means - about 25.75MB is allocated to Ruby heaps. (1648 * (2 << 13)).to_f /
18
+ # (2 << 19)
19
+ # HEAP_INCREMENT: Is the number of extra heaps to be allocated, next time Ruby
20
+ # grows the number of heaps (as it does after it runs a GC and discovers it
21
+ # does not have enough free space), this number is updated each GC run to be
22
+ # 1.8 * heap_used. In later versions of Ruby this multiplier is configurable.
23
+ # HEAP_LIVE_NUM: This is the running number objects in Ruby heaps, it will
24
+ # change every time you call GC.stat
25
+ # HEAP_FREE_NUM: This is a slightly confusing number, it changes after a GC
26
+ # runs, it will let you know how many objects were left in the heaps after the
27
+ # GC finished running. So, in this example we had 102447 slots empty after the
28
+ # last GC. (it also increased when objects are recycled internally - which can
29
+ # happen between GCs)
30
+ # HEAP_FINAL_NUM: Is the count of objects that were not finalized during the
31
+ # last GC
32
+ # TOTAL_ALLOCATED_OBJECT: The running total of allocated objects from the
33
+ # beginning of the process. This number will change every time you allocate
34
+ # objects. Note: in a corner case this value may overflow.
35
+ # TOTAL_FREED_OBJECT: The number of objects that were freed by the GC from the
36
+ # beginning of the process.
37
+
38
+ def output_gc_data
39
+ puts GC.stat
40
+ end
41
+
42
+ iterations.times do
43
+ loads_per_iteration.times do
44
+ client = EventStore::Client.new(rand(300)+1, :device)
45
+ client.snapshot
46
+ end
47
+ output_gc_data
48
+ end
@@ -0,0 +1,45 @@
1
+ require 'event_store'
2
+
3
+ # db_config = Hash[
4
+ # :username => 'postgres',
5
+ # :password => 'Password1',
6
+ # host: 'ec2-54-221-80-232.compute-1.amazonaws.com',
7
+ # encoding: 'utf8',
8
+ # pool: 1000,
9
+ # adapter: :postgres,
10
+ # database: 'event_store_performance'
11
+ # ]
12
+ # EventStore.connect ( db_config )
13
+ # EventStore.connect :adapter => :postgres, :database => 'event_store_performance', host: 'localhost'
14
+ EventStore.connect :adapter => :vertica, :database => 'nexia_history', host: '192.168.180.65', username: 'dbadmin', password: 'password'
15
+ EventStore.redis_connect host: 'localhost'
16
+
17
+
18
+ DEVICES = 1000
19
+ EVENTS_PER_DEVICE = 5_000
20
+ EVENT_TYPES = 1000
21
+
22
+ event_types = Array.new(EVENT_TYPES) { |i| "event_type_#{i}" }
23
+
24
+ EventStore.redis.flushall
25
+
26
+ puts "Creating #{DEVICES} Aggregates with #{EVENTS_PER_DEVICE} events each. There are #{EVENT_TYPES} types of events."
27
+
28
+ (1..DEVICES).each do |device_id|
29
+ client = EventStore::Client.new(device_id, :device)
30
+ records = []
31
+
32
+ EVENTS_PER_DEVICE.times do |version|
33
+ records << EventStore::Event.new(device_id.to_s, Time.now, event_types.sample, rand(9999999999999).to_s(2), version)
34
+ end
35
+
36
+ puts "Appending #{EVENTS_PER_DEVICE} events for #{device_id} of #{DEVICES} Aggregates."
37
+ start_time = Time.now
38
+ client.append(records)
39
+ end_time = Time.now
40
+ total_time = end_time - start_time
41
+ puts "Success! (Total Time: #{total_time} = #{(EVENTS_PER_DEVICE) / total_time} inserts per second)"
42
+ end
43
+
44
+
45
+
@@ -0,0 +1,287 @@
1
+ require 'spec_helper'
2
+
3
+ describe EventStore::Client do
4
+ let(:es_client) { EventStore::Client }
5
+
6
+ before do
7
+ client_1 = es_client.new('1', :device)
8
+ client_2 = es_client.new('2', :device)
9
+
10
+ events_by_aggregate_id = {'1' => [], '2' => []}
11
+ @event_time = Time.parse("2001-01-01 00:00:00 UTC")
12
+ ([1]*10 + [2]*10).shuffle.each_with_index do |aggregate_id, version|
13
+ events_by_aggregate_id[aggregate_id.to_s] << EventStore::Event.new(aggregate_id.to_s, @event_time, 'event_name', "#{234532.to_s(2)}_foo}", version)
14
+ end
15
+ client_1.append events_by_aggregate_id['1']
16
+ client_2.append events_by_aggregate_id['2']
17
+ end
18
+
19
+ describe '#raw_event_stream' do
20
+ it "should be an array of hashes that represent database records, not EventStore::SerializedEvent objects" do
21
+ raw_stream = es_client.new(1, :device).raw_event_stream
22
+ raw_stream.class.should == Array
23
+ raw_event = raw_stream.first
24
+ raw_event.class.should == Hash
25
+ raw_event.keys.should == [:id, :version, :aggregate_id, :fully_qualified_name, :occurred_at, :serialized_event]
26
+ end
27
+
28
+ it 'should be empty for aggregates without events' do
29
+ stream = es_client.new(100, :device).raw_event_stream
30
+ expect(stream.empty?).to be_true
31
+ end
32
+
33
+ it 'should only have events for a single aggregate' do
34
+ stream = es_client.new(1, :device).raw_event_stream
35
+ stream.each { |event| event[:aggregate_id].should == '1' }
36
+ end
37
+
38
+ it 'should have all events for that aggregate' do
39
+ stream = es_client.new(1, :device).raw_event_stream
40
+ expect(stream.count).to eq(10)
41
+ end
42
+ end
43
+
44
+ describe '#event_stream' do
45
+ it "should be an array of EventStore::SerializedEvent objects" do
46
+ stream = es_client.new(1, :device).event_stream
47
+ stream.class.should == Array
48
+ event = stream.first
49
+ event.class.should == EventStore::SerializedEvent
50
+ end
51
+
52
+ it 'should be empty for aggregates without events' do
53
+ stream = es_client.new(100, :device).raw_event_stream
54
+ expect(stream.empty?).to be_true
55
+ end
56
+
57
+ it 'should only have events for a single aggregate' do
58
+ raw_stream = es_client.new(1, :device).raw_event_stream
59
+ stream = es_client.new(1, :device).event_stream
60
+ stream.map(&:fully_qualified_name).should == raw_stream.inject([]){|m, event| m << event[:fully_qualified_name]; m}
61
+ end
62
+
63
+ it 'should have all events for that aggregate' do
64
+ stream = es_client.new(1, :device).event_stream
65
+ expect(stream.count).to eq(10)
66
+ end
67
+ end
68
+
69
+
70
+ describe '#raw_event_streams_from_version' do
71
+ subject { es_client.new(1, :device) }
72
+
73
+ it 'should return all the raw events in the stream starting from a certain version' do
74
+ minimum_event_version = 2
75
+ raw_stream = subject.raw_event_stream_from(minimum_event_version)
76
+ event_versions = raw_stream.inject([]){|m, event| m << event[:version]; m}
77
+ event_versions.min.should >= minimum_event_version
78
+ end
79
+
80
+ it 'should return no more than the maximum number of events specified above the ' do
81
+ max_number_of_events = 5
82
+ minimum_event_version = 2
83
+ raw_stream = subject.raw_event_stream_from(minimum_event_version, max_number_of_events)
84
+ expect(raw_stream.count).to eq(max_number_of_events)
85
+ end
86
+
87
+ it 'should be empty for version above the current highest version number' do
88
+ raw_stream = subject.raw_event_stream_from(subject.version + 1)
89
+ expect(raw_stream).to be_empty
90
+ end
91
+ end
92
+
93
+ describe 'event_stream_from_version' do
94
+ subject { es_client.new(1, :device) }
95
+
96
+ it 'should return all the raw events in the stream starting from a certain version' do
97
+ minimum_event_version = 2
98
+ raw_stream = subject.raw_event_stream_from(minimum_event_version)
99
+ event_versions = raw_stream.inject([]){|m, event| m << event[:version]; m}
100
+ event_versions.min.should >= minimum_event_version
101
+ end
102
+
103
+ it 'should return no more than the maximum number of events specified above the ' do
104
+ max_number_of_events = 5
105
+ minimum_event_version = 2
106
+ raw_stream = subject.raw_event_stream_from(minimum_event_version, max_number_of_events)
107
+ expect(raw_stream.count).to eq(max_number_of_events)
108
+ end
109
+
110
+ it 'should be empty for version above the current highest version number' do
111
+ raw_stream = subject.raw_event_stream_from(subject.version + 1)
112
+ raw_stream.should == []
113
+ end
114
+ end
115
+
116
+ describe '#peek' do
117
+ let(:client) {es_client.new(1, :device)}
118
+ subject { client.peek }
119
+
120
+ it 'should return the last event in the event stream' do
121
+ last_event = EventStore.db.from(client.event_table).where(aggregate_id: '1').order(:version).last
122
+ subject.should == EventStore::SerializedEvent.new(last_event[:fully_qualified_name], last_event[:serialized_event].to_s, last_event[:version], @event_time)
123
+ end
124
+ end
125
+
126
+ describe '#append' do
127
+ before do
128
+ @client = EventStore::Client.new('1', :device)
129
+ @event = @client.peek
130
+ version = @client.version
131
+ @old_event = EventStore::Event.new('1', (@event_time - 2000).utc, "old", "#{1000.to_s(2)}_foo", version += 1)
132
+ @new_event = EventStore::Event.new('1', (@event_time - 1000).utc, "new", "#{1001.to_s(2)}_foo", version += 1)
133
+ @really_new_event = EventStore::Event.new('1', (@event_time + 100).utc, "really_new", "#{1002.to_s(2)}_foo", version += 1)
134
+ @duplicate_event = EventStore::Event.new('1', (@event_time).utc, 'duplicate', "#{12.to_s(2)}_foo", version += 1)
135
+ end
136
+
137
+ describe "when expected version number is greater than the last version" do
138
+ describe 'and there are no prior events of type' do
139
+ before do
140
+ @client.append([@old_event])
141
+ end
142
+
143
+ it 'should append a single event of a new type without raising an error' do
144
+ initial_count = @client.count
145
+ events = [@new_event]
146
+ @client.append(events)
147
+ @client.count.should == initial_count + events.length
148
+ end
149
+
150
+ it 'should append multiple events of a new type without raising and error' do
151
+ initial_count = @client.count
152
+ events = [@new_event, @new_event]
153
+ @client.append(events)
154
+ @client.count.should == initial_count + events.length
155
+ end
156
+
157
+ it "should increment the version number by the number of events added" do
158
+ events = [@new_event, @really_new_event]
159
+ initial_version = @client.version
160
+ @client.append(events)
161
+ @client.version.should == (initial_version + events.length)
162
+ end
163
+
164
+ it "should set the snapshot version number to match that of the last event in the aggregate's event stream" do
165
+ events = [@new_event, @really_new_event]
166
+ initial_stream_version = @client.raw_event_stream.last[:version]
167
+ @client.snapshot.last.version.should == initial_stream_version
168
+ @client.append(events)
169
+ updated_stream_version = @client.raw_event_stream.last[:version]
170
+ @client.snapshot.last.version.should == updated_stream_version
171
+ end
172
+
173
+ it "should write-through-cache the event in a snapshot without duplicating events" do
174
+ @client.destroy!
175
+ @client.append([@old_event, @new_event, @really_new_event])
176
+ @client.snapshot.should == @client.event_stream
177
+ end
178
+ end
179
+
180
+ describe 'with prior events of same type' do
181
+ it 'should raise a ConcurrencyError if the the event version is less than current version' do
182
+ @client.append([@duplicate_event])
183
+ reset_current_version_for(@client)
184
+ expect { @client.append([@duplicate_event]) }.to raise_error(EventStore::ConcurrencyError)
185
+ end
186
+
187
+ it 'should not raise an error when two events of the same type are appended' do
188
+ @client.append([@duplicate_event])
189
+ @duplicate_event[:version] += 1
190
+ @client.append([@duplicate_event]) #will fail automatically if it throws an error, no need for assertions (which now print warning for some reason)
191
+ end
192
+
193
+ it "should write-through-cache the event in a snapshot without duplicating events" do
194
+ @client.destroy!
195
+ @client.append([@old_event, @new_event, @new_event])
196
+ expected = []
197
+ expected << @client.event_stream.first
198
+ expected << @client.event_stream.last
199
+ @client.snapshot.should == expected
200
+ end
201
+
202
+ it "should increment the version number by the number of unique events added" do
203
+ events = [@old_event, @old_event, @old_event]
204
+ initial_version = @client.version
205
+ @client.append(events)
206
+ @client.version.should == (initial_version + events.uniq.length)
207
+ end
208
+
209
+ it "should set the snapshot version number to match that of the last event in the aggregate's event stream" do
210
+ events = [@old_event, @old_event]
211
+ initial_stream_version = @client.raw_event_stream.last[:version]
212
+ @client.snapshot.last.version.should == initial_stream_version
213
+ @client.append(events)
214
+ updated_stream_version = @client.raw_event_stream.last[:version]
215
+ @client.snapshot.last.version.should == updated_stream_version
216
+ end
217
+ end
218
+ end
219
+
220
+ describe 'transactional' do
221
+ before do
222
+ @bad_event = @new_event.dup
223
+ @bad_event.fully_qualified_name = nil
224
+ end
225
+
226
+ it 'should revert all append events if one fails' do
227
+ starting_count = @client.count
228
+ expect { @client.append([@new_event, @bad_event]) }.to raise_error(EventStore::AttributeMissingError)
229
+ expect(@client.count).to eq(starting_count)
230
+ end
231
+
232
+ it 'does not yield to the block if it fails' do
233
+ x = 0
234
+ expect { @client.append([@bad_event]) { x += 1 } }.to raise_error(EventStore::AttributeMissingError)
235
+ expect(x).to eq(0)
236
+ end
237
+
238
+ it 'yield to the block after event creation' do
239
+ x = 0
240
+ @client.append([]) { x += 1 }
241
+ expect(x).to eq(1)
242
+ end
243
+
244
+ it 'should pass the raw event_data to the block' do
245
+ @client.append([@new_event]) do |raw_event_data|
246
+ expect(raw_event_data).to eq([@new_event])
247
+ end
248
+ end
249
+ end
250
+
251
+ describe 'snapshot' do
252
+ before do
253
+ @client = es_client.new('10', :device)
254
+ @client.snapshot.length.should == 0
255
+ version = @client.version
256
+ @client.append %w{ e1 e2 e3 e1 e2 e4 e5 e2 e5 e4}.map {|fqn|EventStore::Event.new('10', Time.now.utc, fqn, 234532.to_s(2), version += 1)}
257
+ end
258
+
259
+ it "finds the most recent records for each type" do
260
+ version = @client.version
261
+ expected_snapshot = %w{ e1 e2 e3 e4 e5 }.map {|fqn| EventStore::SerializedEvent.new(fqn, 234532.to_s(2), version +=1 ) }
262
+ @client.event_stream.length.should == 10
263
+ actual_snapshot = @client.snapshot
264
+ actual_snapshot.length.should == 5
265
+ actual_snapshot.map(&:fully_qualified_name).should == ["e3", "e1", "e2", "e5", "e4"] #sorted by version no
266
+ actual_snapshot.map(&:serialized_event).should == expected_snapshot.map(&:serialized_event)
267
+ most_recent_events_of_each_type = {}
268
+ @client.event_stream.each do |e|
269
+ if most_recent_events_of_each_type[e.fully_qualified_name].nil? || most_recent_events_of_each_type[e.fully_qualified_name].version < e.version
270
+ most_recent_events_of_each_type[e.fully_qualified_name] = e
271
+ end
272
+ end
273
+ actual_snapshot.map(&:version).should == most_recent_events_of_each_type.values.map(&:version).sort
274
+ end
275
+
276
+ it "increments the version number of the snapshot when an event is appended" do
277
+ @client.snapshot.last.version.should == @client.raw_event_stream.last[:version]
278
+ end
279
+ end
280
+
281
+
282
+ def reset_current_version_for(client)
283
+ aggregate = client.instance_variable_get("@aggregate")
284
+ EventStore.redis.hset(aggregate.snapshot_version_table, :current_version, 1000)
285
+ end
286
+ end
287
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+ module EventStore
3
+ describe "Snapshots" do
4
+ let(:aggregate_id) {'100A' }
5
+ let(:new_aggregate) { Aggregate.new(aggregate_id) }
6
+ let(:client) { Client.new(aggregate_id) }
7
+ let(:appender) { EventAppender.new(new_aggregate) }
8
+ let(:events) { Array.new }
9
+
10
+ before do
11
+ @event_time = Time.parse("2001-01-01 00:00:00 UTC")
12
+ (0...10).to_a.each_with_index do |version|
13
+ events << EventStore::Event.new(aggregate_id, @event_time, 'event_name', "#{234532.to_s(2)}_foo}", version)
14
+ end
15
+ end
16
+
17
+ it "should build an empty snapshot for a new client" do
18
+ new_aggregate.snapshot.should == []
19
+ new_aggregate.version.should == -1
20
+ EventStore.redis.hget(new_aggregate.snapshot_version_table, :current_version).should == nil
21
+ end
22
+
23
+ it "should rebuild a snapshot after it is deleted" do
24
+ appender.append(events)
25
+ snapshot = new_aggregate.snapshot
26
+ version = new_aggregate.version
27
+ new_aggregate.delete_snapshot!
28
+ new_aggregate.rebuild_snapshot!
29
+ new_aggregate.snapshot.should == snapshot
30
+ end
31
+
32
+ it "a client should rebuild a snapshot" do
33
+ Aggregate.any_instance.should_receive(:delete_snapshot!)
34
+ Aggregate.any_instance.should_receive(:rebuild_snapshot!)
35
+ client.rebuild_snapshot!
36
+ end
37
+
38
+ it "should rebuild the snapshot if events exist, but the snapshot is empty" do
39
+ appender.append(events)
40
+ snapshot = new_aggregate.snapshot
41
+ new_aggregate.delete_snapshot!
42
+ new_aggregate.snapshot.should == snapshot
43
+ end
44
+ end
45
+ end