ruby_event_store 0.18.2 → 0.19.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cbe2a5460bf30467128c5abc45d81cbd9898b44f
4
- data.tar.gz: 4c8e7701c16e6e7d08173b22a20799a8d82bf57d
3
+ metadata.gz: cf38d679db05a58d03f5cf03128e1515a44ae1e1
4
+ data.tar.gz: ffabae418e7ec0fb99fe6b546790a155d2a1c361
5
5
  SHA512:
6
- metadata.gz: c268df5cf9ca40e57dbb76c954c7c161383df97fa50d5d895c4e3afeccd7a0fcb60718fd423f454a16b866b59f73a50040e07c41ec34a9adace72b653a7c3f89
7
- data.tar.gz: c6a36bc896db65bcc54dabaf5621d694b14795dd3f61a6c42717676728363645e3e9568410ba5be4248d561f371e78285a76a7d27f0d49a2c99ebc21fca3a6e1
6
+ metadata.gz: 7ab222cbac66327aa236df8dc5c8f5b789854559d59a2d731914e031f1874f861ce9743b51a4d6c2a5f0351458f83b6fa0c6bbce16f887de69d600d8ee70a406
7
+ data.tar.gz: 0b89629ee2da94623d4bb04998e0e686e424d94a1a05f306e168b1d0afb51d19ffd16ccb72d989249821bf0688a991237e99ce41d3d2746cba2cc664ea8a9455
data/Makefile CHANGED
@@ -10,8 +10,7 @@ test: ## Run tests
10
10
 
11
11
  mutate: test ## Run mutation tests
12
12
  @echo "Running mutation tests - only 100% free mutation will be accepted"
13
- @bundle exec mutant --include lib --require ruby_event_store --use rspec "RubyEventStore*" \
14
- --ignore-subject "RubyEventStore.const_missing"
13
+ @bundle exec mutant --include lib --require ruby_event_store --use rspec "RubyEventStore*" --ignore-subject "RubyEventStore.const_missing" --ignore-subject "RubyEventStore::InMemoryRepository#append_with_synchronize" --ignore-subject "RubyEventStore::InMemoryRepository#normalize_to_array" --ignore-subject "RubyEventStore::Client#normalize_to_array"
15
14
 
16
15
  build:
17
16
  @gem build -V ruby_event_store.gemspec
@@ -12,16 +12,22 @@ module RubyEventStore
12
12
  @clock = clock
13
13
  end
14
14
 
15
- def publish_event(event, stream_name: GLOBAL_STREAM, expected_version: :any)
16
- append_to_stream(event, stream_name: stream_name, expected_version: expected_version)
17
- event_broker.notify_subscribers(event)
15
+ def publish_events(events, stream_name: GLOBAL_STREAM, expected_version: :any)
16
+ append_to_stream(events, stream_name: stream_name, expected_version: expected_version)
17
+ events.each do |ev|
18
+ event_broker.notify_subscribers(ev)
19
+ end
18
20
  :ok
19
21
  end
20
22
 
21
- def append_to_stream(event, stream_name: GLOBAL_STREAM, expected_version: :any)
22
- validate_expected_version(stream_name, expected_version)
23
- enriched_event = enrich_event_metadata(event)
24
- repository.create(enriched_event, stream_name)
23
+ def publish_event(event, stream_name: GLOBAL_STREAM, expected_version: :any)
24
+ publish_events([event], stream_name: stream_name, expected_version: expected_version)
25
+ end
26
+
27
+ def append_to_stream(events, stream_name: GLOBAL_STREAM, expected_version: :any)
28
+ events = normalize_to_array(events)
29
+ events.each{|event| enrich_event_metadata(event) }
30
+ repository.append_to_stream(events, stream_name, expected_version)
25
31
  :ok
26
32
  end
27
33
 
@@ -64,13 +70,13 @@ module RubyEventStore
64
70
  end
65
71
 
66
72
  def subscribe(subscriber, event_types, &proc)
67
- event_broker.add_subscriber(subscriber_or_proxy(subscriber), event_types).tap do |unsub|
73
+ event_broker.add_subscriber(subscriber, event_types).tap do |unsub|
68
74
  handle_subscribe(unsub, &proc)
69
75
  end
70
76
  end
71
77
 
72
78
  def subscribe_to_all_events(subscriber, &proc)
73
- event_broker.add_global_subscriber(subscriber_or_proxy(subscriber)).tap do |unsub|
79
+ event_broker.add_global_subscriber(subscriber).tap do |unsub|
74
80
  handle_subscribe(unsub, &proc)
75
81
  end
76
82
  end
@@ -78,8 +84,8 @@ module RubyEventStore
78
84
  private
79
85
  attr_reader :repository, :page_size, :event_broker, :metadata_proc, :clock
80
86
 
81
- def subscriber_or_proxy(subscriber)
82
- subscriber.instance_of?(Class) ? event_broker.proxy_for(subscriber) : subscriber
87
+ def normalize_to_array(events)
88
+ return *events
83
89
  end
84
90
 
85
91
  def enrich_event_metadata(event)
@@ -87,14 +93,15 @@ module RubyEventStore
87
93
  metadata[:timestamp] ||= clock.()
88
94
  metadata.merge!(metadata_proc.call || {}) if metadata_proc
89
95
 
90
- event.class.new(event_id: event.event_id, metadata: metadata, data: event.data)
96
+ # event.class.new(event_id: event.event_id, metadata: metadata, data: event.data)
91
97
  end
92
98
 
93
- def handle_subscribe(unsub)
94
- if block_given?
95
- yield
99
+ def handle_subscribe(unsub, &proc)
100
+ begin
101
+ proc.call
102
+ ensure
96
103
  unsub.()
97
- end
104
+ end if proc
98
105
  end
99
106
 
100
107
  class Page
@@ -113,22 +120,5 @@ module RubyEventStore
113
120
  attr_reader :start, :count
114
121
  end
115
122
 
116
- def validate_expected_version(stream_name, expected_version)
117
- raise InvalidExpectedVersion if expected_version.nil?
118
- case expected_version
119
- when :any
120
- return
121
- when :none
122
- return if last_stream_event_id(stream_name).nil?
123
- else
124
- return if last_stream_event_id(stream_name).eql?(expected_version)
125
- end
126
- raise WrongExpectedEventVersion
127
- end
128
-
129
- def last_stream_event_id(stream_name)
130
- last = repository.last_stream_event(stream_name)
131
- last.event_id if last
132
- end
133
123
  end
134
124
  end
@@ -6,10 +6,11 @@ module RubyEventStore
6
6
  SubscriberNotExist = Class.new(StandardError)
7
7
  InvalidPageStart = Class.new(ArgumentError)
8
8
  InvalidPageSize = Class.new(ArgumentError)
9
+ EventDuplicatedInStream = Class.new(StandardError)
9
10
 
10
11
  class InvalidHandler < StandardError
11
- def initialize(subscriber_klass)
12
- super("#call method not found in #{subscriber_klass} subscriber. Are you sure it is a valid subscriber?")
12
+ def initialize(object)
13
+ super("#call method not found in #{object.inspect} subscriber. Are you sure it is a valid subscriber?")
13
14
  end
14
15
  end
15
16
  end
@@ -27,6 +27,17 @@ module RubyEventStore
27
27
  other_event.data.eql?(data)
28
28
  end
29
29
 
30
+ BIG_VALUE = 0b111111100100000010010010110011101011000101010101001100100110000
31
+
32
+ # We don't use metadata because == does not use metadata
33
+ def hash
34
+ [
35
+ self.class,
36
+ event_id,
37
+ data
38
+ ].hash ^ BIG_VALUE
39
+ end
40
+
30
41
  alias_method :eql?, :==
31
42
  end
32
- end
43
+ end
@@ -1,24 +1,34 @@
1
1
  require 'ostruct'
2
+ require 'thread'
2
3
 
3
4
  module RubyEventStore
4
5
  class InMemoryRepository
5
6
  def initialize
6
7
  @all = Array.new
7
8
  @streams = Hash.new
9
+ @mutex = Mutex.new
8
10
  end
9
11
 
10
- def create(event, stream_name)
12
+ def append_to_stream(events, stream_name, expected_version)
13
+ raise InvalidExpectedVersion if !expected_version.equal?(:any) && stream_name.eql?(GLOBAL_STREAM)
14
+ events = normalize_to_array(events)
11
15
  stream = read_stream_events_forward(stream_name)
12
- all.push(event)
13
- stream.push(event)
14
- streams[stream_name] = stream
15
- event
16
+ expected_version = case expected_version
17
+ when :none
18
+ -1
19
+ when :auto, :any
20
+ stream.size - 1
21
+ when Integer
22
+ expected_version
23
+ else
24
+ raise InvalidExpectedVersion
25
+ end
26
+ append_with_synchronize(events, expected_version, stream, stream_name)
27
+ self
16
28
  end
17
29
 
18
30
  def delete_stream(stream_name)
19
- removed = read_stream_events_forward(stream_name).map(&:event_id)
20
31
  streams.delete(stream_name)
21
- all.delete_if{|ev| removed.include?(ev.event_id)}
22
32
  end
23
33
 
24
34
  def has_event?(event_id)
@@ -58,6 +68,34 @@ module RubyEventStore
58
68
  private
59
69
  attr_accessor :streams, :all
60
70
 
71
+ def normalize_to_array(events)
72
+ return *events
73
+ end
74
+
75
+ def append_with_synchronize(events, expected_version, stream, stream_name)
76
+ # expected_version :auto assumes external lock is used
77
+ # which makes reading stream before writing safe.
78
+ #
79
+ # To emulate potential concurrency issues of :auto strategy without
80
+ # such external lock we use Thread.pass to make race
81
+ # conditions more likely. And we only use mutex.synchronize for writing
82
+ # not for the whole read+write algorithm.
83
+ Thread.pass
84
+ @mutex.synchronize do
85
+ append(events, expected_version, stream, stream_name)
86
+ end
87
+ end
88
+
89
+ def append(events, expected_version, stream, stream_name)
90
+ raise WrongExpectedEventVersion unless (stream.size - 1).equal?(expected_version)
91
+ events.each do |event|
92
+ all.push(event)
93
+ raise EventDuplicatedInStream if stream.any?{|ev| ev.event_id.eql?(event.event_id) }
94
+ stream.push(event)
95
+ end
96
+ streams[stream_name] = stream
97
+ end
98
+
61
99
  def read_batch(source, start_event_id, count)
62
100
  return source[0..count-1] if start_event_id.equal?(:head)
63
101
  start_index = index_of(source, start_event_id)
@@ -27,16 +27,12 @@ module RubyEventStore
27
27
  end
28
28
  end
29
29
 
30
- def proxy_for(klass)
31
- dispatcher.proxy_for(klass)
32
- end
33
-
34
30
  private
35
31
  attr_reader :subscribers, :dispatcher
36
32
 
37
33
  def verify_subscriber(subscriber)
38
34
  raise SubscriberNotExist if subscriber.nil?
39
- raise InvalidHandler.new(subscriber.class) unless subscriber.respond_to?(:call)
35
+ dispatcher.verify(subscriber)
40
36
  end
41
37
 
42
38
  def subscribe(subscriber, event_types)
@@ -1,14 +1,25 @@
1
1
  module RubyEventStore
2
2
  module PubSub
3
+
3
4
  class Dispatcher
4
5
  def call(subscriber, event)
6
+ subscriber = subscriber.new if Class === subscriber
5
7
  subscriber.call(event)
6
8
  end
7
9
 
8
- def proxy_for(klass)
9
- raise InvalidHandler.new(klass) unless klass.method_defined?(:call)
10
- ->(e) { klass.new.call(e) }
10
+ def verify(subscriber)
11
+ subscriber = klassify(subscriber)
12
+ subscriber.respond_to?(:call) or raise InvalidHandler.new(subscriber)
13
+ end
14
+
15
+ private
16
+
17
+ def klassify(subscriber)
18
+ Class === subscriber ? subscriber.new : subscriber
19
+ rescue ArgumentError
20
+ raise InvalidHandler.new(subscriber)
11
21
  end
12
22
  end
23
+
13
24
  end
14
25
  end
@@ -1,30 +1,34 @@
1
1
  RSpec.shared_examples :dispatcher do |dispatcher|
2
- specify "calls subscribed handler" do
3
- handler = double(:handler)
2
+ specify "calls subscribed instance" do
3
+ handler = HandlerClass.new
4
4
  event = instance_double(::RubyEventStore::Event)
5
5
 
6
6
  expect(handler).to receive(:call).with(event)
7
7
  dispatcher.(handler, event)
8
8
  end
9
9
 
10
- specify "returns callable proxy" do
10
+ specify "calls subscribed class" do
11
11
  event = instance_double(::RubyEventStore::Event)
12
12
 
13
- handler = dispatcher.proxy_for(HandlerClass)
14
- expect(handler).to receive(:call).with(event).and_call_original
15
- dispatcher.(handler, event)
16
- expect(HandlerClass.received).to eq(event)
13
+ expect(HandlerClass).to receive(:new).and_return( h = HandlerClass.new )
14
+ expect(h).to receive(:call).with(event)
15
+ dispatcher.(HandlerClass, event)
17
16
  end
18
17
 
19
- specify "fails to build proxy when no call method defined on class" do
20
- message = "#call method not found " +
21
- "in String subscriber." +
22
- " Are you sure it is a valid subscriber?"
23
-
24
- expect { dispatcher.proxy_for(String) }.to raise_error(::RubyEventStore::InvalidHandler, message)
18
+ specify "allows callable classes and instances" do
19
+ expect do
20
+ dispatcher.verify(HandlerClass)
21
+ end.not_to raise_error
22
+ expect do
23
+ dispatcher.verify(HandlerClass.new)
24
+ end.not_to raise_error
25
+ expect do
26
+ dispatcher.verify(Proc.new{ "yo" })
27
+ end.not_to raise_error
25
28
  end
26
29
 
27
30
  private
31
+
28
32
  class HandlerClass
29
33
  @@received = nil
30
34
  def self.received
@@ -23,6 +23,9 @@ RSpec.shared_examples :event_broker do |broker_class|
23
23
  @dispatched = []
24
24
  end
25
25
 
26
+ def verify(_subscriber)
27
+ end
28
+
26
29
  def call(subscriber, event)
27
30
  @dispatched << {subscriber: subscriber, event: event}
28
31
  end
@@ -58,19 +61,17 @@ RSpec.shared_examples :event_broker do |broker_class|
58
61
  end
59
62
 
60
63
  it 'raises error when no valid method on handler' do
61
- message = "#call method not found " +
62
- "in InvalidTestHandler subscriber." +
63
- " Are you sure it is a valid subscriber?"
64
64
  subscriber = InvalidTestHandler.new
65
- expect { broker.add_subscriber(subscriber, [Test1DomainEvent]) }.to raise_error(RubyEventStore::InvalidHandler, message)
65
+ expect do
66
+ broker.add_subscriber(subscriber, [Test1DomainEvent])
67
+ end.to raise_error(RubyEventStore::InvalidHandler)
66
68
  end
67
69
 
68
70
  it 'raises error when no valid method on global handler' do
69
- message = "#call method not found " +
70
- "in InvalidTestHandler subscriber." +
71
- " Are you sure it is a valid subscriber?"
72
71
  subscriber = InvalidTestHandler.new
73
- expect { broker.add_global_subscriber(subscriber) }.to raise_error(RubyEventStore::InvalidHandler, message)
72
+ expect do
73
+ broker.add_global_subscriber(subscriber)
74
+ end.to raise_error(RubyEventStore::InvalidHandler)
74
75
  end
75
76
 
76
77
  it 'returns lambda as an output of global subscribe methods' do
@@ -121,19 +122,6 @@ RSpec.shared_examples :event_broker do |broker_class|
121
122
  expect(dispatcher.dispatched).to eq([{subscriber: handler, event: event1}])
122
123
  end
123
124
 
124
- it "returns callable proxy" do
125
- proxy = broker.proxy_for(TestHandler)
126
- expect(proxy.respond_to?(:call)).to be_truthy
127
- end
128
-
129
- specify "fails to build proxy when no call method defined on class" do
130
- message = "#call method not found " +
131
- "in InvalidTestHandler subscriber." +
132
- " Are you sure it is a valid subscriber?"
133
-
134
- expect { broker.proxy_for(InvalidTestHandler) }.to raise_error(::RubyEventStore::InvalidHandler, message)
135
- end
136
-
137
125
  private
138
126
  class HandlerClass
139
127
  @@received = nil
@@ -6,15 +6,249 @@ RSpec.shared_examples :event_repository do |repository_class|
6
6
  expect(repository.read_all_streams_forward(:head, 1)).to be_empty
7
7
  end
8
8
 
9
- it 'what you get is what you gave' do
10
- created = repository.create(event = TestDomainEvent.new, 'stream')
11
- expect(created.object_id).to eq event.object_id
9
+ specify 'publish fail if expected version is nil' do
10
+ expect do
11
+ repository.append_to_stream(event = TestDomainEvent.new, 'stream', nil)
12
+ end.to raise_error(RubyEventStore::InvalidExpectedVersion)
12
13
  end
13
14
 
14
- it 'created event is stored in given stream' do
15
+ specify 'append_to_stream returns self' do
16
+ repository.
17
+ append_to_stream(event = TestDomainEvent.new, 'stream', -1).
18
+ append_to_stream(event = TestDomainEvent.new, 'stream', 0)
19
+ end
20
+
21
+ specify 'adds initial event to a new stream' do
22
+ repository.append_to_stream(event = TestDomainEvent.new, 'stream', :none)
23
+ expect(repository.read_all_streams_forward(:head, 1).first).to eq(event)
24
+ expect(repository.read_stream_events_forward('stream').first).to eq(event)
25
+ expect(repository.read_stream_events_forward('other_stream')).to be_empty
26
+ end
27
+
28
+ specify 'adds multiple initial events to a new stream' do
29
+ repository.append_to_stream([
30
+ event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
31
+ event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
32
+ ], 'stream', :none)
33
+ expect(repository.read_all_streams_forward(:head, 2)).to eq([event0, event1])
34
+ expect(repository.read_stream_events_forward('stream')).to eq([event0, event1])
35
+ end
36
+
37
+ specify 'correct expected version on second write' do
38
+ repository.append_to_stream([
39
+ event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
40
+ event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
41
+ ], 'stream', :none)
42
+ repository.append_to_stream([
43
+ event2 = TestDomainEvent.new(event_id: SecureRandom.uuid),
44
+ event3 = TestDomainEvent.new(event_id: SecureRandom.uuid),
45
+ ], 'stream', 1)
46
+ expect(repository.read_all_streams_forward(:head, 4)).to eq([event0, event1, event2, event3])
47
+ expect(repository.read_stream_events_forward('stream')).to eq([event0, event1, event2, event3])
48
+ end
49
+
50
+ specify 'incorrect expected version on second write' do
51
+ repository.append_to_stream([
52
+ event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
53
+ event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
54
+ ], 'stream', :none)
55
+ expect do
56
+ repository.append_to_stream([
57
+ event2 = TestDomainEvent.new(event_id: SecureRandom.uuid),
58
+ event3 = TestDomainEvent.new(event_id: SecureRandom.uuid),
59
+ ], 'stream', 0)
60
+ end.to raise_error(RubyEventStore::WrongExpectedEventVersion)
61
+
62
+ expect(repository.read_all_streams_forward(:head, 4)).to eq([event0, event1])
63
+ expect(repository.read_stream_events_forward('stream')).to eq([event0, event1])
64
+ end
65
+
66
+ specify ':none on first and subsequent write' do
67
+ repository.append_to_stream([
68
+ eventA = TestDomainEvent.new(event_id: SecureRandom.uuid),
69
+ ], 'stream', :none)
70
+ expect do
71
+ repository.append_to_stream([
72
+ eventB = TestDomainEvent.new(event_id: SecureRandom.uuid),
73
+ ], 'stream', :none)
74
+ end.to raise_error(RubyEventStore::WrongExpectedEventVersion)
75
+ expect(repository.read_all_streams_forward(:head, 1)).to eq([eventA])
76
+ expect(repository.read_stream_events_forward('stream')).to eq([eventA])
77
+ end
78
+
79
+ specify ':any allows stream with best-effort order and no guarantee' do
80
+ repository.append_to_stream([
81
+ event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
82
+ event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
83
+ ], 'stream', :any)
84
+ repository.append_to_stream([
85
+ event2 = TestDomainEvent.new(event_id: SecureRandom.uuid),
86
+ event3 = TestDomainEvent.new(event_id: SecureRandom.uuid),
87
+ ], 'stream', :any)
88
+ expect(repository.read_all_streams_forward(:head, 4).to_set).to eq(Set.new([event0, event1, event2, event3]))
89
+ expect(repository.read_stream_events_forward('stream').to_set).to eq(Set.new([event0, event1, event2, event3]))
90
+ end
91
+
92
+ specify ':auto queries for last position in given stream' do
93
+ skip unless test_expected_version_auto
94
+ repository.append_to_stream([
95
+ eventA = TestDomainEvent.new(event_id: SecureRandom.uuid),
96
+ eventB = TestDomainEvent.new(event_id: SecureRandom.uuid),
97
+ eventC = TestDomainEvent.new(event_id: SecureRandom.uuid),
98
+ ], 'another', :auto)
99
+ repository.append_to_stream([
100
+ event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
101
+ event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
102
+ ], 'stream', :auto)
103
+ repository.append_to_stream([
104
+ event2 = TestDomainEvent.new(event_id: SecureRandom.uuid),
105
+ event3 = TestDomainEvent.new(event_id: SecureRandom.uuid),
106
+ ], 'stream', 1)
107
+ end
108
+
109
+ specify ':auto starts from 0' do
110
+ skip unless test_expected_version_auto
111
+ repository.append_to_stream([
112
+ event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
113
+ ], 'stream', :auto)
114
+ expect do
115
+ repository.append_to_stream([
116
+ event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
117
+ ], 'stream', -1)
118
+ end.to raise_error(RubyEventStore::WrongExpectedEventVersion)
119
+ end
120
+
121
+ specify ':auto queries for last position and follows in incremental way' do
122
+ skip unless test_expected_version_auto
123
+ # It is expected that there is higher level lock
124
+ # So this query is safe from race conditions
125
+ repository.append_to_stream([
126
+ event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
127
+ event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
128
+ ], 'stream', :auto)
129
+ repository.append_to_stream([
130
+ event2 = TestDomainEvent.new(event_id: SecureRandom.uuid),
131
+ event3 = TestDomainEvent.new(event_id: SecureRandom.uuid),
132
+ ], 'stream', :auto)
133
+ expect(repository.read_all_streams_forward(:head, 4)).to eq([
134
+ event0, event1,
135
+ event2, event3
136
+ ])
137
+ expect(repository.read_stream_events_forward('stream')).to eq([event0, event1, event2, event3])
138
+ end
139
+
140
+ specify ':auto is compatible with manual expectation' do
141
+ skip unless test_expected_version_auto
142
+ repository.append_to_stream([
143
+ event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
144
+ event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
145
+ ], 'stream', :auto)
146
+ repository.append_to_stream([
147
+ event2 = TestDomainEvent.new(event_id: SecureRandom.uuid),
148
+ event3 = TestDomainEvent.new(event_id: SecureRandom.uuid),
149
+ ], 'stream', 1)
150
+ expect(repository.read_all_streams_forward(:head, 4)).to eq([event0, event1, event2, event3])
151
+ expect(repository.read_stream_events_forward('stream')).to eq([event0, event1, event2, event3])
152
+ end
153
+
154
+ specify 'manual is compatible with auto expectation' do
155
+ skip unless test_expected_version_auto
156
+ repository.append_to_stream([
157
+ event0 = TestDomainEvent.new(event_id: SecureRandom.uuid),
158
+ event1 = TestDomainEvent.new(event_id: SecureRandom.uuid),
159
+ ], 'stream', :none)
160
+ repository.append_to_stream([
161
+ event2 = TestDomainEvent.new(event_id: SecureRandom.uuid),
162
+ event3 = TestDomainEvent.new(event_id: SecureRandom.uuid),
163
+ ], 'stream', :auto)
164
+ expect(repository.read_all_streams_forward(:head, 4)).to eq([event0, event1, event2, event3])
165
+ expect(repository.read_stream_events_forward('stream')).to eq([event0, event1, event2, event3])
166
+ end
167
+
168
+ specify 'unlimited concurrency for :any - everything should succeed' do
169
+ skip unless test_race_conditions_any
170
+ verify_conncurency_assumptions
171
+ begin
172
+ concurrency_level = 4
173
+
174
+ fail_occurred = false
175
+ wait_for_it = true
176
+
177
+ threads = concurrency_level.times.map do |i|
178
+ Thread.new do
179
+ true while wait_for_it
180
+ begin
181
+ 100.times do |j|
182
+ eid = "0000000#{i}-#{sprintf("%04d", j)}-0000-0000-000000000000"
183
+ repository.append_to_stream([
184
+ TestDomainEvent.new(event_id: eid),
185
+ ], 'stream', :any)
186
+ end
187
+ rescue RubyEventStore::WrongExpectedEventVersion
188
+ fail_occurred = true
189
+ end
190
+ end
191
+ end
192
+ wait_for_it = false
193
+ threads.each(&:join)
194
+ expect(fail_occurred).to eq(false)
195
+ expect(repository.read_stream_events_forward('stream').size).to eq(400)
196
+ events_in_stream = repository.read_stream_events_forward('stream')
197
+ expect(events_in_stream.size).to eq(400)
198
+ events0 = events_in_stream.select do |ev|
199
+ ev.event_id.start_with?("0-")
200
+ end
201
+ expect(events0).to eq(events0.sort_by{|ev| ev.event_id })
202
+ ensure
203
+ cleanup_concurrency_test
204
+ end
205
+ end
206
+
207
+ specify 'limited concurrency for :auto - some operations will fail without outside lock, stream is ordered' do
208
+ skip unless test_expected_version_auto
209
+ skip unless test_race_conditions_auto
210
+ verify_conncurency_assumptions
211
+ begin
212
+ concurrency_level = 4
213
+
214
+ fail_occurred = 0
215
+ wait_for_it = true
216
+
217
+ threads = concurrency_level.times.map do |i|
218
+ Thread.new do
219
+ true while wait_for_it
220
+ 100.times do |j|
221
+ begin
222
+ eid = "0000000#{i}-#{sprintf("%04d", j)}-0000-0000-000000000000"
223
+ repository.append_to_stream([
224
+ TestDomainEvent.new(event_id: eid),
225
+ ], 'stream', :auto)
226
+ sleep(rand(concurrency_level) / 1000.0)
227
+ rescue RubyEventStore::WrongExpectedEventVersion
228
+ fail_occurred +=1
229
+ end
230
+ end
231
+ end
232
+ end
233
+ wait_for_it = false
234
+ threads.each(&:join)
235
+ expect(fail_occurred).to be > 0
236
+ events_in_stream = repository.read_stream_events_forward('stream')
237
+ expect(events_in_stream.size).to be < 400
238
+ expect(events_in_stream.size).to be >= 100
239
+ events0 = events_in_stream.select do |ev|
240
+ ev.event_id.start_with?("0-")
241
+ end
242
+ expect(events0).to eq(events0.sort_by{|ev| ev.event_id })
243
+ additional_limited_concurrency_for_auto_check
244
+ ensure
245
+ cleanup_concurrency_test
246
+ end
247
+ end
248
+
249
+ it 'appended event is stored in given stream' do
15
250
  expected_event = TestDomainEvent.new(data: {})
16
- created = repository.create(expected_event, 'stream')
17
- expect(created).to eq(expected_event)
251
+ repository.append_to_stream(expected_event, 'stream', :any)
18
252
  expect(repository.read_all_streams_forward(:head, 1).first).to eq(expected_event)
19
253
  expect(repository.read_stream_events_forward('stream').first).to eq(expected_event)
20
254
  expect(repository.read_stream_events_forward('other_stream')).to be_empty
@@ -22,92 +256,180 @@ RSpec.shared_examples :event_repository do |repository_class|
22
256
 
23
257
  it 'data attributes are retrieved' do
24
258
  event = TestDomainEvent.new(data: { order_id: 3 })
25
- repository.create(event, 'stream')
259
+ repository.append_to_stream(event, 'stream', :any)
26
260
  retrieved_event = repository.read_all_streams_forward(:head, 1).first
27
261
  expect(retrieved_event.data[:order_id]).to eq(3)
28
262
  end
29
263
 
30
264
  it 'metadata attributes are retrieved' do
31
265
  event = TestDomainEvent.new(metadata: { request_id: 3 })
32
- repository.create(event, 'stream')
266
+ repository.append_to_stream(event, 'stream', :any)
33
267
  retrieved_event = repository.read_all_streams_forward(:head, 1).first
34
268
  expect(retrieved_event.metadata[:request_id]).to eq(3)
35
269
  end
36
270
 
37
271
  it 'does not have deleted streams' do
38
- repository.create(TestDomainEvent.new, 'stream')
39
- repository.create(TestDomainEvent.new, 'other_stream')
40
-
41
- expect(repository.read_stream_events_forward('stream').count).to eq 1
42
- expect(repository.read_stream_events_forward('other_stream').count).to eq 1
43
- expect(repository.read_all_streams_forward(:head, 10).count).to eq 2
272
+ repository.append_to_stream(e1 = TestDomainEvent.new, 'stream', -1)
273
+ repository.append_to_stream(e2 = TestDomainEvent.new, 'other_stream', -1)
44
274
 
45
275
  repository.delete_stream('stream')
46
276
  expect(repository.read_stream_events_forward('stream')).to be_empty
47
- expect(repository.read_stream_events_forward('other_stream').count).to eq 1
48
- expect(repository.read_all_streams_forward(:head, 10).count).to eq 1
277
+ expect(repository.read_stream_events_forward('other_stream')).to eq([e2])
278
+ expect(repository.read_all_streams_forward(:head, 10)).to eq([e1,e2])
49
279
  end
50
280
 
51
281
  it 'has or has not domain event' do
52
- repository.create(TestDomainEvent.new(event_id: 'just an id'), 'stream')
282
+ just_an_id = 'd5c134c2-db65-4e87-b6ea-d196f8f1a292'
283
+ repository.append_to_stream(TestDomainEvent.new(event_id: just_an_id), 'stream', -1)
53
284
 
54
- expect(repository.has_event?('just an id')).to be_truthy
285
+ expect(repository.has_event?(just_an_id)).to be_truthy
286
+ expect(repository.has_event?(just_an_id.clone)).to be_truthy
55
287
  expect(repository.has_event?('any other id')).to be_falsey
56
288
  end
57
289
 
58
290
  it 'knows last event in stream' do
59
- repository.create(TestDomainEvent.new(event_id: 'event 1'), 'stream')
60
- repository.create(TestDomainEvent.new(event_id: 'event 2'), 'stream')
291
+ repository.append_to_stream(TestDomainEvent.new(event_id: '00000000-0000-0000-0000-000000000001'), 'stream', -1)
292
+ repository.append_to_stream(TestDomainEvent.new(event_id: '00000000-0000-0000-0000-000000000002'), 'stream', 0)
61
293
 
62
- expect(repository.last_stream_event('stream')).to eq(TestDomainEvent.new(event_id: 'event 2'))
294
+ expect(repository.last_stream_event('stream')).to eq(TestDomainEvent.new(event_id: '00000000-0000-0000-0000-000000000002'))
63
295
  expect(repository.last_stream_event('other_stream')).to be_nil
64
296
  end
65
297
 
66
298
  it 'reads batch of events from stream forward & backward' do
67
- event_ids = (1..10).to_a.map(&:to_s)
68
- repository.create(TestDomainEvent.new(event_id: '21'), 'other_stream')
69
- event_ids.each do |id|
70
- repository.create(TestDomainEvent.new(event_id: id), 'stream')
299
+ event_ids = ["96c920b1-cdd0-40f4-907c-861b9fff7d02", "56404f79-0ba0-4aa0-8524-dc3436368ca0", "6a54dd21-f9d8-4857-a195-f5588d9e406c", "0e50a9cd-f981-4e39-93d5-697fc7285b98", "d85589bc-b993-41d4-812f-fc631d9185d5", "96bdacda-77dd-4d7d-973d-cbdaa5842855", "94688199-e6b7-4180-bf8e-825b6808e6cc", "68fab040-741e-4bc2-9cca-5b8855b0ca19", "ab60114c-011d-4d58-ab31-7ba65d99975e", "868cac42-3d19-4b39-84e8-cd32d65c2445"]
300
+ events = event_ids.map{|id| TestDomainEvent.new(event_id: id) }
301
+ repository.append_to_stream(TestDomainEvent.new, 'other_stream', -1)
302
+ events.each.with_index do |event, index|
303
+ repository.append_to_stream(event, 'stream', index - 1)
71
304
  end
72
- repository.create(TestDomainEvent.new(event_id: '22'), 'other_stream')
305
+ repository.append_to_stream(TestDomainEvent.new, 'other_stream', 0)
73
306
 
74
- expect(repository.read_events_forward('stream', :head, 3)).to eq ['1','2','3'].map{|x| TestDomainEvent.new(event_id: x)}
75
- expect(repository.read_events_forward('stream', :head, 100)).to eq event_ids.map{|x| TestDomainEvent.new(event_id: x)}
76
- expect(repository.read_events_forward('stream', '5', 4)).to eq ['6','7','8','9'].map{|x| TestDomainEvent.new(event_id: x)}
77
- expect(repository.read_events_forward('stream', '5', 100)).to eq ['6','7','8','9','10'].map{|x| TestDomainEvent.new(event_id: x)}
307
+ expect(repository.read_events_forward('stream', :head, 3)).to eq(events.first(3))
308
+ expect(repository.read_events_forward('stream', :head, 100)).to eq(events)
309
+ expect(repository.read_events_forward('stream', events[4].event_id, 4)).to eq(events[5..8])
310
+ expect(repository.read_events_forward('stream', events[4].event_id, 100)).to eq(events[5..9])
78
311
 
79
- expect(repository.read_events_backward('stream', :head, 3)).to eq ['10','9','8'].map{|x| TestDomainEvent.new(event_id: x)}
80
- expect(repository.read_events_backward('stream', :head, 100)).to eq event_ids.reverse.map{|x| TestDomainEvent.new(event_id: x)}
81
- expect(repository.read_events_backward('stream', '5', 4)).to eq ['4','3','2','1'].map{|x| TestDomainEvent.new(event_id: x)}
82
- expect(repository.read_events_backward('stream', '5', 100)).to eq ['4','3','2','1'].map{|x| TestDomainEvent.new(event_id: x)}
312
+ expect(repository.read_events_backward('stream', :head, 3)).to eq(events.last(3).reverse)
313
+ expect(repository.read_events_backward('stream', :head, 100)).to eq(events.reverse)
314
+ expect(repository.read_events_backward('stream', events[4].event_id, 4)).to eq(events.first(4).reverse)
315
+ expect(repository.read_events_backward('stream', events[4].event_id, 100)).to eq(events.first(4).reverse)
83
316
  end
84
317
 
85
318
 
86
319
  it 'reads all stream events forward & backward' do
87
- repository.create(TestDomainEvent.new(event_id: '1'), 'stream')
88
- repository.create(TestDomainEvent.new(event_id: '2'), 'other_stream')
89
- repository.create(TestDomainEvent.new(event_id: '3'), 'stream')
90
- repository.create(TestDomainEvent.new(event_id: '4'), 'other_stream')
91
- repository.create(TestDomainEvent.new(event_id: '5'), 'other_stream')
320
+ s1 = 'stream'
321
+ s2 = 'other_stream'
322
+ repository.append_to_stream(a = TestDomainEvent.new(event_id: '7010d298-ab69-4bb1-9251-f3466b5d1282'), s1, -1)
323
+ repository.append_to_stream(b = TestDomainEvent.new(event_id: '34f88aca-aaba-4ca0-9256-8017b47528c5'), s2, -1)
324
+ repository.append_to_stream(c = TestDomainEvent.new(event_id: '8e61c864-ceae-4684-8726-97c34eb8fc4f'), s1, 0)
325
+ repository.append_to_stream(d = TestDomainEvent.new(event_id: '30963ed9-6349-450b-ac9b-8ea50115b3bd'), s2, 0)
326
+ repository.append_to_stream(e = TestDomainEvent.new(event_id: '5bdc58b7-e8a7-4621-afd6-ccb828d72457'), s2, 1)
92
327
 
93
- expect(repository.read_stream_events_forward('stream')).to eq ['1','3'].map{|x| TestDomainEvent.new(event_id: x)}
94
- expect(repository.read_stream_events_backward('stream')).to eq ['3','1'].map{|x| TestDomainEvent.new(event_id: x)}
328
+ expect(repository.read_stream_events_forward(s1)).to eq [a,c]
329
+ expect(repository.read_stream_events_backward(s1)).to eq [c,a]
95
330
  end
96
331
 
97
332
  it 'reads batch of events from all streams forward & backward' do
98
- event_ids = (1..10).to_a.map(&:to_s)
99
- event_ids.each do |id|
100
- repository.create(TestDomainEvent.new(event_id: id), SecureRandom.uuid)
333
+ event_ids = ["96c920b1-cdd0-40f4-907c-861b9fff7d02", "56404f79-0ba0-4aa0-8524-dc3436368ca0", "6a54dd21-f9d8-4857-a195-f5588d9e406c", "0e50a9cd-f981-4e39-93d5-697fc7285b98", "d85589bc-b993-41d4-812f-fc631d9185d5", "96bdacda-77dd-4d7d-973d-cbdaa5842855", "94688199-e6b7-4180-bf8e-825b6808e6cc", "68fab040-741e-4bc2-9cca-5b8855b0ca19", "ab60114c-011d-4d58-ab31-7ba65d99975e", "868cac42-3d19-4b39-84e8-cd32d65c2445"]
334
+ events = event_ids.map{|id| TestDomainEvent.new(event_id: id) }
335
+ events.each do |ev|
336
+ repository.append_to_stream(ev, SecureRandom.uuid, -1)
101
337
  end
102
338
 
103
- expect(repository.read_all_streams_forward(:head, 3)).to eq ['1','2','3'].map{|x| TestDomainEvent.new(event_id: x)}
104
- expect(repository.read_all_streams_forward(:head, 100)).to eq event_ids.map{|x| TestDomainEvent.new(event_id: x)}
105
- expect(repository.read_all_streams_forward('5', 4)).to eq ['6','7','8','9'].map{|x| TestDomainEvent.new(event_id: x)}
106
- expect(repository.read_all_streams_forward('5', 100)).to eq ['6','7','8','9','10'].map{|x| TestDomainEvent.new(event_id: x)}
339
+ expect(repository.read_all_streams_forward(:head, 3)).to eq(events.first(3))
340
+ expect(repository.read_all_streams_forward(:head, 100)).to eq(events)
341
+ expect(repository.read_all_streams_forward(events[4].event_id, 4)).to eq(events[5..8])
342
+ expect(repository.read_all_streams_forward(events[4].event_id, 100)).to eq(events[5..9])
343
+
344
+ expect(repository.read_all_streams_backward(:head, 3)).to eq(events.last(3).reverse)
345
+ expect(repository.read_all_streams_backward(:head, 100)).to eq(events.reverse)
346
+ expect(repository.read_all_streams_backward(events[4].event_id, 4)).to eq(events.first(4).reverse)
347
+ expect(repository.read_all_streams_backward(events[4].event_id, 100)).to eq(events.first(4).reverse)
348
+ end
349
+
350
+ it 'reads events different uuid object but same content' do
351
+ event_ids = [
352
+ "96c920b1-cdd0-40f4-907c-861b9fff7d02",
353
+ "56404f79-0ba0-4aa0-8524-dc3436368ca0"
354
+ ]
355
+ events = event_ids.map{|id| TestDomainEvent.new(event_id: id) }
356
+ repository.append_to_stream(events.first, 'stream', -1)
357
+ repository.append_to_stream(events.last, 'stream', 0)
358
+
359
+ expect(repository.read_all_streams_forward("96c920b1-cdd0-40f4-907c-861b9fff7d02", 1)).to eq([events.last])
360
+ expect(repository.read_all_streams_backward("56404f79-0ba0-4aa0-8524-dc3436368ca0", 1)).to eq([events.first])
361
+
362
+ expect(repository.read_events_forward('stream', "96c920b1-cdd0-40f4-907c-861b9fff7d02", 1)).to eq([events.last])
363
+ expect(repository.read_events_backward('stream', "56404f79-0ba0-4aa0-8524-dc3436368ca0", 1)).to eq([events.first])
364
+ end
365
+
366
+ it 'does not allow same event twice in a stream' do
367
+ repository.append_to_stream(
368
+ TestDomainEvent.new(event_id: "a1b49edb-7636-416f-874a-88f94b859bef"),
369
+ 'stream',
370
+ -1
371
+ )
372
+ expect do
373
+ repository.append_to_stream(
374
+ TestDomainEvent.new(event_id: "a1b49edb-7636-416f-874a-88f94b859bef"),
375
+ 'stream',
376
+ 0
377
+ )
378
+ end.to raise_error(RubyEventStore::EventDuplicatedInStream)
379
+ end
380
+
381
+ it 'allows appending to GLOBAL_STREAM explicitly' do
382
+ event = TestDomainEvent.new(event_id: "df8b2ba3-4e2c-4888-8d14-4364855fa80e")
383
+ repository.append_to_stream(event, "all", :any)
384
+
385
+ expect(repository.read_all_streams_forward(:head, 10)).to eq([event])
386
+ end
387
+
388
+ specify 'GLOBAL_STREAM is unordered, one cannot expect specific version number to work' do
389
+ expect {
390
+ event = TestDomainEvent.new(event_id: "df8b2ba3-4e2c-4888-8d14-4364855fa80e")
391
+ repository.append_to_stream(event, "all", 42)
392
+ }.to raise_error(RubyEventStore::InvalidExpectedVersion)
393
+ end
394
+
395
+ specify 'GLOBAL_STREAM is unordered, one cannot expect :none to work' do
396
+ expect {
397
+ event = TestDomainEvent.new(event_id: "df8b2ba3-4e2c-4888-8d14-4364855fa80e")
398
+ repository.append_to_stream(event, "all", :none)
399
+ }.to raise_error(RubyEventStore::InvalidExpectedVersion)
400
+ end
401
+
402
+ specify 'GLOBAL_STREAM is unordered, one cannot expect :auto to work' do
403
+ expect {
404
+ event = TestDomainEvent.new(event_id: "df8b2ba3-4e2c-4888-8d14-4364855fa80e")
405
+ repository.append_to_stream(event, "all", :auto)
406
+ }.to raise_error(RubyEventStore::InvalidExpectedVersion)
407
+ end
408
+
409
+ specify "only :none, :any, :auto and Integer allowed as expected_version" do
410
+ [Object.new, SecureRandom.uuid, :foo].each do |invalid_expected_version|
411
+ expect {
412
+ repository.append_to_stream(
413
+ TestDomainEvent.new(event_id: SecureRandom.uuid),
414
+ 'some_stream',
415
+ invalid_expected_version
416
+ )
417
+ }.to raise_error(RubyEventStore::InvalidExpectedVersion)
418
+ end
419
+ end
420
+
421
+ specify "events not persisted if append failed" do
422
+ repository.append_to_stream([
423
+ TestDomainEvent.new(event_id: SecureRandom.uuid),
424
+ ], 'stream', :none)
107
425
 
108
- expect(repository.read_all_streams_backward(:head, 3)).to eq ['10','9','8'].map{|x| TestDomainEvent.new(event_id: x)}
109
- expect(repository.read_all_streams_backward(:head, 100)).to eq event_ids.reverse.map{|x| TestDomainEvent.new(event_id: x)}
110
- expect(repository.read_all_streams_backward('5', 4)).to eq ['4','3','2','1'].map{|x| TestDomainEvent.new(event_id: x)}
111
- expect(repository.read_all_streams_backward('5', 100)).to eq ['4','3','2','1'].map{|x| TestDomainEvent.new(event_id: x)}
426
+ expect do
427
+ repository.append_to_stream([
428
+ TestDomainEvent.new(
429
+ event_id: '9bedf448-e4d0-41a3-a8cd-f94aec7aa763'
430
+ ),
431
+ ], 'stream', :none)
432
+ end.to raise_error(RubyEventStore::WrongExpectedEventVersion)
433
+ expect(repository.has_event?('9bedf448-e4d0-41a3-a8cd-f94aec7aa763')).to be_falsey
112
434
  end
113
435
  end
@@ -1,3 +1,3 @@
1
1
  module RubyEventStore
2
- VERSION = "0.18.2"
2
+ VERSION = "0.19.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_event_store
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.2
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arkency
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-10-18 00:00:00.000000000 Z
11
+ date: 2017-11-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler