event_store 0.1.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.
@@ -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