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,106 @@
|
|
1
|
+
module Euston
|
2
|
+
module EventStore
|
3
|
+
class OptimisticEventStream
|
4
|
+
def initialize(options)
|
5
|
+
@persistence = options[:persistence]
|
6
|
+
@committed_events = []
|
7
|
+
@uncommitted_events = []
|
8
|
+
@uncommitted_headers = {}
|
9
|
+
@commit_sequence = 0
|
10
|
+
@identifiers = []
|
11
|
+
|
12
|
+
if options.has_key? :snapshot
|
13
|
+
snapshot = options[:snapshot]
|
14
|
+
@stream_id = snapshot.stream_id
|
15
|
+
commits = @persistence.get_from @stream_id, snapshot.stream_revision, options[:max_revision]
|
16
|
+
populate_stream snapshot.stream_revision + 1, options[:max_revision], commits
|
17
|
+
@stream_revision = snapshot.stream_revision + committed_events.length
|
18
|
+
else
|
19
|
+
@stream_id = options[:stream_id]
|
20
|
+
@stream_revision = 0
|
21
|
+
min_revision = options[:min_revision] ||= nil
|
22
|
+
max_revision = options[:max_revision] ||= nil
|
23
|
+
|
24
|
+
unless min_revision.nil? || max_revision.nil?
|
25
|
+
commits = @persistence.get_from @stream_id, min_revision, max_revision
|
26
|
+
populate_stream min_revision, max_revision, commits
|
27
|
+
|
28
|
+
raise StreamNotFoundError if (min_revision > 0 && committed_events.empty?)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_reader :stream_id, :stream_revision, :commit_sequence, :committed_events, :uncommitted_events, :uncommitted_headers
|
34
|
+
|
35
|
+
def <<(event)
|
36
|
+
@uncommitted_events << event unless event.nil? || event.body.nil?
|
37
|
+
end
|
38
|
+
|
39
|
+
def clear_changes
|
40
|
+
@uncommitted_events = []
|
41
|
+
@uncommitted_headers = {}
|
42
|
+
end
|
43
|
+
|
44
|
+
def commit_changes(commit_id)
|
45
|
+
raise Euston::EventStore::DuplicateCommitError if @identifiers.include? commit_id
|
46
|
+
|
47
|
+
return unless has_changes
|
48
|
+
|
49
|
+
begin
|
50
|
+
persist_changes commit_id
|
51
|
+
rescue ConcurrencyError => e
|
52
|
+
commits = @persistence.get_from stream_id, stream_revision + 1, FIXNUM_MAX
|
53
|
+
populate_stream stream_revision + 1, FIXNUM_MAX, commits
|
54
|
+
|
55
|
+
raise e
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
protected
|
60
|
+
|
61
|
+
def copy_values_to_new_commit(commit_id)
|
62
|
+
Euston::EventStore::Commit.new :stream_id => stream_id,
|
63
|
+
:stream_revision => stream_revision + uncommitted_events.length,
|
64
|
+
:commit_id => commit_id,
|
65
|
+
:commit_sequence => commit_sequence + 1,
|
66
|
+
:commit_timestamp => Time.now.utc,
|
67
|
+
:headers => uncommitted_headers,
|
68
|
+
:events => uncommitted_events
|
69
|
+
end
|
70
|
+
|
71
|
+
def has_changes
|
72
|
+
!uncommitted_events.empty?
|
73
|
+
end
|
74
|
+
|
75
|
+
def persist_changes(commit_id)
|
76
|
+
commit = copy_values_to_new_commit commit_id
|
77
|
+
@persistence.commit commit
|
78
|
+
|
79
|
+
populate_stream stream_revision + 1, commit.stream_revision, [ commit ]
|
80
|
+
clear_changes
|
81
|
+
end
|
82
|
+
|
83
|
+
def populate_stream(min_revision, max_revision, commits = [])
|
84
|
+
commits.each do |commit|
|
85
|
+
@identifiers << commit.commit_id
|
86
|
+
@commit_sequence = commit.commit_sequence
|
87
|
+
|
88
|
+
current_revision = commit.stream_revision - commit.events.length + 1
|
89
|
+
|
90
|
+
return if current_revision > max_revision
|
91
|
+
|
92
|
+
commit.events.each do |event|
|
93
|
+
break if current_revision > max_revision
|
94
|
+
|
95
|
+
unless current_revision < min_revision
|
96
|
+
@committed_events << event
|
97
|
+
@stream_revision = current_revision
|
98
|
+
end
|
99
|
+
|
100
|
+
current_revision += 1
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Euston
|
2
|
+
module EventStore
|
3
|
+
module Persistence
|
4
|
+
module Mongodb
|
5
|
+
module MongoCommit
|
6
|
+
extend ::ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
alias_method :original_initialize, :initialize
|
10
|
+
alias_method :initialize, :mongo_initialize
|
11
|
+
end
|
12
|
+
|
13
|
+
class << self
|
14
|
+
def from_hash(hash)
|
15
|
+
return nil if hash.nil?
|
16
|
+
|
17
|
+
id = hash['_id']
|
18
|
+
events = hash['events'].sort_by { |e| e["stream_revision"] }.to_a
|
19
|
+
stream_revision = events.last['stream_revision']
|
20
|
+
events = events.map { |e| Euston::EventStore::Persistence::Mongodb::MongoEventMessage.from_hash e['payload'] }
|
21
|
+
|
22
|
+
Euston::EventStore::Commit.new :stream_id => id['stream_id'],
|
23
|
+
:stream_revision => stream_revision,
|
24
|
+
:commit_id => hash['commit_id'],
|
25
|
+
:commit_sequence => id['commit_sequence'],
|
26
|
+
:commit_timestamp => hash['commit_timestamp'],
|
27
|
+
:headers => hash['headers'].recursive_symbolize_keys!,
|
28
|
+
:events => events
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def mongo_initialize(hash)
|
33
|
+
original_initialize(hash)
|
34
|
+
@dispatched = hash[:dispatched]
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_reader :dispatched
|
38
|
+
|
39
|
+
def to_hash
|
40
|
+
{
|
41
|
+
:_id => { :stream_id => stream_id, :commit_sequence => commit_sequence },
|
42
|
+
:commit_id => commit_id,
|
43
|
+
:commit_timestamp => commit_timestamp.to_f,
|
44
|
+
:dispatched => dispatched || false,
|
45
|
+
:events => events.map { |e| e.to_hash },
|
46
|
+
:headers => headers
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_mongo_commit
|
51
|
+
mongo_stream_revision = stream_revision - (events.length - 1)
|
52
|
+
mongo_events = events.map do |e|
|
53
|
+
hash = { :stream_revision => mongo_stream_revision, :payload => e.to_hash }
|
54
|
+
mongo_stream_revision += 1
|
55
|
+
hash
|
56
|
+
end
|
57
|
+
|
58
|
+
{
|
59
|
+
:_id => { :stream_id => stream_id, :commit_sequence => commit_sequence },
|
60
|
+
:commit_id => commit_id,
|
61
|
+
:commit_timestamp => commit_timestamp.to_f,
|
62
|
+
:headers => headers,
|
63
|
+
:events => mongo_events,
|
64
|
+
:dispatched => false
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_id_query
|
69
|
+
{
|
70
|
+
'_id.commit_sequence' => commit_sequence,
|
71
|
+
'_id.stream_id' => stream_id
|
72
|
+
}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class Commit
|
79
|
+
include Persistence::Mongodb::MongoCommit
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Euston
|
2
|
+
module EventStore
|
3
|
+
module Persistence
|
4
|
+
module Mongodb
|
5
|
+
module MongoCommitId
|
6
|
+
def initialize(stream_id, commit_sequence)
|
7
|
+
@stream_id = stream_id
|
8
|
+
@commit_sequence = commit_sequence
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :stream_id, :commit_sequence
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -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
|