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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dafb7ab4736d45906755a4126e7e6ace40bbb465
4
- data.tar.gz: 77acaa03589add54ed164de75a59ac1214321ef6
3
+ metadata.gz: 8ca406d692ca12cea54e8e03cd222f28c3baf20a
4
+ data.tar.gz: 8d4d4206228a30c96f0b16df727cacb2daa80f21
5
5
  SHA512:
6
- metadata.gz: c7a2b2a900507663b616b0ec80be72b3d3098cb327ab0a80973c5d3fb941fcdbc307d885f861f7ee089480fa68d36ce50420898033d179217d3f3b7f3701ff38
7
- data.tar.gz: ab25dc86a6bc356bc6d6e78378e82ee6040fe25a89f66ba8b8abfdee32971ca4f6e8501cdc97b51a447fc67cf9e7bf2c84e205b723e36f60d3bd7b5d5c9c1d29
6
+ metadata.gz: 86744d003f7c506b25f84cb1318610a99ba79528e5eec47b013bf38c90a940004834b9baf0dc368b1c444402e41be10bca074124c28d21d1fb7d9f2840ecdb03
7
+ data.tar.gz: f01ac30941112234b5b841a8596146657bb7f5ace7a49fc41d100160a32bb40330e96910edea882d28234e4d72a7b8730f825bff33e54c1c8446e2d95aff353f
@@ -0,0 +1,8 @@
1
+ require 'event_store'
2
+ Sequel.migration do
3
+ change do
4
+ alter_table((EventStore.schema + "__" + EventStore.table_name).to_sym) do
5
+ add_column :sub_key, String
6
+ end
7
+ end
8
+ end
@@ -1,7 +1,26 @@
1
+ require 'forwardable'
2
+
1
3
  module EventStore
2
4
  class Aggregate
3
-
4
- attr_reader :id, :type, :snapshot_table, :snapshot_version_table, :event_table
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
- def rebuild_snapshot!
42
- delete_snapshot!
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 events_from(version_number, max = nil)
48
- events.limit(max).where{ version >= version_number.to_i }.all.map do |event|
49
- event[:serialized_event] = EventStore.unescape_bytea(event[:serialized_event])
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
@@ -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( aggregate_id, aggregate_type = EventStore.table_name)
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 event_data
29
- event_appender.append(event_data)
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 raw_event_stream
42
+ translate_events(raw_event_stream)
40
43
  end
41
44
 
42
- def event_stream_from version_number, max=nil
43
- translate_events @aggregate.events_from(version_number, max)
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 @aggregate.event_stream_between(start_time, end_time, fully_qualified_names)
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 @aggregate.last_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
@@ -1,3 +1,3 @@
1
1
  module EventStore
2
- VERSION = '0.2.11'
2
+ VERSION = '0.3.0'
3
3
  end
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/event_appender'
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, :serialized_event, :version)
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 = "__NexEvStDelim__"
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]*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)
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(10)
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(10)
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', serialized_event, client.version + 1)
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', serialized_event, client.version + 1)
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", 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)
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
- 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)
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
- it 'should raise a ConcurrencyError if the the event version is less than current version' do
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
- it "should increment the version number by the number of unique events added" do
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
- let(:aggregate_id) {'100A' }
5
- let(:new_aggregate) { Aggregate.new(aggregate_id) }
6
- let(:client) { Client.new(aggregate_id) }
7
- let(:appender) { EventAppender.new(new_aggregate) }
8
- let(:events) { Array.new }
9
-
10
- before do
11
- @event_time = Time.parse("2001-01-01 00:00:00 UTC")
12
- (0...10).to_a.each_with_index do |version|
13
- events << EventStore::Event.new(aggregate_id, @event_time, 'event_name', "#{234532.to_s(2)}_foo}", version)
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
- it "should build an empty snapshot for a new client" do
18
- expect(new_aggregate.snapshot).to eq([])
19
- expect(new_aggregate.version).to eq(-1)
20
- expect(EventStore.redis.hget(new_aggregate.snapshot_version_table, :current_version)).to eq(nil)
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
- it "should rebuild a snapshot after it is deleted" do
24
- appender.append(events)
25
- snapshot = new_aggregate.snapshot
26
- version = new_aggregate.version
27
- new_aggregate.delete_snapshot!
28
- new_aggregate.rebuild_snapshot!
29
- expect(new_aggregate.snapshot).to eq(snapshot)
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
- it "a client should rebuild a snapshot" do
33
- expect_any_instance_of(Aggregate).to receive(:delete_snapshot!)
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
- it "should rebuild the snapshot if events exist, but the snapshot is empty" do
39
- appender.append(events)
40
- snapshot = new_aggregate.snapshot
41
- new_aggregate.delete_snapshot!
42
- 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
@@ -1,6 +1,8 @@
1
1
  require 'simplecov'
2
2
  require 'simplecov-rcov'
3
+ require 'ostruct'
3
4
  require 'pry'
5
+ require 'pry-byebug'
4
6
  require 'rspec'
5
7
 
6
8
  class SimpleCov::Formatter::MergedFormatter
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.2.11
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-09-18 00:00:00.000000000 Z
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/event_appender.rb
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