event_sourcery 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|