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
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "event_sourcery"
5
+ require "pry"
6
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,15 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ echo
6
+ echo "--- Bundling"
7
+ echo
8
+
9
+ bundle install
10
+
11
+ echo
12
+ echo "--- Preparing databases"
13
+ echo
14
+
15
+ createdb event_sourcery_test
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'event_sourcery/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'event_sourcery'
8
+ spec.version = EventSourcery::VERSION
9
+ spec.authors = ['Steve Hodgkiss', 'Tao Guo', 'Sebastian von Conrad']
10
+ spec.email = ['steve@hodgkiss.me', 'tao.guo@envato.com', 'sebastian.von.conrad@envato.com']
11
+
12
+ spec.summary = 'Event Sourcing Library'
13
+ spec.description = ''
14
+ spec.homepage = 'https://github.com/envato/event_sourcery'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = 'exe'
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.required_ruby_version = '>= 2.2.0'
22
+
23
+ spec.add_development_dependency 'bundler', '~> 1.10'
24
+ spec.add_development_dependency 'rake', '~> 10.0'
25
+ spec.add_development_dependency 'rspec'
26
+ spec.add_development_dependency 'pry'
27
+ spec.add_development_dependency 'benchmark-ips'
28
+ end
@@ -0,0 +1,49 @@
1
+ require 'json'
2
+ require 'securerandom'
3
+ require 'time'
4
+
5
+ require 'event_sourcery/version'
6
+ require 'event_sourcery/event'
7
+ require 'event_sourcery/event_store/event_sink'
8
+ require 'event_sourcery/event_store/event_source'
9
+ require 'event_sourcery/errors'
10
+ require 'event_sourcery/event_store/each_by_range'
11
+ require 'event_sourcery/event_store/subscription'
12
+ require 'event_sourcery/event_store/poll_waiter'
13
+ require 'event_sourcery/event_store/event_builder'
14
+ require 'event_sourcery/event_store/event_type_serializers/class_name'
15
+ require 'event_sourcery/event_store/event_type_serializers/legacy'
16
+ require 'event_sourcery/event_store/event_type_serializers/underscored'
17
+ require 'event_sourcery/event_store/signal_handling_subscription_master'
18
+ require 'event_sourcery/event_processing/error_handlers/error_handler'
19
+ require 'event_sourcery/event_processing/error_handlers/no_retry'
20
+ require 'event_sourcery/event_processing/error_handlers/constant_retry'
21
+ require 'event_sourcery/event_processing/error_handlers/exponential_backoff_retry'
22
+ require 'event_sourcery/event_processing/esp_process'
23
+ require 'event_sourcery/event_processing/esp_runner'
24
+ require 'event_sourcery/event_processing/event_stream_processor'
25
+ require 'event_sourcery/event_processing/event_stream_processor_registry'
26
+ require 'event_sourcery/config'
27
+ require 'event_sourcery/event_body_serializer'
28
+ require 'event_sourcery/aggregate_root'
29
+ require 'event_sourcery/repository'
30
+ require 'event_sourcery/memory/tracker'
31
+ require 'event_sourcery/memory/event_store'
32
+
33
+ module EventSourcery
34
+ def self.configure
35
+ yield config
36
+ end
37
+
38
+ def self.config
39
+ @config ||= Config.new
40
+ end
41
+
42
+ def self.logger
43
+ config.logger
44
+ end
45
+
46
+ def self.event_stream_processor_registry
47
+ @event_stream_processor_registry ||= EventProcessing::EventStreamProcessorRegistry.new
48
+ end
49
+ end
@@ -0,0 +1,68 @@
1
+ module EventSourcery
2
+ module AggregateRoot
3
+ UnknownEventError = Class.new(RuntimeError)
4
+
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ base.class_eval do
8
+ @event_handlers = Hash.new { |hash, key| hash[key] = [] }
9
+ end
10
+ end
11
+
12
+ module ClassMethods
13
+ attr_reader :event_handlers
14
+
15
+ def apply(*event_classes, &block)
16
+ event_classes.each do |event_class|
17
+ @event_handlers[event_class.type] << block
18
+ end
19
+ end
20
+ end
21
+
22
+ def initialize(id, events, on_unknown_event: EventSourcery.config.on_unknown_event)
23
+ @id = id.to_str
24
+ @version = 0
25
+ @on_unknown_event = on_unknown_event
26
+ @changes = []
27
+ load_history(events)
28
+ end
29
+
30
+ attr_reader :changes, :version
31
+
32
+ def clear_changes
33
+ @changes.clear
34
+ end
35
+
36
+ private
37
+
38
+ def load_history(events)
39
+ events.each do |event|
40
+ mutate_state_from(event)
41
+ end
42
+ end
43
+
44
+ attr_reader :id
45
+
46
+ def apply_event(event_class, options = {})
47
+ event = event_class.new(**options.merge(aggregate_id: id))
48
+ mutate_state_from(event)
49
+ @changes << event
50
+ end
51
+
52
+ def mutate_state_from(event)
53
+ handlers = self.class.event_handlers[event.type]
54
+ if handlers.any?
55
+ handlers.each do |handler|
56
+ instance_exec(event, &handler)
57
+ end
58
+ else
59
+ @on_unknown_event.call(event, self)
60
+ end
61
+ increment_version
62
+ end
63
+
64
+ def increment_version
65
+ @version += 1
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,43 @@
1
+ require 'logger'
2
+
3
+ module EventSourcery
4
+ class Config
5
+ attr_accessor :on_unknown_event,
6
+ :on_event_processor_error,
7
+ :event_type_serializer,
8
+ :error_handler_class
9
+
10
+ attr_writer :logger,
11
+ :event_body_serializer,
12
+ :event_builder
13
+
14
+ def initialize
15
+ @on_unknown_event = proc { |event, aggregate|
16
+ raise AggregateRoot::UnknownEventError, "#{event.type} is unknown to #{aggregate.class.name}"
17
+ }
18
+ @on_event_processor_error = proc { |exception, processor_name|
19
+ # app specific custom logic ie. report to rollbar
20
+ }
21
+ @event_store = nil
22
+ @event_type_serializer = EventStore::EventTypeSerializers::Underscored.new
23
+ @error_handler_class = EventProcessing::ErrorHandlers::ConstantRetry
24
+ end
25
+
26
+ def logger
27
+ @logger ||= ::Logger.new(STDOUT).tap do |logger|
28
+ logger.level = Logger::DEBUG
29
+ end
30
+ end
31
+
32
+ def event_builder
33
+ @event_builder || EventStore::EventBuilder.new(event_type_serializer: @event_type_serializer)
34
+ end
35
+
36
+ def event_body_serializer
37
+ @event_body_serializer ||= EventBodySerializer.new
38
+ .add(Hash, EventBodySerializer::HashSerializer)
39
+ .add(Array, EventBodySerializer::ArraySerializer)
40
+ .add(Time, &:iso8601)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,19 @@
1
+ module EventSourcery
2
+ Error = Class.new(StandardError)
3
+ UnableToLockProcessorError = Class.new(Error)
4
+ UnableToProcessEventError = Class.new(Error)
5
+ ConcurrencyError = Class.new(Error)
6
+ AtomicWriteToMultipleAggregatesNotSupported = Class.new(Error)
7
+
8
+ class EventProcessingError < Error
9
+ attr_reader :event
10
+
11
+ def initialize(event)
12
+ @event = event
13
+ end
14
+
15
+ def message
16
+ cause.message if cause
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,49 @@
1
+ module EventSourcery
2
+ class Event
3
+ include Comparable
4
+
5
+ def self.type
6
+ unless self == Event
7
+ EventSourcery.config.event_type_serializer.serialize(self)
8
+ end
9
+ end
10
+
11
+ attr_reader :id, :uuid, :aggregate_id, :type, :body, :version, :created_at, :correlation_id, :causation_id
12
+
13
+ def initialize(id: nil,
14
+ uuid: SecureRandom.uuid,
15
+ aggregate_id: nil,
16
+ type: nil,
17
+ body: nil,
18
+ version: nil,
19
+ created_at: nil,
20
+ correlation_id: nil,
21
+ causation_id: nil)
22
+ @id = id
23
+ @uuid = uuid && uuid.downcase
24
+ @aggregate_id = aggregate_id
25
+ @type = self.class.type || type.to_s
26
+ @body = body ? EventSourcery::EventBodySerializer.serialize(body) : {}
27
+ @version = version ? Integer(version) : nil
28
+ @created_at = created_at
29
+ @correlation_id = correlation_id
30
+ @causation_id = causation_id
31
+ end
32
+
33
+ def persisted?
34
+ !id.nil?
35
+ end
36
+
37
+ def hash
38
+ [self.class, uuid].hash
39
+ end
40
+
41
+ def eql?(other)
42
+ instance_of?(other.class) && uuid.eql?(other.uuid)
43
+ end
44
+
45
+ def <=>(other)
46
+ id <=> other.id if other.is_a? Event
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,42 @@
1
+ module EventSourcery
2
+ class EventBodySerializer
3
+ def self.serialize(event_body,
4
+ serializer: EventSourcery.config.event_body_serializer)
5
+ serializer.serialize(event_body)
6
+ end
7
+
8
+ def initialize
9
+ @serializers = Hash.new(IdentitySerializer)
10
+ end
11
+
12
+ def add(type, serializer = nil, &block_serializer)
13
+ @serializers[type] = serializer || block_serializer
14
+ self
15
+ end
16
+
17
+ def serialize(object)
18
+ serializer = @serializers[object.class]
19
+ serializer.call(object, &method(:serialize))
20
+ end
21
+
22
+ module HashSerializer
23
+ def self.call(hash, &serialize)
24
+ hash.each_with_object({}) do |(key, value), memo|
25
+ memo[key.to_s] = serialize.call(value)
26
+ end
27
+ end
28
+ end
29
+
30
+ module ArraySerializer
31
+ def self.call(array, &serialize)
32
+ array.map(&serialize)
33
+ end
34
+ end
35
+
36
+ module IdentitySerializer
37
+ def self.call(object)
38
+ object
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,23 @@
1
+ module EventSourcery
2
+ module EventProcessing
3
+ module ErrorHandlers
4
+ class ConstantRetry
5
+ include EventSourcery::EventProcessing::ErrorHandlers::ErrorHandler
6
+ DEFAULT_RETRY_INVERAL = 1
7
+ def initialize(processor_name:)
8
+ @processor_name = processor_name
9
+ @retry_interval = DEFAULT_RETRY_INVERAL
10
+ end
11
+
12
+ def with_error_handling
13
+ yield
14
+ rescue => error
15
+ report_error(error)
16
+ sleep(@retry_interval)
17
+
18
+ retry
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ module EventSourcery
2
+ module EventProcessing
3
+ module ErrorHandlers
4
+ module ErrorHandler
5
+ def with_error_handling
6
+ raise NotImplementedError, 'Please implement #with_error_handling method'
7
+ end
8
+
9
+ private
10
+
11
+ def report_error(error)
12
+ error = error.cause if error.instance_of?(EventSourcery::EventProcessingError)
13
+ EventSourcery.logger.error("Processor #{@processor_name} died with #{error}.\n#{error.backtrace.join("\n")}")
14
+
15
+ EventSourcery.config.on_event_processor_error.call(error, @processor_name)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,40 @@
1
+ module EventSourcery
2
+ module EventProcessing
3
+ module ErrorHandlers
4
+ class ExponentialBackoffRetry
5
+ include EventSourcery::EventProcessing::ErrorHandlers::ErrorHandler
6
+ DEFAULT_RETRY_INVERAL = 1
7
+ MAX_RETRY_INVERVAL = 64
8
+
9
+ def initialize(processor_name:)
10
+ @processor_name = processor_name
11
+ @retry_interval = DEFAULT_RETRY_INVERAL
12
+ end
13
+
14
+ def with_error_handling
15
+ yield
16
+ rescue => error
17
+ report_error(error)
18
+ update_retry_interval(error)
19
+ sleep(@retry_interval)
20
+ retry
21
+ end
22
+
23
+ private
24
+
25
+ def update_retry_interval(error)
26
+ if error.instance_of?(EventSourcery::EventProcessingError)
27
+ if @error_event_uuid == error.event.uuid
28
+ @retry_interval *= 2 if @retry_interval < MAX_RETRY_INVERVAL
29
+ else
30
+ @error_event_uuid = error.event.uuid
31
+ @retry_interval = DEFAULT_RETRY_INVERAL
32
+ end
33
+ else
34
+ @retry_interval = DEFAULT_RETRY_INVERAL
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,19 @@
1
+ module EventSourcery
2
+ module EventProcessing
3
+ module ErrorHandlers
4
+ class NoRetry
5
+ include EventSourcery::EventProcessing::ErrorHandlers::ErrorHandler
6
+ def initialize(processor_name:)
7
+ @processor_name = processor_name
8
+ end
9
+
10
+ def with_error_handling
11
+ yield
12
+ rescue => error
13
+ report_error(error)
14
+ Process.exit(false)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end