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
data/Rakefile
ADDED
data/bin/console
ADDED
data/bin/setup
ADDED
@@ -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
|