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
data/Rakefile
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
|
4
|
+
#############################################################################
|
5
|
+
#
|
6
|
+
# Helper functions
|
7
|
+
#
|
8
|
+
#############################################################################
|
9
|
+
|
10
|
+
def name
|
11
|
+
@name ||= Dir['*.gemspec'].first.split('.').first
|
12
|
+
end
|
13
|
+
|
14
|
+
def version
|
15
|
+
line = File.read("lib/#{name}/version.rb")[/^\s*VERSION\s*=\s*.*/]
|
16
|
+
line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
|
17
|
+
end
|
18
|
+
|
19
|
+
def date
|
20
|
+
Date.today.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
def rubyforge_project
|
24
|
+
name
|
25
|
+
end
|
26
|
+
|
27
|
+
def gemspec_file
|
28
|
+
"#{name}.gemspec"
|
29
|
+
end
|
30
|
+
|
31
|
+
def gem_file
|
32
|
+
"#{name}-#{version}.gem"
|
33
|
+
end
|
34
|
+
|
35
|
+
def replace_header(head, header_name)
|
36
|
+
head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
|
37
|
+
end
|
38
|
+
|
39
|
+
#############################################################################
|
40
|
+
#
|
41
|
+
# Custom tasks
|
42
|
+
#
|
43
|
+
#############################################################################
|
44
|
+
|
45
|
+
default_rspec_opts = %w[--colour --format Fuubar]
|
46
|
+
|
47
|
+
desc "Run all the specs"
|
48
|
+
RSpec::Core::RakeTask.new do |t|
|
49
|
+
t.rspec_opts = default_rspec_opts
|
50
|
+
end
|
51
|
+
|
52
|
+
#############################################################################
|
53
|
+
#
|
54
|
+
# Packaging tasks
|
55
|
+
#
|
56
|
+
#############################################################################
|
57
|
+
|
58
|
+
desc "Create tag v#{version} and build and push #{gem_file} to Rubygems"
|
59
|
+
task :release => :build do
|
60
|
+
unless `git branch` =~ /^\* master$/
|
61
|
+
puts "You must be on the master branch to release!"
|
62
|
+
exit!
|
63
|
+
end
|
64
|
+
sh "git commit --allow-empty -a -m 'Release #{version}'"
|
65
|
+
sh "git tag v#{version}"
|
66
|
+
sh "git push origin master"
|
67
|
+
sh "git push origin v#{version}"
|
68
|
+
sh "gem push pkg/#{name}-#{version}.gem"
|
69
|
+
end
|
70
|
+
|
71
|
+
desc "Build #{gem_file} into the pkg directory"
|
72
|
+
task :build => :gemspec do
|
73
|
+
sh "mkdir -p pkg"
|
74
|
+
sh "gem build #{gemspec_file}"
|
75
|
+
sh "mv #{gem_file} pkg"
|
76
|
+
end
|
77
|
+
|
78
|
+
desc "Generate #{gemspec_file}"
|
79
|
+
task :gemspec => :validate do
|
80
|
+
# read spec file and split out manifest section
|
81
|
+
spec = File.read(gemspec_file)
|
82
|
+
head, manifest, tail = spec.split(" # = MANIFEST =\n")
|
83
|
+
|
84
|
+
# replace name version and date
|
85
|
+
replace_header(head, :name)
|
86
|
+
replace_header(head, :version)
|
87
|
+
replace_header(head, :date)
|
88
|
+
#comment this out if your rubyforge_project has a different name
|
89
|
+
#replace_header(head, :rubyforge_project)
|
90
|
+
|
91
|
+
# determine file list from git ls-files
|
92
|
+
files = `git ls-files`.
|
93
|
+
split("\n").
|
94
|
+
sort.
|
95
|
+
reject { |file| file =~ /^\./ }.
|
96
|
+
reject { |file| file =~ /^(rdoc|pkg)/ }.
|
97
|
+
map { |file| " #{file}" }.
|
98
|
+
join("\n")
|
99
|
+
|
100
|
+
# piece file back together and write
|
101
|
+
manifest = " s.files = %w[\n#{files}\n ]\n"
|
102
|
+
spec = [head, manifest, tail].join(" # = MANIFEST =\n")
|
103
|
+
File.open(gemspec_file, 'w') { |io| io.write(spec) }
|
104
|
+
puts "Updated #{gemspec_file}"
|
105
|
+
end
|
106
|
+
|
107
|
+
desc "Validate #{gemspec_file}"
|
108
|
+
task :validate do
|
109
|
+
libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"]
|
110
|
+
unless libfiles.empty?
|
111
|
+
puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir."
|
112
|
+
exit!
|
113
|
+
end
|
114
|
+
unless Dir['VERSION*'].empty?
|
115
|
+
puts "A `VERSION` file at root level violates Gem best practices."
|
116
|
+
exit!
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'euston-eventstore'
|
3
|
+
s.version = '1.0.0'
|
4
|
+
s.date = '2011-09-15'
|
5
|
+
s.platform = Gem::Platform::RUBY
|
6
|
+
s.authors = ['Lee Henson', 'Guy Boertje']
|
7
|
+
s.email = ['lee.m.henson@gmail.com', 'guyboertje@gmail.com']
|
8
|
+
s.summary = %q{Event store for use with Euston.}
|
9
|
+
s.description = "Ruby port for Jonathan Oliver's EventStore. See https://github.com/joliver/EventStore for details."
|
10
|
+
s.homepage = 'http://github.com/leemhenson/euston-eventstore'
|
11
|
+
# = MANIFEST =
|
12
|
+
s.files = %w[
|
13
|
+
Rakefile
|
14
|
+
euston-eventstore.gemspec
|
15
|
+
lib/euston-eventstore.rb
|
16
|
+
lib/euston-eventstore/commit.rb
|
17
|
+
lib/euston-eventstore/constants.rb
|
18
|
+
lib/euston-eventstore/dispatcher/asynchronous_dispatcher.rb
|
19
|
+
lib/euston-eventstore/dispatcher/null_dispatcher.rb
|
20
|
+
lib/euston-eventstore/dispatcher/synchronous_dispatcher.rb
|
21
|
+
lib/euston-eventstore/errors.rb
|
22
|
+
lib/euston-eventstore/event_message.rb
|
23
|
+
lib/euston-eventstore/optimistic_event_store.rb
|
24
|
+
lib/euston-eventstore/optimistic_event_stream.rb
|
25
|
+
lib/euston-eventstore/persistence/mongodb/mongo_commit.rb
|
26
|
+
lib/euston-eventstore/persistence/mongodb/mongo_commit_id.rb
|
27
|
+
lib/euston-eventstore/persistence/mongodb/mongo_config.rb
|
28
|
+
lib/euston-eventstore/persistence/mongodb/mongo_event_message.rb
|
29
|
+
lib/euston-eventstore/persistence/mongodb/mongo_persistence_engine.rb
|
30
|
+
lib/euston-eventstore/persistence/mongodb/mongo_persistence_factory.rb
|
31
|
+
lib/euston-eventstore/persistence/mongodb/mongo_snapshot.rb
|
32
|
+
lib/euston-eventstore/persistence/mongodb/mongo_stream_head.rb
|
33
|
+
lib/euston-eventstore/persistence/stream_head.rb
|
34
|
+
lib/euston-eventstore/snapshot.rb
|
35
|
+
lib/euston-eventstore/version.rb
|
36
|
+
spec/event_store/dispatcher/asynchronous_dispatcher_spec.rb
|
37
|
+
spec/event_store/dispatcher/synchronous_dispatcher_spec.rb
|
38
|
+
spec/event_store/optimistic_event_store_spec.rb
|
39
|
+
spec/event_store/optimistic_event_stream_spec.rb
|
40
|
+
spec/event_store/persistence/mongodb_spec.rb
|
41
|
+
spec/event_store/serialization/simple_message.rb
|
42
|
+
spec/spec_helper.rb
|
43
|
+
spec/support/array_enumeration_counter.rb
|
44
|
+
]
|
45
|
+
# = MANIFEST =
|
46
|
+
|
47
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
48
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
49
|
+
|
50
|
+
if RUBY_PLATFORM.to_s == 'java'
|
51
|
+
s.add_dependency 'json-jruby', '~> 1.5'
|
52
|
+
s.add_dependency 'jmongo', '~> 1'
|
53
|
+
else
|
54
|
+
s.add_dependency 'bson_ext', '~> 1.1'
|
55
|
+
s.add_dependency 'json', '~> 1.5'
|
56
|
+
s.add_dependency 'mongo', '~> 1.3.1'
|
57
|
+
s.add_development_dependency 'uuid', '~> 2.3'
|
58
|
+
end
|
59
|
+
|
60
|
+
s.add_dependency 'activesupport', '~> 3.0'
|
61
|
+
s.add_dependency 'hash-keys', '~> 1'
|
62
|
+
s.add_dependency 'require_all', '~> 1.2'
|
63
|
+
s.add_development_dependency 'awesome_print', '~> 0.4'
|
64
|
+
s.add_development_dependency 'fuubar', '~> 0.0'
|
65
|
+
s.add_development_dependency 'rspec', '~> 2.6'
|
66
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Euston
|
2
|
+
module EventStore
|
3
|
+
|
4
|
+
# Represents a series of events which have been fully committed as a single unit and which apply to the stream indicated.
|
5
|
+
class Commit
|
6
|
+
def initialize(hash)
|
7
|
+
defaults = {
|
8
|
+
:stream_id => nil,
|
9
|
+
:stream_revision => 1,
|
10
|
+
:commit_id => nil,
|
11
|
+
:commit_sequence => 1,
|
12
|
+
:commit_timestamp => Time.now.utc,
|
13
|
+
:headers => OpenStruct.new,
|
14
|
+
:events => []
|
15
|
+
}
|
16
|
+
values = defaults.merge hash
|
17
|
+
defaults.keys.each { |key| instance_variable_set "@#{key}", values[key] }
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_hash
|
21
|
+
{
|
22
|
+
:stream_id => stream_id,
|
23
|
+
:stream_revision => stream_revision,
|
24
|
+
:commit_id => commit_id,
|
25
|
+
:commit_sequence => commit_sequence,
|
26
|
+
:commit_timestamp => commit_timestamp,
|
27
|
+
:headers => headers.is_a?(OpenStruct) ? headers.instance_variable_get(:@table) : headers,
|
28
|
+
:events => events
|
29
|
+
}
|
30
|
+
end
|
31
|
+
# Gets the value which uniquely identifies the stream to which the commit belongs.
|
32
|
+
attr_reader :stream_id
|
33
|
+
|
34
|
+
# Gets the value which indicates the revision of the most recent event in the stream to which this commit applies.
|
35
|
+
attr_reader :stream_revision
|
36
|
+
|
37
|
+
# Gets the value which uniquely identifies the commit within the stream.
|
38
|
+
attr_reader :commit_id
|
39
|
+
|
40
|
+
# Gets the value which indicates the sequence (or position) in the stream to which this commit applies.
|
41
|
+
attr_reader :commit_sequence
|
42
|
+
|
43
|
+
# Gets the point in time at which the commit was persisted.
|
44
|
+
attr_reader :commit_timestamp
|
45
|
+
|
46
|
+
# Gets the metadata which provides additional, unstructured information about this commit.
|
47
|
+
attr_reader :headers
|
48
|
+
|
49
|
+
# Gets the collection of event messages to be committed as a single unit.
|
50
|
+
attr_reader :events
|
51
|
+
|
52
|
+
def ==(other)
|
53
|
+
(other.is_a? Commit) && (@stream_id == other.stream_id) && (@commit_id == other.commit_id)
|
54
|
+
end
|
55
|
+
|
56
|
+
class << self
|
57
|
+
def empty?(attempt)
|
58
|
+
attempt.nil? || attempt.events.empty?
|
59
|
+
end
|
60
|
+
|
61
|
+
def has_identifier?(attempt)
|
62
|
+
!(attempt.stream_id.nil? || attempt.commit_id.nil?)
|
63
|
+
end
|
64
|
+
|
65
|
+
def valid?(attempt)
|
66
|
+
raise ArgumentError.new('The commit must not be nil.') if attempt.nil?
|
67
|
+
raise ArgumentError.new('The commit must be uniquely identified.') unless Commit.has_identifier? attempt
|
68
|
+
raise ArgumentError.new('The commit sequence must be a positive number.') unless attempt.commit_sequence > 0
|
69
|
+
raise ArgumentError.new('The stream revision must be a positive number.') unless attempt.stream_revision > 0
|
70
|
+
raise ArgumentError.new('The stream revision must always be greater than or equal to the commit sequence.') if (attempt.stream_revision < attempt.commit_sequence)
|
71
|
+
|
72
|
+
true
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Euston
|
2
|
+
module EventStore
|
3
|
+
module Dispatcher
|
4
|
+
class AsynchronousDispatcher
|
5
|
+
def initialize(bus, persistence, &block)
|
6
|
+
@bus = bus
|
7
|
+
@persistence = persistence
|
8
|
+
@handle_exception = block_given? ? block : Proc.new {}
|
9
|
+
|
10
|
+
start
|
11
|
+
end
|
12
|
+
|
13
|
+
def dispatch(commit)
|
14
|
+
Thread.fork(commit) { |c| begin_dispatch c }
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def begin_dispatch(commit)
|
20
|
+
begin
|
21
|
+
@bus.publish commit
|
22
|
+
@persistence.mark_commit_as_dispatched commit
|
23
|
+
rescue Exception => e
|
24
|
+
@handle_exception.call commit, e
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def start
|
31
|
+
@persistence.init
|
32
|
+
@persistence.get_undispatched_commits.each { |commit| dispatch commit }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Euston
|
2
|
+
module EventStore
|
3
|
+
module Dispatcher
|
4
|
+
class SynchronousDispatcher
|
5
|
+
def initialize persistence, &block
|
6
|
+
@persistence = persistence
|
7
|
+
@dispatch = block
|
8
|
+
end
|
9
|
+
|
10
|
+
def dispatch commit
|
11
|
+
@dispatch.call commit
|
12
|
+
@persistence.mark_commit_as_dispatched commit
|
13
|
+
end
|
14
|
+
|
15
|
+
def lookup
|
16
|
+
@persistence.get_undispatched_commits.each { |commit| dispatch commit }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Euston
|
2
|
+
module EventStore
|
3
|
+
# Represents an optimistic concurrency conflict between multiple writers.
|
4
|
+
class ConcurrencyError < RuntimeError; end
|
5
|
+
|
6
|
+
# Represents an attempt to commit the same information more than once.
|
7
|
+
class DuplicateCommitError < RuntimeError; end
|
8
|
+
|
9
|
+
# Represents a loss of communications with the storage
|
10
|
+
class StorageUnavailableError < RuntimeError; end
|
11
|
+
|
12
|
+
# Represents a general failure of the storage engine or persistence infrastructure.
|
13
|
+
class StorageError < RuntimeError; end
|
14
|
+
|
15
|
+
# Represents an attempt to commit the same information more than once.
|
16
|
+
class StreamNotFoundError < RuntimeError; end
|
17
|
+
|
18
|
+
# Represents an error when the proxy returns a non 200 code that does not map to any of the above errors.
|
19
|
+
class ProxyCallError < RuntimeError; end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Euston
|
2
|
+
module EventStore
|
3
|
+
# Represents a single element in a stream of events.
|
4
|
+
class EventMessage
|
5
|
+
|
6
|
+
def initialize(arg = nil)
|
7
|
+
if arg.is_a?(Hash) && (arg.keys & ['body','headers']).size == 2
|
8
|
+
@body, @headers = arg.values_at('body','headers')
|
9
|
+
else
|
10
|
+
@headers = {}
|
11
|
+
@body = arg
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_hash
|
16
|
+
{:headers=>@headers,:body=>@body}
|
17
|
+
end
|
18
|
+
|
19
|
+
# Gets the metadata which provides additional, unstructured information about this message.
|
20
|
+
attr_reader :headers
|
21
|
+
|
22
|
+
# Gets or sets the actual event message body.
|
23
|
+
attr_reader :body
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Euston
|
2
|
+
module EventStore
|
3
|
+
class OptimisticEventStore
|
4
|
+
def initialize(persistence)
|
5
|
+
@persistence = persistence
|
6
|
+
end
|
7
|
+
|
8
|
+
def instrumentation
|
9
|
+
return nil unless @persistence.respond_to?(:instrumentation)
|
10
|
+
@persistence.instrumentation
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_snapshot(snapshot)
|
14
|
+
@persistence.add_snapshot snapshot
|
15
|
+
end
|
16
|
+
|
17
|
+
def commit(attempt)
|
18
|
+
return unless Commit.valid?(attempt) && !Commit.empty?(attempt)
|
19
|
+
|
20
|
+
@persistence.commit attempt
|
21
|
+
end
|
22
|
+
|
23
|
+
def create_stream(stream_id)
|
24
|
+
OptimisticEventStream.new(:stream_id => stream_id,
|
25
|
+
:persistence => self)
|
26
|
+
end
|
27
|
+
|
28
|
+
def get_from(stream_id, min_revision, max_revision)
|
29
|
+
@persistence.get_from(:stream_id => stream_id,
|
30
|
+
:min_revision => min_revision,
|
31
|
+
:max_revision => max_revision).to_enum
|
32
|
+
end
|
33
|
+
|
34
|
+
def get_snapshot(stream_id, max_revision)
|
35
|
+
@persistence.get_snapshot stream_id, validate_max_revision(max_revision)
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_streams_to_snapshot(max_threshold)
|
39
|
+
@persistence.get_streams_to_snapshot max_threshold
|
40
|
+
end
|
41
|
+
|
42
|
+
def open_stream(options)
|
43
|
+
options = { :stream_id => nil,
|
44
|
+
:min_revision => 0,
|
45
|
+
:max_revision => 0,
|
46
|
+
:snapshot => nil }.merge(options)
|
47
|
+
|
48
|
+
options = options.merge(:max_revision => validate_max_revision(options[:max_revision]),
|
49
|
+
:persistence => self)
|
50
|
+
|
51
|
+
if options[:snapshot].nil?
|
52
|
+
options.delete :snapshot
|
53
|
+
else
|
54
|
+
options.delete :stream_id
|
55
|
+
options.delete :min_revision
|
56
|
+
end
|
57
|
+
|
58
|
+
OptimisticEventStream.new options
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def validate_max_revision(max_revision)
|
64
|
+
max_revision <= 0 ? FIXNUM_MAX : max_revision
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|