ruby_event_store 0.30.0 → 0.31.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Makefile +6 -1
- data/lib/ruby_event_store.rb +5 -0
- data/lib/ruby_event_store/async_dispatcher.rb +20 -0
- data/lib/ruby_event_store/async_proxy_strategy.rb +9 -0
- data/lib/ruby_event_store/client.rb +204 -45
- data/lib/ruby_event_store/correlated_commands.rb +37 -0
- data/lib/ruby_event_store/event.rb +68 -1
- data/lib/ruby_event_store/link_by_metadata.rb +55 -0
- data/lib/ruby_event_store/mappers/null_mapper.rb +0 -1
- data/lib/ruby_event_store/mappers/protobuf.rb +13 -5
- data/lib/ruby_event_store/pub_sub/broker.rb +22 -50
- data/lib/ruby_event_store/pub_sub/dispatcher.rb +1 -1
- data/lib/ruby_event_store/pub_sub/subscriptions.rb +110 -0
- data/lib/ruby_event_store/spec/broker_lint.rb +76 -0
- data/lib/ruby_event_store/spec/dispatcher_lint.rb +8 -9
- data/lib/ruby_event_store/spec/event_repository_lint.rb +1 -0
- data/lib/ruby_event_store/spec/subscriptions_lint.rb +111 -0
- data/lib/ruby_event_store/specification.rb +60 -0
- data/lib/ruby_event_store/version.rb +1 -1
- metadata +9 -3
- data/lib/ruby_event_store/spec/event_broker_lint.rb +0 -211
@@ -0,0 +1,37 @@
|
|
1
|
+
module RubyEventStore
|
2
|
+
class CorrelatedCommands
|
3
|
+
|
4
|
+
def initialize(event_store, command_bus)
|
5
|
+
@event_store = event_store
|
6
|
+
@command_bus = command_bus
|
7
|
+
end
|
8
|
+
|
9
|
+
class MiniEvent < Struct.new(:correlation_id, :message_id)
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(command)
|
13
|
+
if (correlation_id = event_store.metadata[:correlation_id]) && (causation_id = event_store.metadata[:causation_id])
|
14
|
+
command.correlate_with(MiniEvent.new(
|
15
|
+
correlation_id,
|
16
|
+
causation_id,
|
17
|
+
)) if command.respond_to?(:correlate_with)
|
18
|
+
event_store.with_metadata(
|
19
|
+
causation_id: command.message_id,
|
20
|
+
) do
|
21
|
+
command_bus.call(command)
|
22
|
+
end
|
23
|
+
else
|
24
|
+
event_store.with_metadata(
|
25
|
+
correlation_id: command.message_id,
|
26
|
+
causation_id: command.message_id,
|
27
|
+
) do
|
28
|
+
command_bus.call(command)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
attr_reader :event_store, :command_bus
|
36
|
+
end
|
37
|
+
end
|
@@ -1,22 +1,42 @@
|
|
1
1
|
require 'securerandom'
|
2
2
|
|
3
3
|
module RubyEventStore
|
4
|
+
|
5
|
+
# Data structure representing an event
|
4
6
|
class Event
|
7
|
+
# Instantiates a new event
|
8
|
+
#
|
9
|
+
# @param event_id [String] event id
|
10
|
+
# @param data [Hash] event data which belong to your application domain
|
11
|
+
# @param metadata [Hash] event metadata which are technical and not
|
12
|
+
# part of your domain such as remote_ip, request_id, correlation_id,
|
13
|
+
# causation_id etc.
|
14
|
+
# @return [Event]
|
5
15
|
def initialize(event_id: SecureRandom.uuid, metadata: nil, data: nil)
|
6
16
|
@event_id = event_id.to_s
|
7
17
|
@metadata = Metadata.new(metadata.to_h)
|
8
18
|
@data = data.to_h
|
9
19
|
end
|
20
|
+
|
10
21
|
attr_reader :event_id, :metadata, :data
|
11
22
|
|
23
|
+
# Event id
|
24
|
+
# @return [String]
|
12
25
|
def message_id
|
13
26
|
event_id
|
14
27
|
end
|
15
28
|
|
29
|
+
# Type of event. Used when matching with subscribed handlers.
|
30
|
+
# @return [String]
|
16
31
|
def type
|
17
32
|
self.class.name
|
18
33
|
end
|
19
34
|
|
35
|
+
# Returns a hash representation of the event.
|
36
|
+
#
|
37
|
+
# Metadata is converted to hash as well
|
38
|
+
#
|
39
|
+
# @return [Hash] with :event_id, :metadata, :data, :type keys
|
20
40
|
def to_h
|
21
41
|
{
|
22
42
|
event_id: event_id,
|
@@ -26,20 +46,43 @@ module RubyEventStore
|
|
26
46
|
}
|
27
47
|
end
|
28
48
|
|
49
|
+
# Timestamp from metadata
|
50
|
+
#
|
51
|
+
# @return [Time, nil]
|
29
52
|
def timestamp
|
30
53
|
metadata[:timestamp]
|
31
54
|
end
|
32
55
|
|
56
|
+
# Two events are equal if:
|
57
|
+
# * they are of the same class
|
58
|
+
# * have identical event id
|
59
|
+
# * have identical data (verified with eql? method)
|
60
|
+
#
|
61
|
+
# @param other_event [Event, Object] object to compare
|
62
|
+
#
|
63
|
+
# Event equality ignores metadata!
|
64
|
+
# @return [TrueClass, FalseClass]
|
33
65
|
def ==(other_event)
|
34
66
|
other_event.instance_of?(self.class) &&
|
35
67
|
other_event.event_id.eql?(event_id) &&
|
36
68
|
other_event.data.eql?(data)
|
37
69
|
end
|
38
70
|
|
71
|
+
# @private
|
39
72
|
BIG_VALUE = 0b111111100100000010010010110011101011000101010101001100100110000
|
40
73
|
|
41
|
-
#
|
74
|
+
# Generates a Fixnum hash value for this object. This function
|
75
|
+
# have the property that a.eql?(b) implies a.hash == b.hash.
|
76
|
+
#
|
77
|
+
# The hash value is used along with eql? by the Hash class to
|
78
|
+
# determine if two objects reference the same hash key.
|
79
|
+
#
|
80
|
+
# This hash is based on
|
81
|
+
# * class
|
82
|
+
# * event_id
|
83
|
+
# * data
|
42
84
|
def hash
|
85
|
+
# We don't use metadata because == does not use metadata
|
43
86
|
[
|
44
87
|
self.class,
|
45
88
|
event_id,
|
@@ -47,22 +90,46 @@ module RubyEventStore
|
|
47
90
|
].hash ^ BIG_VALUE
|
48
91
|
end
|
49
92
|
|
93
|
+
# Reads correlation_id from metadata.
|
94
|
+
# {http://railseventstore.org/docs/correlation_causation/ Find out more}
|
95
|
+
#
|
96
|
+
# @return [String, nil]
|
50
97
|
def correlation_id
|
51
98
|
metadata[:correlation_id]
|
52
99
|
end
|
53
100
|
|
101
|
+
# Sets correlation_id in metadata.
|
102
|
+
# {http://railseventstore.org/docs/correlation_causation/ Find out more}
|
103
|
+
#
|
104
|
+
# @param val [String]
|
105
|
+
# @return [String]
|
54
106
|
def correlation_id=(val)
|
55
107
|
metadata[:correlation_id] = val
|
56
108
|
end
|
57
109
|
|
110
|
+
# Reads causation_id from metadata.
|
111
|
+
# {http://railseventstore.org/docs/correlation_causation/ Find out more}
|
112
|
+
#
|
113
|
+
# @return [String, nil]
|
58
114
|
def causation_id
|
59
115
|
metadata[:causation_id]
|
60
116
|
end
|
61
117
|
|
118
|
+
# Sets causation_id= in metadata.
|
119
|
+
# {http://railseventstore.org/docs/correlation_causation/ Find out more}
|
120
|
+
#
|
121
|
+
# @param val [String]
|
122
|
+
# @return [String]
|
62
123
|
def causation_id=(val)
|
63
124
|
metadata[:causation_id]= val
|
64
125
|
end
|
65
126
|
|
127
|
+
# Sets correlation_id and causation_id in metadata based
|
128
|
+
# on correlation_id and message_id of the provided message.
|
129
|
+
# {http://railseventstore.org/docs/correlation_causation/ Find out more}
|
130
|
+
#
|
131
|
+
# @param other_message [Event, Proto, command] message to correlate with. Most likely an event or a command. Must respond to correlation_id and message_id.
|
132
|
+
# @return [String] set causation_id
|
66
133
|
def correlate_with(other_message)
|
67
134
|
self.correlation_id = other_message.correlation_id || other_message.message_id
|
68
135
|
self.causation_id = other_message.message_id
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module RubyEventStore
|
2
|
+
class LinkByMetadata
|
3
|
+
|
4
|
+
def initialize(event_store:, key:, prefix: nil)
|
5
|
+
@event_store = event_store
|
6
|
+
@key = key
|
7
|
+
@prefix = prefix || ["$by", key, nil].join("_")
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(event)
|
11
|
+
return unless event.metadata.has_key?(@key)
|
12
|
+
|
13
|
+
@event_store.link_to_stream(
|
14
|
+
[event.message_id],
|
15
|
+
stream_name: "#{@prefix}#{event.metadata.fetch(@key)}"
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
class LinkByCorrelationId < LinkByMetadata
|
22
|
+
def initialize(event_store:, prefix: nil)
|
23
|
+
super(
|
24
|
+
event_store: event_store,
|
25
|
+
prefix: prefix,
|
26
|
+
key: :correlation_id,
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class LinkByCausationId < LinkByMetadata
|
32
|
+
def initialize(event_store:, prefix: nil)
|
33
|
+
super(
|
34
|
+
event_store: event_store,
|
35
|
+
prefix: prefix,
|
36
|
+
key: :causation_id,
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class LinkByEventType
|
42
|
+
def initialize(event_store:, prefix: nil)
|
43
|
+
@event_store = event_store
|
44
|
+
@prefix = prefix || "$by_type_"
|
45
|
+
end
|
46
|
+
|
47
|
+
def call(event)
|
48
|
+
@event_store.link_to_stream(
|
49
|
+
[event.message_id],
|
50
|
+
stream_name: "#{@prefix}#{event.type}"
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -55,19 +55,27 @@ module RubyEventStore
|
|
55
55
|
|
56
56
|
def serialized_record_to_event(record)
|
57
57
|
event_type = events_class_remapping.fetch(record.event_type) { record.event_type }
|
58
|
-
data = Google::Protobuf::DescriptorPool.generated_pool.lookup(event_type).msgclass.decode(record.data)
|
59
58
|
Proto.new(
|
60
59
|
event_id: record.event_id,
|
61
|
-
data: data,
|
62
|
-
|
63
|
-
|
64
|
-
end
|
60
|
+
data: load_data(event_type, record.data),
|
61
|
+
metadata: load_metadata(record.metadata)
|
62
|
+
)
|
65
63
|
end
|
66
64
|
|
67
65
|
private
|
68
66
|
|
69
67
|
attr_reader :event_id_getter, :events_class_remapping
|
70
68
|
|
69
|
+
def load_metadata(protobuf_metadata)
|
70
|
+
ProtobufNestedStruct::HashMapStringValue.load(protobuf_metadata).each_with_object({}) do |(k, v), meta|
|
71
|
+
meta[k.to_sym] = v
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def load_data(event_type, protobuf_data)
|
76
|
+
Google::Protobuf::DescriptorPool.generated_pool.lookup(event_type).msgclass.decode(protobuf_data)
|
77
|
+
end
|
78
|
+
|
71
79
|
def require_optional_dependency
|
72
80
|
require 'protobuf_nested_struct'
|
73
81
|
rescue LoadError
|
@@ -1,73 +1,45 @@
|
|
1
|
-
require 'concurrent'
|
2
|
-
|
3
1
|
module RubyEventStore
|
4
2
|
module PubSub
|
5
3
|
class Broker
|
6
|
-
|
7
|
-
|
8
|
-
def initialize(dispatcher: DEFAULT_DISPATCHER)
|
9
|
-
@subscribers = Hash.new {|hsh, key| hsh[key] = [] }
|
10
|
-
@global_subscribers = []
|
11
|
-
|
12
|
-
@thread_global_subscribers = Concurrent::ThreadLocalVar.new([])
|
13
|
-
@thread_subscribers = Concurrent::ThreadLocalVar.new do
|
14
|
-
Hash.new {|hsh, key| hsh[key] = [] }
|
15
|
-
end
|
16
|
-
|
4
|
+
def initialize(subscriptions:, dispatcher:)
|
5
|
+
@subscriptions = subscriptions
|
17
6
|
@dispatcher = dispatcher
|
18
7
|
end
|
19
8
|
|
20
|
-
def
|
21
|
-
|
22
|
-
|
9
|
+
def call(event, serialized_event)
|
10
|
+
subscribers = subscriptions.all_for(event.type)
|
11
|
+
subscribers.each do |subscriber|
|
12
|
+
dispatcher.call(subscriber, event, serialized_event)
|
13
|
+
end
|
23
14
|
end
|
24
15
|
|
25
|
-
def
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
->() { global_subscribers.delete(subscriber) }
|
16
|
+
def add_subscription(subscriber, event_types)
|
17
|
+
verify_subscription(subscriber)
|
18
|
+
subscriptions.add_subscription(subscriber, event_types)
|
30
19
|
end
|
31
20
|
|
32
|
-
def
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
->() { thread_global_subscribers.value -= [subscriber] }
|
21
|
+
def add_global_subscription(subscriber)
|
22
|
+
verify_subscription(subscriber)
|
23
|
+
subscriptions.add_global_subscription(subscriber)
|
37
24
|
end
|
38
25
|
|
39
|
-
def
|
40
|
-
|
41
|
-
|
42
|
-
->() {event_types.each{ |type| thread_subscribers.value.fetch(type.to_s).delete(subscriber) } }
|
26
|
+
def add_thread_subscription(subscriber, event_types)
|
27
|
+
verify_subscription(subscriber)
|
28
|
+
subscriptions.add_thread_subscription(subscriber, event_types)
|
43
29
|
end
|
44
30
|
|
45
|
-
def
|
46
|
-
|
47
|
-
|
48
|
-
end
|
31
|
+
def add_thread_global_subscription(subscriber)
|
32
|
+
verify_subscription(subscriber)
|
33
|
+
subscriptions.add_thread_global_subscription(subscriber)
|
49
34
|
end
|
50
35
|
|
51
36
|
private
|
37
|
+
attr_reader :subscriptions, :dispatcher
|
52
38
|
|
53
|
-
def
|
54
|
-
raise SubscriberNotExist
|
39
|
+
def verify_subscription(subscriber)
|
40
|
+
raise SubscriberNotExist, "subscriber must be first argument or block" unless subscriber
|
55
41
|
dispatcher.verify(subscriber)
|
56
42
|
end
|
57
|
-
|
58
|
-
def subscribe(subscriber, event_types)
|
59
|
-
event_types.each{ |type| subscribers[type.to_s] << subscriber }
|
60
|
-
->() {event_types.each{ |type| subscribers.fetch(type.to_s).delete(subscriber) } }
|
61
|
-
end
|
62
|
-
|
63
|
-
def all_subscribers_for(event_type)
|
64
|
-
subscribers[event_type] +
|
65
|
-
global_subscribers +
|
66
|
-
thread_global_subscribers.value +
|
67
|
-
thread_subscribers.value[event_type]
|
68
|
-
end
|
69
|
-
|
70
|
-
attr_reader :dispatcher, :subscribers, :global_subscribers, :thread_global_subscribers, :thread_subscribers
|
71
43
|
end
|
72
44
|
end
|
73
45
|
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'concurrent'
|
2
|
+
|
3
|
+
module RubyEventStore
|
4
|
+
module PubSub
|
5
|
+
class Subscriptions
|
6
|
+
def initialize
|
7
|
+
@local = LocalSubscriptions.new
|
8
|
+
@global = GlobalSubscriptions.new
|
9
|
+
@thread = ThreadSubscriptions.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_subscription(subscriber, event_types)
|
13
|
+
local.add(subscriber, event_types)
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_global_subscription(subscriber)
|
17
|
+
global.add(subscriber)
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_thread_subscription(subscriber, event_types)
|
21
|
+
thread.local.add(subscriber, event_types)
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_thread_global_subscription(subscriber)
|
25
|
+
thread.global.add(subscriber)
|
26
|
+
end
|
27
|
+
|
28
|
+
def all_for(event_type)
|
29
|
+
[local, global, thread].map{|r| r.all_for(event_type)}.reduce(&:+)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
attr_reader :local, :global, :thread
|
34
|
+
|
35
|
+
class ThreadSubscriptions
|
36
|
+
def initialize
|
37
|
+
@local = ThreadLocalSubscriptions.new
|
38
|
+
@global = ThreadGlobalSubscriptions.new
|
39
|
+
end
|
40
|
+
attr_reader :local, :global
|
41
|
+
|
42
|
+
def all_for(event_type)
|
43
|
+
[global, local].map{|r| r.all_for(event_type)}.reduce(&:+)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class LocalSubscriptions
|
48
|
+
def initialize
|
49
|
+
@subscriptions = Hash.new {|hsh, key| hsh[key] = [] }
|
50
|
+
end
|
51
|
+
|
52
|
+
def add(subscription, event_types)
|
53
|
+
event_types.each{ |type| @subscriptions[type.to_s] << subscription }
|
54
|
+
->() {event_types.each{ |type| @subscriptions.fetch(type.to_s).delete(subscription) } }
|
55
|
+
end
|
56
|
+
|
57
|
+
def all_for(event_type)
|
58
|
+
@subscriptions[event_type]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class GlobalSubscriptions
|
63
|
+
def initialize
|
64
|
+
@subscriptions = []
|
65
|
+
end
|
66
|
+
|
67
|
+
def add(subscription)
|
68
|
+
@subscriptions << subscription
|
69
|
+
->() { @subscriptions.delete(subscription) }
|
70
|
+
end
|
71
|
+
|
72
|
+
def all_for(_event_type)
|
73
|
+
@subscriptions
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class ThreadLocalSubscriptions
|
78
|
+
def initialize
|
79
|
+
@subscriptions = Concurrent::ThreadLocalVar.new do
|
80
|
+
Hash.new {|hsh, key| hsh[key] = [] }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def add(subscription, event_types)
|
85
|
+
event_types.each{ |type| @subscriptions.value[type.to_s] << subscription }
|
86
|
+
->() {event_types.each{ |type| @subscriptions.value.fetch(type.to_s).delete(subscription) } }
|
87
|
+
end
|
88
|
+
|
89
|
+
def all_for(event_type)
|
90
|
+
@subscriptions.value[event_type]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class ThreadGlobalSubscriptions
|
95
|
+
def initialize
|
96
|
+
@subscriptions = Concurrent::ThreadLocalVar.new([])
|
97
|
+
end
|
98
|
+
|
99
|
+
def add(subscription)
|
100
|
+
@subscriptions.value += [subscription]
|
101
|
+
->() { @subscriptions.value -= [subscription] }
|
102
|
+
end
|
103
|
+
|
104
|
+
def all_for(_event_type)
|
105
|
+
@subscriptions.value
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|