nexia_event_store 0.2.11 → 0.3.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.
- checksums.yaml +4 -4
- data/db/pg_migrations/002_create_event_store_events.rb +8 -0
- data/lib/event_store/aggregate.rb +26 -71
- data/lib/event_store/client.rb +11 -12
- data/lib/event_store/event_stream.rb +81 -0
- data/lib/event_store/snapshot.rb +111 -0
- data/lib/event_store/version.rb +1 -1
- data/lib/event_store.rb +6 -3
- data/spec/event_store/client_spec.rb +25 -51
- data/spec/event_store/snapshot_spec.rb +74 -31
- data/spec/spec_helper.rb +2 -0
- metadata +5 -3
- data/lib/event_store/event_appender.rb +0 -84
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8ca406d692ca12cea54e8e03cd222f28c3baf20a
|
4
|
+
data.tar.gz: 8d4d4206228a30c96f0b16df727cacb2daa80f21
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 86744d003f7c506b25f84cb1318610a99ba79528e5eec47b013bf38c90a940004834b9baf0dc368b1c444402e41be10bca074124c28d21d1fb7d9f2840ecdb03
|
7
|
+
data.tar.gz: f01ac30941112234b5b841a8596146657bb7f5ace7a49fc41d100160a32bb40330e96910edea882d28234e4d72a7b8730f825bff33e54c1c8446e2d95aff353f
|
@@ -1,7 +1,26 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
1
3
|
module EventStore
|
2
4
|
class Aggregate
|
3
|
-
|
4
|
-
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
attr_reader :id, :type, :event_table
|
8
|
+
|
9
|
+
def_delegators :@snapshot,
|
10
|
+
:last_event,
|
11
|
+
:snapshot,
|
12
|
+
:rebuild_snapshot!,
|
13
|
+
:delete_snapshot!,
|
14
|
+
:version,
|
15
|
+
:snapshot_version_table
|
16
|
+
|
17
|
+
def_delegators :@event_stream,
|
18
|
+
:events,
|
19
|
+
:events_from,
|
20
|
+
:event_stream,
|
21
|
+
:event_stream_between,
|
22
|
+
:event_table,
|
23
|
+
:delete_events!
|
5
24
|
|
6
25
|
def self.count
|
7
26
|
EventStore.db.from( EventStore.fully_qualified_table).distinct(:aggregate_id).count
|
@@ -14,80 +33,16 @@ module EventStore
|
|
14
33
|
def initialize(id, type = EventStore.table_name)
|
15
34
|
@id = id
|
16
35
|
@type = type
|
17
|
-
@schema = EventStore.schema
|
18
|
-
@event_table = EventStore.fully_qualified_table
|
19
|
-
@snapshot_table = "#{@type}_snapshots_for_#{@id}"
|
20
|
-
@snapshot_version_table = "#{@type}_snapshot_versions_for_#{@id}"
|
21
|
-
end
|
22
|
-
|
23
|
-
def events
|
24
|
-
@events_query ||= EventStore.db.from(@event_table).where(:aggregate_id => @id.to_s).order(:version)
|
25
|
-
end
|
26
|
-
|
27
|
-
def snapshot
|
28
|
-
events_hash = auto_rebuild_snapshot(read_raw_snapshot)
|
29
|
-
snap = []
|
30
|
-
events_hash.each_pair do |key, value|
|
31
|
-
raw_event = value.split(EventStore::SNAPSHOT_DELIMITER)
|
32
|
-
fully_qualified_name = key
|
33
|
-
version = raw_event.first.to_i
|
34
|
-
serialized_event = EventStore.unescape_bytea(raw_event[1])
|
35
|
-
occurred_at = Time.parse(raw_event.last)
|
36
|
-
snap << SerializedEvent.new(fully_qualified_name, serialized_event, version, occurred_at)
|
37
|
-
end
|
38
|
-
snap.sort {|a,b| a.version <=> b.version}
|
39
|
-
end
|
40
36
|
|
41
|
-
|
42
|
-
|
43
|
-
corrected_events = events.all.map{|e| e[:occurred_at] = TimeHacker.translate_occurred_at_from_local_to_gmt(e[:occurred_at]); e}
|
44
|
-
EventAppender.new(self).store_snapshot(corrected_events)
|
37
|
+
@snapshot = Snapshot.new(self)
|
38
|
+
@event_stream = EventStream.new(self)
|
45
39
|
end
|
46
40
|
|
47
|
-
def
|
48
|
-
|
49
|
-
|
50
|
-
event
|
41
|
+
def append(events)
|
42
|
+
@event_stream.append(events) do |prepared_events|
|
43
|
+
@snapshot.store_snapshot(prepared_events)
|
51
44
|
end
|
52
45
|
end
|
53
46
|
|
54
|
-
def event_stream_between(start_time, end_time, fully_qualified_names = [])
|
55
|
-
query = events.where(occurred_at: start_time..end_time)
|
56
|
-
query = query.where(fully_qualified_name: fully_qualified_names) if fully_qualified_names && fully_qualified_names.any?
|
57
|
-
query.all.map {|e| e[:serialized_event] = EventStore.unescape_bytea(e[:serialized_event]); e}
|
58
|
-
end
|
59
|
-
|
60
|
-
def event_stream
|
61
|
-
events.all.map {|e| e[:serialized_event] = EventStore.unescape_bytea(e[:serialized_event]); e}
|
62
|
-
end
|
63
|
-
|
64
|
-
def last_event
|
65
|
-
snapshot.last
|
66
|
-
end
|
67
|
-
|
68
|
-
def version
|
69
|
-
(EventStore.redis.hget(@snapshot_version_table, :current_version) || -1).to_i
|
70
|
-
end
|
71
|
-
|
72
|
-
def delete_snapshot!
|
73
|
-
EventStore.redis.del [@snapshot_table, @snapshot_version_table]
|
74
|
-
end
|
75
|
-
|
76
|
-
def delete_events!
|
77
|
-
events.delete
|
78
|
-
end
|
79
|
-
|
80
|
-
private
|
81
|
-
def auto_rebuild_snapshot(events_hash)
|
82
|
-
return events_hash unless events_hash.empty?
|
83
|
-
event = events.select(:version).limit(1).all
|
84
|
-
return events_hash if event.nil?
|
85
|
-
rebuild_snapshot!
|
86
|
-
events_hash = read_raw_snapshot
|
87
|
-
end
|
88
|
-
|
89
|
-
def read_raw_snapshot
|
90
|
-
EventStore.redis.hgetall(@snapshot_table)
|
91
|
-
end
|
92
47
|
end
|
93
48
|
end
|
data/lib/event_store/client.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
module EventStore
|
2
2
|
class Client
|
3
|
+
extend Forwardable
|
4
|
+
|
5
|
+
def_delegators :@aggregate, :delete_snapshot!, :snapshot_version_table
|
3
6
|
|
4
7
|
def self.count
|
5
8
|
Aggregate.count
|
@@ -9,7 +12,7 @@ module EventStore
|
|
9
12
|
Aggregate.ids(offset, limit)
|
10
13
|
end
|
11
14
|
|
12
|
-
def initialize(
|
15
|
+
def initialize(aggregate_id, aggregate_type = EventStore.table_name)
|
13
16
|
@aggregate = Aggregate.new(aggregate_id, aggregate_type)
|
14
17
|
end
|
15
18
|
|
@@ -25,8 +28,8 @@ module EventStore
|
|
25
28
|
@aggregate.event_table
|
26
29
|
end
|
27
30
|
|
28
|
-
def append
|
29
|
-
|
31
|
+
def append(event_data)
|
32
|
+
@aggregate.append(event_data)
|
30
33
|
yield(event_data) if block_given?
|
31
34
|
nil
|
32
35
|
end
|
@@ -36,19 +39,19 @@ module EventStore
|
|
36
39
|
end
|
37
40
|
|
38
41
|
def event_stream
|
39
|
-
translate_events
|
42
|
+
translate_events(raw_event_stream)
|
40
43
|
end
|
41
44
|
|
42
|
-
def event_stream_from
|
43
|
-
translate_events
|
45
|
+
def event_stream_from(version_number, max=nil)
|
46
|
+
translate_events(@aggregate.events_from(version_number, max))
|
44
47
|
end
|
45
48
|
|
46
49
|
def event_stream_between(start_time, end_time, fully_qualified_names = [])
|
47
|
-
translate_events
|
50
|
+
translate_events(@aggregate.event_stream_between(start_time, end_time, fully_qualified_names))
|
48
51
|
end
|
49
52
|
|
50
53
|
def peek
|
51
|
-
translate_event
|
54
|
+
translate_event(@aggregate.last_event)
|
52
55
|
end
|
53
56
|
|
54
57
|
def raw_snapshot
|
@@ -83,10 +86,6 @@ module EventStore
|
|
83
86
|
|
84
87
|
private
|
85
88
|
|
86
|
-
def event_appender
|
87
|
-
EventAppender.new(@aggregate)
|
88
|
-
end
|
89
|
-
|
90
89
|
def translate_events(event_hashs)
|
91
90
|
event_hashs.map { |eh| translate_event(eh) }
|
92
91
|
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module EventStore
|
2
|
+
class EventStream
|
3
|
+
|
4
|
+
attr_reader :event_table
|
5
|
+
|
6
|
+
def initialize aggregate
|
7
|
+
@aggregate = aggregate
|
8
|
+
@id = @aggregate.id
|
9
|
+
@event_table = EventStore.fully_qualified_table
|
10
|
+
end
|
11
|
+
|
12
|
+
def append(raw_events)
|
13
|
+
EventStore.db.transaction do
|
14
|
+
next_version = last_version + 1
|
15
|
+
|
16
|
+
prepared_events = raw_events.map do |raw_event|
|
17
|
+
event = prepare_event(raw_event, next_version)
|
18
|
+
next_version += 1
|
19
|
+
ensure_all_attributes_have_values!(event)
|
20
|
+
event
|
21
|
+
end
|
22
|
+
|
23
|
+
events.multi_insert(prepared_events)
|
24
|
+
|
25
|
+
yield(prepared_events) if block_given?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def events
|
30
|
+
@events_query ||= EventStore.db.from(@event_table).where(:aggregate_id => @id.to_s).order(:version)
|
31
|
+
end
|
32
|
+
|
33
|
+
def events_from(version_number, max = nil)
|
34
|
+
events.limit(max).where{ version >= version_number.to_i }.all.map do |event|
|
35
|
+
event[:serialized_event] = EventStore.unescape_bytea(event[:serialized_event])
|
36
|
+
event
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def event_stream_between(start_time, end_time, fully_qualified_names = [])
|
41
|
+
query = events.where(occurred_at: start_time..end_time)
|
42
|
+
query = query.where(fully_qualified_name: fully_qualified_names) if fully_qualified_names && fully_qualified_names.any?
|
43
|
+
query.all.map {|e| e[:serialized_event] = EventStore.unescape_bytea(e[:serialized_event]); e}
|
44
|
+
end
|
45
|
+
|
46
|
+
def event_stream
|
47
|
+
events.all.map {|e| e[:serialized_event] = EventStore.unescape_bytea(e[:serialized_event]); e}
|
48
|
+
end
|
49
|
+
|
50
|
+
def delete_events!
|
51
|
+
events.delete
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def prepare_event(raw_event, version_number)
|
57
|
+
raise ArgumentError.new("Cannot Append a Nil Event") unless raw_event
|
58
|
+
{ :version => version_number,
|
59
|
+
:aggregate_id => raw_event.aggregate_id,
|
60
|
+
:occurred_at => Time.parse(raw_event.occurred_at.to_s).utc, #to_s truncates microseconds, which brake Time equality
|
61
|
+
:serialized_event => EventStore.escape_bytea(raw_event.serialized_event),
|
62
|
+
:fully_qualified_name => raw_event.fully_qualified_name,
|
63
|
+
:sub_key => raw_event.sub_key
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def ensure_all_attributes_have_values!(event_hash)
|
68
|
+
[:aggregate_id, :fully_qualified_name, :occurred_at, :serialized_event, :version].each do |attribute_name|
|
69
|
+
if event_hash[attribute_name].to_s.strip.empty?
|
70
|
+
raise AttributeMissingError, "value required for #{attribute_name}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def last_version
|
76
|
+
last = events.last
|
77
|
+
last && last[:version] || -1
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module EventStore
|
2
|
+
class Snapshot
|
3
|
+
|
4
|
+
attr_reader :snapshot_version_table
|
5
|
+
|
6
|
+
def initialize aggregate
|
7
|
+
@aggregate = aggregate
|
8
|
+
@redis = EventStore.redis
|
9
|
+
@snapshot_table = "#{@aggregate.type}_snapshots_for_#{@aggregate.id}"
|
10
|
+
@snapshot_version_table = "#{@aggregate.type}_snapshot_versions_for_#{@aggregate.id}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def last_event
|
14
|
+
snapshot.last
|
15
|
+
end
|
16
|
+
|
17
|
+
def version
|
18
|
+
(@redis.hget(@snapshot_version_table, :current_version) || -1).to_i
|
19
|
+
end
|
20
|
+
|
21
|
+
def snapshot
|
22
|
+
events_hash = auto_rebuild_snapshot(read_raw_snapshot)
|
23
|
+
snap = []
|
24
|
+
events_hash.each_pair do |key, value|
|
25
|
+
fully_qualified_name, _ = key.split(EventStore::SNAPSHOT_KEY_DELIMITER)
|
26
|
+
raw_event = value.split(EventStore::SNAPSHOT_DELIMITER)
|
27
|
+
version = raw_event.first.to_i
|
28
|
+
serialized_event = EventStore.unescape_bytea(raw_event[1])
|
29
|
+
occurred_at = Time.parse(raw_event.last)
|
30
|
+
snap << SerializedEvent.new(fully_qualified_name, serialized_event, version, occurred_at)
|
31
|
+
end
|
32
|
+
snap.sort {|a,b| a.version <=> b.version}
|
33
|
+
end
|
34
|
+
|
35
|
+
def rebuild_snapshot!
|
36
|
+
delete_snapshot!
|
37
|
+
corrected_events = @aggregate.events.all.map{|e| e[:occurred_at] = TimeHacker.translate_occurred_at_from_local_to_gmt(e[:occurred_at]); e}
|
38
|
+
store_snapshot(corrected_events)
|
39
|
+
end
|
40
|
+
|
41
|
+
def delete_snapshot!
|
42
|
+
EventStore.redis.del [@snapshot_table, @snapshot_version_table]
|
43
|
+
end
|
44
|
+
|
45
|
+
def store_snapshot(prepared_events)
|
46
|
+
valid_snapshot_events = []
|
47
|
+
valid_snapshot_versions = []
|
48
|
+
|
49
|
+
prepared_events.each do |event_hash|
|
50
|
+
if event_hash[:version].to_i > current_version_numbers[snapshot_key(event_hash)].to_i
|
51
|
+
valid_snapshot_events += snapshot_event(event_hash)
|
52
|
+
valid_snapshot_versions += snapshot_version(event_hash)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
unless valid_snapshot_versions.empty?
|
57
|
+
valid_snapshot_versions += [:current_version, valid_snapshot_versions.last.to_i]
|
58
|
+
|
59
|
+
@redis.multi do
|
60
|
+
@redis.hmset(@snapshot_version_table, valid_snapshot_versions)
|
61
|
+
@redis.hmset(@snapshot_table, valid_snapshot_events)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def snapshot_key(event)
|
69
|
+
[event[:fully_qualified_name], event[:sub_key] || EventStore::NO_ZONE].join(EventStore::SNAPSHOT_KEY_DELIMITER)
|
70
|
+
end
|
71
|
+
|
72
|
+
def snapshot_event(event)
|
73
|
+
[
|
74
|
+
snapshot_key(event),
|
75
|
+
[ event[:version].to_s,
|
76
|
+
event[:serialized_event],
|
77
|
+
event[:occurred_at].to_s
|
78
|
+
].join(EventStore::SNAPSHOT_DELIMITER)
|
79
|
+
]
|
80
|
+
end
|
81
|
+
|
82
|
+
def snapshot_version(event)
|
83
|
+
[
|
84
|
+
snapshot_key(event),
|
85
|
+
event[:version]
|
86
|
+
]
|
87
|
+
end
|
88
|
+
|
89
|
+
def current_version_numbers
|
90
|
+
current_versions = @redis.hgetall(@snapshot_version_table)
|
91
|
+
current_versions.default = -1
|
92
|
+
current_versions
|
93
|
+
end
|
94
|
+
|
95
|
+
def read_raw_snapshot
|
96
|
+
@redis.hgetall(@snapshot_table)
|
97
|
+
end
|
98
|
+
|
99
|
+
def auto_rebuild_snapshot(events_hash)
|
100
|
+
return events_hash unless events_hash.empty? #got it? return it
|
101
|
+
|
102
|
+
event = @aggregate.events.select(:version).limit(1).all
|
103
|
+
return events_hash if event.nil? #return nil if no events in the ES
|
104
|
+
|
105
|
+
# so there are events in the ES but there is no redis snapshot
|
106
|
+
rebuild_snapshot!
|
107
|
+
events_hash = read_raw_snapshot
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
data/lib/event_store/version.rb
CHANGED
data/lib/event_store.rb
CHANGED
@@ -6,7 +6,8 @@ require 'redis'
|
|
6
6
|
require 'hiredis'
|
7
7
|
require 'event_store/version'
|
8
8
|
require 'event_store/time_hacker'
|
9
|
-
require 'event_store/
|
9
|
+
require 'event_store/event_stream'
|
10
|
+
require 'event_store/snapshot'
|
10
11
|
require 'event_store/aggregate'
|
11
12
|
require 'event_store/client'
|
12
13
|
require 'event_store/errors'
|
@@ -15,9 +16,11 @@ require 'yaml'
|
|
15
16
|
Sequel.extension :migration
|
16
17
|
|
17
18
|
module EventStore
|
18
|
-
Event = Struct.new(:aggregate_id, :occurred_at, :fully_qualified_name, :
|
19
|
+
Event = Struct.new(:aggregate_id, :occurred_at, :fully_qualified_name, :sub_key, :serialized_event)
|
19
20
|
SerializedEvent = Struct.new(:fully_qualified_name, :serialized_event, :version, :occurred_at)
|
20
|
-
SNAPSHOT_DELIMITER
|
21
|
+
SNAPSHOT_DELIMITER = "__NexEvStDelim__"
|
22
|
+
SNAPSHOT_KEY_DELIMITER = ":"
|
23
|
+
NO_SUB_KEY = "NO_SUB_KEY"
|
21
24
|
|
22
25
|
def self.db_config
|
23
26
|
raw_db_config[@environment.to_s][@adapter.to_s]
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'securerandom'
|
3
|
+
|
3
4
|
AGGREGATE_ID_ONE = SecureRandom.uuid
|
4
5
|
AGGREGATE_ID_TWO = SecureRandom.uuid
|
5
6
|
AGGREGATE_ID_THREE = SecureRandom.uuid
|
@@ -13,8 +14,10 @@ describe EventStore::Client do
|
|
13
14
|
|
14
15
|
events_by_aggregate_id = {AGGREGATE_ID_ONE => [], AGGREGATE_ID_TWO => []}
|
15
16
|
@event_time = Time.parse("2001-01-01 00:00:00 UTC")
|
16
|
-
([AGGREGATE_ID_ONE]*
|
17
|
-
events_by_aggregate_id[aggregate_id.to_s] << EventStore::Event.new(aggregate_id.to_s, @event_time,
|
17
|
+
([AGGREGATE_ID_ONE]*5 + [AGGREGATE_ID_TWO]*5).shuffle.each_with_index do |aggregate_id, version|
|
18
|
+
events_by_aggregate_id[aggregate_id.to_s] << EventStore::Event.new(aggregate_id.to_s, @event_time, "zone_1_event", "1", serialized_binary_event_data)
|
19
|
+
events_by_aggregate_id[aggregate_id.to_s] << EventStore::Event.new(aggregate_id.to_s, @event_time, "zone_2_event", "2", serialized_binary_event_data)
|
20
|
+
events_by_aggregate_id[aggregate_id.to_s] << EventStore::Event.new(aggregate_id.to_s, @event_time, "system_event", EventStore::NO_SUB_KEY, serialized_binary_event_data)
|
18
21
|
end
|
19
22
|
client_1.append events_by_aggregate_id[AGGREGATE_ID_ONE]
|
20
23
|
client_2.append events_by_aggregate_id[AGGREGATE_ID_TWO]
|
@@ -36,7 +39,7 @@ describe EventStore::Client do
|
|
36
39
|
expect(raw_stream.class).to eq(Array)
|
37
40
|
raw_event = raw_stream.first
|
38
41
|
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])
|
42
|
+
expect(raw_event.keys).to eq([:id, :version, :aggregate_id, :fully_qualified_name, :occurred_at, :serialized_event, :sub_key])
|
40
43
|
end
|
41
44
|
|
42
45
|
it 'should be empty for aggregates without events' do
|
@@ -51,7 +54,7 @@ describe EventStore::Client do
|
|
51
54
|
|
52
55
|
it 'should have all events for that aggregate' do
|
53
56
|
stream = es_client.new(AGGREGATE_ID_ONE, :device).raw_event_stream
|
54
|
-
expect(stream.count).to eq(
|
57
|
+
expect(stream.count).to eq(15)
|
55
58
|
end
|
56
59
|
end
|
57
60
|
|
@@ -76,14 +79,14 @@ describe EventStore::Client do
|
|
76
79
|
|
77
80
|
it 'should have all events for that aggregate' do
|
78
81
|
stream = es_client.new(AGGREGATE_ID_ONE, :device).event_stream
|
79
|
-
expect(stream.count).to eq(
|
82
|
+
expect(stream.count).to eq(15)
|
80
83
|
end
|
81
84
|
|
82
85
|
context "when the serialized event is terminated prematurely with a null byte" do
|
83
86
|
it "does not truncate the serialized event when there is a binary zero value is at the end" do
|
84
87
|
serialized_event = serialized_event_data_terminated_by_null
|
85
88
|
client = es_client.new("any_device", :device)
|
86
|
-
event = EventStore::Event.new("any_device", @event_time, 'other_event_name',
|
89
|
+
event = EventStore::Event.new("any_device", @event_time, 'other_event_name', "nozone", serialized_event)
|
87
90
|
client.append([event])
|
88
91
|
expect(client.event_stream.last[:serialized_event]).to eql(serialized_event)
|
89
92
|
end
|
@@ -91,7 +94,7 @@ describe EventStore::Client do
|
|
91
94
|
it "conversion of byte array to and from hex should be lossless" do
|
92
95
|
client = es_client.new("any_device", :device)
|
93
96
|
serialized_event = serialized_event_data_terminated_by_null
|
94
|
-
event = EventStore::Event.new("any_device", @event_time, 'terminated_by_null_event',
|
97
|
+
event = EventStore::Event.new("any_device", @event_time, 'terminated_by_null_event', "zone_number", serialized_event)
|
95
98
|
client.append([event])
|
96
99
|
hex_from_db = EventStore.db.from(EventStore.fully_qualified_table).where(fully_qualified_name: 'terminated_by_null_event').first[:serialized_event]
|
97
100
|
expect(hex_from_db).to eql(EventStore.escape_bytea(serialized_event))
|
@@ -150,15 +153,14 @@ describe EventStore::Client do
|
|
150
153
|
subject {es_client.new(AGGREGATE_ID_ONE, :device)}
|
151
154
|
|
152
155
|
before do
|
153
|
-
version = subject.version
|
154
156
|
@oldest_event_time = @event_time + 1
|
155
157
|
@middle_event_time = @event_time + 2
|
156
158
|
@newest_event_time = @event_time + 3
|
157
159
|
|
158
|
-
@outside_event = EventStore::Event.new(AGGREGATE_ID_ONE, (@event_time).utc, "middle_event", "#{1002.to_s(2)}_foo"
|
159
|
-
@event = EventStore::Event.new(AGGREGATE_ID_ONE, (@oldest_event_time).utc, "oldest_event", "#{1002.to_s(2)}_foo"
|
160
|
-
@new_event = EventStore::Event.new(AGGREGATE_ID_ONE, (@middle_event_time).utc, "middle_event", "#{1002.to_s(2)}_foo"
|
161
|
-
@newest_event = EventStore::Event.new(AGGREGATE_ID_ONE, (@newest_event_time).utc, "newest_event_type", "#{1002.to_s(2)}_foo"
|
160
|
+
@outside_event = EventStore::Event.new(AGGREGATE_ID_ONE, (@event_time).utc, "middle_event", "zone", "#{1002.to_s(2)}_foo")
|
161
|
+
@event = EventStore::Event.new(AGGREGATE_ID_ONE, (@oldest_event_time).utc, "oldest_event", "zone", "#{1002.to_s(2)}_foo")
|
162
|
+
@new_event = EventStore::Event.new(AGGREGATE_ID_ONE, (@middle_event_time).utc, "middle_event", "zone", "#{1002.to_s(2)}_foo")
|
163
|
+
@newest_event = EventStore::Event.new(AGGREGATE_ID_ONE, (@newest_event_time).utc, "newest_event_type", "zone", "#{1002.to_s(2)}_foo")
|
162
164
|
subject.append([@event, @new_event, @newest_event])
|
163
165
|
end
|
164
166
|
|
@@ -237,11 +239,10 @@ describe EventStore::Client do
|
|
237
239
|
before do
|
238
240
|
@client = EventStore::Client.new(AGGREGATE_ID_ONE, :device)
|
239
241
|
@event = @client.peek
|
240
|
-
|
241
|
-
@
|
242
|
-
@
|
243
|
-
@
|
244
|
-
@duplicate_event = EventStore::Event.new(AGGREGATE_ID_ONE, (@event_time).utc, 'duplicate', "#{12.to_s(2)}_foo", version += 1)
|
242
|
+
@old_event = EventStore::Event.new(AGGREGATE_ID_ONE, (@event_time - 2000).utc, "old", "zone", "#{1000.to_s(2)}_foo")
|
243
|
+
@new_event = EventStore::Event.new(AGGREGATE_ID_ONE, (@event_time - 1000).utc, "new", "zone", "#{1001.to_s(2)}_foo")
|
244
|
+
@really_new_event = EventStore::Event.new(AGGREGATE_ID_ONE, (@event_time + 100).utc, "really_new", "zone", "#{1002.to_s(2)}_foo")
|
245
|
+
@duplicate_event = EventStore::Event.new(AGGREGATE_ID_ONE, (@event_time).utc, 'duplicate', "zone", "#{12.to_s(2)}_foo")
|
245
246
|
end
|
246
247
|
|
247
248
|
describe "when expected version number is greater than the last version" do
|
@@ -292,7 +293,7 @@ describe EventStore::Client do
|
|
292
293
|
end
|
293
294
|
|
294
295
|
describe 'with prior events of same type' do
|
295
|
-
|
296
|
+
xit 'should raise a ConcurrencyError if the the event version is less than current version' do
|
296
297
|
@client.append([@duplicate_event])
|
297
298
|
reset_current_version_for(@client)
|
298
299
|
expect { @client.append([@duplicate_event]) }.to raise_error(EventStore::ConcurrencyError)
|
@@ -300,7 +301,6 @@ describe EventStore::Client do
|
|
300
301
|
|
301
302
|
it 'should not raise an error when two events of the same type are appended' do
|
302
303
|
@client.append([@duplicate_event])
|
303
|
-
@duplicate_event[:version] += 1
|
304
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
305
|
end
|
306
306
|
|
@@ -313,10 +313,15 @@ describe EventStore::Client do
|
|
313
313
|
expect(@client.snapshot).to eq(expected)
|
314
314
|
end
|
315
315
|
|
316
|
-
|
316
|
+
#TODO if we let the db assign version# then this can't be true anymore
|
317
|
+
# the current snapshot version will be the last version number inserted
|
318
|
+
# if you give me duplicate events, I'm gonna append them and the last one in
|
319
|
+
# is the one that will be in the snapshot
|
320
|
+
xit "should increment the version number by the number of unique events added" do
|
317
321
|
events = [@old_event, @old_event, @old_event]
|
318
322
|
initial_version = @client.version
|
319
323
|
@client.append(events)
|
324
|
+
byebug
|
320
325
|
expect(@client.version).to eq(initial_version + events.uniq.length)
|
321
326
|
end
|
322
327
|
|
@@ -362,37 +367,6 @@ describe EventStore::Client do
|
|
362
367
|
end
|
363
368
|
end
|
364
369
|
|
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
370
|
def reset_current_version_for(client)
|
397
371
|
aggregate = client.instance_variable_get("@aggregate")
|
398
372
|
EventStore.redis.hset(aggregate.snapshot_version_table, :current_version, 1000)
|
@@ -1,45 +1,88 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
AGGREGATE_ID_ONE = SecureRandom.uuid
|
5
|
+
AGGREGATE_ID_TWO = SecureRandom.uuid
|
6
|
+
|
2
7
|
module EventStore
|
3
8
|
describe "Snapshots" do
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
9
|
+
|
10
|
+
context "when there are no events" do
|
11
|
+
let(:client) { EventStore::Client.new(AGGREGATE_ID_ONE) }
|
12
|
+
|
13
|
+
it "should build an empty snapshot for a new client" do
|
14
|
+
expect(client.snapshot).to eq([])
|
15
|
+
expect(client.version).to eq(-1)
|
16
|
+
expect(EventStore.redis.hget(client.snapshot_version_table, :current_version)).to eq(nil)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "a client should rebuild a snapshot" do
|
20
|
+
expect_any_instance_of(EventStore::Aggregate).to receive(:delete_snapshot!)
|
21
|
+
expect_any_instance_of(EventStore::Aggregate).to receive(:rebuild_snapshot!)
|
22
|
+
client.rebuild_snapshot!
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "with events in the stream" do
|
27
|
+
let(:client) { EventStore::Client.new(AGGREGATE_ID_TWO) }
|
28
|
+
|
29
|
+
before do
|
30
|
+
expect(client.snapshot.length).to eq(0)
|
31
|
+
client.append events_for(AGGREGATE_ID_TWO)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "rebuilds a snapshot after it is deleted" do
|
35
|
+
snapshot = client.snapshot
|
36
|
+
client.delete_snapshot!
|
37
|
+
client.rebuild_snapshot!
|
38
|
+
expect(client.snapshot).to eq(snapshot)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "automatically rebuilds the snapshot if events exist, but the snapshot is empty" do
|
42
|
+
snapshot = client.snapshot
|
43
|
+
client.delete_snapshot!
|
44
|
+
expect(client.snapshot).to eq(snapshot)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "finds the most recent records for each type" do
|
48
|
+
expected_snapshot_events = %w{e3 e1 e2 e5 e4 e7 e8 e7} #sorted by version no
|
49
|
+
expected_snapshot = serialized_events(expected_snapshot_events)
|
50
|
+
actual_snapshot = client.snapshot
|
51
|
+
|
52
|
+
expect(client.event_stream.length).to eq(15)
|
53
|
+
expect(actual_snapshot.map(&:fully_qualified_name)).to eq(expected_snapshot_events)
|
54
|
+
expect(actual_snapshot.length).to eq(8)
|
55
|
+
expect(actual_snapshot.map(&:serialized_event)).to eq(expected_snapshot.map(&:serialized_event))
|
56
|
+
end
|
57
|
+
|
58
|
+
it "increments the version number of the snapshot when an event is appended" do
|
59
|
+
expect(client.snapshot.last.version).to eq(client.raw_event_stream.last[:version])
|
14
60
|
end
|
15
61
|
end
|
16
62
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
63
|
+
def events_for(device_id)
|
64
|
+
events = %w{ e1 e2 e3 e1 e2 e4 e5 e2 e5 e4 }.map {|fqn| event_hash(device_id, fqn, EventStore::NO_SUB_KEY) }
|
65
|
+
events += %w{ e7 e7 e8 }.map {|fqn| event_hash(device_id, fqn, "zone1") }
|
66
|
+
events += %w{ e7 e7 }.map {|fqn| event_hash(device_id, fqn, "zone2") }
|
21
67
|
end
|
22
68
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
69
|
+
def event_hash(device_id, fqn, zone_id)
|
70
|
+
EventStore::Event.new(device_id,
|
71
|
+
Time.now.utc,
|
72
|
+
fqn,
|
73
|
+
zone_id,
|
74
|
+
serialized_binary_event_data
|
75
|
+
)
|
30
76
|
end
|
31
77
|
|
32
|
-
|
33
|
-
|
34
|
-
expect_any_instance_of(Aggregate).to receive(:rebuild_snapshot!)
|
35
|
-
client.rebuild_snapshot!
|
78
|
+
def serialized_events(events)
|
79
|
+
events.map {|fqn| EventStore::SerializedEvent.new(fqn, serialized_binary_event_data, 1 ) }
|
36
80
|
end
|
37
81
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
new_aggregate.delete_snapshot!
|
42
|
-
expect(new_aggregate.snapshot).to eq(snapshot)
|
82
|
+
def serialized_binary_event_data
|
83
|
+
@event_data ||= File.open(File.expand_path("../serialized_binary_event_data.txt", __FILE__), 'rb') {|f| f.read}
|
84
|
+
@event_data
|
43
85
|
end
|
86
|
+
|
44
87
|
end
|
45
|
-
end
|
88
|
+
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: nexia_event_store
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Paul Saieg, John Colvin
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-
|
12
|
+
date: 2014-10-01 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -230,13 +230,15 @@ files:
|
|
230
230
|
- db/event_store_sample_data_generator.rb
|
231
231
|
- db/migrations/001_create_event_store_events.rb
|
232
232
|
- db/pg_migrations/001_create_event_store_events.rb
|
233
|
+
- db/pg_migrations/002_create_event_store_events.rb
|
233
234
|
- db/setup_db_user.sql
|
234
235
|
- event_store.gemspec
|
235
236
|
- lib/event_store.rb
|
236
237
|
- lib/event_store/aggregate.rb
|
237
238
|
- lib/event_store/client.rb
|
238
239
|
- lib/event_store/errors.rb
|
239
|
-
- lib/event_store/
|
240
|
+
- lib/event_store/event_stream.rb
|
241
|
+
- lib/event_store/snapshot.rb
|
240
242
|
- lib/event_store/time_hacker.rb
|
241
243
|
- lib/event_store/version.rb
|
242
244
|
- spec/benchmark/bench.rb
|
@@ -1,84 +0,0 @@
|
|
1
|
-
module EventStore
|
2
|
-
class EventAppender
|
3
|
-
|
4
|
-
def initialize aggregate
|
5
|
-
@aggregate = aggregate
|
6
|
-
end
|
7
|
-
|
8
|
-
def append raw_events
|
9
|
-
EventStore.db.transaction do
|
10
|
-
set_current_version
|
11
|
-
|
12
|
-
prepared_events = raw_events.map do |raw_event|
|
13
|
-
event = prepare_event(raw_event)
|
14
|
-
validate! event
|
15
|
-
raise concurrency_error(event) if has_concurrency_issue?(event)
|
16
|
-
event
|
17
|
-
end
|
18
|
-
# All concurrency issues need to be checked before persisting any of the events
|
19
|
-
# Otherwise, the newly appended events may raise erroneous concurrency errors
|
20
|
-
result = @aggregate.events.multi_insert(prepared_events)
|
21
|
-
store_snapshot(prepared_events) unless result.nil?
|
22
|
-
result
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
def store_snapshot(prepared_events)
|
27
|
-
r = EventStore.redis
|
28
|
-
current_version_numbers = r.hgetall(@aggregate.snapshot_version_table)
|
29
|
-
current_version_numbers.default = -1
|
30
|
-
valid_snapshot_events = []
|
31
|
-
valid_snapshot_versions = []
|
32
|
-
prepared_events.each do |event|
|
33
|
-
if event[:version].to_i > current_version_numbers[event[:fully_qualified_name]].to_i
|
34
|
-
valid_snapshot_events << event[:fully_qualified_name]
|
35
|
-
valid_snapshot_events << (event[:version].to_s + EventStore::SNAPSHOT_DELIMITER + event[:serialized_event] + EventStore::SNAPSHOT_DELIMITER + event[:occurred_at].to_s)
|
36
|
-
valid_snapshot_versions << event[:fully_qualified_name]
|
37
|
-
valid_snapshot_versions << event[:version]
|
38
|
-
end
|
39
|
-
end
|
40
|
-
unless valid_snapshot_versions.empty?
|
41
|
-
last_version = valid_snapshot_versions.last
|
42
|
-
valid_snapshot_versions << :current_version
|
43
|
-
valid_snapshot_versions << last_version.to_i
|
44
|
-
r.multi do
|
45
|
-
r.hmset(@aggregate.snapshot_version_table, valid_snapshot_versions)
|
46
|
-
r.hmset(@aggregate.snapshot_table, valid_snapshot_events)
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
private
|
52
|
-
def has_concurrency_issue? event
|
53
|
-
event[:version] <= current_version
|
54
|
-
end
|
55
|
-
|
56
|
-
def prepare_event raw_event
|
57
|
-
raise ArgumentError.new("Cannot Append a Nil Event") unless raw_event
|
58
|
-
{ :version => raw_event.version.to_i,
|
59
|
-
:aggregate_id => raw_event.aggregate_id,
|
60
|
-
:occurred_at => Time.parse(raw_event.occurred_at.to_s).utc, #to_s truncates microseconds, which brake Time equality
|
61
|
-
:serialized_event => EventStore.escape_bytea(raw_event.serialized_event),
|
62
|
-
:fully_qualified_name => raw_event.fully_qualified_name }
|
63
|
-
end
|
64
|
-
|
65
|
-
def concurrency_error event
|
66
|
-
ConcurrencyError.new("The version of the event being added (version #{event[:version]}) is <= the current version (version #{current_version})")
|
67
|
-
end
|
68
|
-
|
69
|
-
private
|
70
|
-
def current_version
|
71
|
-
@current_version ||= @aggregate.version
|
72
|
-
end
|
73
|
-
alias :set_current_version :current_version
|
74
|
-
|
75
|
-
def validate! event_hash
|
76
|
-
[:aggregate_id, :fully_qualified_name, :occurred_at, :serialized_event, :version].each do |attribute_name|
|
77
|
-
if event_hash[attribute_name].to_s.strip.empty?
|
78
|
-
raise AttributeMissingError, "value required for #{attribute_name}"
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
end
|
84
|
-
end
|