tcb 0.5.0 → 0.6.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 +4 -4
- data/CHANGELOG.md +34 -1
- data/README.md +286 -129
- data/lib/generators/tcb/event_store/templates/migration.rb.tt +11 -7
- data/lib/generators/tcb/install/templates/tcb.rb.tt +12 -10
- data/lib/tcb/command_bus.rb +5 -1
- data/lib/tcb/configuration.rb +13 -7
- data/lib/tcb/correlation_query.rb +32 -0
- data/lib/tcb/envelope.rb +31 -0
- data/lib/tcb/event_bus/queue_pressure_monitor.rb +35 -0
- data/lib/tcb/event_bus/running_strategy.rb +32 -2
- data/lib/tcb/event_bus/shutdown_strategy.rb +4 -0
- data/lib/tcb/event_bus.rb +40 -38
- data/lib/tcb/event_bus_queue_pressure.rb +10 -0
- data/lib/tcb/event_store/active_record.rb +69 -19
- data/lib/tcb/event_store/in_memory.rb +17 -7
- data/lib/tcb/publish.rb +7 -4
- data/lib/tcb/record.rb +47 -11
- data/lib/tcb/test_helpers/shared.rb +2 -2
- data/lib/tcb/version.rb +1 -1
- data/lib/tcb.rb +44 -11
- metadata +6 -3
- data/lib/tcb/event_store/event_stream_envelope.rb +0 -13
|
@@ -1,18 +1,20 @@
|
|
|
1
|
+
# Domain modules — which bounded contexts exist
|
|
2
|
+
# Add your domain modules here after generating them:
|
|
3
|
+
# rails generate tcb:event_store orders place_order:order_id,customer
|
|
4
|
+
# rails generate tcb:domain notifications send_welcome_email:user_id,email
|
|
5
|
+
TCB.domain_modules = [
|
|
6
|
+
# Orders,
|
|
7
|
+
# Notifications,
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
# Infrastructure — how events are transported and stored
|
|
11
|
+
# Runs on every Rails reload in development
|
|
1
12
|
Rails.application.config.to_prepare do
|
|
2
13
|
TCB.configure do |c|
|
|
3
14
|
c.event_bus = TCB::EventBus.new(
|
|
4
15
|
handle_signals: true,
|
|
5
16
|
shutdown_timeout: 10.0
|
|
6
17
|
)
|
|
7
|
-
c.event_store =
|
|
8
|
-
: TCB::EventStore::ActiveRecord.new
|
|
9
|
-
|
|
10
|
-
# Add your domain modules here after generating them:
|
|
11
|
-
# rails generate tcb:event_store orders place_order:order_id,customer
|
|
12
|
-
# rails generate tcb:domain notifications send_welcome_email:user_id,email
|
|
13
|
-
c.domain_modules = [
|
|
14
|
-
# Orders,
|
|
15
|
-
# Notifications
|
|
16
|
-
]
|
|
18
|
+
c.event_store = TCB::EventStore::ActiveRecord.new
|
|
17
19
|
end
|
|
18
20
|
end
|
data/lib/tcb/command_bus.rb
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
module TCB
|
|
3
3
|
CommandHandlerNotFound = Class.new(StandardError)
|
|
4
4
|
|
|
5
|
-
def self.dispatch(command)
|
|
5
|
+
def self.dispatch(command, correlation_id: SecureRandom.uuid)
|
|
6
6
|
validate!(command)
|
|
7
7
|
handler = resolve_handler(command)
|
|
8
|
+
Thread.current[:tcb_correlation_id] = correlation_id
|
|
8
9
|
handler.new.call(command)
|
|
10
|
+
correlation_id
|
|
11
|
+
ensure
|
|
12
|
+
Thread.current[:tcb_correlation_id] = nil
|
|
9
13
|
end
|
|
10
14
|
|
|
11
15
|
def self.validate!(command)
|
data/lib/tcb/configuration.rb
CHANGED
|
@@ -62,6 +62,10 @@ module TCB
|
|
|
62
62
|
]
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
def event_bus_configured?
|
|
66
|
+
!!@event_bus
|
|
67
|
+
end
|
|
68
|
+
|
|
65
69
|
private
|
|
66
70
|
|
|
67
71
|
def flush_domain_modules
|
|
@@ -70,8 +74,13 @@ module TCB
|
|
|
70
74
|
|
|
71
75
|
domain_module.event_handler_registrations.each do |registration|
|
|
72
76
|
registration.handlers.each do |handler|
|
|
73
|
-
event_bus.subscribe(registration.event_class) do |
|
|
74
|
-
|
|
77
|
+
event_bus.subscribe(registration.event_class) do |envelope|
|
|
78
|
+
Thread.current[:tcb_correlation_id] = envelope.correlation_id
|
|
79
|
+
Thread.current[:tcb_causation_id] = envelope.event_id
|
|
80
|
+
handler.new.call(envelope.event)
|
|
81
|
+
ensure
|
|
82
|
+
Thread.current[:tcb_correlation_id] = nil
|
|
83
|
+
Thread.current[:tcb_causation_id] = nil
|
|
75
84
|
end
|
|
76
85
|
end
|
|
77
86
|
end
|
|
@@ -93,12 +102,13 @@ module TCB
|
|
|
93
102
|
@persist_registrations = []
|
|
94
103
|
@domain_modules.each do |domain_module|
|
|
95
104
|
next unless domain_module.respond_to?(:persist_registrations)
|
|
105
|
+
next unless domain_module.persist_registrations.any?
|
|
96
106
|
|
|
97
107
|
context = DomainContext.from_module(domain_module).to_s
|
|
98
108
|
domain_module.persist_registrations.each do |registration|
|
|
99
109
|
@persist_registrations << registration.with(context: context)
|
|
100
110
|
end
|
|
101
|
-
define_event_record_for(domain_module)
|
|
111
|
+
define_event_record_for(domain_module)
|
|
102
112
|
end
|
|
103
113
|
end
|
|
104
114
|
|
|
@@ -111,8 +121,4 @@ module TCB
|
|
|
111
121
|
domain_module.const_set(:EventRecord, klass)
|
|
112
122
|
end
|
|
113
123
|
end
|
|
114
|
-
|
|
115
|
-
def self.config
|
|
116
|
-
@config ||= Configuration.new
|
|
117
|
-
end
|
|
118
124
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TCB
|
|
4
|
+
class CorrelationQuery
|
|
5
|
+
def initialize(store:, correlation_id:, domains:, occurred_after: nil, occurred_before: nil)
|
|
6
|
+
@store = store
|
|
7
|
+
@correlation_id = correlation_id
|
|
8
|
+
@domains = domains
|
|
9
|
+
@occurred_after = occurred_after
|
|
10
|
+
@occurred_before = occurred_before
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def occurred_after(time)
|
|
14
|
+
self.class.new(store: @store, correlation_id: @correlation_id, domains: @domains, occurred_after: time, occurred_before: @occurred_before)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def occurred_before(time)
|
|
18
|
+
self.class.new(store: @store, correlation_id: @correlation_id, domains: @domains, occurred_after: @occurred_after, occurred_before: time)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def between(from, to)
|
|
22
|
+
occurred_after(from).occurred_before(to)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_a
|
|
26
|
+
@domains.flat_map do |domain|
|
|
27
|
+
context = DomainContext.from_module(domain).to_s
|
|
28
|
+
@store.read_by_correlation(@correlation_id, context: context, occurred_after: @occurred_after, occurred_before: @occurred_before)
|
|
29
|
+
end.sort_by(&:occurred_at)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/tcb/envelope.rb
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module TCB
|
|
6
|
+
Envelope = Data.define(
|
|
7
|
+
:event,
|
|
8
|
+
:event_id,
|
|
9
|
+
:stream_id,
|
|
10
|
+
:version,
|
|
11
|
+
:occurred_at,
|
|
12
|
+
:correlation_id,
|
|
13
|
+
:causation_id
|
|
14
|
+
) do
|
|
15
|
+
def self.wrap(event, correlation_id: nil, causation_id: nil)
|
|
16
|
+
new(
|
|
17
|
+
event: event,
|
|
18
|
+
event_id: SecureRandom.uuid,
|
|
19
|
+
stream_id: nil,
|
|
20
|
+
version: nil,
|
|
21
|
+
occurred_at: Time.now,
|
|
22
|
+
correlation_id: correlation_id,
|
|
23
|
+
causation_id: causation_id
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.coerce(event_or_envelope)
|
|
28
|
+
event_or_envelope.is_a?(self) ? event_or_envelope : wrap(event_or_envelope)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TCB
|
|
4
|
+
class EventBus
|
|
5
|
+
class QueuePressureMonitor
|
|
6
|
+
def self.for(max_queue_size:, high_water_mark:)
|
|
7
|
+
return new(high_water_mark:) if max_queue_size && high_water_mark
|
|
8
|
+
|
|
9
|
+
NullQueuePressureMonitor.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(high_water_mark:)
|
|
13
|
+
@high_water_mark = high_water_mark
|
|
14
|
+
@emitted = false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def check?(queue_size)
|
|
18
|
+
if queue_size >= @high_water_mark
|
|
19
|
+
return false if @emitted
|
|
20
|
+
@emitted = true
|
|
21
|
+
true
|
|
22
|
+
else
|
|
23
|
+
@emitted = false
|
|
24
|
+
false
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class NullQueuePressureMonitor
|
|
30
|
+
def check?(_queue_size)
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -3,12 +3,31 @@
|
|
|
3
3
|
module TCB
|
|
4
4
|
class EventBus
|
|
5
5
|
class RunningStrategy
|
|
6
|
-
def initialize(event_bus)
|
|
6
|
+
def initialize(event_bus, sync: false)
|
|
7
7
|
@event_bus = event_bus
|
|
8
|
+
@sync = sync
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def start
|
|
12
|
+
return if @sync
|
|
13
|
+
|
|
14
|
+
@event_bus.dispatcher = Thread.new do
|
|
15
|
+
loop do
|
|
16
|
+
event = @event_bus.queue.pop
|
|
17
|
+
break if event == :shutdown_sentinel
|
|
18
|
+
|
|
19
|
+
@event_bus.dispatch(event)
|
|
20
|
+
@event_bus.dispatch(build_pressure_event) if @event_bus.high_water_mark_reached?
|
|
21
|
+
end
|
|
22
|
+
end
|
|
8
23
|
end
|
|
9
24
|
|
|
10
25
|
def publish(event)
|
|
11
|
-
@
|
|
26
|
+
if @sync
|
|
27
|
+
@event_bus.dispatch(event)
|
|
28
|
+
else
|
|
29
|
+
@event_bus.queue << event
|
|
30
|
+
end
|
|
12
31
|
event
|
|
13
32
|
end
|
|
14
33
|
|
|
@@ -19,6 +38,17 @@ module TCB
|
|
|
19
38
|
def shutdown?
|
|
20
39
|
false
|
|
21
40
|
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def build_pressure_event
|
|
45
|
+
EventBusQueuePressure.new(
|
|
46
|
+
queue_size: @event_bus.queue.size,
|
|
47
|
+
max_queue_size: @event_bus.max_queue_size,
|
|
48
|
+
occupancy: @event_bus.queue.size.to_f / @event_bus.max_queue_size,
|
|
49
|
+
occurred_at: Time.now
|
|
50
|
+
)
|
|
51
|
+
end
|
|
22
52
|
end
|
|
23
53
|
end
|
|
24
54
|
end
|
|
@@ -61,12 +61,16 @@ module TCB
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def force_terminate
|
|
64
|
+
return unless @event_bus.dispatcher
|
|
65
|
+
|
|
64
66
|
@event_bus.queue << :shutdown_sentinel
|
|
65
67
|
@event_bus.dispatcher.kill if @event_bus.dispatcher.alive?
|
|
66
68
|
@event_bus.dispatcher.join(0.1)
|
|
67
69
|
end
|
|
68
70
|
|
|
69
71
|
def terminate_dispatcher
|
|
72
|
+
return unless @event_bus.dispatcher
|
|
73
|
+
|
|
70
74
|
@event_bus.queue << :shutdown_sentinel
|
|
71
75
|
@event_bus.dispatcher.join(0.5)
|
|
72
76
|
end
|
data/lib/tcb/event_bus.rb
CHANGED
|
@@ -1,49 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
6
|
-
require_relative
|
|
7
|
-
require_relative
|
|
3
|
+
require "set"
|
|
4
|
+
require_relative "event_bus/running_strategy"
|
|
5
|
+
require_relative "event_bus/shutdown_strategy"
|
|
6
|
+
require_relative "event_bus/termination_signal_handler"
|
|
7
|
+
require_relative "event_bus/subscriber_registry"
|
|
8
8
|
|
|
9
9
|
module TCB
|
|
10
10
|
class EventBus
|
|
11
11
|
class ShutdownError < StandardError; end
|
|
12
12
|
|
|
13
|
-
attr_reader :queue, :registry, :mutex,
|
|
14
|
-
|
|
13
|
+
attr_reader :queue, :registry, :mutex, :active_dispatches, :events_processed_during_shutdown, :max_queue_size
|
|
14
|
+
attr_accessor :dispatcher
|
|
15
15
|
|
|
16
16
|
def initialize(
|
|
17
17
|
handle_signals: false,
|
|
18
18
|
shutdown_timeout: 30.0,
|
|
19
19
|
shutdown_signals: [:TERM, :INT],
|
|
20
|
-
on_signal: nil
|
|
20
|
+
on_signal: nil,
|
|
21
|
+
max_queue_size: nil,
|
|
22
|
+
high_water_mark: nil,
|
|
23
|
+
sync: false
|
|
21
24
|
)
|
|
22
|
-
@
|
|
25
|
+
@sync = sync
|
|
26
|
+
@queue = max_queue_size ? SizedQueue.new(max_queue_size) : Queue.new
|
|
27
|
+
@max_queue_size = max_queue_size
|
|
28
|
+
@pressure_monitor = QueuePressureMonitor.for(max_queue_size:, high_water_mark:)
|
|
23
29
|
@registry = SubscriberRegistry.new
|
|
24
30
|
@mutex = Mutex.new
|
|
25
31
|
@active_dispatches = 0
|
|
26
32
|
@events_processed_during_shutdown = 0
|
|
27
|
-
@execution_strategy = RunningStrategy.new(self)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
loop do
|
|
31
|
-
event = @queue.pop
|
|
32
|
-
break if event == :shutdown_sentinel
|
|
33
|
-
|
|
34
|
-
dispatch(event)
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
if handle_signals
|
|
39
|
-
@termination_signal_handler = TerminationSignalHandler.new(
|
|
40
|
-
event_bus: self,
|
|
41
|
-
shutdown_timeout: shutdown_timeout,
|
|
42
|
-
signals: shutdown_signals,
|
|
43
|
-
on_signal: on_signal
|
|
44
|
-
)
|
|
45
|
-
@termination_signal_handler.install
|
|
46
|
-
end
|
|
33
|
+
@execution_strategy = RunningStrategy.new(self, sync: @sync)
|
|
34
|
+
@execution_strategy.start
|
|
35
|
+
install_signal_handlers(shutdown_timeout:, shutdown_signals:, on_signal:) if handle_signals
|
|
47
36
|
end
|
|
48
37
|
|
|
49
38
|
# Subscribe to a specific event class
|
|
@@ -82,30 +71,33 @@ module TCB
|
|
|
82
71
|
end
|
|
83
72
|
|
|
84
73
|
# Public for strategy access
|
|
85
|
-
def dispatch(
|
|
74
|
+
def dispatch(event_or_envelope)
|
|
86
75
|
@mutex.synchronize { @active_dispatches += 1 }
|
|
76
|
+
envelope = TCB::Envelope.coerce(event_or_envelope)
|
|
87
77
|
|
|
88
78
|
@events_processed_during_shutdown += 1 if shutdown?
|
|
89
|
-
handlers = @registry.handlers_for(event.class)
|
|
79
|
+
handlers = @registry.handlers_for(envelope.event.class)
|
|
90
80
|
|
|
91
81
|
handlers.each do |handler|
|
|
92
|
-
execute_handler(handler,
|
|
82
|
+
execute_handler(handler, envelope)
|
|
93
83
|
end
|
|
94
84
|
ensure
|
|
95
85
|
@mutex.synchronize { @active_dispatches -= 1 }
|
|
96
86
|
end
|
|
97
87
|
|
|
88
|
+
def high_water_mark_reached? = @pressure_monitor.check?(@queue.size)
|
|
89
|
+
|
|
98
90
|
private
|
|
99
91
|
|
|
100
|
-
def execute_handler(handler,
|
|
101
|
-
handler.call(
|
|
92
|
+
def execute_handler(handler, envelope)
|
|
93
|
+
handler.call(envelope)
|
|
102
94
|
rescue => e
|
|
103
|
-
return if event.is_a?(SubscriberInvocationFailed)
|
|
95
|
+
return if envelope.event.is_a?(SubscriberInvocationFailed)
|
|
104
96
|
|
|
105
97
|
failure_event = SubscriberInvocationFailed.build(
|
|
106
|
-
handler:
|
|
107
|
-
original_event: event,
|
|
108
|
-
error:
|
|
98
|
+
handler: handler,
|
|
99
|
+
original_event: envelope.event,
|
|
100
|
+
error: e
|
|
109
101
|
)
|
|
110
102
|
|
|
111
103
|
if shutdown?
|
|
@@ -114,5 +106,15 @@ module TCB
|
|
|
114
106
|
publish(failure_event)
|
|
115
107
|
end
|
|
116
108
|
end
|
|
109
|
+
|
|
110
|
+
def install_signal_handlers(shutdown_timeout:, shutdown_signals:, on_signal:)
|
|
111
|
+
@termination_signal_handler = TerminationSignalHandler.new(
|
|
112
|
+
event_bus: self,
|
|
113
|
+
shutdown_timeout: shutdown_timeout,
|
|
114
|
+
signals: shutdown_signals,
|
|
115
|
+
on_signal: on_signal
|
|
116
|
+
)
|
|
117
|
+
@termination_signal_handler.install
|
|
118
|
+
end
|
|
117
119
|
end
|
|
118
120
|
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TCB
|
|
4
|
+
EventBusQueuePressure = Data.define(
|
|
5
|
+
:queue_size, # current number of elements in queue
|
|
6
|
+
:max_queue_size, # maximum queue capacity
|
|
7
|
+
:occupancy, # queue_size / max_queue_size (float)
|
|
8
|
+
:occurred_at # timestamp
|
|
9
|
+
)
|
|
10
|
+
end
|
|
@@ -9,7 +9,7 @@ module TCB
|
|
|
9
9
|
@mutex = Mutex.new
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
def append(stream_id:, events:, occurred_at: Time.now)
|
|
12
|
+
def append(stream_id:, events:, occurred_at: Time.now, correlation_id: nil, causation_id: nil)
|
|
13
13
|
@mutex.synchronize do
|
|
14
14
|
next_ver = next_version(stream_id)
|
|
15
15
|
|
|
@@ -17,20 +17,24 @@ module TCB
|
|
|
17
17
|
event_id = SecureRandom.uuid
|
|
18
18
|
|
|
19
19
|
event_record_for(stream_id).create!(
|
|
20
|
-
event_id:
|
|
21
|
-
stream_id:
|
|
22
|
-
version:
|
|
23
|
-
event_type:
|
|
24
|
-
payload:
|
|
25
|
-
occurred_at:
|
|
20
|
+
event_id: event_id,
|
|
21
|
+
stream_id: stream_id,
|
|
22
|
+
version: version,
|
|
23
|
+
event_type: event.class.name,
|
|
24
|
+
payload: serialize(event),
|
|
25
|
+
occurred_at: occurred_at,
|
|
26
|
+
correlation_id: correlation_id,
|
|
27
|
+
causation_id: causation_id
|
|
26
28
|
)
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
event:
|
|
30
|
-
event_id:
|
|
31
|
-
stream_id:
|
|
32
|
-
version:
|
|
33
|
-
occurred_at:
|
|
30
|
+
TCB::Envelope.new(
|
|
31
|
+
event: event,
|
|
32
|
+
event_id: event_id,
|
|
33
|
+
stream_id: stream_id,
|
|
34
|
+
version: version,
|
|
35
|
+
occurred_at: occurred_at,
|
|
36
|
+
correlation_id: correlation_id,
|
|
37
|
+
causation_id: causation_id
|
|
34
38
|
)
|
|
35
39
|
end
|
|
36
40
|
|
|
@@ -49,12 +53,51 @@ module TCB
|
|
|
49
53
|
scope = scope.limit(limit) if limit
|
|
50
54
|
|
|
51
55
|
scope.map do |record|
|
|
52
|
-
|
|
53
|
-
event:
|
|
54
|
-
event_id:
|
|
55
|
-
stream_id:
|
|
56
|
-
version:
|
|
57
|
-
occurred_at:
|
|
56
|
+
TCB::Envelope.new(
|
|
57
|
+
event: deserialize(record.payload),
|
|
58
|
+
event_id: record.event_id,
|
|
59
|
+
stream_id: record.stream_id,
|
|
60
|
+
version: record.version,
|
|
61
|
+
occurred_at: record.occurred_at,
|
|
62
|
+
correlation_id: record.correlation_id,
|
|
63
|
+
causation_id: record.causation_id
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def read_by_correlation(correlation_id, context:, occurred_after: nil, occurred_before: nil)
|
|
69
|
+
tables = find_tables_for_context(context)
|
|
70
|
+
return [] if tables.empty?
|
|
71
|
+
|
|
72
|
+
union_sql = tables.map do |table|
|
|
73
|
+
conditions = ["correlation_id = ?"]
|
|
74
|
+
conditions << "occurred_at > ?" if occurred_after
|
|
75
|
+
conditions << "occurred_at < ?" if occurred_before
|
|
76
|
+
|
|
77
|
+
"SELECT * FROM #{table} WHERE #{conditions.join(' AND ')}"
|
|
78
|
+
end.join(" UNION ALL ")
|
|
79
|
+
|
|
80
|
+
bindings = tables.flat_map do
|
|
81
|
+
params = [correlation_id]
|
|
82
|
+
params << occurred_after if occurred_after
|
|
83
|
+
params << occurred_before if occurred_before
|
|
84
|
+
params
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
sql = "#{union_sql} ORDER BY occurred_at ASC"
|
|
88
|
+
records = ::ActiveRecord::Base.connection.exec_query(
|
|
89
|
+
::ActiveRecord::Base.sanitize_sql_array([sql, *bindings])
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
records.map do |record|
|
|
93
|
+
TCB::Envelope.new(
|
|
94
|
+
event: deserialize(record["payload"]),
|
|
95
|
+
event_id: record["event_id"],
|
|
96
|
+
stream_id: record["stream_id"],
|
|
97
|
+
version: record["version"],
|
|
98
|
+
occurred_at: Time.parse(record["occurred_at"].to_s),
|
|
99
|
+
correlation_id: record["correlation_id"],
|
|
100
|
+
causation_id: record["causation_id"]
|
|
58
101
|
)
|
|
59
102
|
end
|
|
60
103
|
end
|
|
@@ -88,6 +131,13 @@ module TCB
|
|
|
88
131
|
def permitted_classes
|
|
89
132
|
TCB.config.permitted_serialization_classes
|
|
90
133
|
end
|
|
134
|
+
|
|
135
|
+
def find_tables_for_context(context)
|
|
136
|
+
::ActiveRecord::Base.connection.tables.select do |table|
|
|
137
|
+
table.start_with?(context.gsub("/", "__").gsub("::", "__")) &&
|
|
138
|
+
table.end_with?("_events")
|
|
139
|
+
end
|
|
140
|
+
end
|
|
91
141
|
end
|
|
92
142
|
end
|
|
93
143
|
end
|
|
@@ -10,15 +10,17 @@ module TCB
|
|
|
10
10
|
@mutex = Mutex.new
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
def append(stream_id:, events:, occurred_at: Time.now)
|
|
13
|
+
def append(stream_id:, events:, occurred_at: Time.now, correlation_id: nil, causation_id: nil)
|
|
14
14
|
@mutex.synchronize do
|
|
15
15
|
envelopes = events.map.with_index(next_version(stream_id)) do |event, version|
|
|
16
|
-
|
|
17
|
-
event:
|
|
18
|
-
event_id:
|
|
19
|
-
stream_id:
|
|
20
|
-
version:
|
|
21
|
-
occurred_at:
|
|
16
|
+
Envelope.new(
|
|
17
|
+
event: event,
|
|
18
|
+
event_id: SecureRandom.uuid,
|
|
19
|
+
stream_id: stream_id,
|
|
20
|
+
version: version,
|
|
21
|
+
occurred_at: occurred_at,
|
|
22
|
+
correlation_id: correlation_id,
|
|
23
|
+
causation_id: causation_id
|
|
22
24
|
)
|
|
23
25
|
end
|
|
24
26
|
@streams[stream_id].concat(envelopes)
|
|
@@ -35,6 +37,14 @@ module TCB
|
|
|
35
37
|
.then { |e| limit ? e.first(limit) : e }
|
|
36
38
|
end
|
|
37
39
|
|
|
40
|
+
def read_by_correlation(correlation_id, context:, occurred_after: nil, occurred_before: nil)
|
|
41
|
+
@mutex.synchronize { @streams.values.flatten.dup }
|
|
42
|
+
.select { |e| e.stream_id.start_with?(context) }
|
|
43
|
+
.select { |e| e.correlation_id == correlation_id }
|
|
44
|
+
.then { |e| occurred_after ? e.select { |env| env.occurred_at > occurred_after } : e }
|
|
45
|
+
.then { |e| occurred_before ? e.select { |env| env.occurred_at < occurred_before } : e }
|
|
46
|
+
end
|
|
47
|
+
|
|
38
48
|
def reset!
|
|
39
49
|
@mutex.synchronize do
|
|
40
50
|
@streams = Hash.new { |h, k| h[k] = [] }
|
data/lib/tcb/publish.rb
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
module TCB
|
|
2
|
-
def self.publish(*
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
def self.publish(*events_or_envelopes)
|
|
3
|
+
events_or_envelopes.each do |e|
|
|
4
|
+
envelope = TCB::Envelope.coerce(e)
|
|
5
|
+
config.event_bus.publish(envelope)
|
|
6
|
+
end
|
|
7
|
+
events_or_envelopes
|
|
5
8
|
end
|
|
6
|
-
end
|
|
9
|
+
end
|
data/lib/tcb/record.rb
CHANGED
|
@@ -4,19 +4,28 @@ module TCB
|
|
|
4
4
|
class Record
|
|
5
5
|
def self.call(events_from:, events:, within:, store:, registrations:, &block)
|
|
6
6
|
raise ArgumentError, "events_from: or events: must be provided" if events_from.empty? && events.empty?
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
|
|
8
|
+
new(
|
|
9
|
+
events_from: events_from,
|
|
10
|
+
events: events,
|
|
11
|
+
store: store,
|
|
12
|
+
registrations: registrations,
|
|
13
|
+
correlation_id: Thread.current[:tcb_correlation_id],
|
|
14
|
+
causation_id: Thread.current[:tcb_causation_id]
|
|
15
|
+
).call(within: within, &block)
|
|
9
16
|
end
|
|
10
17
|
|
|
11
|
-
def initialize(events_from:, events:, store:, registrations:)
|
|
12
|
-
@events_from
|
|
13
|
-
@events
|
|
14
|
-
@store
|
|
15
|
-
@registrations
|
|
18
|
+
def initialize(events_from:, events:, store:, registrations:, correlation_id: nil, causation_id: nil)
|
|
19
|
+
@events_from = events_from
|
|
20
|
+
@events = events
|
|
21
|
+
@store = store
|
|
22
|
+
@registrations = registrations
|
|
23
|
+
@correlation_id = correlation_id
|
|
24
|
+
@causation_id = causation_id
|
|
16
25
|
end
|
|
17
26
|
|
|
18
27
|
def call(within:, &block)
|
|
19
|
-
if within
|
|
28
|
+
if within.respond_to?(:transaction)
|
|
20
29
|
within.transaction { execute(&block) }
|
|
21
30
|
else
|
|
22
31
|
execute(&block)
|
|
@@ -30,15 +39,21 @@ module TCB
|
|
|
30
39
|
events = @events_from.flat_map(&:pull_recorded_events)
|
|
31
40
|
events += @events
|
|
32
41
|
persist(events)
|
|
33
|
-
events
|
|
34
42
|
rescue
|
|
35
43
|
@events_from.each(&:pull_recorded_events)
|
|
36
44
|
raise
|
|
37
45
|
end
|
|
38
46
|
|
|
39
47
|
def persist(events)
|
|
40
|
-
return unless @store
|
|
48
|
+
return events.map { |event| wrap(event) } unless @store
|
|
49
|
+
|
|
50
|
+
persisted = persist_to_store(events)
|
|
51
|
+
remaining = wrap_remaining(events, persisted)
|
|
41
52
|
|
|
53
|
+
order_by_original(events, persisted, remaining)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def persist_to_store(events)
|
|
42
57
|
grouped = Hash.new { |h, k| h[k] = [] }
|
|
43
58
|
|
|
44
59
|
events.each do |event|
|
|
@@ -49,7 +64,28 @@ module TCB
|
|
|
49
64
|
grouped[stream_id.to_s] << event
|
|
50
65
|
end
|
|
51
66
|
|
|
52
|
-
grouped.
|
|
67
|
+
grouped.flat_map do |stream_id, grouped_events|
|
|
68
|
+
@store.append(
|
|
69
|
+
stream_id: stream_id,
|
|
70
|
+
events: grouped_events,
|
|
71
|
+
correlation_id: @correlation_id,
|
|
72
|
+
causation_id: @causation_id
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def wrap_remaining(events, persisted)
|
|
78
|
+
persisted_events = persisted.map(&:event)
|
|
79
|
+
events
|
|
80
|
+
.reject { |event| persisted_events.include?(event) }
|
|
81
|
+
.map { |event| wrap(event) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def order_by_original(events, persisted, remaining)
|
|
85
|
+
all = persisted + remaining
|
|
86
|
+
events.map { |event| all.find { |e| e.event == event } }
|
|
53
87
|
end
|
|
88
|
+
|
|
89
|
+
def wrap(event) = TCB::Envelope.wrap(event, correlation_id: @correlation_id, causation_id: @causation_id)
|
|
54
90
|
end
|
|
55
91
|
end
|
|
@@ -15,8 +15,8 @@ module TCB
|
|
|
15
15
|
def with_subscriptions(*event_classes)
|
|
16
16
|
captured = Hash.new { |h, k| h[k] = [] }
|
|
17
17
|
subscriptions = event_classes.map do |event_class|
|
|
18
|
-
TCB.config.event_bus.subscribe(event_class) do |
|
|
19
|
-
captured[event_class] << event
|
|
18
|
+
TCB.config.event_bus.subscribe(event_class) do |envelope|
|
|
19
|
+
captured[event_class] << (envelope.is_a?(TCB::Envelope) ? envelope.event : envelope)
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
yield captured
|