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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +37 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +8 -0
  5. data/CHANGELOG.md +82 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +5 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +399 -0
  10. data/Rakefile +6 -0
  11. data/bin/console +6 -0
  12. data/bin/setup +15 -0
  13. data/event_sourcery.gemspec +28 -0
  14. data/lib/event_sourcery.rb +49 -0
  15. data/lib/event_sourcery/aggregate_root.rb +68 -0
  16. data/lib/event_sourcery/config.rb +43 -0
  17. data/lib/event_sourcery/errors.rb +19 -0
  18. data/lib/event_sourcery/event.rb +49 -0
  19. data/lib/event_sourcery/event_body_serializer.rb +42 -0
  20. data/lib/event_sourcery/event_processing/error_handlers/constant_retry.rb +23 -0
  21. data/lib/event_sourcery/event_processing/error_handlers/error_handler.rb +20 -0
  22. data/lib/event_sourcery/event_processing/error_handlers/exponential_backoff_retry.rb +40 -0
  23. data/lib/event_sourcery/event_processing/error_handlers/no_retry.rb +19 -0
  24. data/lib/event_sourcery/event_processing/esp_process.rb +41 -0
  25. data/lib/event_sourcery/event_processing/esp_runner.rb +105 -0
  26. data/lib/event_sourcery/event_processing/event_stream_processor.rb +125 -0
  27. data/lib/event_sourcery/event_processing/event_stream_processor_registry.rb +29 -0
  28. data/lib/event_sourcery/event_store/each_by_range.rb +25 -0
  29. data/lib/event_sourcery/event_store/event_builder.rb +19 -0
  30. data/lib/event_sourcery/event_store/event_sink.rb +18 -0
  31. data/lib/event_sourcery/event_store/event_source.rb +21 -0
  32. data/lib/event_sourcery/event_store/event_type_serializers/class_name.rb +19 -0
  33. data/lib/event_sourcery/event_store/event_type_serializers/legacy.rb +17 -0
  34. data/lib/event_sourcery/event_store/event_type_serializers/underscored.rb +68 -0
  35. data/lib/event_sourcery/event_store/poll_waiter.rb +18 -0
  36. data/lib/event_sourcery/event_store/signal_handling_subscription_master.rb +22 -0
  37. data/lib/event_sourcery/event_store/subscription.rb +43 -0
  38. data/lib/event_sourcery/memory/event_store.rb +76 -0
  39. data/lib/event_sourcery/memory/tracker.rb +27 -0
  40. data/lib/event_sourcery/repository.rb +31 -0
  41. data/lib/event_sourcery/rspec/event_store_shared_examples.rb +352 -0
  42. data/lib/event_sourcery/version.rb +3 -0
  43. 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