ruby_event_store 0.18.2 → 0.19.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: cbe2a5460bf30467128c5abc45d81cbd9898b44f
4
- data.tar.gz: 4c8e7701c16e6e7d08173b22a20799a8d82bf57d
3
+ metadata.gz: cf38d679db05a58d03f5cf03128e1515a44ae1e1
4
+ data.tar.gz: ffabae418e7ec0fb99fe6b546790a155d2a1c361
5
5
  SHA512:
6
- metadata.gz: c268df5cf9ca40e57dbb76c954c7c161383df97fa50d5d895c4e3afeccd7a0fcb60718fd423f454a16b866b59f73a50040e07c41ec34a9adace72b653a7c3f89
7
- data.tar.gz: c6a36bc896db65bcc54dabaf5621d694b14795dd3f61a6c42717676728363645e3e9568410ba5be4248d561f371e78285a76a7d27f0d49a2c99ebc21fca3a6e1
6
+ metadata.gz: 7ab222cbac66327aa236df8dc5c8f5b789854559d59a2d731914e031f1874f861ce9743b51a4d6c2a5f0351458f83b6fa0c6bbce16f887de69d600d8ee70a406
7
+ data.tar.gz: 0b89629ee2da94623d4bb04998e0e686e424d94a1a05f306e168b1d0afb51d19ffd16ccb72d989249821bf0688a991237e99ce41d3d2746cba2cc664ea8a9455
data/Makefile CHANGED
@@ -10,8 +10,7 @@ test: ## Run tests
10
10
 
11
11
  mutate: test ## Run mutation tests
12
12
  @echo "Running mutation tests - only 100% free mutation will be accepted"
13
- @bundle exec mutant --include lib --require ruby_event_store --use rspec "RubyEventStore*" \
14
- --ignore-subject "RubyEventStore.const_missing"
13
+ @bundle exec mutant --include lib --require ruby_event_store --use rspec "RubyEventStore*" --ignore-subject "RubyEventStore.const_missing" --ignore-subject "RubyEventStore::InMemoryRepository#append_with_synchronize" --ignore-subject "RubyEventStore::InMemoryRepository#normalize_to_array" --ignore-subject "RubyEventStore::Client#normalize_to_array"
15
14
 
16
15
  build:
17
16
  @gem build -V ruby_event_store.gemspec
@@ -12,16 +12,22 @@ module RubyEventStore
12
12
  @clock = clock
13
13
  end
14
14
 
15
- def publish_event(event, stream_name: GLOBAL_STREAM, expected_version: :any)
16
- append_to_stream(event, stream_name: stream_name, expected_version: expected_version)
17
- event_broker.notify_subscribers(event)
15
+ def publish_events(events, stream_name: GLOBAL_STREAM, expected_version: :any)
16
+ append_to_stream(events, stream_name: stream_name, expected_version: expected_version)
17
+ events.each do |ev|
18
+ event_broker.notify_subscribers(ev)
19
+ end
18
20
  :ok
19
21
  end
20
22
 
21
- def append_to_stream(event, stream_name: GLOBAL_STREAM, expected_version: :any)
22
- validate_expected_version(stream_name, expected_version)
23
- enriched_event = enrich_event_metadata(event)
24
- repository.create(enriched_event, stream_name)
23
+ def publish_event(event, stream_name: GLOBAL_STREAM, expected_version: :any)
24
+ publish_events([event], stream_name: stream_name, expected_version: expected_version)
25
+ end
26
+
27
+ def append_to_stream(events, stream_name: GLOBAL_STREAM, expected_version: :any)
28
+ events = normalize_to_array(events)
29
+ events.each{|event| enrich_event_metadata(event) }
30
+ repository.append_to_stream(events, stream_name, expected_version)
25
31
  :ok
26
32
  end
27
33
 
@@ -64,13 +70,13 @@ module RubyEventStore
64
70
  end
65
71
 
66
72
  def subscribe(subscriber, event_types, &proc)
67
- event_broker.add_subscriber(subscriber_or_proxy(subscriber), event_types).tap do |unsub|
73
+ event_broker.add_subscriber(subscriber, event_types).tap do |unsub|
68
74
  handle_subscribe(unsub, &proc)
69
75
  end
70
76
  end
71
77
 
72
78
  def subscribe_to_all_events(subscriber, &proc)
73
- event_broker.add_global_subscriber(subscriber_or_proxy(subscriber)).tap do |unsub|
79
+ event_broker.add_global_subscriber(subscriber).tap do |unsub|
74
80
  handle_subscribe(unsub, &proc)
75
81
  end
76
82
  end
@@ -78,8 +84,8 @@ module RubyEventStore
78
84
  private
79
85
  attr_reader :repository, :page_size, :event_broker, :metadata_proc, :clock
80
86
 
81
- def subscriber_or_proxy(subscriber)
82
- subscriber.instance_of?(Class) ? event_broker.proxy_for(subscriber) : subscriber
87
+ def normalize_to_array(events)
88
+ return *events
83
89
  end
84
90
 
85
91
  def enrich_event_metadata(event)
@@ -87,14 +93,15 @@ module RubyEventStore
87
93
  metadata[:timestamp] ||= clock.()
88
94
  metadata.merge!(metadata_proc.call || {}) if metadata_proc
89
95
 
90
- event.class.new(event_id: event.event_id, metadata: metadata, data: event.data)
96
+ # event.class.new(event_id: event.event_id, metadata: metadata, data: event.data)
91
97
  end
92
98
 
93
- def handle_subscribe(unsub)
94
- if block_given?
95
- yield
99
+ def handle_subscribe(unsub, &proc)
100
+ begin
101
+ proc.call
102
+ ensure
96
103
  unsub.()
97
- end
104
+ end if proc
98
105
  end
99
106
 
100
107
  class Page
@@ -113,22 +120,5 @@ module RubyEventStore
113
120
  attr_reader :start, :count
114
121
  end
115
122
 
116
- def validate_expected_version(stream_name, expected_version)
117
- raise InvalidExpectedVersion if expected_version.nil?
118
- case expected_version
119
- when :any
120
- return
121
- when :none
122
- return if last_stream_event_id(stream_name).nil?
123
- else
124
- return if last_stream_event_id(stream_name).eql?(expected_version)
125
- end
126
- raise WrongExpectedEventVersion
127
- end
128
-
129
- def last_stream_event_id(stream_name)
130
- last = repository.last_stream_event(stream_name)
131
- last.event_id if last
132
- end
133
123
  end
134
124
  end
@@ -6,10 +6,11 @@ module RubyEventStore
6
6
  SubscriberNotExist = Class.new(StandardError)
7
7
  InvalidPageStart = Class.new(ArgumentError)
8
8
  InvalidPageSize = Class.new(ArgumentError)
9
+ EventDuplicatedInStream = Class.new(StandardError)
9
10
 
10
11
  class InvalidHandler < StandardError
11
- def initialize(subscriber_klass)
12
- super("#call method not found in #{subscriber_klass} subscriber. Are you sure it is a valid subscriber?")
12
+ def initialize(object)
13
+ super("#call method not found in #{object.inspect} subscriber. Are you sure it is a valid subscriber?")
13
14
  end
14
15
  end
15
16
  end
@@ -27,6 +27,17 @@ module RubyEventStore
27
27
  other_event.data.eql?(data)
28
28
  end
29
29
 
30
+ BIG_VALUE = 0b111111100100000010010010110011101011000101010101001100100110000
31
+
32
+ # We don't use metadata because == does not use metadata
33
+ def hash
34
+ [
35
+ self.class,
36
+ event_id,
37
+ data
38
+ ].hash ^ BIG_VALUE
39
+ end
40
+
30
41
  alias_method :eql?, :==
31
42
  end
32
- end
43
+ end
@@ -1,24 +1,34 @@
1
1
  require 'ostruct'
2
+ require 'thread'
2
3
 
3
4
  module RubyEventStore
4
5
  class InMemoryRepository
5
6
  def initialize
6
7
  @all = Array.new
7
8
  @streams = Hash.new
9
+ @mutex = Mutex.new
8
10
  end
9
11
 
10
- def create(event, stream_name)
12
+ def append_to_stream(events, stream_name, expected_version)
13
+ raise InvalidExpectedVersion if !expected_version.equal?(:any) && stream_name.eql?(GLOBAL_STREAM)
14
+ events = normalize_to_array(events)
11
15
  stream = read_stream_events_forward(stream_name)
12
- all.push(event)
13
- stream.push(event)
14
- streams[stream_name] = stream
15
- event
16
+ expected_version = case expected_version
17
+ when :none
18
+ -1
19
+ when :auto, :any
20
+ stream.size - 1
21
+ when Integer
22
+ expected_version
23
+ else
24
+ raise InvalidExpectedVersion
25
+ end
26
+ append_with_synchronize(events, expected_version, stream, stream_name)
27
+ self
16
28
  end
17
29
 
18
30
  def delete_stream(stream_name)
19
- removed = read_stream_events_forward(stream_name).map(&:event_id)
20
31
  streams.delete(stream_name)
21
- all.delete_if{|ev| removed.include?(ev.event_id)}
22
32
  end
23
33
 
24
34
  def has_event?(event_id)
@@ -58,6 +68,34 @@ module RubyEventStore
58
68
  private
59
69
  attr_accessor :streams, :all
60
70
 
71
+ def normalize_to_array(events)
72
+ return *events
73
+ end
74
+
75
+ def append_with_synchronize(events, expected_version, stream, stream_name)
76
+ # expected_version :auto assumes external lock is used
77
+ # which makes reading stream before writing safe.
78
+ #
79
+ # To emulate potential concurrency issues of :auto strategy without
80
+ # such external lock we use Thread.pass to make race
81
+ # conditions more likely. And we only use mutex.synchronize for writing
82
+ # not for the whole read+write algorithm.
83
+ Thread.pass
84
+ @mutex.synchronize do
85
+ append(events, expected_version, stream, stream_name)
86
+ end
87
+ end
88
+
89
+ def append(events, expected_version, stream, stream_name)
90
+ raise WrongExpectedEventVersion unless (stream.size - 1).equal?(expected_version)
91
+ events.each do |event|
92
+ all.push(event)
93
+ raise EventDuplicatedInStream if stream.any?{|ev| ev.event_id.eql?(event.event_id) }
94
+ stream.push(event)
95
+ end
96
+ streams[stream_name] = stream
97
+ end
98
+
61
99
  def read_batch(source, start_event_id, count)
62
100
  return source[0..count-1] if start_event_id.equal?(:head)
63
101
  start_index = index_of(source, start_event_id)
@@ -27,16 +27,12 @@ module RubyEventStore
27
27
  end
28
28
  end
29
29
 
30
- def proxy_for(klass)
31
- dispatcher.proxy_for(klass)
32
- end
33
-
34
30
  private
35
31
  attr_reader :subscribers, :dispatcher
36
32
 
37
33
  def verify_subscriber(subscriber)
38
34
  raise SubscriberNotExist if subscriber.nil?
39
- raise InvalidHandler.new(subscriber.class) unless subscriber.respond_to?(:call)
35
+ dispatcher.verify(subscriber)
40
36
  end
41
37
 
42
38
  def subscribe(subscriber, event_types)
@@ -1,14 +1,25 @@
1
1
  module RubyEventStore
2
2
  module PubSub
3
+
3
4
  class Dispatcher
4
5
  def call(subscriber, event)
6
+ subscriber = subscriber.new if Class === subscriber
5
7
  subscriber.call(event)
6
8
  end
7
9
 
8
- def proxy_for(klass)
9
- raise InvalidHandler.new(klass) unless klass.method_defined?(:call)
10
- ->(e) { klass.new.call(e) }
10
+ def verify(subscriber)
11
+ subscriber = klassify(subscriber)
12
+ subscriber.respond_to?(:call) or raise InvalidHandler.new(subscriber)
13
+ end
14
+
15
+ private
16
+
17
+ def klassify(subscriber)
18
+ Class === subscriber ? subscriber.new : subscriber
19
+ rescue ArgumentError
20
+ raise InvalidHandler.new(subscriber)
11
21
  end
12
22
  end
23
+
13
24
  end
14
25
  end
@@ -1,30 +1,34 @@
1
1
  RSpec.shared_examples :dispatcher do |dispatcher|
2
- specify "calls subscribed handler" do
3
- handler = double(:handler)
2
+ specify "calls subscribed instance" do
3
+ handler = HandlerClass.new
4
4
  event = instance_double(::RubyEventStore::Event)
5
5
 
6
6
  expect(handler).to receive(:call).with(event)
7
7
  dispatcher.(handler, event)
8
8
  end
9
9
 
10
- specify "returns callable proxy" do
10
+ specify "calls subscribed class" do
11
11
  event = instance_double(::RubyEventStore::Event)
12
12
 
13
- handler = dispatcher.proxy_for(HandlerClass)
14
- expect(handler).to receive(:call).with(event).and_call_original
15
- dispatcher.(handler, event)
16
- expect(HandlerClass.received).to eq(event)
13
+ expect(HandlerClass).to receive(:new).and_return( h = HandlerClass.new )
14
+ expect(h).to receive(:call).with(event)
15
+ dispatcher.(HandlerClass, event)
17
16
  end
18
17
 
19
- specify "fails to build proxy when no call method defined on class" do
20
- message = "#call method not found " +
21
- "in String subscriber." +
22
- " Are you sure it is a valid subscriber?"
23
-
24
- expect { dispatcher.proxy_for(String) }.to raise_error(::RubyEventStore::InvalidHandler, message)
18
+ specify "allows callable classes and instances" do
19
+ expect do
20
+ dispatcher.verify(HandlerClass)
21
+ end.not_to raise_error
22
+ expect do
23
+ dispatcher.verify(HandlerClass.new)
24
+ end.not_to raise_error
25
+ expect do
26
+ dispatcher.verify(Proc.new{ "yo" })
27
+ end.not_to raise_error
25
28
  end
26
29
 
27
30
  private
31
+
28
32
  class HandlerClass
29
33
  @@received = nil
30
34
  def self.received
@@ -23,6 +23,9 @@ RSpec.shared_examples :event_broker do |broker_class|
23
23
  @dispatched = []
24
24
  end
25
25
 
26
+ def verify(_subscriber)
27
+ end
28
+
26
29
  def call(subscriber, event)
27
30
  @dispatched << {subscriber: subscriber, event: event}
28
31
  end
@@ -58,19 +61,17 @@ RSpec.shared_examples :event_broker do |broker_class|
58
61
  end
59
62
 
60
63
  it 'raises error when no valid method on handler' do
61
- message = "#call method not found " +
62
- "in InvalidTestHandler subscriber." +
63
- " Are you sure it is a valid subscriber?"
64
64
  subscriber = InvalidTestHandler.new
65
- expect { broker.add_subscriber(subscriber, [Test1DomainEvent]) }.to raise_error(RubyEventStore::InvalidHandler, message)
65
+ expect do
66
+ broker.add_subscriber(subscriber, [Test1DomainEvent])
67
+ end.to raise_error(RubyEventStore::InvalidHandler)
66
68
  end
67
69
 
68
70
  it 'raises error when no valid method on global handler' do
69
- message = "#call method not found " +
70
- "in InvalidTestHandler subscriber." +
71
- " Are you sure it is a valid subscriber?"
72
71
  subscriber = InvalidTestHandler.new
73
- expect { broker.add_global_subscriber(subscriber) }.to raise_error(RubyEventStore::InvalidHandler, message)
72
+ expect do
73
+ broker.add_global_subscriber(subscriber)
74
+ end.to raise_error(RubyEventStore::InvalidHandler)
74
75
  end
75
76
 
76
77
  it 'returns lambda as an output of global subscribe methods' do
@@ -121,19 +122,6 @@ RSpec.shared_examples :event_broker do |broker_class|
121
122
  expect(dispatcher.dispatched).to eq([{subscriber: handler, event: event1}])
122
123
  end
123
124
 
124
- it "returns callable proxy" do
125
- proxy = broker.proxy_for(TestHandler)
126
- expect(proxy.respond_to?(:call)).to be_truthy
127
- end
128
-
129
- specify "fails to build proxy when no call method defined on class" do
130
- message = "#call method not found " +
131
- "in InvalidTestHandler subscriber." +
132
- " Are you sure it is a valid subscriber?"
133
-
134
- expect { broker.proxy_for(InvalidTestHandler) }.to raise_error(::RubyEventStore::InvalidHandler, message)
135
- end
136
-
137
125
  private
138
126
  class HandlerClass
139
127
  @@received = nil
@@ -6,15 +6,249 @@ RSpec.shared_examples :event_repository do |repository_class|
6
6
  expect(repository.read_all_streams_forward(:head, 1)).to be_empty
7
7
  end
8
8
 
9
- it 'what you get is what you gave' do
10
- created = repository.create(event = TestDomainEvent.new, 'stream')
11
- expect(created.object_id).to eq event.object_id
9
+ specify 'publish fail if expected version is nil' do
10
+ expect do
11
+ repository.append_to_stream(event = TestDomainEvent.new, 'stream', nil)
12
+ end.to raise_error(RubyEventStore::InvalidExpectedVersion)
12
13
  end
13
14
 
14
- it 'created event is stored in given stream' do
15
+ specify 'append_to_stream returns self' do
16
+ repository.
17
+ append_to_stream(event = TestDomainEvent.new, 'stream', -1).
18
+ append_to_stream(event = TestDomainEvent.new, 'stream', 0)
19
+ end
20
+
21
+ specify 'adds initial event to a new stream' do
22
+ repository.append_to_stream(event = TestDomainEvent.new, 'stream', :none)
23
+ expect(repository.read_all_streams_forward(:head, 1).first).to eq(event)
24
+ expect(repository.read_stream_events_forward('stream').first).to eq(event)
25
+ expect(repository.read_stream_events_forward('other_stream')).to be_empty
26
+ end
27
+
28
+ specify 'adds multiple initial events to a new stream' do
29
+ repository.append_to_stream([
30
+ event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
31
+ event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
32
+ ], 'stream', :none)
33
+ expect(repository.read_all_streams_forward(:head, 2)).to eq([event0, event1])
34
+ expect(repository.read_stream_events_forward('stream')).to eq([event0, event1])
35
+ end
36
+
37
+ specify 'correct expected version on second write' do
38
+ repository.append_to_stream([
39
+ event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
40
+ event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
41
+ ], 'stream', :none)
42
+ repository.append_to_stream([
43
+ event2 = TestDomainEvent.new(event_id: SecureRandom.uuid),
44
+ event3 = TestDomainEvent.new(event_id: SecureRandom.uuid),
45
+ ], 'stream', 1)
46
+ expect(repository.read_all_streams_forward(:head, 4)).to eq([event0, event1, event2, event3])
47
+ expect(repository.read_stream_events_forward('stream')).to eq([event0, event1, event2, event3])
48
+ end
49
+
50
+ specify 'incorrect expected version on second write' do
51
+ repository.append_to_stream([
52
+ event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
53
+ event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
54
+ ], 'stream', :none)
55
+ expect do
56
+ repository.append_to_stream([
57
+ event2 = TestDomainEvent.new(event_id: SecureRandom.uuid),
58
+ event3 = TestDomainEvent.new(event_id: SecureRandom.uuid),
59
+ ], 'stream', 0)
60
+ end.to raise_error(RubyEventStore::WrongExpectedEventVersion)
61
+
62
+ expect(repository.read_all_streams_forward(:head, 4)).to eq([event0, event1])
63
+ expect(repository.read_stream_events_forward('stream')).to eq([event0, event1])
64
+ end
65
+
66
+ specify ':none on first and subsequent write' do
67
+ repository.append_to_stream([
68
+ eventA = TestDomainEvent.new(event_id: SecureRandom.uuid),
69
+ ], 'stream', :none)
70
+ expect do
71
+ repository.append_to_stream([
72
+ eventB = TestDomainEvent.new(event_id: SecureRandom.uuid),
73
+ ], 'stream', :none)
74
+ end.to raise_error(RubyEventStore::WrongExpectedEventVersion)
75
+ expect(repository.read_all_streams_forward(:head, 1)).to eq([eventA])
76
+ expect(repository.read_stream_events_forward('stream')).to eq([eventA])
77
+ end
78
+
79
+ specify ':any allows stream with best-effort order and no guarantee' do
80
+ repository.append_to_stream([
81
+ event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
82
+ event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
83
+ ], 'stream', :any)
84
+ repository.append_to_stream([
85
+ event2 = TestDomainEvent.new(event_id: SecureRandom.uuid),
86
+ event3 = TestDomainEvent.new(event_id: SecureRandom.uuid),
87
+ ], 'stream', :any)
88
+ expect(repository.read_all_streams_forward(:head, 4).to_set).to eq(Set.new([event0, event1, event2, event3]))
89
+ expect(repository.read_stream_events_forward('stream').to_set).to eq(Set.new([event0, event1, event2, event3]))
90
+ end
91
+
92
+ specify ':auto queries for last position in given stream' do
93
+ skip unless test_expected_version_auto
94
+ repository.append_to_stream([
95
+ eventA = TestDomainEvent.new(event_id: SecureRandom.uuid),
96
+ eventB = TestDomainEvent.new(event_id: SecureRandom.uuid),
97
+ eventC = TestDomainEvent.new(event_id: SecureRandom.uuid),
98
+ ], 'another', :auto)
99
+ repository.append_to_stream([
100
+ event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
101
+ event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
102
+ ], 'stream', :auto)
103
+ repository.append_to_stream([
104
+ event2 = TestDomainEvent.new(event_id: SecureRandom.uuid),
105
+ event3 = TestDomainEvent.new(event_id: SecureRandom.uuid),
106
+ ], 'stream', 1)
107
+ end
108
+
109
+ specify ':auto starts from 0' do
110
+ skip unless test_expected_version_auto
111
+ repository.append_to_stream([
112
+ event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
113
+ ], 'stream', :auto)
114
+ expect do
115
+ repository.append_to_stream([
116
+ event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
117
+ ], 'stream', -1)
118
+ end.to raise_error(RubyEventStore::WrongExpectedEventVersion)
119
+ end
120
+
121
+ specify ':auto queries for last position and follows in incremental way' do
122
+ skip unless test_expected_version_auto
123
+ # It is expected that there is higher level lock
124
+ # So this query is safe from race conditions
125
+ repository.append_to_stream([
126
+ event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
127
+ event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
128
+ ], 'stream', :auto)
129
+ repository.append_to_stream([
130
+ event2 = TestDomainEvent.new(event_id: SecureRandom.uuid),
131
+ event3 = TestDomainEvent.new(event_id: SecureRandom.uuid),
132
+ ], 'stream', :auto)
133
+ expect(repository.read_all_streams_forward(:head, 4)).to eq([
134
+ event0, event1,
135
+ event2, event3
136
+ ])
137
+ expect(repository.read_stream_events_forward('stream')).to eq([event0, event1, event2, event3])
138
+ end
139
+
140
+ specify ':auto is compatible with manual expectation' do
141
+ skip unless test_expected_version_auto
142
+ repository.append_to_stream([
143
+ event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
144
+ event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
145
+ ], 'stream', :auto)
146
+ repository.append_to_stream([
147
+ event2 = TestDomainEvent.new(event_id: SecureRandom.uuid),
148
+ event3 = TestDomainEvent.new(event_id: SecureRandom.uuid),
149
+ ], 'stream', 1)
150
+ expect(repository.read_all_streams_forward(:head, 4)).to eq([event0, event1, event2, event3])
151
+ expect(repository.read_stream_events_forward('stream')).to eq([event0, event1, event2, event3])
152
+ end
153
+
154
+ specify 'manual is compatible with auto expectation' do
155
+ skip unless test_expected_version_auto
156
+ repository.append_to_stream([
157
+ event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
158
+ event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
159
+ ], 'stream', :none)
160
+ repository.append_to_stream([
161
+ event2 = TestDomainEvent.new(event_id: SecureRandom.uuid),
162
+ event3 = TestDomainEvent.new(event_id: SecureRandom.uuid),
163
+ ], 'stream', :auto)
164
+ expect(repository.read_all_streams_forward(:head, 4)).to eq([event0, event1, event2, event3])
165
+ expect(repository.read_stream_events_forward('stream')).to eq([event0, event1, event2, event3])
166
+ end
167
+
168
+ specify 'unlimited concurrency for :any - everything should succeed' do
169
+ skip unless test_race_conditions_any
170
+ verify_conncurency_assumptions
171
+ begin
172
+ concurrency_level = 4
173
+
174
+ fail_occurred = false
175
+ wait_for_it = true
176
+
177
+ threads = concurrency_level.times.map do |i|
178
+ Thread.new do
179
+ true while wait_for_it
180
+ begin
181
+ 100.times do |j|
182
+ eid = "0000000#{i}-#{sprintf("%04d", j)}-0000-0000-000000000000"
183
+ repository.append_to_stream([
184
+ TestDomainEvent.new(event_id: eid),
185
+ ], 'stream', :any)
186
+ end
187
+ rescue RubyEventStore::WrongExpectedEventVersion
188
+ fail_occurred = true
189
+ end
190
+ end
191
+ end
192
+ wait_for_it = false
193
+ threads.each(&:join)
194
+ expect(fail_occurred).to eq(false)
195
+ expect(repository.read_stream_events_forward('stream').size).to eq(400)
196
+ events_in_stream = repository.read_stream_events_forward('stream')
197
+ expect(events_in_stream.size).to eq(400)
198
+ events0 = events_in_stream.select do |ev|
199
+ ev.event_id.start_with?("0-")
200
+ end
201
+ expect(events0).to eq(events0.sort_by{|ev| ev.event_id })
202
+ ensure
203
+ cleanup_concurrency_test
204
+ end
205
+ end
206
+
207
+ specify 'limited concurrency for :auto - some operations will fail without outside lock, stream is ordered' do
208
+ skip unless test_expected_version_auto
209
+ skip unless test_race_conditions_auto
210
+ verify_conncurency_assumptions
211
+ begin
212
+ concurrency_level = 4
213
+
214
+ fail_occurred = 0
215
+ wait_for_it = true
216
+
217
+ threads = concurrency_level.times.map do |i|
218
+ Thread.new do
219
+ true while wait_for_it
220
+ 100.times do |j|
221
+ begin
222
+ eid = "0000000#{i}-#{sprintf("%04d", j)}-0000-0000-000000000000"
223
+ repository.append_to_stream([
224
+ TestDomainEvent.new(event_id: eid),
225
+ ], 'stream', :auto)
226
+ sleep(rand(concurrency_level) / 1000.0)
227
+ rescue RubyEventStore::WrongExpectedEventVersion
228
+ fail_occurred +=1
229
+ end
230
+ end
231
+ end
232
+ end
233
+ wait_for_it = false
234
+ threads.each(&:join)
235
+ expect(fail_occurred).to be > 0
236
+ events_in_stream = repository.read_stream_events_forward('stream')
237
+ expect(events_in_stream.size).to be < 400
238
+ expect(events_in_stream.size).to be >= 100
239
+ events0 = events_in_stream.select do |ev|
240
+ ev.event_id.start_with?("0-")
241
+ end
242
+ expect(events0).to eq(events0.sort_by{|ev| ev.event_id })
243
+ additional_limited_concurrency_for_auto_check
244
+ ensure
245
+ cleanup_concurrency_test
246
+ end
247
+ end
248
+
249
+ it 'appended event is stored in given stream' do
15
250
  expected_event = TestDomainEvent.new(data: {})
16
- created = repository.create(expected_event, 'stream')
17
- expect(created).to eq(expected_event)
251
+ repository.append_to_stream(expected_event, 'stream', :any)
18
252
  expect(repository.read_all_streams_forward(:head, 1).first).to eq(expected_event)
19
253
  expect(repository.read_stream_events_forward('stream').first).to eq(expected_event)
20
254
  expect(repository.read_stream_events_forward('other_stream')).to be_empty
@@ -22,92 +256,180 @@ RSpec.shared_examples :event_repository do |repository_class|
22
256
 
23
257
  it 'data attributes are retrieved' do
24
258
  event = TestDomainEvent.new(data: { order_id: 3 })
25
- repository.create(event, 'stream')
259
+ repository.append_to_stream(event, 'stream', :any)
26
260
  retrieved_event = repository.read_all_streams_forward(:head, 1).first
27
261
  expect(retrieved_event.data[:order_id]).to eq(3)
28
262
  end
29
263
 
30
264
  it 'metadata attributes are retrieved' do
31
265
  event = TestDomainEvent.new(metadata: { request_id: 3 })
32
- repository.create(event, 'stream')
266
+ repository.append_to_stream(event, 'stream', :any)
33
267
  retrieved_event = repository.read_all_streams_forward(:head, 1).first
34
268
  expect(retrieved_event.metadata[:request_id]).to eq(3)
35
269
  end
36
270
 
37
271
  it 'does not have deleted streams' do
38
- repository.create(TestDomainEvent.new, 'stream')
39
- repository.create(TestDomainEvent.new, 'other_stream')
40
-
41
- expect(repository.read_stream_events_forward('stream').count).to eq 1
42
- expect(repository.read_stream_events_forward('other_stream').count).to eq 1
43
- expect(repository.read_all_streams_forward(:head, 10).count).to eq 2
272
+ repository.append_to_stream(e1 = TestDomainEvent.new, 'stream', -1)
273
+ repository.append_to_stream(e2 = TestDomainEvent.new, 'other_stream', -1)
44
274
 
45
275
  repository.delete_stream('stream')
46
276
  expect(repository.read_stream_events_forward('stream')).to be_empty
47
- expect(repository.read_stream_events_forward('other_stream').count).to eq 1
48
- expect(repository.read_all_streams_forward(:head, 10).count).to eq 1
277
+ expect(repository.read_stream_events_forward('other_stream')).to eq([e2])
278
+ expect(repository.read_all_streams_forward(:head, 10)).to eq([e1,e2])
49
279
  end
50
280
 
51
281
  it 'has or has not domain event' do
52
- repository.create(TestDomainEvent.new(event_id: 'just an id'), 'stream')
282
+ just_an_id = 'd5c134c2-db65-4e87-b6ea-d196f8f1a292'
283
+ repository.append_to_stream(TestDomainEvent.new(event_id: just_an_id), 'stream', -1)
53
284
 
54
- expect(repository.has_event?('just an id')).to be_truthy
285
+ expect(repository.has_event?(just_an_id)).to be_truthy
286
+ expect(repository.has_event?(just_an_id.clone)).to be_truthy
55
287
  expect(repository.has_event?('any other id')).to be_falsey
56
288
  end
57
289
 
58
290
  it 'knows last event in stream' do
59
- repository.create(TestDomainEvent.new(event_id: 'event 1'), 'stream')
60
- repository.create(TestDomainEvent.new(event_id: 'event 2'), 'stream')
291
+ repository.append_to_stream(TestDomainEvent.new(event_id: '00000000-0000-0000-0000-000000000001'), 'stream', -1)
292
+ repository.append_to_stream(TestDomainEvent.new(event_id: '00000000-0000-0000-0000-000000000002'), 'stream', 0)
61
293
 
62
- expect(repository.last_stream_event('stream')).to eq(TestDomainEvent.new(event_id: 'event 2'))
294
+ expect(repository.last_stream_event('stream')).to eq(TestDomainEvent.new(event_id: '00000000-0000-0000-0000-000000000002'))
63
295
  expect(repository.last_stream_event('other_stream')).to be_nil
64
296
  end
65
297
 
66
298
  it 'reads batch of events from stream forward & backward' do
67
- event_ids = (1..10).to_a.map(&:to_s)
68
- repository.create(TestDomainEvent.new(event_id: '21'), 'other_stream')
69
- event_ids.each do |id|
70
- repository.create(TestDomainEvent.new(event_id: id), 'stream')
299
+ event_ids = ["96c920b1-cdd0-40f4-907c-861b9fff7d02", "56404f79-0ba0-4aa0-8524-dc3436368ca0", "6a54dd21-f9d8-4857-a195-f5588d9e406c", "0e50a9cd-f981-4e39-93d5-697fc7285b98", "d85589bc-b993-41d4-812f-fc631d9185d5", "96bdacda-77dd-4d7d-973d-cbdaa5842855", "94688199-e6b7-4180-bf8e-825b6808e6cc", "68fab040-741e-4bc2-9cca-5b8855b0ca19", "ab60114c-011d-4d58-ab31-7ba65d99975e", "868cac42-3d19-4b39-84e8-cd32d65c2445"]
300
+ events = event_ids.map{|id| TestDomainEvent.new(event_id: id) }
301
+ repository.append_to_stream(TestDomainEvent.new, 'other_stream', -1)
302
+ events.each.with_index do |event, index|
303
+ repository.append_to_stream(event, 'stream', index - 1)
71
304
  end
72
- repository.create(TestDomainEvent.new(event_id: '22'), 'other_stream')
305
+ repository.append_to_stream(TestDomainEvent.new, 'other_stream', 0)
73
306
 
74
- expect(repository.read_events_forward('stream', :head, 3)).to eq ['1','2','3'].map{|x| TestDomainEvent.new(event_id: x)}
75
- expect(repository.read_events_forward('stream', :head, 100)).to eq event_ids.map{|x| TestDomainEvent.new(event_id: x)}
76
- expect(repository.read_events_forward('stream', '5', 4)).to eq ['6','7','8','9'].map{|x| TestDomainEvent.new(event_id: x)}
77
- expect(repository.read_events_forward('stream', '5', 100)).to eq ['6','7','8','9','10'].map{|x| TestDomainEvent.new(event_id: x)}
307
+ expect(repository.read_events_forward('stream', :head, 3)).to eq(events.first(3))
308
+ expect(repository.read_events_forward('stream', :head, 100)).to eq(events)
309
+ expect(repository.read_events_forward('stream', events[4].event_id, 4)).to eq(events[5..8])
310
+ expect(repository.read_events_forward('stream', events[4].event_id, 100)).to eq(events[5..9])
78
311
 
79
- expect(repository.read_events_backward('stream', :head, 3)).to eq ['10','9','8'].map{|x| TestDomainEvent.new(event_id: x)}
80
- expect(repository.read_events_backward('stream', :head, 100)).to eq event_ids.reverse.map{|x| TestDomainEvent.new(event_id: x)}
81
- expect(repository.read_events_backward('stream', '5', 4)).to eq ['4','3','2','1'].map{|x| TestDomainEvent.new(event_id: x)}
82
- expect(repository.read_events_backward('stream', '5', 100)).to eq ['4','3','2','1'].map{|x| TestDomainEvent.new(event_id: x)}
312
+ expect(repository.read_events_backward('stream', :head, 3)).to eq(events.last(3).reverse)
313
+ expect(repository.read_events_backward('stream', :head, 100)).to eq(events.reverse)
314
+ expect(repository.read_events_backward('stream', events[4].event_id, 4)).to eq(events.first(4).reverse)
315
+ expect(repository.read_events_backward('stream', events[4].event_id, 100)).to eq(events.first(4).reverse)
83
316
  end
84
317
 
85
318
 
86
319
  it 'reads all stream events forward & backward' do
87
- repository.create(TestDomainEvent.new(event_id: '1'), 'stream')
88
- repository.create(TestDomainEvent.new(event_id: '2'), 'other_stream')
89
- repository.create(TestDomainEvent.new(event_id: '3'), 'stream')
90
- repository.create(TestDomainEvent.new(event_id: '4'), 'other_stream')
91
- repository.create(TestDomainEvent.new(event_id: '5'), 'other_stream')
320
+ s1 = 'stream'
321
+ s2 = 'other_stream'
322
+ repository.append_to_stream(a = TestDomainEvent.new(event_id: '7010d298-ab69-4bb1-9251-f3466b5d1282'), s1, -1)
323
+ repository.append_to_stream(b = TestDomainEvent.new(event_id: '34f88aca-aaba-4ca0-9256-8017b47528c5'), s2, -1)
324
+ repository.append_to_stream(c = TestDomainEvent.new(event_id: '8e61c864-ceae-4684-8726-97c34eb8fc4f'), s1, 0)
325
+ repository.append_to_stream(d = TestDomainEvent.new(event_id: '30963ed9-6349-450b-ac9b-8ea50115b3bd'), s2, 0)
326
+ repository.append_to_stream(e = TestDomainEvent.new(event_id: '5bdc58b7-e8a7-4621-afd6-ccb828d72457'), s2, 1)
92
327
 
93
- expect(repository.read_stream_events_forward('stream')).to eq ['1','3'].map{|x| TestDomainEvent.new(event_id: x)}
94
- expect(repository.read_stream_events_backward('stream')).to eq ['3','1'].map{|x| TestDomainEvent.new(event_id: x)}
328
+ expect(repository.read_stream_events_forward(s1)).to eq [a,c]
329
+ expect(repository.read_stream_events_backward(s1)).to eq [c,a]
95
330
  end
96
331
 
97
332
  it 'reads batch of events from all streams forward & backward' do
98
- event_ids = (1..10).to_a.map(&:to_s)
99
- event_ids.each do |id|
100
- repository.create(TestDomainEvent.new(event_id: id), SecureRandom.uuid)
333
+ event_ids = ["96c920b1-cdd0-40f4-907c-861b9fff7d02", "56404f79-0ba0-4aa0-8524-dc3436368ca0", "6a54dd21-f9d8-4857-a195-f5588d9e406c", "0e50a9cd-f981-4e39-93d5-697fc7285b98", "d85589bc-b993-41d4-812f-fc631d9185d5", "96bdacda-77dd-4d7d-973d-cbdaa5842855", "94688199-e6b7-4180-bf8e-825b6808e6cc", "68fab040-741e-4bc2-9cca-5b8855b0ca19", "ab60114c-011d-4d58-ab31-7ba65d99975e", "868cac42-3d19-4b39-84e8-cd32d65c2445"]
334
+ events = event_ids.map{|id| TestDomainEvent.new(event_id: id) }
335
+ events.each do |ev|
336
+ repository.append_to_stream(ev, SecureRandom.uuid, -1)
101
337
  end
102
338
 
103
- expect(repository.read_all_streams_forward(:head, 3)).to eq ['1','2','3'].map{|x| TestDomainEvent.new(event_id: x)}
104
- expect(repository.read_all_streams_forward(:head, 100)).to eq event_ids.map{|x| TestDomainEvent.new(event_id: x)}
105
- expect(repository.read_all_streams_forward('5', 4)).to eq ['6','7','8','9'].map{|x| TestDomainEvent.new(event_id: x)}
106
- expect(repository.read_all_streams_forward('5', 100)).to eq ['6','7','8','9','10'].map{|x| TestDomainEvent.new(event_id: x)}
339
+ expect(repository.read_all_streams_forward(:head, 3)).to eq(events.first(3))
340
+ expect(repository.read_all_streams_forward(:head, 100)).to eq(events)
341
+ expect(repository.read_all_streams_forward(events[4].event_id, 4)).to eq(events[5..8])
342
+ expect(repository.read_all_streams_forward(events[4].event_id, 100)).to eq(events[5..9])
343
+
344
+ expect(repository.read_all_streams_backward(:head, 3)).to eq(events.last(3).reverse)
345
+ expect(repository.read_all_streams_backward(:head, 100)).to eq(events.reverse)
346
+ expect(repository.read_all_streams_backward(events[4].event_id, 4)).to eq(events.first(4).reverse)
347
+ expect(repository.read_all_streams_backward(events[4].event_id, 100)).to eq(events.first(4).reverse)
348
+ end
349
+
350
+ it 'reads events different uuid object but same content' do
351
+ event_ids = [
352
+ "96c920b1-cdd0-40f4-907c-861b9fff7d02",
353
+ "56404f79-0ba0-4aa0-8524-dc3436368ca0"
354
+ ]
355
+ events = event_ids.map{|id| TestDomainEvent.new(event_id: id) }
356
+ repository.append_to_stream(events.first, 'stream', -1)
357
+ repository.append_to_stream(events.last, 'stream', 0)
358
+
359
+ expect(repository.read_all_streams_forward("96c920b1-cdd0-40f4-907c-861b9fff7d02", 1)).to eq([events.last])
360
+ expect(repository.read_all_streams_backward("56404f79-0ba0-4aa0-8524-dc3436368ca0", 1)).to eq([events.first])
361
+
362
+ expect(repository.read_events_forward('stream', "96c920b1-cdd0-40f4-907c-861b9fff7d02", 1)).to eq([events.last])
363
+ expect(repository.read_events_backward('stream', "56404f79-0ba0-4aa0-8524-dc3436368ca0", 1)).to eq([events.first])
364
+ end
365
+
366
+ it 'does not allow same event twice in a stream' do
367
+ repository.append_to_stream(
368
+ TestDomainEvent.new(event_id: "a1b49edb-7636-416f-874a-88f94b859bef"),
369
+ 'stream',
370
+ -1
371
+ )
372
+ expect do
373
+ repository.append_to_stream(
374
+ TestDomainEvent.new(event_id: "a1b49edb-7636-416f-874a-88f94b859bef"),
375
+ 'stream',
376
+ 0
377
+ )
378
+ end.to raise_error(RubyEventStore::EventDuplicatedInStream)
379
+ end
380
+
381
+ it 'allows appending to GLOBAL_STREAM explicitly' do
382
+ event = TestDomainEvent.new(event_id: "df8b2ba3-4e2c-4888-8d14-4364855fa80e")
383
+ repository.append_to_stream(event, "all", :any)
384
+
385
+ expect(repository.read_all_streams_forward(:head, 10)).to eq([event])
386
+ end
387
+
388
+ specify 'GLOBAL_STREAM is unordered, one cannot expect specific version number to work' do
389
+ expect {
390
+ event = TestDomainEvent.new(event_id: "df8b2ba3-4e2c-4888-8d14-4364855fa80e")
391
+ repository.append_to_stream(event, "all", 42)
392
+ }.to raise_error(RubyEventStore::InvalidExpectedVersion)
393
+ end
394
+
395
+ specify 'GLOBAL_STREAM is unordered, one cannot expect :none to work' do
396
+ expect {
397
+ event = TestDomainEvent.new(event_id: "df8b2ba3-4e2c-4888-8d14-4364855fa80e")
398
+ repository.append_to_stream(event, "all", :none)
399
+ }.to raise_error(RubyEventStore::InvalidExpectedVersion)
400
+ end
401
+
402
+ specify 'GLOBAL_STREAM is unordered, one cannot expect :auto to work' do
403
+ expect {
404
+ event = TestDomainEvent.new(event_id: "df8b2ba3-4e2c-4888-8d14-4364855fa80e")
405
+ repository.append_to_stream(event, "all", :auto)
406
+ }.to raise_error(RubyEventStore::InvalidExpectedVersion)
407
+ end
408
+
409
+ specify "only :none, :any, :auto and Integer allowed as expected_version" do
410
+ [Object.new, SecureRandom.uuid, :foo].each do |invalid_expected_version|
411
+ expect {
412
+ repository.append_to_stream(
413
+ TestDomainEvent.new(event_id: SecureRandom.uuid),
414
+ 'some_stream',
415
+ invalid_expected_version
416
+ )
417
+ }.to raise_error(RubyEventStore::InvalidExpectedVersion)
418
+ end
419
+ end
420
+
421
+ specify "events not persisted if append failed" do
422
+ repository.append_to_stream([
423
+ TestDomainEvent.new(event_id: SecureRandom.uuid),
424
+ ], 'stream', :none)
107
425
 
108
- expect(repository.read_all_streams_backward(:head, 3)).to eq ['10','9','8'].map{|x| TestDomainEvent.new(event_id: x)}
109
- expect(repository.read_all_streams_backward(:head, 100)).to eq event_ids.reverse.map{|x| TestDomainEvent.new(event_id: x)}
110
- expect(repository.read_all_streams_backward('5', 4)).to eq ['4','3','2','1'].map{|x| TestDomainEvent.new(event_id: x)}
111
- expect(repository.read_all_streams_backward('5', 100)).to eq ['4','3','2','1'].map{|x| TestDomainEvent.new(event_id: x)}
426
+ expect do
427
+ repository.append_to_stream([
428
+ TestDomainEvent.new(
429
+ event_id: '9bedf448-e4d0-41a3-a8cd-f94aec7aa763'
430
+ ),
431
+ ], 'stream', :none)
432
+ end.to raise_error(RubyEventStore::WrongExpectedEventVersion)
433
+ expect(repository.has_event?('9bedf448-e4d0-41a3-a8cd-f94aec7aa763')).to be_falsey
112
434
  end
113
435
  end
@@ -1,3 +1,3 @@
1
1
  module RubyEventStore
2
- VERSION = "0.18.2"
2
+ VERSION = "0.19.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_event_store
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.2
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arkency
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-10-18 00:00:00.000000000 Z
11
+ date: 2017-11-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler