nexia_event_store 0.2.10

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,3 @@
1
+ module EventStore
2
+ VERSION = '0.2.10'
3
+ end
@@ -0,0 +1,142 @@
1
+ require 'sequel'
2
+ require 'sequel/core'
3
+ require 'vertica'
4
+ require 'sequel-vertica'
5
+ require 'redis'
6
+ require 'hiredis'
7
+ require 'event_store/version'
8
+ require 'event_store/time_hacker'
9
+ require 'event_store/event_appender'
10
+ require 'event_store/aggregate'
11
+ require 'event_store/client'
12
+ require 'event_store/errors'
13
+ require 'yaml'
14
+
15
+ Sequel.extension :migration
16
+
17
+ module EventStore
18
+ Event = Struct.new(:aggregate_id, :occurred_at, :fully_qualified_name, :serialized_event, :version)
19
+ SerializedEvent = Struct.new(:fully_qualified_name, :serialized_event, :version, :occurred_at)
20
+ SNAPSHOT_DELIMITER = "__NexEvStDelim__"
21
+
22
+ def self.db_config
23
+ raw_db_config[@environment.to_s][@adapter.to_s]
24
+ end
25
+
26
+ def self.raw_db_config
27
+ if @raw_db_config.nil?
28
+ file_path = File.expand_path(__FILE__ + '/../../db/database.yml')
29
+ @config_file = File.open(file_path,'r')
30
+ @raw_db_config = YAML.load(@config_file)
31
+ @config_file.close
32
+ end
33
+ @raw_db_config
34
+ end
35
+
36
+ def self.db
37
+ @db
38
+ end
39
+
40
+ def self.redis
41
+ @redis
42
+ end
43
+
44
+ def self.connect(*args)
45
+ @db ||= Sequel.connect(*args)
46
+ end
47
+
48
+ def self.redis_connect(config_hash)
49
+ @redis ||= Redis.new(config_hash)
50
+ end
51
+
52
+ def self.local_redis_config
53
+ @redis_connection ||= raw_db_config['redis']
54
+ end
55
+
56
+ def self.schema
57
+ @schema ||= raw_db_config[@environment][@adapter]['schema']
58
+ end
59
+
60
+ def self.table_name
61
+ @table_name ||= raw_db_config['table_name']
62
+ end
63
+
64
+ def self.fully_qualified_table
65
+ @fully_qualified_table ||= Sequel.lit "#{schema}.#{table_name}"
66
+ end
67
+
68
+ def self.connected?
69
+ !!EventStore.db
70
+ end
71
+
72
+ def self.clear!
73
+ return unless connected?
74
+ EventStore.db.from(fully_qualified_table).delete
75
+ EventStore.redis.flushdb
76
+ end
77
+
78
+ def self.postgres(environment = 'test', table_name = 'events', schema = 'event_store_test')
79
+ @schema = schema
80
+ @table_name = table_name
81
+ @environment = environment.to_s
82
+ @adapter = 'postgres'
83
+ @db_config ||= self.db_config
84
+ custom_config(@db_config, local_redis_config, @table_name, environment)
85
+ end
86
+
87
+ #To find the ip address of vertica on your local box (running in a vm)
88
+ #1. open Settings -> Network and select Wi-Fi
89
+ #2. open a terminal in the VM
90
+ #3. do /sbin/ifconfig (ifconfig is not in $PATH)
91
+ #4. the inet address for en0 is what you want
92
+ #Hint: if it just hangs, you have have the wrong IP
93
+ def self.vertica(environment = 'test', table_name = 'events', schema = 'event_store_test')
94
+ @schema = schema
95
+ @table_name = table_name
96
+ @environment = environment.to_s
97
+ @adapter = 'vertica'
98
+ @db_config ||= self.db_config
99
+ @db_config['host'] ||= ENV['VERTICA_HOST'] || vertica_host
100
+ custom_config(@db_config, local_redis_config, @table_name, environment)
101
+ end
102
+
103
+ def self.escape_bytea(binary_string)
104
+ binary_string.unpack('H*').join
105
+ end
106
+
107
+ def self.unescape_bytea(binary_string)
108
+ [binary_string].pack("H*")
109
+ end
110
+
111
+ def self.custom_config(database_config, redis_config, table_name = 'events', environment = 'production')
112
+ self.redis_connect(redis_config)
113
+ database_config = database_config.inject({}) {|memo, (k,v)| memo[k.to_s] = v; memo}
114
+ redis_config = redis_config.inject({}) {|memo, (k,v)| memo[k.to_s] = v; memo}
115
+
116
+ @adapter = database_config['adapter'].to_s
117
+ @environment = environment
118
+ @db_config = database_config
119
+ @table_name = table_name
120
+ @schema = database_config['schema'].to_s
121
+ connect_db
122
+ end
123
+
124
+ def self.migrations_dir
125
+ @adapter == 'vertica' ? 'migrations' : 'pg_migrations'
126
+ end
127
+
128
+ def self.connect_db
129
+ self.connect(@db_config)
130
+ end
131
+
132
+ def self.create_db
133
+ connect_db
134
+ table = "#{schema}__schema_info".to_sym
135
+ @db.run "CREATE SCHEMA #{EventStore.schema};" unless @db.table_exists?(table)
136
+ Sequel::Migrator.run(@db, File.expand_path(File.join('..','..','db', self.migrations_dir), __FILE__), table: table)
137
+ end
138
+
139
+ def self.vertica_host
140
+ File.read File.expand_path(File.join('..','..','db', 'vertica_host_address.txt'), __FILE__)
141
+ end
142
+ 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,410 @@
1
+ require 'spec_helper'
2
+ require 'securerandom'
3
+ AGGREGATE_ID_ONE = SecureRandom.uuid
4
+ AGGREGATE_ID_TWO = SecureRandom.uuid
5
+ AGGREGATE_ID_THREE = SecureRandom.uuid
6
+
7
+ describe EventStore::Client do
8
+ let(:es_client) { EventStore::Client }
9
+
10
+ before do
11
+ client_1 = es_client.new(AGGREGATE_ID_ONE, :device)
12
+ client_2 = es_client.new(AGGREGATE_ID_TWO, :device)
13
+
14
+ events_by_aggregate_id = {AGGREGATE_ID_ONE => [], AGGREGATE_ID_TWO => []}
15
+ @event_time = Time.parse("2001-01-01 00:00:00 UTC")
16
+ ([AGGREGATE_ID_ONE]*10 + [AGGREGATE_ID_TWO]*10).shuffle.each_with_index do |aggregate_id, version|
17
+ events_by_aggregate_id[aggregate_id.to_s] << EventStore::Event.new(aggregate_id.to_s, @event_time, 'event_name', serialized_binary_event_data, version)
18
+ end
19
+ client_1.append events_by_aggregate_id[AGGREGATE_ID_ONE]
20
+ client_2.append events_by_aggregate_id[AGGREGATE_ID_TWO]
21
+ end
22
+
23
+ it "counts the number of aggregates or clients" do
24
+ expect(es_client.count).to eql(2)
25
+ end
26
+
27
+ it "returns a partial list of aggregates" do
28
+ offset = 0
29
+ limit = 1
30
+ expect(es_client.ids(offset, limit)).to eq([[AGGREGATE_ID_ONE, AGGREGATE_ID_TWO].sort.first])
31
+ end
32
+
33
+ describe '#raw_event_stream' do
34
+ it "should be an array of hashes that represent database records, not EventStore::SerializedEvent objects" do
35
+ raw_stream = es_client.new(AGGREGATE_ID_ONE, :device).raw_event_stream
36
+ expect(raw_stream.class).to eq(Array)
37
+ raw_event = raw_stream.first
38
+ expect(raw_event.class).to eq(Hash)
39
+ expect(raw_event.keys).to eq([:id, :version, :aggregate_id, :fully_qualified_name, :occurred_at, :serialized_event])
40
+ end
41
+
42
+ it 'should be empty for aggregates without events' do
43
+ stream = es_client.new(100, :device).raw_event_stream
44
+ expect(stream.empty?).to be_truthy
45
+ end
46
+
47
+ it 'should only have events for a single aggregate' do
48
+ stream = es_client.new(AGGREGATE_ID_ONE, :device).raw_event_stream
49
+ stream.each { |event| expect(event[:aggregate_id]).to eq(AGGREGATE_ID_ONE) }
50
+ end
51
+
52
+ it 'should have all events for that aggregate' do
53
+ stream = es_client.new(AGGREGATE_ID_ONE, :device).raw_event_stream
54
+ expect(stream.count).to eq(10)
55
+ end
56
+ end
57
+
58
+ describe '#event_stream' do
59
+ it "should be an array of EventStore::SerializedEvent objects" do
60
+ stream = es_client.new(AGGREGATE_ID_ONE, :device).event_stream
61
+ expect(stream.class).to eq(Array)
62
+ event = stream.first
63
+ expect(event.class).to eq(EventStore::SerializedEvent)
64
+ end
65
+
66
+ it 'should be empty for aggregates without events' do
67
+ stream = es_client.new(100, :device).raw_event_stream
68
+ expect(stream.empty?).to be_truthy
69
+ end
70
+
71
+ it 'should only have events for a single aggregate' do
72
+ raw_stream = es_client.new(AGGREGATE_ID_ONE, :device).raw_event_stream
73
+ stream = es_client.new(AGGREGATE_ID_ONE, :device).event_stream
74
+ expect(stream.map(&:fully_qualified_name)).to eq(raw_stream.inject([]){|m, event| m << event[:fully_qualified_name]; m})
75
+ end
76
+
77
+ it 'should have all events for that aggregate' do
78
+ stream = es_client.new(AGGREGATE_ID_ONE, :device).event_stream
79
+ expect(stream.count).to eq(10)
80
+ end
81
+
82
+ context "when the serialized event is terminated prematurely with a null byte" do
83
+ it "does not truncate the serialized event when there is a binary zero value is at the end" do
84
+ serialized_event = serialized_event_data_terminated_by_null
85
+ client = es_client.new("any_device", :device)
86
+ event = EventStore::Event.new("any_device", @event_time, 'other_event_name', serialized_event, client.version + 1)
87
+ client.append([event])
88
+ expect(client.event_stream.last[:serialized_event]).to eql(serialized_event)
89
+ end
90
+
91
+ it "conversion of byte array to and from hex should be lossless" do
92
+ client = es_client.new("any_device", :device)
93
+ serialized_event = serialized_event_data_terminated_by_null
94
+ event = EventStore::Event.new("any_device", @event_time, 'terminated_by_null_event', serialized_event, client.version + 1)
95
+ client.append([event])
96
+ hex_from_db = EventStore.db.from(EventStore.fully_qualified_table).where(fully_qualified_name: 'terminated_by_null_event').first[:serialized_event]
97
+ expect(hex_from_db).to eql(EventStore.escape_bytea(serialized_event))
98
+ end
99
+ end
100
+ end
101
+
102
+
103
+ describe '#raw_event_streams_from_version' do
104
+ subject { es_client.new(AGGREGATE_ID_ONE, :device) }
105
+
106
+ it 'should return all the raw events in the stream starting from a certain version' do
107
+ minimum_event_version = 2
108
+ raw_stream = subject.raw_event_stream_from(minimum_event_version)
109
+ event_versions = raw_stream.inject([]){|m, event| m << event[:version]; m}
110
+ expect(event_versions.min).to be >= minimum_event_version
111
+ end
112
+
113
+ it 'should return no more than the maximum number of events specified above the ' do
114
+ max_number_of_events = 5
115
+ minimum_event_version = 2
116
+ raw_stream = subject.raw_event_stream_from(minimum_event_version, max_number_of_events)
117
+ expect(raw_stream.count).to eq(max_number_of_events)
118
+ end
119
+
120
+ it 'should be empty for version above the current highest version number' do
121
+ raw_stream = subject.raw_event_stream_from(subject.version + 1)
122
+ expect(raw_stream).to be_empty
123
+ end
124
+ end
125
+
126
+ describe 'event_stream_from_version' do
127
+ subject { es_client.new(AGGREGATE_ID_ONE, :device) }
128
+
129
+ it 'should return all the raw events in the stream starting from a certain version' do
130
+ minimum_event_version = 2
131
+ raw_stream = subject.raw_event_stream_from(minimum_event_version)
132
+ event_versions = raw_stream.inject([]){|m, event| m << event[:version]; m}
133
+ expect(event_versions.min).to be >= minimum_event_version
134
+ end
135
+
136
+ it 'should return no more than the maximum number of events specified above the ' do
137
+ max_number_of_events = 5
138
+ minimum_event_version = 2
139
+ raw_stream = subject.raw_event_stream_from(minimum_event_version, max_number_of_events)
140
+ expect(raw_stream.count).to eq(max_number_of_events)
141
+ end
142
+
143
+ it 'should be empty for version above the current highest version number' do
144
+ raw_stream = subject.raw_event_stream_from(subject.version + 1)
145
+ expect(raw_stream).to eq([])
146
+ end
147
+ end
148
+
149
+ describe '#event_stream_between' do
150
+ subject {es_client.new(AGGREGATE_ID_ONE, :device)}
151
+
152
+ before do
153
+ version = subject.version
154
+ @oldest_event_time = @event_time + 1
155
+ @middle_event_time = @event_time + 2
156
+ @newest_event_time = @event_time + 3
157
+
158
+ @outside_event = EventStore::Event.new(AGGREGATE_ID_ONE, (@event_time).utc, "middle_event", "#{1002.to_s(2)}_foo", version += 1)
159
+ @event = EventStore::Event.new(AGGREGATE_ID_ONE, (@oldest_event_time).utc, "oldest_event", "#{1002.to_s(2)}_foo", version += 1)
160
+ @new_event = EventStore::Event.new(AGGREGATE_ID_ONE, (@middle_event_time).utc, "middle_event", "#{1002.to_s(2)}_foo", version += 1)
161
+ @newest_event = EventStore::Event.new(AGGREGATE_ID_ONE, (@newest_event_time).utc, "newest_event_type", "#{1002.to_s(2)}_foo", version += 1)
162
+ subject.append([@event, @new_event, @newest_event])
163
+ end
164
+
165
+ it "returns all events between a start and an end time" do
166
+ start_time = @oldest_event_time
167
+ end_time = @newest_event_time
168
+ expect(subject.event_stream_between(start_time, end_time).length).to eq(3)
169
+ end
170
+
171
+ it "returns an empty array if start time is before end time" do
172
+ start_time = @newest_event_time
173
+ end_time = @oldest_event_time
174
+ expect(subject.event_stream_between(start_time, end_time).length).to eq(0)
175
+ end
176
+
177
+ it "returns all the events at a given time if the start time is the same as the end time" do
178
+ start_time = @oldest_event_time
179
+ end_time = @oldest_event_time
180
+ expect(subject.event_stream_between(start_time, end_time).length).to eq(1)
181
+ end
182
+
183
+ it "returns unencodes the serialized_event fields out of the database encoding" do
184
+ expect(EventStore).to receive(:unescape_bytea).once
185
+ start_time = @oldest_event_time
186
+ end_time = @oldest_event_time
187
+ expect(subject.event_stream_between(start_time, end_time).length).to eq(1)
188
+ end
189
+
190
+ it "returns the raw events translated into SerializedEvents" do
191
+ expect(subject).to receive(:translate_events).once.and_call_original
192
+ start_time = @oldest_event_time
193
+ end_time = @oldest_event_time
194
+ expect(subject.event_stream_between(start_time, end_time).length).to eq(1)
195
+ end
196
+
197
+ it "returns types requested within the time range" do
198
+ start_time = @oldest_event_time
199
+ end_time = @newest_event_time
200
+ fully_qualified_name = 'middle_event'
201
+ expect(subject.event_stream_between(start_time, end_time, [fully_qualified_name]).length).to eq(1)
202
+ end
203
+
204
+ it "returns types requested within the time range for more than one type" do
205
+ start_time = @oldest_event_time
206
+ end_time = @newest_event_time
207
+ fully_qualified_names = ['middle_event', 'newest_event_type']
208
+ expect(subject.event_stream_between(start_time, end_time, fully_qualified_names).length).to eq(2)
209
+ end
210
+
211
+ it "returns an empty array if there are no events of the requested types in the time range" do
212
+ start_time = @oldest_event_time
213
+ end_time = @newest_event_time
214
+ fully_qualified_names = ['random_strings']
215
+ expect(subject.event_stream_between(start_time, end_time, fully_qualified_names).length).to eq(0)
216
+ end
217
+
218
+ it "returns only events of types that exist within the time range" do
219
+ start_time = @oldest_event_time
220
+ end_time = @newest_event_time
221
+ fully_qualified_names = ['middle_event', 'event_name']
222
+ expect(subject.event_stream_between(start_time, end_time, fully_qualified_names).length).to eq(1)
223
+ end
224
+ end
225
+
226
+ describe '#peek' do
227
+ let(:client) {es_client.new(AGGREGATE_ID_ONE, :device)}
228
+ subject { client.peek }
229
+
230
+ it 'should return the last event in the event stream' do
231
+ last_event = EventStore.db.from(client.event_table).where(aggregate_id: AGGREGATE_ID_ONE).order(:version).last
232
+ expect(subject).to eq(EventStore::SerializedEvent.new(last_event[:fully_qualified_name], EventStore.unescape_bytea(last_event[:serialized_event]), last_event[:version], @event_time))
233
+ end
234
+ end
235
+
236
+ describe '#append' do
237
+ before do
238
+ @client = EventStore::Client.new(AGGREGATE_ID_ONE, :device)
239
+ @event = @client.peek
240
+ version = @client.version
241
+ @old_event = EventStore::Event.new(AGGREGATE_ID_ONE, (@event_time - 2000).utc, "old", "#{1000.to_s(2)}_foo", version += 1)
242
+ @new_event = EventStore::Event.new(AGGREGATE_ID_ONE, (@event_time - 1000).utc, "new", "#{1001.to_s(2)}_foo", version += 1)
243
+ @really_new_event = EventStore::Event.new(AGGREGATE_ID_ONE, (@event_time + 100).utc, "really_new", "#{1002.to_s(2)}_foo", version += 1)
244
+ @duplicate_event = EventStore::Event.new(AGGREGATE_ID_ONE, (@event_time).utc, 'duplicate', "#{12.to_s(2)}_foo", version += 1)
245
+ end
246
+
247
+ describe "when expected version number is greater than the last version" do
248
+ describe 'and there are no prior events of type' do
249
+ before do
250
+ @client.append([@old_event])
251
+ end
252
+
253
+ it 'should append a single event of a new type without raising an error' do
254
+ initial_count = @client.count
255
+ events = [@new_event]
256
+ @client.append(events)
257
+ expect(@client.count).to eq(initial_count + events.length)
258
+ end
259
+
260
+ it 'should append multiple events of a new type without raising and error' do
261
+ initial_count = @client.count
262
+ events = [@new_event, @new_event]
263
+ @client.append(events)
264
+ expect(@client.count).to eq(initial_count + events.length)
265
+ end
266
+
267
+ it "should increment the version number by the number of events added" do
268
+ events = [@new_event, @really_new_event]
269
+ initial_version = @client.version
270
+ @client.append(events)
271
+ expect(@client.version).to eq(initial_version + events.length)
272
+ end
273
+
274
+ it "should set the snapshot version number to match that of the last event in the aggregate's event stream" do
275
+ events = [@new_event, @really_new_event]
276
+ initial_stream_version = @client.raw_event_stream.last[:version]
277
+ expect(@client.snapshot.last.version).to eq(initial_stream_version)
278
+ @client.append(events)
279
+ updated_stream_version = @client.raw_event_stream.last[:version]
280
+ expect(@client.snapshot.last.version).to eq(updated_stream_version)
281
+ end
282
+
283
+ it "should write-through-cache the event in a snapshot without duplicating events" do
284
+ @client.destroy!
285
+ @client.append([@old_event, @new_event, @really_new_event])
286
+ expect(@client.snapshot).to eq(@client.event_stream)
287
+ end
288
+
289
+ it "should raise a meaningful exception when a nil event given to it to append" do
290
+ expect {@client.append([nil])}.to raise_exception(ArgumentError)
291
+ end
292
+ end
293
+
294
+ describe 'with prior events of same type' do
295
+ it 'should raise a ConcurrencyError if the the event version is less than current version' do
296
+ @client.append([@duplicate_event])
297
+ reset_current_version_for(@client)
298
+ expect { @client.append([@duplicate_event]) }.to raise_error(EventStore::ConcurrencyError)
299
+ end
300
+
301
+ it 'should not raise an error when two events of the same type are appended' do
302
+ @client.append([@duplicate_event])
303
+ @duplicate_event[:version] += 1
304
+ @client.append([@duplicate_event]) #will fail automatically if it throws an error, no need for assertions (which now print warning for some reason)
305
+ end
306
+
307
+ it "should write-through-cache the event in a snapshot without duplicating events" do
308
+ @client.destroy!
309
+ @client.append([@old_event, @new_event, @new_event])
310
+ expected = []
311
+ expected << @client.event_stream.first
312
+ expected << @client.event_stream.last
313
+ expect(@client.snapshot).to eq(expected)
314
+ end
315
+
316
+ it "should increment the version number by the number of unique events added" do
317
+ events = [@old_event, @old_event, @old_event]
318
+ initial_version = @client.version
319
+ @client.append(events)
320
+ expect(@client.version).to eq(initial_version + events.uniq.length)
321
+ end
322
+
323
+ it "should set the snapshot version number to match that of the last event in the aggregate's event stream" do
324
+ events = [@old_event, @old_event]
325
+ initial_stream_version = @client.raw_event_stream.last[:version]
326
+ expect(@client.snapshot.last.version).to eq(initial_stream_version)
327
+ @client.append(events)
328
+ updated_stream_version = @client.raw_event_stream.last[:version]
329
+ expect(@client.snapshot.last.version).to eq(updated_stream_version)
330
+ end
331
+ end
332
+ end
333
+
334
+ describe 'transactional' do
335
+ before do
336
+ @bad_event = @new_event.dup
337
+ @bad_event.fully_qualified_name = nil
338
+ end
339
+
340
+ it 'should revert all append events if one fails' do
341
+ starting_count = @client.count
342
+ expect { @client.append([@new_event, @bad_event]) }.to raise_error(EventStore::AttributeMissingError)
343
+ expect(@client.count).to eq(starting_count)
344
+ end
345
+
346
+ it 'does not yield to the block if it fails' do
347
+ x = 0
348
+ expect { @client.append([@bad_event]) { x += 1 } }.to raise_error(EventStore::AttributeMissingError)
349
+ expect(x).to eq(0)
350
+ end
351
+
352
+ it 'yield to the block after event creation' do
353
+ x = 0
354
+ @client.append([]) { x += 1 }
355
+ expect(x).to eq(1)
356
+ end
357
+
358
+ it 'should pass the raw event_data to the block' do
359
+ @client.append([@new_event]) do |raw_event_data|
360
+ expect(raw_event_data).to eq([@new_event])
361
+ end
362
+ end
363
+ end
364
+
365
+ describe 'snapshot' do
366
+ before do
367
+ @client = es_client.new(AGGREGATE_ID_THREE, :device)
368
+ expect(@client.snapshot.length).to eq(0)
369
+ version = @client.version
370
+ @client.append %w{ e1 e2 e3 e1 e2 e4 e5 e2 e5 e4}.map {|fqn|EventStore::Event.new(AGGREGATE_ID_THREE, Time.now.utc, fqn, serialized_binary_event_data, version += 1)}
371
+ end
372
+
373
+ it "finds the most recent records for each type" do
374
+ version = @client.version
375
+ expected_snapshot = %w{ e1 e2 e3 e4 e5 }.map {|fqn| EventStore::SerializedEvent.new(fqn, serialized_binary_event_data, version +=1 ) }
376
+ actual_snapshot = @client.snapshot
377
+ expect(@client.event_stream.length).to eq(10)
378
+ expect(actual_snapshot.length).to eq(5)
379
+ expect(actual_snapshot.map(&:fully_qualified_name)).to eq(["e3", "e1", "e2", "e5", "e4"]) #sorted by version no
380
+ expect(actual_snapshot.map(&:serialized_event)).to eq(expected_snapshot.map(&:serialized_event))
381
+ most_recent_events_of_each_type = {}
382
+ @client.event_stream.each do |e|
383
+ 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
384
+ most_recent_events_of_each_type[e.fully_qualified_name] = e
385
+ end
386
+ end
387
+ expect(actual_snapshot.map(&:version)).to eq(most_recent_events_of_each_type.values.map(&:version).sort)
388
+ end
389
+
390
+ it "increments the version number of the snapshot when an event is appended" do
391
+ expect(@client.snapshot.last.version).to eq(@client.raw_event_stream.last[:version])
392
+ end
393
+ end
394
+
395
+
396
+ def reset_current_version_for(client)
397
+ aggregate = client.instance_variable_get("@aggregate")
398
+ EventStore.redis.hset(aggregate.snapshot_version_table, :current_version, 1000)
399
+ end
400
+
401
+ end
402
+ def serialized_event_data_terminated_by_null
403
+ @term_data ||= File.open(File.expand_path("../binary_string_term_with_null_byte.txt", __FILE__), 'rb') {|f| f.read}
404
+ @term_data
405
+ end
406
+ def serialized_binary_event_data
407
+ @event_data ||= File.open(File.expand_path("../serialized_binary_event_data.txt", __FILE__), 'rb') {|f| f.read}
408
+ @event_data
409
+ end
410
+ end