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,28 @@
1
+ require 'singleton'
2
+
3
+ module Euston
4
+ module EventStore
5
+ module Persistence
6
+ module Mongodb
7
+ class Config
8
+ include ::Singleton
9
+
10
+ def host
11
+ @host ||= 'localhost'
12
+ end
13
+
14
+ def port
15
+ @port ||= 27017
16
+ end
17
+
18
+ def options
19
+ @options ||= { :safe => { :fsync => true }}
20
+ end
21
+
22
+ attr_writer :host, :port, :options
23
+ attr_accessor :database, :logger
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+ module Euston
2
+ module EventStore
3
+ module Persistence
4
+ module Mongodb
5
+ module MongoEventMessage
6
+ extend ::ActiveSupport::Concern
7
+
8
+ class << self
9
+ def from_hash(hash)
10
+ {}.recursive_symbolize_keys!
11
+ message = EventMessage.new hash['body'].recursive_symbolize_keys!
12
+ message.instance_variable_set :@headers, hash['headers'].recursive_symbolize_keys!
13
+ message
14
+ end
15
+ end
16
+
17
+ def to_hash
18
+ {
19
+ :headers => headers,
20
+ :body => body.to_hash.recursive_stringify_symbol_values!
21
+ }
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ class EventMessage
28
+ include Persistence::Mongodb::MongoEventMessage
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,167 @@
1
+ module Euston
2
+ module EventStore
3
+ module Persistence
4
+ module Mongodb
5
+ class MongoPersistenceEngine
6
+ def initialize(store)
7
+ @store = store
8
+
9
+ collection_names = store.collection_names
10
+ store.create_collection 'commits' unless collection_names.include? 'commits' # :safe = true
11
+ store.create_collection 'snapshot' unless collection_names.include? 'snapshot' # :safe = false
12
+ store.create_collection 'streams' unless collection_names.include? 'streams' # :safe = false
13
+ end
14
+
15
+ def add_snapshot(snapshot)
16
+ return false if snapshot.nil?
17
+
18
+ begin
19
+ mongo_snapshot = snapshot.is_a?(Hash) ? snapshot : snapshot.to_hash
20
+ id = { '_id' => mongo_snapshot[:_id] }
21
+
22
+ persisted_snapshots.update(id, { 'payload' => mongo_snapshot[:payload] }.merge(id), { :upsert => true })
23
+
24
+ # jmongo's find_one is broken
25
+ if defined?(JMongo)
26
+ stream_head = MongoStreamHead.from_hash persisted_stream_heads.find({ '_id' => snapshot.stream_id }).to_a.first
27
+ else
28
+ stream_head = MongoStreamHead.from_hash persisted_stream_heads.find_one({ '_id' => snapshot.stream_id })
29
+ end
30
+
31
+ unsnapshotted = stream_head.head_revision - snapshot.stream_revision
32
+ persisted_stream_heads.update({ '_id' => snapshot.stream_id },
33
+ { '$set' => { 'snapshot_revision' => snapshot.stream_revision, 'unsnapshotted' => unsnapshotted } })
34
+ return true
35
+ rescue Mongo::OperationFailure
36
+ return false
37
+ end
38
+ end
39
+
40
+ def commit(attempt)
41
+ try_mongo do
42
+ commit = attempt.to_mongo_commit
43
+
44
+ begin
45
+ # for concurrency / duplicate commit detection safe mode is required
46
+ persisted_commits.insert commit, :safe => true
47
+ update_stream_head_async attempt.stream_id, attempt.stream_revision, attempt.events.length
48
+ rescue Mongo::OperationFailure, NativeException => e
49
+ raise(Euston::EventStore::StorageError, e.message, e.backtrace) unless e.message.include? CONCURRENCY_EXCEPTION
50
+
51
+ # jmongo's find_one is broken
52
+ if defined?(JMongo)
53
+ committed = persisted_commits.find(attempt.to_id_query).to_a.first
54
+ else
55
+ committed = persisted_commits.find_one(attempt.to_id_query)
56
+ end
57
+
58
+ raise Euston::EventStore::DuplicateCommitError if !committed.nil? && committed['commit_id'] == attempt.commit_id
59
+ raise Euston::EventStore::ConcurrencyError
60
+ end
61
+ end
62
+ end
63
+
64
+ def get_from(options)
65
+ try_mongo do
66
+ if options.has_key? :timestamp
67
+ query = { 'commit_timestamp' => { '$gte' => options[:timestamp].to_f } }
68
+ order = [ 'commit_timestamp', Mongo::ASCENDING ]
69
+ else
70
+ query = { '_id.stream_id' => options[:stream_id],
71
+ 'events.stream_revision' => { '$gte' => options[:min_revision], '$lte' => options[:max_revision] } }
72
+
73
+ order = [ 'events.stream_revision', Mongo::ASCENDING ]
74
+ end
75
+
76
+ persisted_commits.find(query).sort(order).to_a.map { |hash| MongoCommit.from_hash hash }
77
+ end
78
+ end
79
+
80
+ def get_snapshot(stream_id, max_revision)
81
+ try_mongo do
82
+ query = { '_id' => { '$gt' => { 'stream_id' => stream_id, 'stream_revision' => nil },
83
+ '$lte' => { 'stream_id' => stream_id, 'stream_revision' => max_revision } } }
84
+ order = [ '_id', Mongo::DESCENDING ]
85
+
86
+ persisted_snapshots.find(query).sort(order).limit(1).to_a.map { |hash| MongoSnapshot::from_hash hash }.first
87
+ end
88
+ end
89
+
90
+ def get_streams_to_snapshot(max_threshold)
91
+ try_mongo do
92
+ query = { 'unsnapshotted' => { '$gte' => max_threshold } }
93
+ order = [ 'unsnapshotted', Mongo::DESCENDING ]
94
+
95
+ persisted_stream_heads.find(query).sort(order).to_a.map { |hash| MongoStreamHead.from_hash hash }
96
+ end
97
+ end
98
+
99
+ def get_undispatched_commits
100
+ try_mongo do
101
+ query = { 'dispatched' => false }
102
+ order = [ 'commit_timestamp', Mongo::ASCENDING ]
103
+
104
+ persisted_commits.find(query).sort(order).to_a.map { |hash| MongoCommit.from_hash hash }
105
+ end
106
+ end
107
+
108
+ def init
109
+ try_mongo do
110
+ persisted_commits.ensure_index [ ['dispatched', Mongo::ASCENDING],
111
+ ['commit_timestamp', Mongo::ASCENDING] ], :unique => false, :name => 'dispatched_index'
112
+
113
+ persisted_commits.ensure_index [ ['_id.stream_id', Mongo::ASCENDING],
114
+ ['events.stream_revision', Mongo::ASCENDING] ], :unique => true, :name => 'get_from_index'
115
+
116
+ persisted_commits.ensure_index [ ['commit_timestamp', Mongo::ASCENDING] ], :unique => false, :name => 'commit_timestamp_index'
117
+
118
+ persisted_stream_heads.ensure_index [ ['unsnapshotted', Mongo::ASCENDING] ], :unique => false, :name => 'unsnapshotted_index'
119
+ end
120
+ self
121
+ end
122
+
123
+ def mark_commit_as_dispatched(commit)
124
+ try_mongo do
125
+ persisted_commits.update commit.to_id_query, { '$set' => { 'dispatched' => true }}
126
+ end
127
+ end
128
+
129
+ private
130
+
131
+ def persisted_commits
132
+ @store.collection 'commits'
133
+ end
134
+
135
+ def persisted_snapshots
136
+ @store.collection 'snapshots'
137
+ end
138
+
139
+ def persisted_stream_heads
140
+ @store.collection 'streams'
141
+ end
142
+
143
+ def try_mongo(&block)
144
+ begin
145
+ yield block
146
+ rescue Mongo::ConnectionError => e
147
+ raise Euston::EventStore::StorageUnavailableError, e.to_s, e.backtrace
148
+ rescue Mongo::MongoDBError => e
149
+ raise Euston::EventStore::StorageError, e.to_s, e.backtrace
150
+ end
151
+ end
152
+
153
+ def update_stream_head_async(stream_id, stream_revision, events_count)
154
+ Thread.fork do
155
+ persisted_stream_heads.update(
156
+ { '_id' => stream_id },
157
+ { '$set' => { 'head_revision' => stream_revision }, '$inc' => { 'snapshot_revision' => 0, 'unsnapshotted' => events_count } },
158
+ { :upsert => true })
159
+ end
160
+ end
161
+
162
+ CONCURRENCY_EXCEPTION = "E1100"
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,31 @@
1
+ if RUBY_PLATFORM.to_s == 'java'
2
+ module JMongo
3
+ module BasicDBObjectExtentions
4
+ include HashKeys
5
+ end
6
+ end
7
+
8
+ require 'jmongo'
9
+ else
10
+ require 'mongo'
11
+ end
12
+
13
+ module Euston
14
+ module EventStore
15
+ module Persistence
16
+ module Mongodb
17
+ class MongoPersistenceFactory
18
+ def self.build
19
+ config = Config.instance
20
+ connection = ::Mongo::Connection.new(config.host, config.port, config.options)
21
+
22
+ MongoPersistenceEngine.new connection.db(config.database)
23
+ end
24
+ def self.build_with_proxy()
25
+ ZmqPersistenceEngineProxy.new(build.init)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -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.2"
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ require 'active_support/concern'
2
+ require 'hash-keys'
3
+ require 'require_all'
4
+
5
+ require_rel 'euston-eventstore'
6
+
7
+ Json = JSON if defined?(JSON) && !defined?(Json)
@@ -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