event_sourcery 0.14.0 → 0.15.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +3 -1
- data/lib/event_sourcery/aggregate_root.rb +80 -1
- data/lib/event_sourcery/config.rb +32 -5
- data/lib/event_sourcery/errors.rb +8 -3
- data/lib/event_sourcery/event.rb +61 -0
- data/lib/event_sourcery/event_body_serializer.rb +16 -0
- data/lib/event_sourcery/event_processing/error_handlers/constant_retry.rb +9 -2
- data/lib/event_sourcery/event_processing/error_handlers/error_handler.rb +4 -1
- data/lib/event_sourcery/event_processing/error_handlers/exponential_backoff_retry.rb +15 -6
- data/lib/event_sourcery/event_processing/error_handlers/no_retry.rb +1 -0
- data/lib/event_sourcery/event_processing/esp_process.rb +6 -1
- data/lib/event_sourcery/event_processing/esp_runner.rb +2 -0
- data/lib/event_sourcery/event_processing/event_stream_processor.rb +42 -1
- data/lib/event_sourcery/event_processing/event_stream_processor_registry.rb +16 -0
- data/lib/event_sourcery/event_store/poll_waiter.rb +10 -0
- data/lib/event_sourcery/event_store/signal_handling_subscription_master.rb +7 -0
- data/lib/event_sourcery/event_store/subscription.rb +15 -0
- data/lib/event_sourcery/memory/config.rb +36 -0
- data/lib/event_sourcery/memory/event_store.rb +59 -0
- data/lib/event_sourcery/memory/projector.rb +25 -0
- data/lib/event_sourcery/memory/tracker.rb +19 -0
- data/lib/event_sourcery/repository.rb +28 -0
- data/lib/event_sourcery/rspec/event_store_shared_examples.rb +59 -74
- data/lib/event_sourcery/version.rb +2 -1
- data/lib/event_sourcery.rb +28 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c1952be35c93c960587ad23886643f74d4825e18
|
4
|
+
data.tar.gz: 58fbfc8343e6c673b6f003a7d478e05dbdd11fef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b2aecc3786ef6dfbb7063c50201f0c7b0ff9ad73a368258cc090204c0bd7b865308e9d138475c9f8cdeac3598b6f588de630ccb021335e6f35af1932674a835e
|
7
|
+
data.tar.gz: f423097efb6782559310b3ce0697753338a2eceeadddb9e6a199ca5eebf83fb814985b8f1da76389f34dbda63f05f9edbb64c1c348826462177e2ea6e539d470
|
data/CHANGELOG.md
CHANGED
@@ -6,6 +6,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|
6
6
|
|
7
7
|
## [Unreleased]
|
8
8
|
|
9
|
+
## [0.15.0] - 2017-11-29
|
10
|
+
### Added
|
11
|
+
- Added in the first version of the yard documentation.
|
12
|
+
|
13
|
+
### Changed
|
14
|
+
- Improved EventProcessingError messages
|
15
|
+
- Fixed typo in constant name `EventSourcery::EventProcessing::ErrorHandlers::ConstantRetry::DEFAULT_RETRY_INTERVAL`
|
16
|
+
- Fixed typo in constant name `EventSourcery::EventProcessing::ErrorHandlers::ExponentialBackoffRetry::DEFAULT_RETRY_INTERVAL`
|
17
|
+
- Fixed typo in constant name `EventSourcery::EventProcessing::ErrorHandlers::ExponentialBackoffRetry::MAX_RETRY_INTERVAL`
|
18
|
+
- Errors of type `Exception` are now logged before being allowed to propagate.
|
19
|
+
|
9
20
|
## [0.14.0] - 2016-6-21
|
10
21
|
### Added
|
11
22
|
- Added `Event#to_h` method. This returns a hash of the event attributes.
|
data/README.md
CHANGED
@@ -56,6 +56,8 @@ The [example EventSourcery application](https://github.com/envato/event_sourcery
|
|
56
56
|
|
57
57
|
Otherwise you will generally need to add both event_sourcery and [event_sourcery-postgres](https://github.com/envato/event_sourcery-postgres) to your application.
|
58
58
|
|
59
|
+
If Event Sourcing or CQRS is a new concept to you, we highly recommend you watch [An In-Depth Look at Event Sourcing With CQRS](https://www.youtube.com/watch?v=EqpalkqJD8M&t=2680s). It explores some of the theory behind both Event Sourcing & CQRS and will help you better understand the building blocks of the Event Sourcery framework.
|
60
|
+
|
59
61
|
## Configuration
|
60
62
|
|
61
63
|
There are several ways to configure EventSourcery to your liking. The following presents some examples:
|
@@ -63,7 +65,7 @@ There are several ways to configure EventSourcery to your liking. The following
|
|
63
65
|
```ruby
|
64
66
|
EventSourcery.configure do |config|
|
65
67
|
# Add custom reporting of errors occurring during event processing.
|
66
|
-
# One might set up Rollbar here.
|
68
|
+
# One might set up an error reporting service like Rollbar here.
|
67
69
|
config.on_event_processor_error = proc { |exception, processor_name| … }
|
68
70
|
|
69
71
|
# Enable EventSourcery logging.
|
@@ -1,5 +1,55 @@
|
|
1
1
|
module EventSourcery
|
2
|
+
#
|
3
|
+
# EventSourcery::AggregateRoot provides a foundation for writing your own aggregate root classes.
|
4
|
+
# You can use it by including it in your classes, as show in the example code.
|
5
|
+
#
|
6
|
+
# Excerpt from {https://github.com/envato/event_sourcery/blob/master/docs/core-concepts.md EventSourcery Core Concepts} on Aggregates follows:
|
7
|
+
# === Aggregates and Command Handling
|
8
|
+
#
|
9
|
+
# An aggregate is a cluster of domain objects that can be treated as a single unit.
|
10
|
+
# Every transaction is scoped to a single aggregate. An aggregate will have one of its component objects be
|
11
|
+
# the aggregate root. Any references from outside the aggregate should only go to the aggregate root.
|
12
|
+
# The root can thus ensure the integrity of the aggregate as a whole.
|
13
|
+
#
|
14
|
+
# — DDD Aggregate
|
15
|
+
#
|
16
|
+
# Clients execute domain transactions against the system by issuing commands against aggregate roots.
|
17
|
+
# The result of these commands is new events being saved to the event store.
|
18
|
+
# A typical EventSourcery application will have one or more aggregate roots with multiple commands.
|
19
|
+
#
|
20
|
+
# The following partial example is taken from the EventSourceryTodoApp.
|
21
|
+
# Refer a more complete example {https://github.com/envato/event_sourcery_todo_app/blob/master/app/aggregates/todo.rb here}.
|
22
|
+
#
|
23
|
+
# @example
|
24
|
+
# module EventSourceryTodoApp
|
25
|
+
# module Aggregates
|
26
|
+
# class Todo
|
27
|
+
# include EventSourcery::AggregateRoot
|
28
|
+
#
|
29
|
+
# # An event handler that updates the aggregate's state from a event
|
30
|
+
# apply TodoAdded do |event|
|
31
|
+
# @added = true
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# # Method on the aggregate that processes a command and emits an event as a result
|
35
|
+
# def add(payload)
|
36
|
+
# raise UnprocessableEntity, "Todo #{id.inspect} already exists" if added
|
37
|
+
#
|
38
|
+
# apply_event(TodoAdded,
|
39
|
+
# aggregate_id: id,
|
40
|
+
# body: payload,
|
41
|
+
# )
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# private
|
45
|
+
#
|
46
|
+
# attr_reader :added
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
# end
|
2
50
|
module AggregateRoot
|
51
|
+
# Raised when the aggregate doesn't have a method to handle a given event.
|
52
|
+
# Consider implementing one if you get this error.
|
3
53
|
UnknownEventError = Class.new(RuntimeError)
|
4
54
|
|
5
55
|
def self.included(base)
|
@@ -10,8 +60,20 @@ module EventSourcery
|
|
10
60
|
end
|
11
61
|
|
12
62
|
module ClassMethods
|
63
|
+
# Collection of event handlers for the events that this aggregate cares about
|
64
|
+
#
|
65
|
+
# @return Hash
|
13
66
|
attr_reader :event_handlers
|
14
67
|
|
68
|
+
# Register an event handler for the specified event(s)
|
69
|
+
#
|
70
|
+
# @param event_classes one or more event types for which the handler is for
|
71
|
+
# @param block the event handler
|
72
|
+
#
|
73
|
+
# @example
|
74
|
+
# apply TodoAdded do |event|
|
75
|
+
# @added = true
|
76
|
+
# end
|
15
77
|
def apply(*event_classes, &block)
|
16
78
|
event_classes.each do |event_class|
|
17
79
|
@event_handlers[event_class.type] << block
|
@@ -19,6 +81,11 @@ module EventSourcery
|
|
19
81
|
end
|
20
82
|
end
|
21
83
|
|
84
|
+
# Load an aggregate instance based on the given ID and events
|
85
|
+
#
|
86
|
+
# @param id [String] ID (a UUID represented as a string) of the aggregate instance to be loaded
|
87
|
+
# @param events [Array] Events from which the aggregate's current state will be formed
|
88
|
+
# @param on_unknown_event [Proc] Optional. The proc to be run if an unknown event type (for which no event handler is registered using {ClassMethods#apply}) is to be loaded.
|
22
89
|
def initialize(id, events, on_unknown_event: EventSourcery.config.on_unknown_event)
|
23
90
|
@id = id.to_str
|
24
91
|
@version = 0
|
@@ -27,8 +94,20 @@ module EventSourcery
|
|
27
94
|
load_history(events)
|
28
95
|
end
|
29
96
|
|
30
|
-
|
97
|
+
# Collection of new events that are yet to be persisted
|
98
|
+
#
|
99
|
+
# @return Array
|
100
|
+
attr_reader :changes
|
31
101
|
|
102
|
+
# Current version of the aggregate. This is the same as the number of events
|
103
|
+
# currently loaded by the aggregate.
|
104
|
+
#
|
105
|
+
# @return Integer
|
106
|
+
attr_reader :version
|
107
|
+
|
108
|
+
# Clears any changes present in {changes}
|
109
|
+
#
|
110
|
+
# @api private
|
32
111
|
def clear_changes
|
33
112
|
@changes.clear
|
34
113
|
end
|
@@ -2,37 +2,64 @@ require 'logger'
|
|
2
2
|
|
3
3
|
module EventSourcery
|
4
4
|
class Config
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
# The default Proc to be run when an aggregate loads an event type that
|
6
|
+
# it doesn't know how to handle.
|
7
|
+
# What's specified here can be overridden when instantiating an aggregate
|
8
|
+
# instance. {AggregateRoot#initialize}
|
9
|
+
#
|
10
|
+
# If no custom Proc is set, by default behaviour is to raise {AggregateRoot::UnknownEventError}
|
11
|
+
#
|
12
|
+
# @return Proc
|
13
|
+
attr_accessor :on_unknown_event
|
14
|
+
|
15
|
+
# A Proc to be executed on an event processor error.
|
16
|
+
# App specific custom logic can be provided.
|
17
|
+
# i.e. report to an error reporting service like Rollbar.
|
18
|
+
#
|
19
|
+
# @return Proc
|
20
|
+
attr_accessor :on_event_processor_error
|
21
|
+
|
22
|
+
# @return EventStore::EventTypeSerializers::Underscored
|
23
|
+
attr_accessor :event_type_serializer
|
24
|
+
|
25
|
+
# @return EventProcessing::ErrorHandlers::ConstantRetry
|
26
|
+
attr_accessor :error_handler_class
|
9
27
|
|
10
28
|
attr_writer :logger,
|
11
29
|
:event_body_serializer,
|
12
30
|
:event_builder
|
13
31
|
|
32
|
+
# @api private
|
14
33
|
def initialize
|
15
34
|
@on_unknown_event = proc { |event, aggregate|
|
16
35
|
raise AggregateRoot::UnknownEventError, "#{event.type} is unknown to #{aggregate.class.name}"
|
17
36
|
}
|
18
37
|
@on_event_processor_error = proc { |exception, processor_name|
|
19
|
-
# app specific custom logic ie. report to
|
38
|
+
# app specific custom logic ie. report to an error reporting service like Rollbar.
|
20
39
|
}
|
21
40
|
@event_store = nil
|
22
41
|
@event_type_serializer = EventStore::EventTypeSerializers::Underscored.new
|
23
42
|
@error_handler_class = EventProcessing::ErrorHandlers::ConstantRetry
|
24
43
|
end
|
25
44
|
|
45
|
+
# Logger instance used by EventSourcery.
|
46
|
+
# By default EventSourcery will log to STDOUT with a log level of Logger::DEBUG
|
26
47
|
def logger
|
27
48
|
@logger ||= ::Logger.new(STDOUT).tap do |logger|
|
28
49
|
logger.level = Logger::DEBUG
|
29
50
|
end
|
30
51
|
end
|
31
52
|
|
53
|
+
# The event builder used by an event store to build event instances.
|
54
|
+
# By default {EventStore::EventBuilder} will be used.
|
55
|
+
# Provide a custom builder here to change how an event is built.
|
32
56
|
def event_builder
|
33
57
|
@event_builder || EventStore::EventBuilder.new(event_type_serializer: @event_type_serializer)
|
34
58
|
end
|
35
59
|
|
60
|
+
# The event body serializer used by the default event builder
|
61
|
+
# ({EventStore::EventBuilder}). By default {EventBodySerializer} will be used.
|
62
|
+
# Provide a custom serializer here to change how the event body is serialized.
|
36
63
|
def event_body_serializer
|
37
64
|
@event_body_serializer ||= EventBodySerializer.new
|
38
65
|
.add(Hash, EventBodySerializer::HashSerializer)
|
@@ -6,14 +6,19 @@ module EventSourcery
|
|
6
6
|
AtomicWriteToMultipleAggregatesNotSupported = Class.new(Error)
|
7
7
|
|
8
8
|
class EventProcessingError < Error
|
9
|
-
attr_reader :event
|
9
|
+
attr_reader :event, :processor
|
10
10
|
|
11
|
-
def initialize(event)
|
11
|
+
def initialize(event:, processor:)
|
12
12
|
@event = event
|
13
|
+
@processor = processor
|
13
14
|
end
|
14
15
|
|
15
16
|
def message
|
16
|
-
|
17
|
+
parts = []
|
18
|
+
parts << "#<#{processor.class} @@processor_name=#{processor.processor_name.inspect}>"
|
19
|
+
parts << "#<#{event.class} @id=#{event.id.inspect}, @uuid=#{event.uuid.inspect}, @type=#{event.type.inspect}>"
|
20
|
+
parts << "#<#{cause.class}: #{cause.message}>"
|
21
|
+
parts.join("\n") + "\n"
|
17
22
|
end
|
18
23
|
end
|
19
24
|
end
|
data/lib/event_sourcery/event.rb
CHANGED
@@ -1,7 +1,11 @@
|
|
1
1
|
module EventSourcery
|
2
|
+
# Represents an Event
|
2
3
|
class Event
|
3
4
|
include Comparable
|
4
5
|
|
6
|
+
# Event type
|
7
|
+
#
|
8
|
+
# Will return `nil` if called on an instance of {EventSourcery::Event}.
|
5
9
|
def self.type
|
6
10
|
unless self == Event
|
7
11
|
EventSourcery.config.event_type_serializer.serialize(self)
|
@@ -10,6 +14,42 @@ module EventSourcery
|
|
10
14
|
|
11
15
|
attr_reader :id, :uuid, :aggregate_id, :type, :body, :version, :created_at, :correlation_id, :causation_id
|
12
16
|
|
17
|
+
# @!attribute [r] id
|
18
|
+
# @return [Integer] unique identifier at the persistent layer
|
19
|
+
|
20
|
+
# @!attribute [r] uuid
|
21
|
+
# @return [String] unique identifier (UUID) for this event.
|
22
|
+
|
23
|
+
# @!attribute [r] aggregate_id
|
24
|
+
# @return [String] aggregate instance UUID to which this event belongs to.
|
25
|
+
|
26
|
+
# @!attribute [r] type
|
27
|
+
# @return event type
|
28
|
+
|
29
|
+
# @!attribute [r] body
|
30
|
+
# @return [Hash] Content of the event body.
|
31
|
+
|
32
|
+
# @!attribute [r] version
|
33
|
+
# @return [String] event version. Used by some event stores to guard against concurrency errors.
|
34
|
+
|
35
|
+
# @!attribute [r] created_at
|
36
|
+
# @return [Time] Created at timestamp (in UTC) for the event.
|
37
|
+
|
38
|
+
# @!attribute [r] correlation_id
|
39
|
+
# @return [String] UUID attached to the event that allows reference to a particular transaction or event chain. This value is often supplied as part of a command issued by clients.
|
40
|
+
|
41
|
+
# @!attribute [r] causation_id
|
42
|
+
# @return [String] UUID of the event that caused this event.
|
43
|
+
|
44
|
+
#
|
45
|
+
# @param id [Integer] Optional. Unique identifier at the persistent layer. By default this will be set by the underlying persistence layer when persisting the event.
|
46
|
+
# @param uuid [String] UUID as a string. Optional. Unique identifier for this event. A random UUID will be generated by default.
|
47
|
+
# @param aggregate_id [String] UUID as a string. Aggregate instance UUID to which this event belongs to.
|
48
|
+
# @param type [Class] Optional. Event type. {Event.type} will be used by default.
|
49
|
+
# @param version [String] Optional. Event's aggregate version. Used by some event stores to guard against concurrency errors.
|
50
|
+
# @param created_at [Time] Optional. Created at timestamp (in UTC) for the event.
|
51
|
+
# @param correlation_id [String] Optional. UUID attached to the event that allows reference to a particular transaction or event chain. This value is often supplied as part of a command issued by clients.
|
52
|
+
# @param causation_id [String] Optional. UUID of the event that caused this event.
|
13
53
|
def initialize(id: nil,
|
14
54
|
uuid: SecureRandom.uuid,
|
15
55
|
aggregate_id: nil,
|
@@ -30,6 +70,7 @@ module EventSourcery
|
|
30
70
|
@causation_id = causation_id
|
31
71
|
end
|
32
72
|
|
73
|
+
# Is this event persisted?
|
33
74
|
def persisted?
|
34
75
|
!id.nil?
|
35
76
|
end
|
@@ -46,10 +87,30 @@ module EventSourcery
|
|
46
87
|
id <=> other.id if other.is_a? Event
|
47
88
|
end
|
48
89
|
|
90
|
+
# create a new event identical to the old event except for the provided changes
|
91
|
+
#
|
92
|
+
# @param attributes [Hash]
|
93
|
+
# @return Event
|
94
|
+
# @example
|
95
|
+
# old_event = EventSourcery::Event.new(type: "item_added", causation_id: nil)
|
96
|
+
# new_event = old_event.with(causation_id: "05781bd6-796a-4a58-8573-b109f683fd99")
|
97
|
+
#
|
98
|
+
# new_event.type # => "item_added"
|
99
|
+
# new_event.causation_id # => "05781bd6-796a-4a58-8573-b109f683fd99"
|
100
|
+
#
|
101
|
+
# old_event.type # => "item_added"
|
102
|
+
# old_event.causation_id # => nil
|
103
|
+
#
|
104
|
+
# # Of course, with can accept any number of event attributes:
|
105
|
+
#
|
106
|
+
# new_event = old_event.with(id: 42, version: 77, body: { 'attr' => 'value' })
|
49
107
|
def with(**attributes)
|
50
108
|
self.class.new(**to_h.merge!(attributes))
|
51
109
|
end
|
52
110
|
|
111
|
+
# returns a hash of the event attributes
|
112
|
+
#
|
113
|
+
# @return Hash
|
53
114
|
def to_h
|
54
115
|
{
|
55
116
|
id: id,
|
@@ -1,5 +1,10 @@
|
|
1
1
|
module EventSourcery
|
2
|
+
# EventBodySerializer is used for serializing event bodies when persisting events.
|
2
3
|
class EventBodySerializer
|
4
|
+
# Serialize the given event body, with the default or the provided, serializer
|
5
|
+
#
|
6
|
+
# @param event_body event body to be serialized
|
7
|
+
# @param serializer Optional. Serializer to be used. By default {Config#event_body_serializer EventSourcery.config.event_body_serializer} will be used.
|
3
8
|
def self.serialize(event_body,
|
4
9
|
serializer: EventSourcery.config.event_body_serializer)
|
5
10
|
serializer.serialize(event_body)
|
@@ -9,16 +14,25 @@ module EventSourcery
|
|
9
14
|
@serializers = Hash.new(IdentitySerializer)
|
10
15
|
end
|
11
16
|
|
17
|
+
# Register a serializer (instance or block) for the specified type
|
18
|
+
#
|
19
|
+
# @param type The type for which the provided serializer will be used for
|
20
|
+
# @param serializer Optional. A serializer instance for the given type
|
21
|
+
# @param block_serializer [Proc] Optional. A block that performs the serialization
|
12
22
|
def add(type, serializer = nil, &block_serializer)
|
13
23
|
@serializers[type] = serializer || block_serializer
|
14
24
|
self
|
15
25
|
end
|
16
26
|
|
27
|
+
# Serialize the given event body
|
28
|
+
#
|
29
|
+
# @param object event body to be serialized
|
17
30
|
def serialize(object)
|
18
31
|
serializer = @serializers[object.class]
|
19
32
|
serializer.call(object, &method(:serialize))
|
20
33
|
end
|
21
34
|
|
35
|
+
# Default serializer for Hash objects
|
22
36
|
module HashSerializer
|
23
37
|
def self.call(hash, &serialize)
|
24
38
|
hash.each_with_object({}) do |(key, value), memo|
|
@@ -27,12 +41,14 @@ module EventSourcery
|
|
27
41
|
end
|
28
42
|
end
|
29
43
|
|
44
|
+
# Default serializer for Array objects
|
30
45
|
module ArraySerializer
|
31
46
|
def self.call(array, &serialize)
|
32
47
|
array.map(&serialize)
|
33
48
|
end
|
34
49
|
end
|
35
50
|
|
51
|
+
# Default catch all implementation for serializing any object
|
36
52
|
module IdentitySerializer
|
37
53
|
def self.call(object)
|
38
54
|
object
|
@@ -1,14 +1,21 @@
|
|
1
1
|
module EventSourcery
|
2
2
|
module EventProcessing
|
3
3
|
module ErrorHandlers
|
4
|
+
# Strategies for error handling.
|
4
5
|
class ConstantRetry
|
5
6
|
include EventSourcery::EventProcessing::ErrorHandlers::ErrorHandler
|
6
|
-
|
7
|
+
|
8
|
+
# The retry interval used with {with_error_handling}.
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
DEFAULT_RETRY_INTERVAL = 1
|
12
|
+
|
7
13
|
def initialize(processor_name:)
|
8
14
|
@processor_name = processor_name
|
9
|
-
@retry_interval =
|
15
|
+
@retry_interval = DEFAULT_RETRY_INTERVAL
|
10
16
|
end
|
11
17
|
|
18
|
+
# Will yield the block and attempt to retry after a defined retry interval {DEFAULT_RETRY_INTERVAL}.
|
12
19
|
def with_error_handling
|
13
20
|
yield
|
14
21
|
rescue => error
|
@@ -2,10 +2,13 @@ module EventSourcery
|
|
2
2
|
module EventProcessing
|
3
3
|
module ErrorHandlers
|
4
4
|
module ErrorHandler
|
5
|
+
# The default with_error_handling method. Will always raise NotImplementedError
|
6
|
+
#
|
7
|
+
# @raise [NotImplementedError]
|
5
8
|
def with_error_handling
|
6
9
|
raise NotImplementedError, 'Please implement #with_error_handling method'
|
7
10
|
end
|
8
|
-
|
11
|
+
|
9
12
|
private
|
10
13
|
|
11
14
|
def report_error(error)
|
@@ -3,14 +3,23 @@ module EventSourcery
|
|
3
3
|
module ErrorHandlers
|
4
4
|
class ExponentialBackoffRetry
|
5
5
|
include EventSourcery::EventProcessing::ErrorHandlers::ErrorHandler
|
6
|
-
|
7
|
-
|
6
|
+
|
7
|
+
# The starting value for the retry interval used with {with_error_handling}.
|
8
|
+
#
|
9
|
+
# @api private
|
10
|
+
DEFAULT_RETRY_INTERVAL = 1
|
11
|
+
|
12
|
+
# The maximum retry interval value to be used with {with_error_handling}.
|
13
|
+
#
|
14
|
+
# @api private
|
15
|
+
MAX_RETRY_INTERVAL = 64
|
8
16
|
|
9
17
|
def initialize(processor_name:)
|
10
18
|
@processor_name = processor_name
|
11
|
-
@retry_interval =
|
19
|
+
@retry_interval = DEFAULT_RETRY_INTERVAL
|
12
20
|
end
|
13
21
|
|
22
|
+
# Will yield the block and attempt to retry in an exponential backoff.
|
14
23
|
def with_error_handling
|
15
24
|
yield
|
16
25
|
rescue => error
|
@@ -25,13 +34,13 @@ module EventSourcery
|
|
25
34
|
def update_retry_interval(error)
|
26
35
|
if error.instance_of?(EventSourcery::EventProcessingError)
|
27
36
|
if @error_event_uuid == error.event.uuid
|
28
|
-
@retry_interval *= 2 if @retry_interval <
|
37
|
+
@retry_interval *= 2 if @retry_interval < MAX_RETRY_INTERVAL
|
29
38
|
else
|
30
39
|
@error_event_uuid = error.event.uuid
|
31
|
-
@retry_interval =
|
40
|
+
@retry_interval = DEFAULT_RETRY_INTERVAL
|
32
41
|
end
|
33
42
|
else
|
34
|
-
@retry_interval =
|
43
|
+
@retry_interval = DEFAULT_RETRY_INTERVAL
|
35
44
|
end
|
36
45
|
end
|
37
46
|
end
|
@@ -8,7 +8,9 @@ module EventSourcery
|
|
8
8
|
@event_source = event_source
|
9
9
|
@subscription_master = subscription_master
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
|
+
# This will start the Event Stream Processor which will subscribe
|
13
|
+
# to the event stream source.
|
12
14
|
def start
|
13
15
|
name_process
|
14
16
|
error_handler.with_error_handling do
|
@@ -16,6 +18,9 @@ module EventSourcery
|
|
16
18
|
subscribe_to_event_stream
|
17
19
|
EventSourcery.logger.info("Stopping #{@event_processor.processor_name}")
|
18
20
|
end
|
21
|
+
rescue Exception => e
|
22
|
+
EventSourcery.logger.error(e)
|
23
|
+
raise e
|
19
24
|
end
|
20
25
|
|
21
26
|
private
|
@@ -15,6 +15,7 @@ module EventSourcery
|
|
15
15
|
@exit_status = true
|
16
16
|
end
|
17
17
|
|
18
|
+
# Start each Event Stream Processor in a new child process.
|
18
19
|
def start!
|
19
20
|
with_logging do
|
20
21
|
start_processes
|
@@ -73,6 +74,7 @@ module EventSourcery
|
|
73
74
|
send_signal_to_remaining_processes(:KILL)
|
74
75
|
end
|
75
76
|
|
77
|
+
|
76
78
|
def send_signal_to_remaining_processes(signal)
|
77
79
|
Process.kill(signal, *@pids) unless all_processes_terminated?
|
78
80
|
rescue Errno::ESRCH
|
@@ -18,6 +18,12 @@ module EventSourcery
|
|
18
18
|
end
|
19
19
|
|
20
20
|
module ProcessHandler
|
21
|
+
# Handler that processes the given event.
|
22
|
+
#
|
23
|
+
# @raise [EventProcessingError] error raised due to processing isssues
|
24
|
+
# @raise [UnableToProcessEventError] raised if unable to process event type
|
25
|
+
#
|
26
|
+
# @param event [Event] the event to process
|
21
27
|
def process(event)
|
22
28
|
@_event = event
|
23
29
|
handlers = self.class.event_handlers[event.type]
|
@@ -34,28 +40,46 @@ module EventSourcery
|
|
34
40
|
end
|
35
41
|
@_event = nil
|
36
42
|
rescue
|
37
|
-
raise EventProcessingError.new(event)
|
43
|
+
raise EventProcessingError.new(event: event, processor: self)
|
38
44
|
end
|
39
45
|
end
|
40
46
|
|
41
47
|
module ClassMethods
|
48
|
+
|
49
|
+
# @attr_reader processes_event_types [Array] Process Event Types
|
50
|
+
# @attr_reader event_handlers [Hash] Hash of handler blocks keyed by event
|
42
51
|
attr_reader :processes_event_types, :event_handlers
|
43
52
|
|
53
|
+
# Registers the event types to process.
|
54
|
+
#
|
55
|
+
# @param event_types a collection of event types to process
|
44
56
|
def processes_events(*event_types)
|
45
57
|
@processes_event_types = Array(@processes_event_types) | event_types.map(&:to_s)
|
46
58
|
end
|
47
59
|
|
60
|
+
# Indicate that this class can process all event types. Note that you need to call this method if you
|
61
|
+
# intend to process all event types, without calling {ProcessHandler#process} for each event type.
|
48
62
|
def processes_all_events
|
49
63
|
define_singleton_method :processes? do |_|
|
50
64
|
true
|
51
65
|
end
|
52
66
|
end
|
53
67
|
|
68
|
+
# Can this class process this event type.
|
69
|
+
# If you use process_all_events this will always return true
|
70
|
+
#
|
71
|
+
# @param event_type the event type to check
|
72
|
+
#
|
73
|
+
# @return [True, False]
|
54
74
|
def processes?(event_type)
|
55
75
|
processes_event_types &&
|
56
76
|
processes_event_types.include?(event_type.to_s)
|
57
77
|
end
|
58
78
|
|
79
|
+
# Set the name of the processor.
|
80
|
+
# Returns the class name if no name is given.
|
81
|
+
#
|
82
|
+
# @param name [String] the name of the processor to set
|
59
83
|
def processor_name(name = nil)
|
60
84
|
if name
|
61
85
|
@processor_name = name
|
@@ -64,6 +88,10 @@ module EventSourcery
|
|
64
88
|
end
|
65
89
|
end
|
66
90
|
|
91
|
+
# Process the events for the given event types with the given block.
|
92
|
+
#
|
93
|
+
# @param event_classes the event type classes to process
|
94
|
+
# @param block the code block used to process
|
67
95
|
def process(*event_classes, &block)
|
68
96
|
event_classes.each do |event_class|
|
69
97
|
processes_events event_class.type
|
@@ -72,30 +100,42 @@ module EventSourcery
|
|
72
100
|
end
|
73
101
|
end
|
74
102
|
|
103
|
+
# Calls processes_event_types method on the instance class
|
75
104
|
def processes_event_types
|
76
105
|
self.class.processes_event_types
|
77
106
|
end
|
78
107
|
|
108
|
+
# Set up the event tracker
|
79
109
|
def setup
|
80
110
|
tracker.setup(processor_name)
|
81
111
|
end
|
82
112
|
|
113
|
+
# Reset the event tracker
|
83
114
|
def reset
|
84
115
|
tracker.reset_last_processed_event_id(processor_name)
|
85
116
|
end
|
86
117
|
|
118
|
+
# Return the last processed event id
|
119
|
+
#
|
120
|
+
# @return [Int] the id of the last processed event
|
87
121
|
def last_processed_event_id
|
88
122
|
tracker.last_processed_event_id(processor_name)
|
89
123
|
end
|
90
124
|
|
125
|
+
# Calls processor_name method on the instance class
|
91
126
|
def processor_name
|
92
127
|
self.class.processor_name
|
93
128
|
end
|
94
129
|
|
130
|
+
# Calls processes? method on the instance class
|
95
131
|
def processes?(event_type)
|
96
132
|
self.class.processes?(event_type)
|
97
133
|
end
|
98
134
|
|
135
|
+
# Subscribe to the given event source.
|
136
|
+
#
|
137
|
+
# @param event_source the event source to subscribe to
|
138
|
+
# @param subscription_master [SignalHandlingSubscriptionMaster]
|
99
139
|
def subscribe_to(event_source, subscription_master: EventStore::SignalHandlingSubscriptionMaster.new)
|
100
140
|
setup
|
101
141
|
event_source.subscribe(from_id: last_processed_event_id + 1,
|
@@ -105,6 +145,7 @@ module EventSourcery
|
|
105
145
|
end
|
106
146
|
end
|
107
147
|
|
148
|
+
# @attr_writer tracker the tracker for the class
|
108
149
|
attr_accessor :tracker
|
109
150
|
|
110
151
|
private
|