event_sourcery 0.13.0

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