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