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.
- checksums.yaml +7 -0
- data/.agignore +1 -0
- data/.gitignore +28 -0
- data/.rspec +3 -0
- data/.simplecov +16 -0
- data/Gemfile +3 -0
- data/Guardfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +110 -0
- data/Rakefile +35 -0
- data/db/database.yml +42 -0
- data/db/event_store_db_designer_common_queries.sql +4 -0
- data/db/event_store_db_designer_event_data.sql +3585 -0
- data/db/event_store_sample_data_generator.rb +30 -0
- data/db/migrations/001_create_event_store_events.rb +62 -0
- data/db/pg_migrations/001_create_event_store_events.rb +17 -0
- data/db/setup_db_user.sql +4 -0
- data/event_store.gemspec +36 -0
- data/lib/event_store/aggregate.rb +93 -0
- data/lib/event_store/client.rb +99 -0
- data/lib/event_store/errors.rb +4 -0
- data/lib/event_store/event_appender.rb +84 -0
- data/lib/event_store/time_hacker.rb +16 -0
- data/lib/event_store/version.rb +3 -0
- data/lib/event_store.rb +142 -0
- data/spec/benchmark/bench.rb +34 -0
- data/spec/benchmark/memory_profile.rb +48 -0
- data/spec/benchmark/seed_db.rb +45 -0
- data/spec/event_store/binary_string_term_with_null_byte.txt +0 -0
- data/spec/event_store/client_spec.rb +410 -0
- data/spec/event_store/config_spec.rb +8 -0
- data/spec/event_store/serialized_binary_event_data.txt +4 -0
- data/spec/event_store/snapshot_spec.rb +45 -0
- data/spec/event_store/vertica guy notes.txt +23 -0
- data/spec/spec_helper.rb +33 -0
- metadata +286 -0
data/lib/event_store.rb
ADDED
@@ -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
|
+
|
Binary file
|
@@ -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
|