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.
- 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
|