euston-eventstore 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. data/Rakefile +118 -0
  2. data/euston-eventstore.gemspec +66 -0
  3. data/lib/euston-eventstore.rb +7 -0
  4. data/lib/euston-eventstore/commit.rb +77 -0
  5. data/lib/euston-eventstore/constants.rb +5 -0
  6. data/lib/euston-eventstore/dispatcher/asynchronous_dispatcher.rb +37 -0
  7. data/lib/euston-eventstore/dispatcher/null_dispatcher.rb +11 -0
  8. data/lib/euston-eventstore/dispatcher/synchronous_dispatcher.rb +21 -0
  9. data/lib/euston-eventstore/errors.rb +21 -0
  10. data/lib/euston-eventstore/event_message.rb +26 -0
  11. data/lib/euston-eventstore/optimistic_event_store.rb +68 -0
  12. data/lib/euston-eventstore/optimistic_event_stream.rb +106 -0
  13. data/lib/euston-eventstore/persistence/mongodb/mongo_commit.rb +82 -0
  14. data/lib/euston-eventstore/persistence/mongodb/mongo_commit_id.rb +16 -0
  15. data/lib/euston-eventstore/persistence/mongodb/mongo_config.rb +28 -0
  16. data/lib/euston-eventstore/persistence/mongodb/mongo_event_message.rb +31 -0
  17. data/lib/euston-eventstore/persistence/mongodb/mongo_persistence_engine.rb +167 -0
  18. data/lib/euston-eventstore/persistence/mongodb/mongo_persistence_factory.rb +31 -0
  19. data/lib/euston-eventstore/persistence/mongodb/mongo_snapshot.rb +32 -0
  20. data/lib/euston-eventstore/persistence/mongodb/mongo_stream_head.rb +29 -0
  21. data/lib/euston-eventstore/persistence/stream_head.rb +23 -0
  22. data/lib/euston-eventstore/snapshot.rb +21 -0
  23. data/lib/euston-eventstore/version.rb +5 -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 +189 -0
@@ -0,0 +1,32 @@
1
+ module Euston
2
+ module EventStore
3
+ module Persistence
4
+ module Mongodb
5
+ module MongoSnapshot
6
+ extend ::ActiveSupport::Concern
7
+
8
+ class << self
9
+ def from_hash(hash)
10
+ return nil if hash.nil?
11
+
12
+ id = hash['_id']
13
+
14
+ Euston::EventStore::Snapshot.new id['stream_id'], id['stream_revision'], hash['payload']
15
+ end
16
+ end
17
+
18
+ def to_hash
19
+ {
20
+ :_id => { :stream_id => stream_id, :stream_revision => stream_revision },
21
+ :payload => payload.recursive_stringify_symbol_values!
22
+ }
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ class Snapshot
29
+ include Persistence::Mongodb::MongoSnapshot
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,29 @@
1
+ module Euston
2
+ module EventStore
3
+ module Persistence
4
+ module Mongodb
5
+ module MongoStreamHead
6
+ extend ::ActiveSupport::Concern
7
+
8
+ class << self
9
+ def from_hash(hash)
10
+ Euston::EventStore::Persistence::StreamHead.new hash['_id'], hash['head_revision'], hash['snapshot_revision']
11
+ end
12
+ end
13
+
14
+ def to_hash
15
+ {
16
+ :stream_id => @stream_id,
17
+ :head_revision => @head_revision,
18
+ :snapshot_revision => @snapshot_revision
19
+ }
20
+ end
21
+ end
22
+ end
23
+
24
+ class StreamHead
25
+ include Mongodb::MongoStreamHead
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,23 @@
1
+ module Euston
2
+ module EventStore
3
+ module Persistence
4
+ # Indicates the most recent information representing the head of a given stream.
5
+ class StreamHead
6
+ def initialize(stream_id, head_revision, snapshot_revision)
7
+ @stream_id = stream_id
8
+ @head_revision = head_revision
9
+ @snapshot_revision = snapshot_revision
10
+ end
11
+
12
+ # Gets the value which uniquely identifies the stream where the last snapshot exceeds the allowed threshold.
13
+ attr_reader :stream_id
14
+
15
+ # Gets the value which indicates the revision, length, or number of events committed to the stream.
16
+ attr_reader :head_revision
17
+
18
+ # Gets the value which indicates the revision at which the last snapshot was taken.
19
+ attr_reader :snapshot_revision
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ module Euston
2
+ module EventStore
3
+ # Represents a materialized view of a stream at specific revision.
4
+ class Snapshot
5
+ def initialize(stream_id, stream_revision, payload)
6
+ @stream_id = stream_id
7
+ @stream_revision = stream_revision
8
+ @payload = payload
9
+ end
10
+
11
+ # Gets the value which uniquely identifies the stream to which the snapshot applies.
12
+ attr_reader :stream_id
13
+
14
+ # Gets the position at which the snapshot applies.
15
+ attr_reader :stream_revision
16
+
17
+ # Gets the snapshot or materialized view of the stream at the revision indicated.
18
+ attr_reader :payload
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ module Euston
2
+ module EventStore
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
@@ -0,0 +1,75 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
2
+
3
+ describe Euston::EventStore do
4
+ let(:uuid) { Uuid }
5
+
6
+ describe 'asynchronous dispatcher' do
7
+ context 'when instantiating the asynchronous dispatcher' do
8
+ let(:stream_id) { uuid.generate }
9
+ let(:commits) { [ new_commit(:stream_id => stream_id), new_commit(:stream_id => stream_id) ] }
10
+ let(:bus) { stub('bus').as_null_object }
11
+ let(:persistence) { stub('persistence').as_null_object }
12
+
13
+ before do
14
+ persistence.should_receive(:init).once
15
+ persistence.should_receive(:get_undispatched_commits).once { commits }
16
+ bus.should_receive(:publish).with(commits.first).once
17
+ bus.should_receive(:publish).with(commits.last).once
18
+
19
+ Euston::EventStore::Dispatcher::AsynchronousDispatcher.new bus, persistence
20
+ sleep 0.25
21
+ end
22
+
23
+ it('initializes the persistence engine') { persistence.rspec_verify }
24
+ it('gets the set of undispatched commits') { persistence.rspec_verify }
25
+ it('provides the commits to the published') { bus.rspec_verify }
26
+ end
27
+
28
+ context 'when asynchronously dispatching a commit' do
29
+ let(:commit) { new_commit }
30
+ let(:bus) { stub('bus').as_null_object }
31
+ let(:persistence) { stub('persistence').as_null_object }
32
+
33
+ before do
34
+ bus.should_receive(:publish).with(commit).once
35
+ persistence.should_receive(:mark_commit_as_dispatched).with(commit).once
36
+
37
+ @dispatcher = Euston::EventStore::Dispatcher::AsynchronousDispatcher.new bus, persistence
38
+ @dispatcher.dispatch commit
39
+ sleep 0.25
40
+ end
41
+
42
+ it('provides the commit to the message bus') { bus.rspec_verify }
43
+ it('marks the commit as dispatched') { persistence.rspec_verify }
44
+ end
45
+
46
+ context 'when an asynchronously dispatched commit throws an exception' do
47
+ let(:commit) { new_commit }
48
+ let(:persistence) { stub('persistence').as_null_object }
49
+
50
+ before do
51
+ persistence.stub(:get_undispatched_commits) { [] }
52
+
53
+ @dispatcher = Euston::EventStore::Dispatcher::AsynchronousDispatcher.new nil, persistence do |commit, exception|
54
+ @caught_commit = commit
55
+ @caught_exception = exception
56
+ end
57
+
58
+ @dispatcher.dispatch commit
59
+ sleep 0.25
60
+ end
61
+
62
+ it('provides the commit that caused the error') { @caught_commit.should be_an(Euston::EventStore::Commit) }
63
+ it('provides the exception') { @caught_exception.should be_an(Exception) }
64
+ end
65
+
66
+ def new_commit(options = {})
67
+ defaults = { :stream_id => uuid.generate,
68
+ :stream_revision => 0,
69
+ :commit_id => uuid.generate,
70
+ :commit_sequence => 0 }
71
+
72
+ Euston::EventStore::Commit.new(defaults.merge options)
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,39 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
2
+
3
+ describe Euston::EventStore do
4
+ let(:uuid) { Uuid }
5
+
6
+ describe 'synchronous dispatcher' do
7
+ let(:bus) { stub('bus').as_null_object }
8
+ let(:persistence) { stub('persistence').as_null_object }
9
+
10
+ context 'when synchronously dispatching a commit' do
11
+ let(:commit) { new_commit }
12
+
13
+ before do
14
+ persistence.stub(:get_undispatched_commits) { [] }
15
+ persistence.should_receive(:mark_commit_as_dispatched).with(commit).once
16
+
17
+ @dispatched_commits = []
18
+
19
+ @dispatcher = Euston::EventStore::Dispatcher::SynchronousDispatcher.new(persistence) do |c|
20
+ @dispatched_commits << c
21
+ end
22
+
23
+ @dispatcher.dispatch commit
24
+ end
25
+
26
+ it('provides the commit to the message bus') { @dispatched_commits.should have(1).item }
27
+ it('marks the commit as dispatched') { persistence.rspec_verify }
28
+ end
29
+
30
+ def new_commit(options = {})
31
+ defaults = { :stream_id => uuid.generate,
32
+ :stream_revision => 0,
33
+ :commit_id => uuid.generate,
34
+ :commit_sequence => 0 }
35
+
36
+ Euston::EventStore::Commit.new(defaults.merge options)
37
+ end
38
+ end
39
+ end
@@ -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