euston-eventstore 1.0.2-java

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.
Files changed (32) hide show
  1. data/Rakefile +126 -0
  2. data/euston-eventstore.gemspec +68 -0
  3. data/lib/euston-eventstore/commit.rb +77 -0
  4. data/lib/euston-eventstore/constants.rb +5 -0
  5. data/lib/euston-eventstore/dispatcher/asynchronous_dispatcher.rb +37 -0
  6. data/lib/euston-eventstore/dispatcher/null_dispatcher.rb +11 -0
  7. data/lib/euston-eventstore/dispatcher/synchronous_dispatcher.rb +21 -0
  8. data/lib/euston-eventstore/errors.rb +21 -0
  9. data/lib/euston-eventstore/event_message.rb +26 -0
  10. data/lib/euston-eventstore/optimistic_event_store.rb +68 -0
  11. data/lib/euston-eventstore/optimistic_event_stream.rb +106 -0
  12. data/lib/euston-eventstore/persistence/mongodb/mongo_commit.rb +82 -0
  13. data/lib/euston-eventstore/persistence/mongodb/mongo_commit_id.rb +16 -0
  14. data/lib/euston-eventstore/persistence/mongodb/mongo_config.rb +28 -0
  15. data/lib/euston-eventstore/persistence/mongodb/mongo_event_message.rb +31 -0
  16. data/lib/euston-eventstore/persistence/mongodb/mongo_persistence_engine.rb +167 -0
  17. data/lib/euston-eventstore/persistence/mongodb/mongo_persistence_factory.rb +31 -0
  18. data/lib/euston-eventstore/persistence/mongodb/mongo_snapshot.rb +32 -0
  19. data/lib/euston-eventstore/persistence/mongodb/mongo_stream_head.rb +29 -0
  20. data/lib/euston-eventstore/persistence/stream_head.rb +23 -0
  21. data/lib/euston-eventstore/snapshot.rb +21 -0
  22. data/lib/euston-eventstore/version.rb +5 -0
  23. data/lib/euston-eventstore.rb +7 -0
  24. data/spec/event_store/dispatcher/asynchronous_dispatcher_spec.rb +75 -0
  25. data/spec/event_store/dispatcher/synchronous_dispatcher_spec.rb +39 -0
  26. data/spec/event_store/optimistic_event_store_spec.rb +292 -0
  27. data/spec/event_store/optimistic_event_stream_spec.rb +318 -0
  28. data/spec/event_store/persistence/mongodb_spec.rb +301 -0
  29. data/spec/event_store/serialization/simple_message.rb +12 -0
  30. data/spec/spec_helper.rb +39 -0
  31. data/spec/support/array_enumeration_counter.rb +20 -0
  32. metadata +178 -0
@@ -0,0 +1,292 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ describe Euston::EventStore do
4
+ let(:uuid) { Uuid }
5
+ let(:stream_id) { uuid.generate }
6
+ let(:persistence) { double('persistence').as_null_object }
7
+ let(:store) { Euston::EventStore::OptimisticEventStore.new persistence }
8
+
9
+ after { stream_id = uuid.generate }
10
+
11
+ describe 'optimistic event store' do
12
+ context 'when creating a stream' do
13
+ let(:stream) { store.create_stream stream_id }
14
+
15
+ it('returns a new stream') { stream.should_not be_nil }
16
+ it('returns a stream with the correct stream identifier') { stream.stream_id.should == stream_id }
17
+ it('returns a stream with a zero stream revision') { stream.stream_revision.should == 0 }
18
+ it('returns a stream with a zero commit sequence') { stream.commit_sequence.should == 0 }
19
+ it('returns a stream with no committed events') { stream.committed_events.should have(0).items }
20
+ it('returns a stream with no uncommitted events') { stream.uncommitted_events.should have(0).items }
21
+ it('returns a stream with no uncommitted headers') { stream.uncommitted_headers.should have(0).items }
22
+ end
23
+
24
+ context 'when opening an empty stream starting at revision zero' do
25
+ before do
26
+ persistence.stub(:get_from).with({ :stream_id => stream_id,
27
+ :min_revision => 0,
28
+ :max_revision => Euston::EventStore::FIXNUM_MAX }) { [] }
29
+
30
+ @stream = store.open_stream :stream_id => stream_id, :min_revision => 0, :max_revision => 0
31
+ end
32
+
33
+ it('returns a new stream') { @stream.should_not be_nil }
34
+ it('returns a stream with the correct stream identifier') { @stream.stream_id.should == stream_id }
35
+ it('returns a stream with a zero stream revision') { @stream.stream_revision.should == 0 }
36
+ it('returns a stream with a zero commit sequence') { @stream.commit_sequence.should == 0 }
37
+ it('returns a stream with no committed events') { @stream.committed_events.should have(0).items }
38
+ it('returns a stream with no uncommitted events') { @stream.uncommitted_events.should have(0).items }
39
+ it('returns a stream with no uncommitted headers') { @stream.uncommitted_headers.should have(0).items }
40
+ end
41
+
42
+ context 'when opening an empty stream starting above revision zero' do
43
+ let(:min_revision) { 1 }
44
+
45
+ before do
46
+ persistence.stub(:get_from).with({ :stream_id => stream_id,
47
+ :min_revision => min_revision,
48
+ :max_revision => Euston::EventStore::FIXNUM_MAX }) { [] }
49
+
50
+ begin
51
+ store.open_stream :stream_id => stream_id,
52
+ :min_revision => min_revision,
53
+ :max_revision => Euston::EventStore::FIXNUM_MAX
54
+ rescue Exception => e
55
+ @caught = e
56
+ end
57
+ end
58
+
59
+ it('throws a StreamNotFoundError') { @caught.should be_an(Euston::EventStore::StreamNotFoundError) }
60
+ end
61
+
62
+ context 'when opening a populated stream' do
63
+ let(:min_revision) { 17 }
64
+ let(:max_revision) { 42 }
65
+ let(:committed) { [ commit(:stream_revision => min_revision,
66
+ :commit_sequence => 1) ] }
67
+
68
+ before do
69
+ persistence.stub(:get_from).with({ :stream_id => stream_id,
70
+ :min_revision => min_revision,
71
+ :max_revision => max_revision }) { @invoked = true; committed }
72
+
73
+ @stream = store.open_stream :stream_id => stream_id,
74
+ :min_revision => min_revision,
75
+ :max_revision => max_revision
76
+ end
77
+
78
+ it('invokes the underlying infrastructure with the values provided') { @invoked.should be_true }
79
+ it('returns an event stream containing the correct stream identifier') { @stream.stream_id.should == stream_id }
80
+ end
81
+
82
+ context 'when opening a populated stream' do
83
+ let(:min_revision) { 17 }
84
+ let(:max_revision) { 42 }
85
+ let(:committed) { [ commit(:stream_revision => min_revision) ] }
86
+
87
+ before do
88
+ persistence.stub(:get_from).with({ :stream_id => stream_id,
89
+ :min_revision => min_revision,
90
+ :max_revision => max_revision }) { @invoked = true; committed }
91
+
92
+ @stream = store.open_stream :stream_id => stream_id,
93
+ :min_revision => min_revision,
94
+ :max_revision => max_revision
95
+ end
96
+
97
+ it('invokes the underlying infrastructure with the values provided') { @invoked.should be_true }
98
+ it('returns an event stream containing the correct stream identifier') { @stream.stream_id.should == stream_id }
99
+ end
100
+
101
+ context 'when opening a populated stream from a snapshot' do
102
+ let(:min_revision) { 42 }
103
+ let(:max_revision) { Euston::EventStore::FIXNUM_MAX }
104
+ let(:snapshot) { Euston::EventStore::Snapshot.new stream_id, min_revision, 'snapshot' }
105
+ let(:committed) { [ commit(:stream_revision => min_revision, :commit_sequence => 0) ] }
106
+
107
+ before do
108
+ persistence.stub(:get_from).with({ :stream_id => stream_id,
109
+ :min_revision => min_revision,
110
+ :max_revision => max_revision }) { @invoked = true; committed }
111
+
112
+ store.open_stream :snapshot => snapshot,
113
+ :max_revision => max_revision
114
+ end
115
+
116
+ it('invokes the underlying infrastructure with the values provided') { @invoked.should be_true }
117
+ end
118
+
119
+ context 'when opening a stream from a snapshot that is at the revision of the stream head' do
120
+ let(:head_stream_revision) { 42 }
121
+ let(:head_commit_sequence) { 15 }
122
+ let(:snapshot) { Euston::EventStore::Snapshot.new stream_id, head_stream_revision, 'snapshot' }
123
+ let(:committed) { Euston::EventStore::ArrayEnumerationCounter.new [ commit(:stream_revision => head_stream_revision,
124
+ :commit_sequence => head_commit_sequence) ] }
125
+
126
+ before do
127
+ persistence.stub(:get_from).with({ :stream_id => stream_id,
128
+ :min_revision => head_stream_revision,
129
+ :max_revision => Euston::EventStore::FIXNUM_MAX }) { committed }
130
+
131
+ @stream = store.open_stream :snapshot => snapshot,
132
+ :max_revision => Euston::EventStore::FIXNUM_MAX
133
+ end
134
+
135
+ it('returns a stream with the correct stream identifier') { @stream.stream_id.should == stream_id }
136
+ it('returns a stream with the revision of the stream head') { @stream.stream_revision.should == head_stream_revision }
137
+ it('returns a stream with a commit sequence of the stream head') { @stream.commit_sequence.should == head_commit_sequence }
138
+ it('returns a stream with no committed events') { @stream.committed_events.should have(0).items }
139
+ it('returns a stream with no uncommitted events') { @stream.uncommitted_events.should have(0).items }
140
+ it('only enumerates the set of commits once') { committed.invocations.should == 1 }
141
+ end
142
+
143
+ context 'when reading from revision zero' do
144
+ before do
145
+ persistence.stub(:get_from).with({ :stream_id => stream_id,
146
+ :min_revision => 0,
147
+ :max_revision => Euston::EventStore::FIXNUM_MAX }) { @invoked = true; [] }
148
+
149
+ store.get_from stream_id, 0, Euston::EventStore::FIXNUM_MAX
150
+ end
151
+
152
+ it('passes a revision range to the persistence infrastructure') { @invoked.should be_true }
153
+ end
154
+
155
+ describe 'when reading up to revision zero' do
156
+ let(:committed) { [ commit ] }
157
+
158
+ before do
159
+ persistence.stub(:get_from).with({ :stream_id => stream_id,
160
+ :min_revision => 0,
161
+ :max_revision => Euston::EventStore::FIXNUM_MAX }) { @invoked = true; committed }
162
+
163
+ store.open_stream :stream_id => stream_id,
164
+ :min_revision => 0,
165
+ :max_revision => 0
166
+ end
167
+
168
+ it('passes the maximum possible revision to the persistence infrastructure') { @invoked.should be_true }
169
+ end
170
+
171
+ context 'when reading from a snapshot up to revision zero' do
172
+ let(:snapshot) { Euston::EventStore::Snapshot.new stream_id, 1, 'snapshot' }
173
+ let(:committed) { [ commit ] }
174
+
175
+ before do
176
+ persistence.stub(:get_from).with({ :stream_id => stream_id,
177
+ :min_revision => snapshot.stream_revision,
178
+ :max_revision => Euston::EventStore::FIXNUM_MAX }) { @invoked = true; committed }
179
+
180
+ store.open_stream :snapshot => snapshot,
181
+ :max_revision => 0
182
+ end
183
+
184
+ it('passes the maximum possible revision to the persistence infrastructure') { @invoked.should be_true }
185
+ end
186
+
187
+ context 'when committing a null attempt back to the stream' do
188
+ before do
189
+ begin
190
+ store.commit nil
191
+ rescue Exception => e
192
+ @caught = e
193
+ end
194
+ end
195
+
196
+ it('throws an ArgumentError') { @caught.should be_an(ArgumentError) }
197
+ end
198
+
199
+ context 'when committing with an unidentified attempt back to the stream' do
200
+ let(:empty_identifier) { nil }
201
+ let(:unidentified) { commit(:commit_id => empty_identifier, :events => [] ) }
202
+
203
+ before do
204
+ begin
205
+ store.commit unidentified
206
+ rescue Exception => e
207
+ @caught = e
208
+ end
209
+ end
210
+
211
+ it('throws an ArgumentError') { @caught.should be_an(ArgumentError) }
212
+ end
213
+
214
+ context 'when the number of commits is greater than the number of revisions' do
215
+ let(:stream_revision) { 1 }
216
+ let(:commit_sequence) { 2 }
217
+ let(:corrupt) { commit(:stream_revision => stream_revision, :commit_sequence => commit_sequence) }
218
+
219
+ before do
220
+ begin
221
+ store.commit corrupt
222
+ rescue Exception => e
223
+ @caught = e
224
+ end
225
+ end
226
+
227
+ it('throws an ArgumentError') { @caught.should be_an(ArgumentError) }
228
+ end
229
+
230
+ context 'when committing with a non-positive commit sequence back to the stream' do
231
+ let(:stream_revision) { 1 }
232
+ let(:invalid_commit_sequence) { 0 }
233
+ let(:invalid_commit) { commit(:stream_revision => stream_revision, :commit_sequence => invalid_commit_sequence) }
234
+
235
+ before do
236
+ begin
237
+ store.commit invalid_commit
238
+ rescue Exception => e
239
+ @caught = e
240
+ end
241
+ end
242
+
243
+ it('throw an ArgumentError') { @caught.should be_an(ArgumentError) }
244
+ end
245
+
246
+ context 'when committing with a non-positive stream revision back to the stream' do
247
+ let(:invalid_stream_revision) { 0 }
248
+ let(:commit_sequence) { 1 }
249
+ let(:invalid_commit) { commit(:stream_revision => invalid_stream_revision, :commit_sequence => commit_sequence) }
250
+
251
+ before do
252
+ begin
253
+ store.commit invalid_commit
254
+ rescue Exception => e
255
+ @caught = e
256
+ end
257
+ end
258
+
259
+ it('throw an ArgumentError') { @caught.should be_an(ArgumentError) }
260
+ end
261
+
262
+ context 'when committing an empty attempt to a stream' do
263
+ let(:attempt_with_no_events) { commit }
264
+
265
+ before do
266
+ persistence.stub(:commit).with(attempt_with_no_events) { @invoked = true }
267
+ end
268
+
269
+ it('drops the commit provided') { @invoked.should be_nil }
270
+ end
271
+
272
+ context 'when committing with a valid and populated attempt to a stream' do
273
+ let(:populated_attempt) { commit }
274
+
275
+ before do
276
+ persistence.stub(:commit).with(populated_attempt) { @commit_invoked = true }
277
+
278
+ store.commit populated_attempt
279
+ end
280
+
281
+ it('provides the commit attempt to the configured persistence mechanism') { @commit_invoked.should be_true }
282
+ end
283
+ end
284
+
285
+ def commit(options = {})
286
+ defaults = { :stream_id => stream_id,
287
+ :commit_id => uuid.generate,
288
+ :events => [ Euston::EventStore::EventMessage.new ]}
289
+
290
+ Euston::EventStore::Commit.new(defaults.merge options)
291
+ end
292
+ end
@@ -0,0 +1,318 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ describe Euston::EventStore do
4
+ let(:uuid) { Uuid }
5
+ let(:default_stream_revision) { 1 }
6
+ let(:default_commit_sequence) { 1 }
7
+ let(:stream_id) { uuid.generate }
8
+ let(:persistence) { double('persistence') }
9
+ let(:stream) { Euston::EventStore::OptimisticEventStream.new(:stream_id => stream_id, :persistence => persistence) }
10
+
11
+ after { stream_id = uuid.generate }
12
+
13
+ def build_commit_stub(stream_id, revision, sequence, length)
14
+ Euston::EventStore::Commit.new( :stream_id => stream_id,
15
+ :stream_revision => revision,
16
+ :commit_sequence => sequence,
17
+ :events => length.times.map{ Euston::EventStore::EventMessage.new })
18
+ end
19
+
20
+ describe 'optimistic event stream' do
21
+ context 'when constructing a new stream' do
22
+ let(:min_revision) { 2 }
23
+ let(:max_revision) { 7 }
24
+ let(:commit_length) { 2 }
25
+ let(:committed) { [
26
+ build_commit_stub(stream_id, 2, 1, commit_length),
27
+ build_commit_stub(stream_id, 4, 2, commit_length),
28
+ build_commit_stub(stream_id, 6, 3, commit_length),
29
+ build_commit_stub(stream_id, 8, 3, commit_length)
30
+ ] }
31
+
32
+ before do
33
+ persistence.stub(:get_from).with(stream_id, min_revision, max_revision) { committed }
34
+ @stream = Euston::EventStore::OptimisticEventStream.new(:stream_id => stream_id,
35
+ :persistence => persistence,
36
+ :min_revision => min_revision,
37
+ :max_revision => max_revision)
38
+ end
39
+
40
+ it 'has the correct stream identifier' do
41
+ @stream.stream_id.should == stream_id
42
+ end
43
+
44
+ it 'has the correct head stream revision' do
45
+ @stream.stream_revision.should == max_revision
46
+ end
47
+
48
+ it 'has the correct head commit sequence' do
49
+ @stream.commit_sequence.should == committed.last.commit_sequence
50
+ end
51
+
52
+ it 'does not include the event below the minimum revision indicated' do
53
+ @stream.committed_events.first.should == committed.first.events.last
54
+ end
55
+
56
+ it 'does not include events above the maximum revision indicated' do
57
+ @stream.committed_events.last.should == committed.last.events.first
58
+ end
59
+
60
+ it 'has all of the committed events up to the stream revision specified' do
61
+ @stream.committed_events.length.should == max_revision - min_revision + 1
62
+ end
63
+ end
64
+
65
+ context 'when constructing the head event revision is less than the max desired revision' do
66
+ let(:commit_length) { 2 }
67
+ let(:committed) { [
68
+ build_commit_stub(stream_id, 2, 1, commit_length),
69
+ build_commit_stub(stream_id, 4, 2, commit_length),
70
+ build_commit_stub(stream_id, 6, 3, commit_length),
71
+ build_commit_stub(stream_id, 8, 3, commit_length)
72
+ ] }
73
+
74
+ before do
75
+ persistence.stub(:get_from).with(stream_id, 0, Euston::EventStore::FIXNUM_MAX) { committed }
76
+ @stream = Euston::EventStore::OptimisticEventStream.new(:stream_id => stream_id,
77
+ :persistence => persistence,
78
+ :min_revision => 0,
79
+ :max_revision => Euston::EventStore::FIXNUM_MAX)
80
+ end
81
+
82
+ it 'sets the stream revision to the revision of the most recent event' do
83
+ @stream.stream_revision.should == committed.last.stream_revision
84
+ end
85
+ end
86
+
87
+ context 'when adding a null event message' do
88
+ before do
89
+ stream << nil
90
+ end
91
+
92
+ it 'is ignored' do
93
+ stream.uncommitted_events.should be_empty
94
+ end
95
+ end
96
+
97
+ context 'when adding an unpopulated event message' do
98
+ before do
99
+ stream << Euston::EventStore::EventMessage.new(nil)
100
+ end
101
+
102
+ it 'is ignored' do
103
+ stream.uncommitted_events.should be_empty
104
+ end
105
+ end
106
+
107
+ context 'when adding a fully populated event message' do
108
+ before do
109
+ stream << Euston::EventStore::EventMessage.new('populated')
110
+ end
111
+
112
+ it 'adds the event to the set of uncommitted events' do
113
+ stream.uncommitted_events.should have(1).items
114
+ end
115
+ end
116
+
117
+ context 'when adding multiple populated event messages' do
118
+ before do
119
+ stream << Euston::EventStore::EventMessage.new('populated')
120
+ stream << Euston::EventStore::EventMessage.new('also populated')
121
+ end
122
+
123
+ it 'adds all the events provided to the set of uncommitted events' do
124
+ stream.uncommitted_events.should have(2).items
125
+ end
126
+ end
127
+
128
+ context 'when adding a simple object as an event message' do
129
+ let(:my_event) { 'some event data' }
130
+
131
+ before do
132
+ stream << Euston::EventStore::EventMessage.new(my_event)
133
+ end
134
+
135
+ it 'adds the uncommitted event to the set of uncommitted events' do
136
+ stream.uncommitted_events.should have(1).items
137
+ end
138
+
139
+ it 'wraps the uncommitted event in an EventMessage object' do
140
+ stream.uncommitted_events.first.body.should == my_event
141
+ end
142
+ end
143
+
144
+ context 'when clearing any uncommitted changes' do
145
+ before do
146
+ stream << Euston::EventStore::EventMessage.new('')
147
+ stream.clear_changes
148
+ end
149
+
150
+ it 'clears all uncommitted events' do
151
+ stream.uncommitted_events.should be_empty
152
+ end
153
+ end
154
+
155
+ context 'when committing an empty changeset' do
156
+ before do
157
+ persistence.stub(:commit) { @persisted = true }
158
+ stream.commit_changes uuid.generate
159
+ end
160
+
161
+ it 'does not call the underlying infrastructure' do
162
+ @persisted.should be_nil
163
+ end
164
+
165
+ it 'does not increment the current stream revision' do
166
+ stream.stream_revision.should == 0
167
+ end
168
+
169
+ it 'does not increment the current commit sequence' do
170
+ stream.commit_sequence.should == 0
171
+ end
172
+ end
173
+
174
+ context 'when committing any uncommitted changes' do
175
+ let(:commit_id) { uuid.generate }
176
+ let(:uncommitted) { Euston::EventStore::EventMessage.new '' }
177
+ let(:headers) { { :key => :value } }
178
+
179
+ before do
180
+ persistence.stub(:commit) { |c| @constructed = c }
181
+ stream << uncommitted
182
+ headers.each { |key, value| stream.uncommitted_headers[key] = value }
183
+ stream.commit_changes commit_id
184
+ end
185
+
186
+ it 'provides a commit to the underlying infrastructure' do
187
+ @constructed.should_not be_nil
188
+ end
189
+
190
+ it 'builds the commit with the correct stream identifier' do
191
+ @constructed.stream_id.should == stream_id
192
+ end
193
+
194
+ it 'builds the commit with the correct stream revision' do
195
+ @constructed.stream_revision.should == default_stream_revision
196
+ end
197
+
198
+ it 'builds the commit with the correct commit identifier' do
199
+ @constructed.commit_id.should == commit_id
200
+ end
201
+
202
+ it 'builds the commit with an incremented commit sequence' do
203
+ @constructed.commit_sequence.should == default_commit_sequence
204
+ end
205
+
206
+ it 'builds the commit with the correct commit stamp' do
207
+ ((Time.now - @constructed.commit_timestamp) < 0.05).should be_true
208
+ end
209
+
210
+ it 'builds the commit with the headers provided' do
211
+ @constructed.headers.each do |key, value|
212
+ headers[key].should == value
213
+ end
214
+ @constructed.headers.keys.length.should == headers.keys.length
215
+ end
216
+
217
+ it 'builds the commit containing all uncommitted events' do
218
+ @constructed.events.should have(1).items
219
+ end
220
+
221
+ it 'builds the commit using the event messages provided' do
222
+ @constructed.events.first.should == uncommitted
223
+ end
224
+
225
+ it 'updates the stream revision' do
226
+ stream.stream_revision.should == @constructed.stream_revision
227
+ end
228
+
229
+ it 'updates the commit sequence' do
230
+ stream.commit_sequence.should == @constructed.commit_sequence
231
+ end
232
+
233
+ it 'adds the uncommitted events to the committed events' do
234
+ stream.committed_events.last.should == uncommitted
235
+ end
236
+
237
+ it 'clears the uncommitted events' do
238
+ stream.uncommitted_events.should have(0).items
239
+ end
240
+
241
+ it 'clears the uncommitted headers' do
242
+ stream.uncommitted_headers.should have(0).items
243
+ end
244
+ end
245
+
246
+ context 'when committing with an identifier that was previously read' do
247
+ let(:committed) { [ build_commit_stub(stream_id, 1, 1, 1) ] }
248
+ let(:duplicate_commit_id) { committed.first.commit_id }
249
+
250
+ before do
251
+ persistence.stub(:get_from).with(stream_id, 0, Euston::EventStore::FIXNUM_MAX) { committed }
252
+
253
+ @stream = Euston::EventStore::OptimisticEventStream.new(:stream_id => stream_id,
254
+ :persistence => persistence,
255
+ :min_revision => 0,
256
+ :max_revision => Euston::EventStore::FIXNUM_MAX)
257
+
258
+ begin
259
+ @stream.commit_changes duplicate_commit_id
260
+ rescue Exception => e
261
+ @thrown = e
262
+ end
263
+ end
264
+
265
+ it 'throws a DuplicateCommitError' do
266
+ @thrown.should be_a(Euston::EventStore::DuplicateCommitError)
267
+ end
268
+ end
269
+
270
+ context 'when committing after another thread or process has moved the stream head' do
271
+ let(:stream_revision) { 1 }
272
+ let(:committed) { [ build_commit_stub(stream_id, 1, 1, 1) ] }
273
+ let(:uncommitted) { Euston::EventStore::EventMessage.new '' }
274
+ let(:discovered_on_commit) { [ build_commit_stub(stream_id, 3, 2, 2) ] }
275
+
276
+ before do
277
+ persistence.stub(:commit) { raise Euston::EventStore::ConcurrencyError.new }
278
+ persistence.stub(:get_from).with(stream_id, stream_revision, Euston::EventStore::FIXNUM_MAX) { committed }
279
+ persistence.stub(:get_from).with(stream_id, stream_revision + 1, Euston::EventStore::FIXNUM_MAX) do
280
+ @queried_for_new_commits = true
281
+ discovered_on_commit
282
+ end
283
+
284
+ @stream = Euston::EventStore::OptimisticEventStream.new(:stream_id => stream_id,
285
+ :persistence => persistence,
286
+ :min_revision => stream_revision,
287
+ :max_revision => Euston::EventStore::FIXNUM_MAX)
288
+ @stream << uncommitted
289
+
290
+ begin
291
+ @stream.commit_changes uuid.generate
292
+ rescue Exception => e
293
+ @thrown = e
294
+ end
295
+ end
296
+
297
+ it 'throws a ConcurrencyError' do
298
+ @thrown.should be_a(Euston::EventStore::ConcurrencyError)
299
+ end
300
+
301
+ it 'queries the underlying storage to discover the new commits' do
302
+ @queried_for_new_commits.should be_true
303
+ end
304
+
305
+ it 'updates the stream revision accordingly' do
306
+ @stream.stream_revision.should == discovered_on_commit.first.stream_revision
307
+ end
308
+
309
+ it 'updates the commit sequence accordingly' do
310
+ @stream.commit_sequence.should == discovered_on_commit.first.commit_sequence
311
+ end
312
+
313
+ it 'add the newly discovered committed events to the set of committed events accordingly' do
314
+ @stream.committed_events.should have(discovered_on_commit.first.events.length + 1).items
315
+ end
316
+ end
317
+ end
318
+ end