nexia_event_store 0.10.1 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8e9d038884a630f02031c484aa423b935018288d
4
- data.tar.gz: 2d3c0d69031fea0dddeed6805e483a9b14c33901
3
+ metadata.gz: a36ee7d65c27df5609edfe9b2807da29142fd332
4
+ data.tar.gz: 9e3e8394fae1ff84857675343bee94e9daa08738
5
5
  SHA512:
6
- metadata.gz: a10cbfaf1914a3cf6b0ee03a45e2548f56a552268deb082459b97e929790c0c03da0854fbc4048b840f59712c3cf3bdc1955187bfdb1a4994d97e297fad5688a
7
- data.tar.gz: b4da3da2eb6921f930c7dff3d64db1ece57bd2b4a99ae862321893d705af51f389d9dc978ffea1dc6d433e8ee152ccf7253492aac0a1f4fd11d1b0f1ddd7f1dc
6
+ metadata.gz: 8971a7a3bc6b4d3a6af6d439d7058720a56ef77b02e49e56df5e81d78631c034260b85b4007c6981b0662448cf1f7bdb359222b867eba2cfed4c03aaecb48b9f
7
+ data.tar.gz: a0012132f78ea8b63d6d2ab010f8480ee3e2b57321b5ae1abcf197e053489a88eac7714a38c830cc14bb0103742f1ea120ec4ed062e7e85bfc7583fa7f33df63
data/lib/event_store.rb CHANGED
@@ -12,6 +12,7 @@ require 'event_store/aggregate'
12
12
  require 'event_store/client'
13
13
  require 'event_store/errors'
14
14
  require 'yaml'
15
+ require 'zlib'
15
16
 
16
17
  Sequel.extension :migration
17
18
 
@@ -40,8 +41,9 @@ module EventStore
40
41
  @db
41
42
  end
42
43
 
43
- def self.redis
44
- @redis
44
+ def self.redis(hostname)
45
+ hash = Zlib::crc32(hostname)
46
+ @redis[hash % @redis.length]
45
47
  end
46
48
 
47
49
  def self.connect(*args)
@@ -49,7 +51,14 @@ module EventStore
49
51
  end
50
52
 
51
53
  def self.redis_connect(config_hash)
52
- @redis ||= Redis.new(config_hash)
54
+ if config_hash["hosts"]
55
+ generic_config = config_hash["hosts"].reject { |k, _| k == "hosts" }
56
+ @redis = config_hash["hosts"].map { |hostname|
57
+ Redis.new(generic_config.merge("host" => hostname))
58
+ }
59
+ else
60
+ @redis ||= [Redis.new(config_hash)]
61
+ end
53
62
  end
54
63
 
55
64
  def self.local_redis_config
@@ -103,7 +112,7 @@ module EventStore
103
112
  return unless connected?
104
113
  EventStore.db.from(fully_qualified_table).delete
105
114
  EventStore.db.from(fully_qualified_names_table).delete
106
- EventStore.redis.flushdb
115
+ @redis.map(&:flushdb)
107
116
  end
108
117
 
109
118
  def self.postgres(environment = 'test', table_name = 'events', schema = 'event_store_test')
@@ -142,14 +151,14 @@ module EventStore
142
151
 
143
152
  def self.custom_config(database_config, redis_config, table_name = 'events', environment = 'production')
144
153
  self.redis_connect(redis_config)
145
- database_config = database_config.inject({}) {|memo, (k,v)| memo[k.to_s] = v; memo}
146
- redis_config = redis_config.inject({}) {|memo, (k,v)| memo[k.to_s] = v; memo}
147
-
148
- @adapter = database_config["adapter"].to_s
149
- @environment = environment
150
- @db_config = database_config
151
- @table_name = table_name
152
- @schema = database_config["schema"].to_s
154
+ database_config = database_config.each_with_object({}) {|(k,v), memo| memo[k.to_s] = v}
155
+ redis_config = redis_config.each_with_object({}) {|(k,v), memo| memo[k.to_s] = v}
156
+
157
+ @adapter = database_config["adapter"].to_s
158
+ @environment = environment
159
+ @db_config = database_config
160
+ @table_name = table_name
161
+ @schema = database_config["schema"].to_s
153
162
  @use_names_table = database_config.fetch("use_names_table", true)
154
163
  connect_db
155
164
  end
@@ -168,7 +177,7 @@ module EventStore
168
177
 
169
178
  def self.create_db
170
179
  connect_db
171
- table = "#{schema}__schema_info".to_sym
180
+ table = Sequel.qualify(schema, "schema_info")
172
181
  @db.run("CREATE SCHEMA IF NOT EXISTS #{schema}")
173
182
  Sequel::Migrator.run(@db, File.expand_path(File.join('..','..','db', self.migrations_dir), __FILE__), table: table)
174
183
  end
@@ -4,7 +4,7 @@ module EventStore
4
4
  class Aggregate
5
5
  extend Forwardable
6
6
 
7
- attr_reader :id, :type, :event_table, :snapshot, :event_stream, :checkpoint_event
7
+ attr_reader :id, :type, :event_table, :snapshot, :event_stream, :checkpoint_events
8
8
 
9
9
  def_delegators :snapshot,
10
10
  :last_event,
@@ -35,13 +35,13 @@ module EventStore
35
35
  EventStore.db.from(EventStore.fully_qualified_table).select(:aggregate_id).distinct.order(:aggregate_id).limit(limit, offset).all.map{|item| item[:aggregate_id]}
36
36
  end
37
37
 
38
- def initialize(id, type = EventStore.table_name, checkpoint_event = nil)
38
+ def initialize(id, type = EventStore.table_name, checkpoint_events = [])
39
39
  @id = id
40
40
  @type = type
41
41
 
42
- @checkpoint_event = checkpoint_event
43
- @snapshot = Snapshot.new(self)
44
- @event_stream = EventStream.new(self)
42
+ @checkpoint_events = checkpoint_events
43
+ @snapshot = Snapshot.new(self)
44
+ @event_stream = EventStream.new(self)
45
45
  end
46
46
 
47
47
  def append(events, logger)
@@ -22,8 +22,9 @@ module EventStore
22
22
  Aggregate.ids(offset, limit)
23
23
  end
24
24
 
25
- def initialize(aggregate_id, aggregate_type = EventStore.table_name, checkpoint_event = nil)
26
- @aggregate = Aggregate.new(aggregate_id, aggregate_type, checkpoint_event)
25
+ def initialize(aggregate_id, aggregate_type = EventStore.table_name, checkpoint_events = [])
26
+ checkpoint_events = [checkpoint_events].flatten
27
+ @aggregate = Aggregate.new(aggregate_id, aggregate_type, checkpoint_events)
27
28
  end
28
29
 
29
30
  def exists?
@@ -2,15 +2,15 @@ module EventStore
2
2
  class EventStream
3
3
  include Enumerable
4
4
 
5
- attr_reader :event_table, :checkpoint_event
5
+ attr_reader :event_table, :checkpoint_events
6
6
 
7
7
  def initialize aggregate
8
8
  @aggregate = aggregate
9
9
  @id = @aggregate.id
10
- @checkpoint_event = aggregate.checkpoint_event
10
+ @checkpoint_events = aggregate.checkpoint_events
11
11
  @event_table_alias = "events"
12
- @event_table = "#{EventStore.schema}__#{EventStore.table_name}".to_sym
13
- @aliased_event_table = "#{event_table}___#{@event_table_alias}".to_sym
12
+ @event_table = Sequel.qualify(EventStore.schema, EventStore.table_name)
13
+ @aliased_event_table = event_table.as(@event_table_alias)
14
14
  @names_table = EventStore.fully_qualified_names_table
15
15
  end
16
16
 
@@ -57,17 +57,25 @@ module EventStore
57
57
  begin
58
58
  query = EventStore.db.from(@aliased_event_table).where(:aggregate_id => @id.to_s)
59
59
  query = query.join(@names_table, id: :fully_qualified_name_id) if EventStore.use_names_table?
60
- query = query.order("#{@event_table_alias}__id".to_sym).select_all(:events)
60
+ query = query.order { events[:id] }.select_all(:events)
61
61
  query = query.select_append(:fully_qualified_name) if EventStore.use_names_table?
62
62
  query
63
63
  end
64
64
  end
65
65
 
66
66
  def snapshot_events
67
- last_checkpoint = last_event_before(Time.now.utc, [checkpoint_event]).first if checkpoint_event
67
+ last_checkpoint = nil
68
+
69
+ if checkpoint_events
70
+ checkpoints = last_event_before(Time.now.utc, checkpoint_events)
71
+ if checkpoints.map { |e| e[:fully_qualified_name] }.uniq.length > 1
72
+ raise "unexpected multiple checkpoint event types"
73
+ end
74
+ last_checkpoint = checkpoints.last
75
+ end
68
76
 
69
77
  if last_checkpoint
70
- events.where{ events__id >= last_checkpoint[:id].to_i }
78
+ events.where{ events[:id] >= last_checkpoint[:id].to_i }
71
79
  else
72
80
  events
73
81
  end
@@ -75,7 +83,7 @@ module EventStore
75
83
 
76
84
  def events_from(event_id, max = nil)
77
85
  # note: this depends on the events table being aliased to "events" above.
78
- events.limit(max).where{events__id >= event_id.to_i }.all.map do |event|
86
+ events.limit(max).where{events[:id] >= event_id.to_i }.all.map do |event|
79
87
  event[:serialized_event] = EventStore.unescape_bytea(event[:serialized_event])
80
88
  event
81
89
  end
@@ -99,8 +107,8 @@ module EventStore
99
107
  timestampz = start_time.strftime("%Y-%m-%d %H:%M:%S%z")
100
108
 
101
109
  rows = fully_qualified_names.inject([]) { |memo, name|
102
- memo + events.where(events__id: events.where(fully_qualified_name: name).where { occurred_at < timestampz }
103
- .select { max(:events__id) }.unordered.group(:sub_key)).all
110
+ memo + events.where(Sequel.qualify("events", "id") => events.where(fully_qualified_name: name).where { occurred_at < timestampz }
111
+ .select { max(events[:id]) }.unordered.group(:sub_key)).all
104
112
  }.sort_by { |r| r[:occurred_at] }
105
113
 
106
114
  rows.map {|r| r[:serialized_event] = EventStore.unescape_bytea(r[:serialized_event]); r}
@@ -8,7 +8,7 @@ module EventStore
8
8
 
9
9
  def initialize aggregate
10
10
  @aggregate = aggregate
11
- @redis = EventStore.redis
11
+ @redis = EventStore.redis(aggregate.id)
12
12
  @snapshot_table = "#{@aggregate.type}_snapshots_for_#{@aggregate.id}"
13
13
  @snapshot_event_id_table = "#{@aggregate.type}_snapshot_event_ids_for_#{@aggregate.id}"
14
14
  end
@@ -65,7 +65,7 @@ module EventStore
65
65
  end
66
66
 
67
67
  def delete_snapshot!
68
- EventStore.redis.del [snapshot_table, snapshot_event_id_table]
68
+ @redis.del [snapshot_table, snapshot_event_id_table]
69
69
  end
70
70
 
71
71
  def store_snapshot(prepared_events, logger=default_logger)
@@ -1,3 +1,3 @@
1
1
  module EventStore
2
- VERSION = '0.10.1'
2
+ VERSION = '0.11.0'
3
3
  end
@@ -56,7 +56,7 @@ describe EventStore::Client do
56
56
  end
57
57
 
58
58
  it "should be empty for aggregates without events" do
59
- stream = es_client.new(100, :device).raw_event_stream
59
+ stream = es_client.new("100", :device).raw_event_stream
60
60
  expect(stream.empty?).to be_truthy
61
61
  end
62
62
 
@@ -80,7 +80,7 @@ describe EventStore::Client do
80
80
  end
81
81
 
82
82
  it "should be empty for aggregates without events" do
83
- stream = es_client.new(100, :device).raw_event_stream
83
+ stream = es_client.new("100", :device).raw_event_stream
84
84
  expect(stream.empty?).to be_truthy
85
85
  end
86
86
 
@@ -116,6 +116,7 @@ describe EventStore::Client do
116
116
  end
117
117
  end
118
118
 
119
+
119
120
  describe "#raw_event_streams_from_event_id" do
120
121
  subject { es_client.new(AGGREGATE_ID_ONE, :device) }
121
122
  let(:raw_stream) { subject.raw_event_stream }
@@ -0,0 +1,69 @@
1
+ require "spec_helper"
2
+
3
+ module EventStore
4
+ describe EventStream do
5
+ let(:aggregate_id) { SecureRandom.uuid }
6
+ let(:checkpoint_events) {
7
+ %w[ checkpoint_event_1 checkpoint_event_2 ]
8
+ }
9
+ let(:aggregate) {
10
+ Aggregate.new(
11
+ aggregate_id,
12
+ EventStore.table_name,
13
+ checkpoint_events
14
+ )
15
+ }
16
+
17
+ let(:event_time) { Time.parse("2001-01-01 00:00:00 UTC") }
18
+
19
+ let(:events) {
20
+ [EventStore::Event.new(aggregate_id, (event_time - 2000).utc, "old_event", "zone", "#{1000.to_s(2)}_foo"),
21
+ EventStore::Event.new(aggregate_id, (event_time - 1000).utc, "checkpoint_event_2", "zone", "#{1001.to_s(2)}_foo"),
22
+ EventStore::Event.new(aggregate_id, (event_time + 100).utc, "after_checkpoint_1", "zone", "#{1002.to_s(2)}_foo"),
23
+ EventStore::Event.new(aggregate_id, (event_time).utc, "after_checkpoint_2", "zone", "#{12.to_s(2)}_foo")]
24
+ }
25
+
26
+ subject(:event_stream) { EventStream.new aggregate }
27
+
28
+ let(:logger) { Logger.new("/dev/null") }
29
+
30
+ before(:each) do
31
+ event_stream.append events, logger
32
+ end
33
+
34
+ describe "#snapshot_events" do
35
+ it "returns events since the last of one of multiple checkpoint events" do
36
+ snapshot_events = event_stream.snapshot_events
37
+
38
+ expect(snapshot_events.count).to eql(3)
39
+
40
+ expect(
41
+ snapshot_events.find { |event| event[:fully_qualified_name] == 'checkpoint_event_2' }
42
+ ).not_to be_nil
43
+
44
+ expect(
45
+ snapshot_events.find { |event| event[:fully_qualified_name] == 'after_checkpoint_1' }
46
+ ).not_to be_nil
47
+
48
+ expect(
49
+ snapshot_events.find { |event| event[:fully_qualified_name] == 'after_checkpoint_2' }
50
+ ).not_to be_nil
51
+ end
52
+
53
+ it "raises an exception if multiple event types are returned" do
54
+ event = EventStore::Event.new(aggregate_id,
55
+ (event_time - 1000).utc,
56
+ "checkpoint_event_1",
57
+ "zone",
58
+ "#{1001.to_s(2)}_foo")
59
+
60
+ event_stream.append [event], logger
61
+
62
+ expect { event_stream.snapshot_events }.to raise_error do |error|
63
+ expect(error).to be_a(RuntimeError)
64
+ expect(error.message).to eql("unexpected multiple checkpoint event types")
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -6,13 +6,15 @@ AGGREGATE_ID_TWO = SecureRandom.uuid
6
6
 
7
7
  module EventStore
8
8
  describe "Snapshots" do
9
+ let(:redis) { EventStore.redis("") }
10
+
9
11
  context "when there are no events" do
10
- let(:client) { EventStore::Client.new(AGGREGATE_ID_ONE) }
12
+ let(:client) { EventStore::Client.new(AGGREGATE_ID_ONE) }
11
13
 
12
14
  it "should build an empty snapshot for a new client" do
13
15
  expect(client.snapshot.any?).to eq(false)
14
16
  expect(client.event_id).to eq(-1)
15
- expect(EventStore.redis.hget(client.snapshot_event_id_table, :current_event_id)).to eq(nil)
17
+ expect(redis.hget(client.snapshot_event_id_table, :current_event_id)).to eq(nil)
16
18
  end
17
19
 
18
20
  it "a client should rebuild a snapshot" do
@@ -104,8 +106,8 @@ module EventStore
104
106
  before(:each) { snapshot.reject! { true } }
105
107
 
106
108
  it "deletes the snapshot out of Redis" do
107
- expect(EventStore.redis.keys(snapshot.snapshot_table).length).to eq(0)
108
- expect(EventStore.redis.keys(snapshot.snapshot_event_id_table).length).to eq(0)
109
+ expect(redis.keys(snapshot.snapshot_table).length).to eq(0)
110
+ expect(redis.keys(snapshot.snapshot_event_id_table).length).to eq(0)
109
111
  end
110
112
  end
111
113
  end
@@ -3,7 +3,7 @@ require "mock_redis"
3
3
 
4
4
  module EventStore
5
5
  describe Snapshot do
6
- let(:redis) { EventStore.redis }
6
+ let(:redis) { EventStore.redis("") } # there's only one in test env anyway
7
7
  let(:aggregate_type) { "awesome" }
8
8
  let(:aggregate_id) { "superman" }
9
9
  let(:events) { [] }
@@ -14,7 +14,7 @@ module EventStore
14
14
  type: aggregate_type,
15
15
  id: aggregate_id,
16
16
  events: double(all: events),
17
- checkpoint_event: checkpoint_event,
17
+ checkpoint_events: [checkpoint_event],
18
18
  snapshot_events: double(all: snapshot_events))
19
19
  }
20
20
 
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.10.1
4
+ version: 0.11.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: 2016-08-08 00:00:00.000000000 Z
12
+ date: 2017-08-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -208,6 +208,7 @@ files:
208
208
  - spec/event_store/client_spec.rb
209
209
  - spec/event_store/config_spec.rb
210
210
  - spec/event_store/event_store_spec.rb
211
+ - spec/event_store/event_stream_spec.rb
211
212
  - spec/event_store/serialized_binary_event_data.txt
212
213
  - spec/event_store/snapshot_spec.rb
213
214
  - spec/event_store/vertica guy notes.txt
@@ -235,7 +236,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
235
236
  version: '0'
236
237
  requirements: []
237
238
  rubyforge_project:
238
- rubygems_version: 2.4.5.1
239
+ rubygems_version: 2.5.1
239
240
  signing_key:
240
241
  specification_version: 4
241
242
  summary: Ruby implementation of an EventSource (A+ES) for the Nexia Ecosystem
@@ -247,6 +248,7 @@ test_files:
247
248
  - spec/event_store/client_spec.rb
248
249
  - spec/event_store/config_spec.rb
249
250
  - spec/event_store/event_store_spec.rb
251
+ - spec/event_store/event_stream_spec.rb
250
252
  - spec/event_store/serialized_binary_event_data.txt
251
253
  - spec/event_store/snapshot_spec.rb
252
254
  - spec/event_store/vertica guy notes.txt