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.
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