euston-eventstore 1.0.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.
- data/Rakefile +118 -0
- data/euston-eventstore.gemspec +66 -0
- data/lib/euston-eventstore.rb +7 -0
- data/lib/euston-eventstore/commit.rb +77 -0
- data/lib/euston-eventstore/constants.rb +5 -0
- data/lib/euston-eventstore/dispatcher/asynchronous_dispatcher.rb +37 -0
- data/lib/euston-eventstore/dispatcher/null_dispatcher.rb +11 -0
- data/lib/euston-eventstore/dispatcher/synchronous_dispatcher.rb +21 -0
- data/lib/euston-eventstore/errors.rb +21 -0
- data/lib/euston-eventstore/event_message.rb +26 -0
- data/lib/euston-eventstore/optimistic_event_store.rb +68 -0
- data/lib/euston-eventstore/optimistic_event_stream.rb +106 -0
- data/lib/euston-eventstore/persistence/mongodb/mongo_commit.rb +82 -0
- data/lib/euston-eventstore/persistence/mongodb/mongo_commit_id.rb +16 -0
- data/lib/euston-eventstore/persistence/mongodb/mongo_config.rb +28 -0
- data/lib/euston-eventstore/persistence/mongodb/mongo_event_message.rb +31 -0
- data/lib/euston-eventstore/persistence/mongodb/mongo_persistence_engine.rb +167 -0
- data/lib/euston-eventstore/persistence/mongodb/mongo_persistence_factory.rb +31 -0
- data/lib/euston-eventstore/persistence/mongodb/mongo_snapshot.rb +32 -0
- data/lib/euston-eventstore/persistence/mongodb/mongo_stream_head.rb +29 -0
- data/lib/euston-eventstore/persistence/stream_head.rb +23 -0
- data/lib/euston-eventstore/snapshot.rb +21 -0
- data/lib/euston-eventstore/version.rb +5 -0
- data/spec/event_store/dispatcher/asynchronous_dispatcher_spec.rb +75 -0
- data/spec/event_store/dispatcher/synchronous_dispatcher_spec.rb +39 -0
- data/spec/event_store/optimistic_event_store_spec.rb +292 -0
- data/spec/event_store/optimistic_event_stream_spec.rb +318 -0
- data/spec/event_store/persistence/mongodb_spec.rb +301 -0
- data/spec/event_store/serialization/simple_message.rb +12 -0
- data/spec/spec_helper.rb +39 -0
- data/spec/support/array_enumeration_counter.rb +20 -0
- metadata +189 -0
@@ -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
|
@@ -0,0 +1,301 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
|
2
|
+
|
3
|
+
describe Euston::EventStore do
|
4
|
+
describe 'mongodb persistence' do
|
5
|
+
let(:uuid) { Uuid }
|
6
|
+
let(:database) { 'event_store_tests' }
|
7
|
+
let(:config) { Euston::EventStore::Persistence::Mongodb::Config }
|
8
|
+
let(:factory) { Euston::EventStore::Persistence::Mongodb::MongoPersistenceFactory }
|
9
|
+
let(:stream_id) { uuid.generate }
|
10
|
+
|
11
|
+
before do
|
12
|
+
config.instance.database = database
|
13
|
+
|
14
|
+
@persistence = factory.build
|
15
|
+
@persistence.init
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'when a commit is successfully persisted' do
|
19
|
+
let(:now) { Time.now.utc + (60 * 60 * 24 * 7 * 52) }
|
20
|
+
let(:attempt) { new_attempt(:commit_timestamp => now) }
|
21
|
+
|
22
|
+
before do
|
23
|
+
@persistence.commit attempt
|
24
|
+
|
25
|
+
sleep 0.25
|
26
|
+
|
27
|
+
@persisted = @persistence.get_from(:stream_id => stream_id,
|
28
|
+
:min_revision => 0,
|
29
|
+
:max_revision => Euston::EventStore::FIXNUM_MAX).first
|
30
|
+
end
|
31
|
+
|
32
|
+
it('correctly persists the stream identifier') { @persisted.stream_id.should == attempt.stream_id }
|
33
|
+
it('correctly persists the stream revision') { @persisted.stream_revision.should == attempt.stream_revision }
|
34
|
+
it('correctly persists the commit identifier') { @persisted.commit_id.should == attempt.commit_id }
|
35
|
+
it('correctly persists the commit sequence') { @persisted.commit_sequence.should == attempt.commit_sequence }
|
36
|
+
|
37
|
+
# persistence engines have varying levels of precision with respect to time.
|
38
|
+
it('correctly persists the commit timestamp') { (@persisted.commit_timestamp - now.to_f).should be <= 1 }
|
39
|
+
|
40
|
+
it('correctly persists the headers') { @persisted.headers.should have(attempt.headers.length).items }
|
41
|
+
it('correctly persists the events') { @persisted.events.should have(attempt.events.length).items }
|
42
|
+
it('makes the commit available to be read from the stream') {
|
43
|
+
@persistence.get_from(:stream_id => stream_id,
|
44
|
+
:min_revision => 0,
|
45
|
+
:max_revision => Euston::EventStore::FIXNUM_MAX).first.commit_id.should == attempt.commit_id }
|
46
|
+
|
47
|
+
it('adds the commit to the set of undispatched commits') {
|
48
|
+
@persistence.get_undispatched_commits.detect { |x| x.commit_id == attempt.commit_id }.should_not be_nil }
|
49
|
+
|
50
|
+
it('causes the stream to be found in the list of streams to snapshot') {
|
51
|
+
@persistence.get_streams_to_snapshot(1).detect { |x| x.stream_id == stream_id }.should_not be_nil }
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'when reading from a given revision' do
|
55
|
+
let(:load_from_commit_containing_revision) { 3 }
|
56
|
+
let(:up_to_commit_containing_revision) { 5 }
|
57
|
+
let(:oldest) { new_attempt }
|
58
|
+
let(:oldest2) { next_attempt(oldest) }
|
59
|
+
let(:oldest3) { next_attempt(oldest2) }
|
60
|
+
let(:newest) { next_attempt(oldest3) }
|
61
|
+
|
62
|
+
before do
|
63
|
+
@persistence.commit oldest
|
64
|
+
@persistence.commit oldest2
|
65
|
+
@persistence.commit oldest3
|
66
|
+
@persistence.commit newest
|
67
|
+
|
68
|
+
@committed = @persistence.get_from(:stream_id => stream_id,
|
69
|
+
:min_revision => load_from_commit_containing_revision,
|
70
|
+
:max_revision => up_to_commit_containing_revision).to_a
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
it('starts from the commit which contains the minimum stream revision specified') { @committed.first.commit_id.should == oldest2.commit_id }
|
75
|
+
it('reads up to the commit which contains the maximum stream revision specified') { @committed.last.commit_id.should == oldest3.commit_id }
|
76
|
+
end
|
77
|
+
|
78
|
+
context 'when committing a stream with the same revision' do
|
79
|
+
let(:persistence1) { factory.build }
|
80
|
+
let(:persistence2) { factory.build }
|
81
|
+
let(:attempt1) { new_attempt }
|
82
|
+
let(:attempt2) { new_attempt }
|
83
|
+
|
84
|
+
before do
|
85
|
+
persistence1.init
|
86
|
+
persistence2.init
|
87
|
+
|
88
|
+
persistence1.commit attempt1
|
89
|
+
|
90
|
+
begin
|
91
|
+
persistence2.commit attempt2
|
92
|
+
rescue Exception => e
|
93
|
+
@caught = e
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
it('raises a ConcurrencyError') { @caught.should be_an(Euston::EventStore::ConcurrencyError) }
|
98
|
+
end
|
99
|
+
|
100
|
+
context 'when committing a stream with the same sequence' do
|
101
|
+
let(:persistence1) { factory.build }
|
102
|
+
let(:persistence2) { factory.build }
|
103
|
+
let(:attempt1) { new_attempt }
|
104
|
+
let(:attempt2) { new_attempt }
|
105
|
+
|
106
|
+
before do
|
107
|
+
persistence1.init
|
108
|
+
persistence2.init
|
109
|
+
|
110
|
+
persistence1.commit attempt1
|
111
|
+
|
112
|
+
begin
|
113
|
+
persistence2.commit attempt2
|
114
|
+
rescue Exception => e
|
115
|
+
@caught = e
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
it('raises a ConcurrencyError') { @caught.should be_an(Euston::EventStore::ConcurrencyError) }
|
120
|
+
end
|
121
|
+
|
122
|
+
context 'when attempting to overwrite a committed sequence' do
|
123
|
+
let(:successful_attempt) { new_attempt }
|
124
|
+
let(:failed_attempt) { new_attempt }
|
125
|
+
|
126
|
+
before do
|
127
|
+
@persistence.commit successful_attempt
|
128
|
+
|
129
|
+
begin
|
130
|
+
@persistence.commit failed_attempt
|
131
|
+
rescue Exception => e
|
132
|
+
@caught = e
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
it('raises a ConcurrencyError') { @caught.should be_an(Euston::EventStore::ConcurrencyError) }
|
137
|
+
end
|
138
|
+
|
139
|
+
context 'when attempting to persist a commit twice' do
|
140
|
+
let(:attempt) { new_attempt }
|
141
|
+
|
142
|
+
before do
|
143
|
+
@persistence.commit attempt
|
144
|
+
|
145
|
+
begin
|
146
|
+
@persistence.commit attempt
|
147
|
+
rescue Exception => e
|
148
|
+
@caught = e
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
it('raises a DuplicateCommitError') { @caught.should be_an(Euston::EventStore::DuplicateCommitError) }
|
153
|
+
end
|
154
|
+
|
155
|
+
context 'when a commit has been marked as dispatched' do
|
156
|
+
let(:attempt) { new_attempt }
|
157
|
+
|
158
|
+
before do
|
159
|
+
@persistence.commit attempt
|
160
|
+
@persistence.mark_commit_as_dispatched attempt
|
161
|
+
end
|
162
|
+
|
163
|
+
it('is no longer found in the set of undispatched commits') {
|
164
|
+
@persistence.get_undispatched_commits.detect { |c| c.commit_id == attempt.commit_id }.should be_nil }
|
165
|
+
end
|
166
|
+
|
167
|
+
context 'when saving a snapshot' do
|
168
|
+
let(:snapshot) { Euston::EventStore::Snapshot.new stream_id, 1, { :key => :value } }
|
169
|
+
|
170
|
+
before do
|
171
|
+
@persistence.commit new_attempt
|
172
|
+
|
173
|
+
sleep 0.25
|
174
|
+
|
175
|
+
@added = @persistence.add_snapshot snapshot
|
176
|
+
end
|
177
|
+
|
178
|
+
it('indicates the snapshot was added') { @added.should be_true }
|
179
|
+
it('is able to retrieve the snapshot') { @persistence.get_snapshot(stream_id, snapshot.stream_revision).should_not be_nil }
|
180
|
+
end
|
181
|
+
|
182
|
+
context 'when retrieving a snapshot' do
|
183
|
+
let(:too_far_back) { Euston::EventStore::Snapshot.new stream_id, 1, {} }
|
184
|
+
let(:correct) { Euston::EventStore::Snapshot.new stream_id, 3, { 'key' => 'value' } }
|
185
|
+
let(:too_far_forward) { Euston::EventStore::Snapshot.new stream_id, 5, {} }
|
186
|
+
let(:commit1) { new_attempt }
|
187
|
+
let(:commit2) { next_attempt commit1 }
|
188
|
+
let(:commit3) { next_attempt commit2 }
|
189
|
+
|
190
|
+
before do
|
191
|
+
@persistence.commit commit1
|
192
|
+
@persistence.commit commit2
|
193
|
+
@persistence.commit commit3
|
194
|
+
|
195
|
+
sleep 0.25
|
196
|
+
|
197
|
+
@persistence.add_snapshot too_far_back
|
198
|
+
@persistence.add_snapshot correct
|
199
|
+
@persistence.add_snapshot too_far_forward
|
200
|
+
|
201
|
+
@snapshot = @persistence.get_snapshot stream_id, too_far_forward.stream_revision - 1
|
202
|
+
end
|
203
|
+
|
204
|
+
it('loads the most recent prior snapshot') { @snapshot.stream_revision.should == correct.stream_revision }
|
205
|
+
it('has the correct snapshot payload') { @snapshot.payload.should == correct.payload }
|
206
|
+
end
|
207
|
+
|
208
|
+
context 'when a snapshot has been added to the most recent commit of a stream' do
|
209
|
+
let(:oldest) { new_attempt }
|
210
|
+
let(:oldest2) { next_attempt oldest }
|
211
|
+
let(:newest) { next_attempt oldest2 }
|
212
|
+
let(:snapshot) { Euston::EventStore::Snapshot.new stream_id, newest.stream_revision, { :key => :value } }
|
213
|
+
|
214
|
+
before do
|
215
|
+
@persistence.commit oldest
|
216
|
+
@persistence.commit oldest2
|
217
|
+
@persistence.commit newest
|
218
|
+
|
219
|
+
sleep 0.25
|
220
|
+
|
221
|
+
@persistence.add_snapshot snapshot
|
222
|
+
end
|
223
|
+
|
224
|
+
it('no longer finds the stream in the set of streams to be snapshot') {
|
225
|
+
@persistence.get_streams_to_snapshot(1).detect { |x| x.stream_id == stream_id }.should be_nil }
|
226
|
+
end
|
227
|
+
|
228
|
+
# Timing issues with this one?
|
229
|
+
#
|
230
|
+
# context 'when adding a commit after a snapshot' do
|
231
|
+
# let(:within_threshold) { 2 }
|
232
|
+
# let(:over_threshold) { 3 }
|
233
|
+
# let(:snapshot_data) { { :key => :value } }
|
234
|
+
# let(:oldest) { new_attempt }
|
235
|
+
# let(:oldest2) { next_attempt oldest }
|
236
|
+
# let(:newest) { next_attempt oldest2 }
|
237
|
+
|
238
|
+
# before do
|
239
|
+
# @persistence.commit oldest
|
240
|
+
# @persistence.commit oldest2
|
241
|
+
|
242
|
+
# sleep 0.25
|
243
|
+
|
244
|
+
# @persistence.add_snapshot Euston::EventStore::Snapshot.new(stream_id, oldest2.stream_revision, snapshot_data)
|
245
|
+
# @persistence.commit newest
|
246
|
+
# end
|
247
|
+
|
248
|
+
# it 'finds the stream in the set of streams to be snapshot when within the threshold' do
|
249
|
+
# @persistence.get_streams_to_snapshot(within_threshold).detect { |x| x.stream_id == stream_id }.should_not be_nil
|
250
|
+
# end
|
251
|
+
|
252
|
+
# it 'does not find the stream in the set of stream to be snapshot when over the threshold' do
|
253
|
+
# @persistence.get_streams_to_snapshot(over_threshold).detect { |x| x.stream_id == stream_id }.should be_nil
|
254
|
+
# end
|
255
|
+
# end
|
256
|
+
|
257
|
+
context 'when reading all commits from a particular point in time' do
|
258
|
+
let(:now) { Time.now.utc + (60 * 60 * 24 * 7 * 52) }
|
259
|
+
let(:first) { new_attempt(:commit_timestamp => now + 1) }
|
260
|
+
let(:second) { next_attempt first }
|
261
|
+
let(:third) { next_attempt second }
|
262
|
+
let(:fourth) { next_attempt third }
|
263
|
+
|
264
|
+
before do
|
265
|
+
@persistence.commit first
|
266
|
+
@persistence.commit second
|
267
|
+
@persistence.commit third
|
268
|
+
@persistence.commit fourth
|
269
|
+
|
270
|
+
@committed = @persistence.get_from :timestamp => now
|
271
|
+
end
|
272
|
+
|
273
|
+
it('returns all commits on or after the point in time specified') { @committed.should have(4).items }
|
274
|
+
end
|
275
|
+
|
276
|
+
def new_attempt(options = {})
|
277
|
+
defaults = { :stream_id => stream_id,
|
278
|
+
:stream_revision => 2,
|
279
|
+
:commit_id => uuid.generate,
|
280
|
+
:commit_sequence => 1,
|
281
|
+
:commit_timestamp => Time.now.utc,
|
282
|
+
:headers => { 'A header' => 'A string value',
|
283
|
+
'Another header' => 2 },
|
284
|
+
:events => [ Euston::EventStore::EventMessage.new(:some_property => 'test'),
|
285
|
+
Euston::EventStore::EventMessage.new(:some_property => 'test2') ] }
|
286
|
+
|
287
|
+
Euston::EventStore::Commit.new(defaults.merge options)
|
288
|
+
end
|
289
|
+
|
290
|
+
def next_attempt(attempt)
|
291
|
+
Euston::EventStore::Commit.new(:stream_id => attempt.stream_id,
|
292
|
+
:stream_revision => attempt.stream_revision + 2,
|
293
|
+
:commit_id => uuid.generate,
|
294
|
+
:commit_sequence => attempt.commit_sequence + 1,
|
295
|
+
:commit_timestamp => attempt.commit_timestamp,
|
296
|
+
:headers => {},
|
297
|
+
:events => [ Euston::EventStore::EventMessage.new(:some_property => 'Another test'),
|
298
|
+
Euston::EventStore::EventMessage.new(:some_property => 'Another test2') ])
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|