event_sourcery 0.13.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 +7 -0
- data/.gitignore +37 -0
- data/.rspec +3 -0
- data/.travis.yml +8 -0
- data/CHANGELOG.md +82 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +399 -0
- data/Rakefile +6 -0
- data/bin/console +6 -0
- data/bin/setup +15 -0
- data/event_sourcery.gemspec +28 -0
- data/lib/event_sourcery.rb +49 -0
- data/lib/event_sourcery/aggregate_root.rb +68 -0
- data/lib/event_sourcery/config.rb +43 -0
- data/lib/event_sourcery/errors.rb +19 -0
- data/lib/event_sourcery/event.rb +49 -0
- data/lib/event_sourcery/event_body_serializer.rb +42 -0
- data/lib/event_sourcery/event_processing/error_handlers/constant_retry.rb +23 -0
- data/lib/event_sourcery/event_processing/error_handlers/error_handler.rb +20 -0
- data/lib/event_sourcery/event_processing/error_handlers/exponential_backoff_retry.rb +40 -0
- data/lib/event_sourcery/event_processing/error_handlers/no_retry.rb +19 -0
- data/lib/event_sourcery/event_processing/esp_process.rb +41 -0
- data/lib/event_sourcery/event_processing/esp_runner.rb +105 -0
- data/lib/event_sourcery/event_processing/event_stream_processor.rb +125 -0
- data/lib/event_sourcery/event_processing/event_stream_processor_registry.rb +29 -0
- data/lib/event_sourcery/event_store/each_by_range.rb +25 -0
- data/lib/event_sourcery/event_store/event_builder.rb +19 -0
- data/lib/event_sourcery/event_store/event_sink.rb +18 -0
- data/lib/event_sourcery/event_store/event_source.rb +21 -0
- data/lib/event_sourcery/event_store/event_type_serializers/class_name.rb +19 -0
- data/lib/event_sourcery/event_store/event_type_serializers/legacy.rb +17 -0
- data/lib/event_sourcery/event_store/event_type_serializers/underscored.rb +68 -0
- data/lib/event_sourcery/event_store/poll_waiter.rb +18 -0
- data/lib/event_sourcery/event_store/signal_handling_subscription_master.rb +22 -0
- data/lib/event_sourcery/event_store/subscription.rb +43 -0
- data/lib/event_sourcery/memory/event_store.rb +76 -0
- data/lib/event_sourcery/memory/tracker.rb +27 -0
- data/lib/event_sourcery/repository.rb +31 -0
- data/lib/event_sourcery/rspec/event_store_shared_examples.rb +352 -0
- data/lib/event_sourcery/version.rb +3 -0
- metadata +158 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
module EventSourcery
|
2
|
+
module EventProcessing
|
3
|
+
class ESPProcess
|
4
|
+
def initialize(event_processor:,
|
5
|
+
event_source:,
|
6
|
+
subscription_master: EventStore::SignalHandlingSubscriptionMaster.new)
|
7
|
+
@event_processor = event_processor
|
8
|
+
@event_source = event_source
|
9
|
+
@subscription_master = subscription_master
|
10
|
+
end
|
11
|
+
|
12
|
+
def start
|
13
|
+
name_process
|
14
|
+
error_handler.with_error_handling do
|
15
|
+
EventSourcery.logger.info("Starting #{processor_name}")
|
16
|
+
subscribe_to_event_stream
|
17
|
+
EventSourcery.logger.info("Stopping #{@event_processor.processor_name}")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def processor_name
|
24
|
+
@event_processor.processor_name.to_s
|
25
|
+
end
|
26
|
+
|
27
|
+
def error_handler
|
28
|
+
@error_handler ||= EventSourcery.config.error_handler_class.new(processor_name: processor_name)
|
29
|
+
end
|
30
|
+
|
31
|
+
def name_process
|
32
|
+
Process.setproctitle(@event_processor.class.name)
|
33
|
+
end
|
34
|
+
|
35
|
+
def subscribe_to_event_stream
|
36
|
+
@event_processor.subscribe_to(@event_source,
|
37
|
+
subscription_master: @subscription_master)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module EventSourcery
|
2
|
+
module EventProcessing
|
3
|
+
# NOTE: databases should be disconnected before running this
|
4
|
+
# EventSourcery.config.postgres.event_store_database.disconnect
|
5
|
+
class ESPRunner
|
6
|
+
def initialize(event_processors:,
|
7
|
+
event_source:,
|
8
|
+
max_seconds_for_processes_to_terminate: 30,
|
9
|
+
shutdown_requested: false)
|
10
|
+
@event_processors = event_processors
|
11
|
+
@event_source = event_source
|
12
|
+
@pids = []
|
13
|
+
@max_seconds_for_processes_to_terminate = max_seconds_for_processes_to_terminate
|
14
|
+
@shutdown_requested = shutdown_requested
|
15
|
+
@exit_status = true
|
16
|
+
end
|
17
|
+
|
18
|
+
def start!
|
19
|
+
with_logging do
|
20
|
+
start_processes
|
21
|
+
listen_for_shutdown_signals
|
22
|
+
wait_till_shutdown_requested
|
23
|
+
record_terminated_processes
|
24
|
+
terminate_remaining_processes
|
25
|
+
until all_processes_terminated? || waited_long_enough?
|
26
|
+
record_terminated_processes
|
27
|
+
end
|
28
|
+
kill_remaining_processes
|
29
|
+
record_terminated_processes until all_processes_terminated?
|
30
|
+
end
|
31
|
+
exit_indicating_status_of_processes
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def with_logging
|
37
|
+
EventSourcery.logger.info { 'Forking ESP processes' }
|
38
|
+
yield
|
39
|
+
EventSourcery.logger.info { 'ESP processes shutdown' }
|
40
|
+
end
|
41
|
+
|
42
|
+
def start_processes
|
43
|
+
@event_processors.each(&method(:start_process))
|
44
|
+
end
|
45
|
+
|
46
|
+
def start_process(event_processor)
|
47
|
+
process = ESPProcess.new(
|
48
|
+
event_processor: event_processor,
|
49
|
+
event_source: @event_source
|
50
|
+
)
|
51
|
+
@pids << Process.fork { process.start }
|
52
|
+
end
|
53
|
+
|
54
|
+
def listen_for_shutdown_signals
|
55
|
+
%i(TERM INT).each do |signal|
|
56
|
+
Signal.trap(signal) { shutdown }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def shutdown
|
61
|
+
@shutdown_requested = true
|
62
|
+
end
|
63
|
+
|
64
|
+
def wait_till_shutdown_requested
|
65
|
+
sleep(1) until @shutdown_requested
|
66
|
+
end
|
67
|
+
|
68
|
+
def terminate_remaining_processes
|
69
|
+
send_signal_to_remaining_processes(:TERM)
|
70
|
+
end
|
71
|
+
|
72
|
+
def kill_remaining_processes
|
73
|
+
send_signal_to_remaining_processes(:KILL)
|
74
|
+
end
|
75
|
+
|
76
|
+
def send_signal_to_remaining_processes(signal)
|
77
|
+
Process.kill(signal, *@pids) unless all_processes_terminated?
|
78
|
+
rescue Errno::ESRCH
|
79
|
+
record_terminated_processes
|
80
|
+
retry
|
81
|
+
end
|
82
|
+
|
83
|
+
def record_terminated_processes
|
84
|
+
until all_processes_terminated? ||
|
85
|
+
((pid, status) = Process.wait2(-1, Process::WNOHANG)).nil?
|
86
|
+
@pids.delete(pid)
|
87
|
+
@exit_status &&= status.success?
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def all_processes_terminated?
|
92
|
+
@pids.empty?
|
93
|
+
end
|
94
|
+
|
95
|
+
def waited_long_enough?
|
96
|
+
@timeout ||= Time.now + @max_seconds_for_processes_to_terminate
|
97
|
+
Time.now >= @timeout
|
98
|
+
end
|
99
|
+
|
100
|
+
def exit_indicating_status_of_processes
|
101
|
+
Process.exit(@exit_status)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module EventSourcery
|
2
|
+
module EventProcessing
|
3
|
+
module EventStreamProcessor
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
base.include(InstanceMethods)
|
7
|
+
base.prepend(ProcessHandler)
|
8
|
+
EventSourcery.event_stream_processor_registry.register(base)
|
9
|
+
base.class_eval do
|
10
|
+
@event_handlers = Hash.new { |hash, key| hash[key] = [] }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module InstanceMethods
|
15
|
+
def initialize(tracker:)
|
16
|
+
@tracker = tracker
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module ProcessHandler
|
21
|
+
def process(event)
|
22
|
+
@_event = event
|
23
|
+
handlers = self.class.event_handlers[event.type]
|
24
|
+
if handlers.any?
|
25
|
+
handlers.each do |handler|
|
26
|
+
instance_exec(event, &handler)
|
27
|
+
end
|
28
|
+
elsif self.class.processes?(event.type)
|
29
|
+
if defined?(super)
|
30
|
+
super(event)
|
31
|
+
else
|
32
|
+
raise UnableToProcessEventError, "I don't know how to process '#{event.type}' events."
|
33
|
+
end
|
34
|
+
end
|
35
|
+
@_event = nil
|
36
|
+
rescue
|
37
|
+
raise EventProcessingError.new(event)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
module ClassMethods
|
42
|
+
attr_reader :processes_event_types, :event_handlers
|
43
|
+
|
44
|
+
def processes_events(*event_types)
|
45
|
+
@processes_event_types = Array(@processes_event_types) | event_types.map(&:to_s)
|
46
|
+
end
|
47
|
+
|
48
|
+
def processes_all_events
|
49
|
+
define_singleton_method :processes? do |_|
|
50
|
+
true
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def processes?(event_type)
|
55
|
+
processes_event_types &&
|
56
|
+
processes_event_types.include?(event_type.to_s)
|
57
|
+
end
|
58
|
+
|
59
|
+
def processor_name(name = nil)
|
60
|
+
if name
|
61
|
+
@processor_name = name
|
62
|
+
else
|
63
|
+
(defined?(@processor_name) && @processor_name) || self.name
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def process(*event_classes, &block)
|
68
|
+
event_classes.each do |event_class|
|
69
|
+
processes_events event_class.type
|
70
|
+
@event_handlers[event_class.type] << block
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def processes_event_types
|
76
|
+
self.class.processes_event_types
|
77
|
+
end
|
78
|
+
|
79
|
+
def setup
|
80
|
+
tracker.setup(processor_name)
|
81
|
+
end
|
82
|
+
|
83
|
+
def reset
|
84
|
+
tracker.reset_last_processed_event_id(processor_name)
|
85
|
+
end
|
86
|
+
|
87
|
+
def last_processed_event_id
|
88
|
+
tracker.last_processed_event_id(processor_name)
|
89
|
+
end
|
90
|
+
|
91
|
+
def processor_name
|
92
|
+
self.class.processor_name
|
93
|
+
end
|
94
|
+
|
95
|
+
def processes?(event_type)
|
96
|
+
self.class.processes?(event_type)
|
97
|
+
end
|
98
|
+
|
99
|
+
def subscribe_to(event_source, subscription_master: EventStore::SignalHandlingSubscriptionMaster.new)
|
100
|
+
setup
|
101
|
+
event_source.subscribe(from_id: last_processed_event_id + 1,
|
102
|
+
event_types: processes_event_types,
|
103
|
+
subscription_master: subscription_master) do |events|
|
104
|
+
process_events(events, subscription_master)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
attr_accessor :tracker
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
attr_reader :_event
|
113
|
+
|
114
|
+
def process_events(events, subscription_master)
|
115
|
+
events.each do |event|
|
116
|
+
subscription_master.shutdown_if_requested
|
117
|
+
process(event)
|
118
|
+
tracker.processed_event(processor_name, event.id)
|
119
|
+
EventSourcery.logger.debug { "[#{processor_name}] Processed event: #{event.inspect}" }
|
120
|
+
end
|
121
|
+
EventSourcery.logger.info { "[#{processor_name}] Processed up to event id: #{events.last.id}" }
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module EventSourcery
|
2
|
+
module EventProcessing
|
3
|
+
class EventStreamProcessorRegistry
|
4
|
+
def initialize
|
5
|
+
@processors = []
|
6
|
+
end
|
7
|
+
|
8
|
+
def register(klass)
|
9
|
+
@processors << klass
|
10
|
+
end
|
11
|
+
|
12
|
+
def find(processor_name)
|
13
|
+
@processors.find do |processor|
|
14
|
+
processor.processor_name == processor_name
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def by_type(constant)
|
19
|
+
@processors.select do |processor|
|
20
|
+
processor.included_modules.include?(constant)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def all
|
25
|
+
@processors
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module EventSourcery
|
2
|
+
module EventStore
|
3
|
+
module EachByRange
|
4
|
+
def each_by_range(from_event_id, to_event_id, event_types: nil)
|
5
|
+
caught_up = false
|
6
|
+
no_events_left = false
|
7
|
+
event_id = from_event_id
|
8
|
+
begin
|
9
|
+
events = get_next_from(event_id, event_types: event_types)
|
10
|
+
no_events_left = true if events.empty?
|
11
|
+
events.each do |event|
|
12
|
+
yield event
|
13
|
+
if event.id == to_event_id
|
14
|
+
caught_up = true
|
15
|
+
break
|
16
|
+
end
|
17
|
+
end
|
18
|
+
unless no_events_left
|
19
|
+
event_id = events.last.id + 1
|
20
|
+
end
|
21
|
+
end while !caught_up && !no_events_left
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module EventSourcery
|
2
|
+
module EventStore
|
3
|
+
class EventBuilder
|
4
|
+
def initialize(event_type_serializer:)
|
5
|
+
@event_type_serializer = event_type_serializer
|
6
|
+
end
|
7
|
+
|
8
|
+
def build(event_data)
|
9
|
+
type = event_data.fetch(:type)
|
10
|
+
klass = event_type_serializer.deserialize(type)
|
11
|
+
klass.new(event_data)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
attr_reader :event_type_serializer
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module EventSourcery
|
4
|
+
module EventStore
|
5
|
+
class EventSink
|
6
|
+
def initialize(event_store)
|
7
|
+
@event_store = event_store
|
8
|
+
end
|
9
|
+
|
10
|
+
extend Forwardable
|
11
|
+
def_delegators :event_store, :sink
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
attr_reader :event_store
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module EventSourcery
|
2
|
+
module EventStore
|
3
|
+
class EventSource
|
4
|
+
def initialize(event_store)
|
5
|
+
@event_store = event_store
|
6
|
+
end
|
7
|
+
|
8
|
+
extend Forwardable
|
9
|
+
def_delegators :event_store,
|
10
|
+
:get_next_from,
|
11
|
+
:latest_event_id,
|
12
|
+
:get_events_for_aggregate_id,
|
13
|
+
:each_by_range,
|
14
|
+
:subscribe
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :event_store
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module EventSourcery
|
2
|
+
module EventStore
|
3
|
+
module EventTypeSerializers
|
4
|
+
# Stores event types by their class name and falls back to the generic
|
5
|
+
# Event class if the constant is not found
|
6
|
+
class ClassName
|
7
|
+
def serialize(event_class)
|
8
|
+
event_class.name
|
9
|
+
end
|
10
|
+
|
11
|
+
def deserialize(event_type)
|
12
|
+
Object.const_get(event_type)
|
13
|
+
rescue NameError
|
14
|
+
Event
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module EventSourcery
|
2
|
+
module EventStore
|
3
|
+
module EventTypeSerializers
|
4
|
+
# To support legacy implementations. Type is provided when initializing
|
5
|
+
# the event, not derived from the class constant
|
6
|
+
class Legacy
|
7
|
+
def serialize(event_class)
|
8
|
+
nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def deserialize(event_type)
|
12
|
+
Event
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module EventSourcery
|
2
|
+
module EventStore
|
3
|
+
module EventTypeSerializers
|
4
|
+
# Stores event types by the underscored version of the class name and
|
5
|
+
# falls back to the generic Event class if the constant is not found
|
6
|
+
#
|
7
|
+
# Replace inflector with ActiveSupport like this:
|
8
|
+
# EventSourcery::EventStore::EventTypeSerializers::Underscored.inflector = ActiveSupport::Inflector
|
9
|
+
class Underscored
|
10
|
+
class Inflector
|
11
|
+
# Inflection methods are taken from active support 3.2
|
12
|
+
# https://github.com/rails/rails/blob/3-2-stable/activesupport/lib/active_support/inflector/methods.rb
|
13
|
+
def underscore(camel_cased_word)
|
14
|
+
word = camel_cased_word.to_s.dup
|
15
|
+
word.gsub!(/::/, '/')
|
16
|
+
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
|
17
|
+
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
18
|
+
word.tr!("-", "_")
|
19
|
+
word.downcase!
|
20
|
+
word
|
21
|
+
end
|
22
|
+
|
23
|
+
def camelize(term, uppercase_first_letter = true)
|
24
|
+
string = term.to_s
|
25
|
+
if uppercase_first_letter
|
26
|
+
string = string.sub(/^[a-z\d]*/) { capitalize($&) }
|
27
|
+
else
|
28
|
+
string = string.sub(/^(?:(?=\b|[A-Z_])|\w)/) { $&.downcase }
|
29
|
+
end
|
30
|
+
string.gsub(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{capitalize($2)}" }.gsub('/', '::')
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def capitalize(lower_case_and_underscored_word)
|
36
|
+
result = lower_case_and_underscored_word.to_s.dup
|
37
|
+
result.gsub!(/_id$/, "")
|
38
|
+
result.gsub!(/_/, ' ')
|
39
|
+
result.gsub(/([a-z\d]*)/i) { |match|
|
40
|
+
"#{match.downcase}"
|
41
|
+
}.gsub(/^\w/) { $&.upcase }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class << self
|
46
|
+
attr_accessor :inflector
|
47
|
+
end
|
48
|
+
@inflector = Inflector.new
|
49
|
+
|
50
|
+
def serialize(event_class)
|
51
|
+
underscore_class_name(event_class.name)
|
52
|
+
end
|
53
|
+
|
54
|
+
def deserialize(event_type)
|
55
|
+
Object.const_get(self.class.inflector.camelize(event_type))
|
56
|
+
rescue NameError
|
57
|
+
Event
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def underscore_class_name(class_name)
|
63
|
+
self.class.inflector.underscore(class_name)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|