synapse-core 0.1.2
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.
- data/lib/synapse.rb +351 -0
- data/lib/synapse/command/command_bus.rb +45 -0
- data/lib/synapse/command/command_callback.rb +18 -0
- data/lib/synapse/command/command_filter.rb +17 -0
- data/lib/synapse/command/command_handler.rb +13 -0
- data/lib/synapse/command/dispatch_interceptor.rb +16 -0
- data/lib/synapse/command/duplication.rb +43 -0
- data/lib/synapse/command/errors.rb +27 -0
- data/lib/synapse/command/filters/validation.rb +32 -0
- data/lib/synapse/command/gateway.rb +34 -0
- data/lib/synapse/command/interceptor_chain.rb +31 -0
- data/lib/synapse/command/interceptors/serialization.rb +35 -0
- data/lib/synapse/command/message.rb +19 -0
- data/lib/synapse/command/rollback_policy.rb +22 -0
- data/lib/synapse/command/simple_command_bus.rb +138 -0
- data/lib/synapse/command/wiring.rb +47 -0
- data/lib/synapse/domain/aggregate_root.rb +121 -0
- data/lib/synapse/domain/event_container.rb +127 -0
- data/lib/synapse/domain/message.rb +82 -0
- data/lib/synapse/domain/message_builder.rb +34 -0
- data/lib/synapse/domain/stream.rb +108 -0
- data/lib/synapse/duplication.rb +60 -0
- data/lib/synapse/errors.rb +13 -0
- data/lib/synapse/event_bus/event_bus.rb +40 -0
- data/lib/synapse/event_bus/event_listener.rb +16 -0
- data/lib/synapse/event_bus/event_listener_proxy.rb +12 -0
- data/lib/synapse/event_bus/simple_event_bus.rb +69 -0
- data/lib/synapse/event_bus/wiring.rb +23 -0
- data/lib/synapse/event_sourcing/aggregate_factory.rb +69 -0
- data/lib/synapse/event_sourcing/aggregate_root.rb +104 -0
- data/lib/synapse/event_sourcing/conflict_resolver.rb +80 -0
- data/lib/synapse/event_sourcing/entity.rb +64 -0
- data/lib/synapse/event_sourcing/member.rb +72 -0
- data/lib/synapse/event_sourcing/repository.rb +119 -0
- data/lib/synapse/event_sourcing/snapshot/count_stream.rb +86 -0
- data/lib/synapse/event_sourcing/snapshot/count_trigger.rb +91 -0
- data/lib/synapse/event_sourcing/snapshot/taker.rb +73 -0
- data/lib/synapse/event_sourcing/storage_listener.rb +34 -0
- data/lib/synapse/event_sourcing/stream_decorator.rb +25 -0
- data/lib/synapse/event_store/errors.rb +16 -0
- data/lib/synapse/event_store/event_store.rb +43 -0
- data/lib/synapse/event_store/in_memory.rb +59 -0
- data/lib/synapse/event_store/mongo/cursor_event_stream.rb +63 -0
- data/lib/synapse/event_store/mongo/event_store.rb +86 -0
- data/lib/synapse/event_store/mongo/per_commit_strategy.rb +253 -0
- data/lib/synapse/event_store/mongo/per_event_strategy.rb +143 -0
- data/lib/synapse/event_store/mongo/storage_strategy.rb +113 -0
- data/lib/synapse/event_store/mongo/template.rb +73 -0
- data/lib/synapse/identifier.rb +23 -0
- data/lib/synapse/message.rb +101 -0
- data/lib/synapse/message_builder.rb +38 -0
- data/lib/synapse/process_manager/correlation.rb +32 -0
- data/lib/synapse/process_manager/correlation_resolver.rb +14 -0
- data/lib/synapse/process_manager/correlation_set.rb +58 -0
- data/lib/synapse/process_manager/process.rb +71 -0
- data/lib/synapse/repository/errors.rb +26 -0
- data/lib/synapse/repository/lock_manager.rb +40 -0
- data/lib/synapse/repository/locking.rb +97 -0
- data/lib/synapse/repository/pessimistic_lock_manager.rb +61 -0
- data/lib/synapse/repository/repository.rb +109 -0
- data/lib/synapse/serialization/converter.rb +39 -0
- data/lib/synapse/serialization/converter/chain.rb +45 -0
- data/lib/synapse/serialization/converter/factory.rb +68 -0
- data/lib/synapse/serialization/converter/identity.rb +29 -0
- data/lib/synapse/serialization/converter/json.rb +31 -0
- data/lib/synapse/serialization/converter/ox.rb +31 -0
- data/lib/synapse/serialization/errors.rb +12 -0
- data/lib/synapse/serialization/lazy_object.rb +61 -0
- data/lib/synapse/serialization/message/data.rb +25 -0
- data/lib/synapse/serialization/message/metadata.rb +13 -0
- data/lib/synapse/serialization/message/serialization_aware.rb +17 -0
- data/lib/synapse/serialization/message/serialization_aware_message.rb +66 -0
- data/lib/synapse/serialization/message/serialized_message.rb +201 -0
- data/lib/synapse/serialization/message/serialized_message_builder.rb +64 -0
- data/lib/synapse/serialization/message/serialized_object_cache.rb +50 -0
- data/lib/synapse/serialization/message/serializer.rb +47 -0
- data/lib/synapse/serialization/revision_resolver.rb +30 -0
- data/lib/synapse/serialization/serialized_object.rb +37 -0
- data/lib/synapse/serialization/serialized_type.rb +31 -0
- data/lib/synapse/serialization/serializer.rb +98 -0
- data/lib/synapse/serialization/serializer/marshal.rb +32 -0
- data/lib/synapse/serialization/serializer/oj.rb +34 -0
- data/lib/synapse/serialization/serializer/ox.rb +31 -0
- data/lib/synapse/uow/factory.rb +28 -0
- data/lib/synapse/uow/listener.rb +79 -0
- data/lib/synapse/uow/listener_collection.rb +93 -0
- data/lib/synapse/uow/nesting.rb +262 -0
- data/lib/synapse/uow/provider.rb +71 -0
- data/lib/synapse/uow/storage_listener.rb +14 -0
- data/lib/synapse/uow/transaction_manager.rb +27 -0
- data/lib/synapse/uow/uow.rb +178 -0
- data/lib/synapse/upcasting/chain.rb +78 -0
- data/lib/synapse/upcasting/context.rb +58 -0
- data/lib/synapse/upcasting/data.rb +30 -0
- data/lib/synapse/upcasting/single_upcaster.rb +57 -0
- data/lib/synapse/upcasting/upcaster.rb +55 -0
- data/lib/synapse/version.rb +3 -0
- data/lib/synapse/wiring/message_wiring.rb +41 -0
- data/lib/synapse/wiring/wire.rb +55 -0
- data/lib/synapse/wiring/wire_registry.rb +61 -0
- data/test/command/duplication_test.rb +54 -0
- data/test/command/gateway_test.rb +25 -0
- data/test/command/interceptor_chain_test.rb +26 -0
- data/test/command/serialization_test.rb +37 -0
- data/test/command/simple_command_bus_test.rb +141 -0
- data/test/command/validation_test.rb +42 -0
- data/test/command/wiring_test.rb +73 -0
- data/test/domain/aggregate_root_test.rb +57 -0
- data/test/domain/fixtures.rb +31 -0
- data/test/domain/message_test.rb +61 -0
- data/test/domain/stream_test.rb +35 -0
- data/test/duplication_test.rb +40 -0
- data/test/event_bus/wiring_test.rb +46 -0
- data/test/event_sourcing/aggregate_factory_test.rb +28 -0
- data/test/event_sourcing/aggregate_root_test.rb +76 -0
- data/test/event_sourcing/entity_test.rb +34 -0
- data/test/event_sourcing/fixtures.rb +85 -0
- data/test/event_sourcing/repository_test.rb +102 -0
- data/test/event_sourcing/snapshot/aggregate_taker_test.rb +39 -0
- data/test/event_sourcing/snapshot/deferred_taker_test.rb +19 -0
- data/test/event_sourcing/snapshot/integration_test.rb +65 -0
- data/test/event_sourcing/storage_listener_test.rb +77 -0
- data/test/event_store/in_memory_test.rb +47 -0
- data/test/process_manager/correlation_set_test.rb +49 -0
- data/test/process_manager/correlation_test.rb +24 -0
- data/test/process_manager/process_test.rb +52 -0
- data/test/repository/locking_test.rb +101 -0
- data/test/serialization/converter/factory_test.rb +33 -0
- data/test/serialization/converter/identity_test.rb +17 -0
- data/test/serialization/converter/json_test.rb +31 -0
- data/test/serialization/converter/ox_test.rb +40 -0
- data/test/serialization/fixtures.rb +17 -0
- data/test/serialization/lazy_object_test.rb +32 -0
- data/test/serialization/message/metadata_test.rb +19 -0
- data/test/serialization/message/serialization_aware_message_test.rb +88 -0
- data/test/serialization/message/serialized_message_builder_test.rb +41 -0
- data/test/serialization/message/serialized_message_test.rb +140 -0
- data/test/serialization/message/serializer_test.rb +50 -0
- data/test/serialization/revision_resolver_test.rb +12 -0
- data/test/serialization/serialized_object_test.rb +36 -0
- data/test/serialization/serialized_type_test.rb +27 -0
- data/test/serialization/serializer/marshal_test.rb +22 -0
- data/test/serialization/serializer/oj_test.rb +24 -0
- data/test/serialization/serializer/ox_test.rb +36 -0
- data/test/serialization/serializer_test.rb +20 -0
- data/test/test_helper.rb +19 -0
- data/test/uow/factory_test.rb +23 -0
- data/test/uow/outer_commit_listener_test.rb +50 -0
- data/test/uow/provider_test.rb +70 -0
- data/test/uow/uow_test.rb +337 -0
- data/test/upcasting/chain_test.rb +29 -0
- data/test/upcasting/fixtures.rb +66 -0
- data/test/wiring/wire_registry_test.rb +60 -0
- data/test/wiring/wire_test.rb +51 -0
- metadata +263 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module Command
|
|
3
|
+
# Filter that prevents duplicate commands from reaching the command handlers
|
|
4
|
+
class DuplicationFilter < CommandFilter
|
|
5
|
+
# @param [DuplicationRecorder] recorder
|
|
6
|
+
# @return [undefined]
|
|
7
|
+
def initialize(recorder)
|
|
8
|
+
@recorder = recorder
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @param [CommandMessage] command
|
|
12
|
+
# @return [CommandMessage] The command to dispatch on the bus
|
|
13
|
+
def filter(command)
|
|
14
|
+
@recorder.record command
|
|
15
|
+
command
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Interceptor that removes commands from the duplication recorder if their execution results
|
|
20
|
+
# in a transient error (like concurrency error) being raised. This way, the same command can
|
|
21
|
+
# be retried by the client or command gateway
|
|
22
|
+
class DuplicationCleanupInterceptor < DispatchInterceptor
|
|
23
|
+
# @param [DuplicationRecorder] recorder
|
|
24
|
+
# @return [undefined]
|
|
25
|
+
def initialize(recorder)
|
|
26
|
+
@recorder = recorder
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param [CommandMessage] command
|
|
30
|
+
# @param [UnitOfWork] unit The current unit of work for this command dispatch
|
|
31
|
+
# @param [InterceptorChain] chain
|
|
32
|
+
# @return [Object] The result of the execution of the command
|
|
33
|
+
def intercept(command, unit, chain)
|
|
34
|
+
begin
|
|
35
|
+
chain.proceed command
|
|
36
|
+
rescue TransientError
|
|
37
|
+
@recorder.forget command
|
|
38
|
+
raise
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module Command
|
|
3
|
+
# Raised when an error is raised during the handling of a command
|
|
4
|
+
class CommandExecutionError < SynapseError
|
|
5
|
+
# @return [Exception]
|
|
6
|
+
attr_reader :cause
|
|
7
|
+
|
|
8
|
+
# @param [Exception] cause
|
|
9
|
+
# @return [undefined]
|
|
10
|
+
def initialize(cause)
|
|
11
|
+
@cause = cause
|
|
12
|
+
set_backtrace cause.backtrace
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @return [String]
|
|
16
|
+
def inspect
|
|
17
|
+
@cause.inspect
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Raised when a command is refused because of structural validation errors
|
|
22
|
+
class CommandValidationError < NonTransientError; end
|
|
23
|
+
|
|
24
|
+
# Raised when a dispatched command has no handler subscribed to it
|
|
25
|
+
class NoHandlerError < NonTransientError; end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module Command
|
|
3
|
+
class ActiveModelValidationFilter < CommandFilter
|
|
4
|
+
# @raise [ActiveModelValidationError] If command doesn't pass validation
|
|
5
|
+
# @param [CommandMessage] command
|
|
6
|
+
# @return [CommandMessage] The command to dispatch on the bus
|
|
7
|
+
def filter(command)
|
|
8
|
+
payload = command.payload
|
|
9
|
+
|
|
10
|
+
if payload.respond_to? :valid?
|
|
11
|
+
unless payload.valid?
|
|
12
|
+
raise ActiveModelValidationError, payload.errors
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
command
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Raised when a command with ActiveModel doesn't pass validation
|
|
21
|
+
class ActiveModelValidationError < CommandValidationError
|
|
22
|
+
# @return [ActiveModel::Errors]
|
|
23
|
+
attr_reader :errors
|
|
24
|
+
|
|
25
|
+
# @param [ActiveModel::Errors] errors
|
|
26
|
+
# @return [undefined]
|
|
27
|
+
def initialize(errors)
|
|
28
|
+
@errors = errors
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module Command
|
|
3
|
+
# Simplified interface to the command bus
|
|
4
|
+
class CommandGateway
|
|
5
|
+
# @param [CommandBus] command_bus
|
|
6
|
+
# @return [undefined]
|
|
7
|
+
def initialize(command_bus)
|
|
8
|
+
@command_bus = command_bus
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Fire and forget method of sending a command to the command bus
|
|
12
|
+
#
|
|
13
|
+
# @param [Object] command
|
|
14
|
+
# @return [undefined]
|
|
15
|
+
def send(command)
|
|
16
|
+
@command_bus.dispatch(as_command_message(command))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# @param [Object] command
|
|
22
|
+
# @return [CommandMessage]
|
|
23
|
+
def as_command_message(command)
|
|
24
|
+
unless command.is_a? CommandMessage
|
|
25
|
+
command = CommandMessage.build do |builder|
|
|
26
|
+
builder.payload = command
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
command
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module Command
|
|
3
|
+
# Represents a mechanism for controlling the flow through a chain of interceptors that
|
|
4
|
+
# eventually ends in the invocation of the command handler
|
|
5
|
+
#
|
|
6
|
+
# Interceptors can either continue the dispatch of a command by calling {#proceed} or choose
|
|
7
|
+
# to block the dispatch by not calling {#proceed} at all. Interceptors can also replace the
|
|
8
|
+
# command message that will be passed on in the chain.
|
|
9
|
+
class InterceptorChain
|
|
10
|
+
# @param [UnitOfWork] unit The current unit of work for this command dispatch
|
|
11
|
+
# @param [Array] interceptors
|
|
12
|
+
# @param [CommandHandler] handler
|
|
13
|
+
# @return [undefined]
|
|
14
|
+
def initialize(unit, interceptors, handler)
|
|
15
|
+
@unit = unit
|
|
16
|
+
@interceptors = interceptors.each
|
|
17
|
+
@handler = handler
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param [CommandMessage] command The command to proceed with
|
|
21
|
+
# @return [Object] The result of the execution of the command
|
|
22
|
+
def proceed(command)
|
|
23
|
+
begin
|
|
24
|
+
@interceptors.next.intercept command, @unit, self
|
|
25
|
+
rescue StopIteration
|
|
26
|
+
@handler.handle command, @unit
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module Command
|
|
3
|
+
# Interceptor that registers a unit of work listener that wraps each event message in a
|
|
4
|
+
# serialization-aware message. This provides optimization in cases where storage (in an event
|
|
5
|
+
# store) and publication (on the event bus) use the same serialization mechansim.
|
|
6
|
+
class SerializationOptimizingInterceptor < DispatchInterceptor
|
|
7
|
+
def initialize
|
|
8
|
+
@listener = SerializationOptimizingListener.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @param [CommandMessage] command
|
|
12
|
+
# @param [UnitOfWork] unit The current unit of work for this command dispatch
|
|
13
|
+
# @param [InterceptorChain] chain
|
|
14
|
+
# @return [Object] The result of the execution of the command
|
|
15
|
+
def intercept(command, unit, chain)
|
|
16
|
+
unit.register_listener @listener
|
|
17
|
+
chain.proceed command
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @api private
|
|
22
|
+
class SerializationOptimizingListener < UnitOfWork::UnitOfWorkListener
|
|
23
|
+
# @param [UnitOfWork] unit
|
|
24
|
+
# @param [EventMessage] event
|
|
25
|
+
# @return [EventMessage]
|
|
26
|
+
def on_event_registered(unit, event)
|
|
27
|
+
if event.is_a? Domain::DomainEventMessage
|
|
28
|
+
Serialization::SerializationAwareDomainEventMessage.decorate event
|
|
29
|
+
else
|
|
30
|
+
Serialization::SerializationAwareEventMessage.decorate event
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module Command
|
|
3
|
+
# Represents an intent to change application state
|
|
4
|
+
class CommandMessage < Message
|
|
5
|
+
# @return [Class]
|
|
6
|
+
def self.builder
|
|
7
|
+
CommandMessageBuilder
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Message builder capable of producing CommandMessage instances
|
|
12
|
+
class CommandMessageBuilder < MessageBuilder
|
|
13
|
+
# @return [CommandMessage]
|
|
14
|
+
def build
|
|
15
|
+
CommandMessage.new @id, @metadata, @payload
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module Command
|
|
3
|
+
# Represents a mechanism for determining whether or not to rollback the unit of work for a
|
|
4
|
+
# command dispatch in case an exception occurs during the dispatch
|
|
5
|
+
class RollbackPolicy
|
|
6
|
+
# Returns true if the unit of work should be rolled back
|
|
7
|
+
#
|
|
8
|
+
# @param [Exception] exception
|
|
9
|
+
# @return [Boolean]
|
|
10
|
+
def should_rollback(exception); end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Implementation of a rollback policy that performs a rollback on any exception
|
|
14
|
+
class RollbackOnAnyExceptionPolicy < RollbackPolicy
|
|
15
|
+
# @param [Exception] exception
|
|
16
|
+
# @return [Boolean]
|
|
17
|
+
def should_rollback(exception)
|
|
18
|
+
true
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module Command
|
|
3
|
+
# Implementation of a command bus that dispatches commands in the calling thread
|
|
4
|
+
# @todo Thread safety?
|
|
5
|
+
class SimpleCommandBus < CommandBus
|
|
6
|
+
# @return [RollbackPolicy]
|
|
7
|
+
attr_accessor :rollback_policy
|
|
8
|
+
|
|
9
|
+
# @return [Array]
|
|
10
|
+
attr_reader :filters
|
|
11
|
+
|
|
12
|
+
# @return [Array]
|
|
13
|
+
attr_reader :interceptors
|
|
14
|
+
|
|
15
|
+
# @param [UnitOfWorkFactory] unit_factory
|
|
16
|
+
# @return [undefined]
|
|
17
|
+
def initialize(unit_factory)
|
|
18
|
+
@handlers = Hash.new
|
|
19
|
+
@filters = Array.new
|
|
20
|
+
@interceptors = Array.new
|
|
21
|
+
@logger = Logging.logger.new self.class
|
|
22
|
+
@rollback_policy = RollbackOnAnyExceptionPolicy.new
|
|
23
|
+
@unit_factory = unit_factory
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param [CommandMessage] command
|
|
27
|
+
# @return [undefined]
|
|
28
|
+
def dispatch(command)
|
|
29
|
+
dispatch_with_callback command, CommandCallback.new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @param [CommandMessage] command
|
|
33
|
+
# @param [CommandCallback] callback
|
|
34
|
+
# @return [undefined]
|
|
35
|
+
def dispatch_with_callback(command, callback)
|
|
36
|
+
begin
|
|
37
|
+
result = perform_dispatch command
|
|
38
|
+
callback.on_success result
|
|
39
|
+
rescue => exception
|
|
40
|
+
backtrace = exception.backtrace.join $/
|
|
41
|
+
@logger.error 'Exception occured while dispatching command [%s] [%s]: %s %s' %
|
|
42
|
+
[command.payload_type, command.id, exception.inspect, backtrace]
|
|
43
|
+
|
|
44
|
+
callback.on_failure exception
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @param [Class] command_type
|
|
49
|
+
# @param [CommandHandler] handler
|
|
50
|
+
# @return [undefined]
|
|
51
|
+
def subscribe(command_type, handler)
|
|
52
|
+
if @handlers.has_key? command_type
|
|
53
|
+
current_handler = @handlers.fetch command_type
|
|
54
|
+
@logger.info 'Command handler [%s] is being replaced by [%s] for command type [%s]' %
|
|
55
|
+
[current_handler.class, handler.class, command_type]
|
|
56
|
+
else
|
|
57
|
+
@logger.debug 'Command handler [%s] subscribed to command type [%s]' %
|
|
58
|
+
[handler.class, command_type]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
@handlers.store command_type, handler
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @param [Class] command_type
|
|
65
|
+
# @param [CommandHandler] handler
|
|
66
|
+
# @return [undefined]
|
|
67
|
+
def unsubscribe(command_type, handler)
|
|
68
|
+
if @handlers.has_key? command_type
|
|
69
|
+
current_handler = @handlers.fetch command_type
|
|
70
|
+
if current_handler.equal? handler
|
|
71
|
+
@handlers.delete command_type
|
|
72
|
+
|
|
73
|
+
@logger.debug 'Command handler [%s] unsubscribed from command type [%s]' %
|
|
74
|
+
[handler.class, command_type]
|
|
75
|
+
else
|
|
76
|
+
@logger.info 'Command type [%s] subscribed to handler [%s] not [%s]' %
|
|
77
|
+
[command_type, current_handler.class, handler.class]
|
|
78
|
+
end
|
|
79
|
+
else
|
|
80
|
+
@logger.info 'Command type [%s] not subscribed to any handler' % command_type
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
protected
|
|
85
|
+
|
|
86
|
+
# @raise [CommandExecutionError]
|
|
87
|
+
# If an error occurs during the handling of the command
|
|
88
|
+
# @raise [NoHandlerError]
|
|
89
|
+
# If no handler is subscribed that is capable of handling the command
|
|
90
|
+
# @param [CommandMessage] command
|
|
91
|
+
# @return [Object] The result from the command handler
|
|
92
|
+
def perform_dispatch(command)
|
|
93
|
+
@filters.each do |filter|
|
|
94
|
+
command = filter.filter command
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
handler = handler_for command
|
|
98
|
+
unit = @unit_factory.create
|
|
99
|
+
|
|
100
|
+
chain = InterceptorChain.new unit, @interceptors, handler
|
|
101
|
+
|
|
102
|
+
begin
|
|
103
|
+
@logger.debug 'Dispatching command [%s] [%s] to handler [%s]' %
|
|
104
|
+
[command.id, command.payload_type, handler.class]
|
|
105
|
+
|
|
106
|
+
result = chain.proceed command
|
|
107
|
+
rescue => exception
|
|
108
|
+
if @rollback_policy.should_rollback exception
|
|
109
|
+
@logger.debug 'Unit of work is being rolled back due to rollback policy'
|
|
110
|
+
unit.rollback exception
|
|
111
|
+
else
|
|
112
|
+
@logger.info 'Unit of work is being committed due to rollback policy'
|
|
113
|
+
unit.commit
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
raise CommandExecutionError, exception
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
unit.commit
|
|
120
|
+
|
|
121
|
+
result
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# @raise [NoHandlerError]
|
|
125
|
+
# @param [CommandMessage] command
|
|
126
|
+
# @return [CommandHandler]
|
|
127
|
+
def handler_for(command)
|
|
128
|
+
type = command.payload_type
|
|
129
|
+
|
|
130
|
+
begin
|
|
131
|
+
@handlers.fetch type
|
|
132
|
+
rescue KeyError
|
|
133
|
+
raise NoHandlerError, 'No handler subscribed for command [%s]' % type
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module Command
|
|
3
|
+
# Mixin for a command handler that wishes to use the wiring DSL
|
|
4
|
+
module WiringCommandHandler
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
include CommandHandler
|
|
7
|
+
include Wiring::MessageWiring
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
self.wire_registry = Wiring::WireRegistry.new false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# @param [CommandMessage] command
|
|
14
|
+
# @param [UnitOfWork] current_unit Current unit of work
|
|
15
|
+
# @return [Object] The result of handling the given command
|
|
16
|
+
def handle(command, current_unit)
|
|
17
|
+
wire = self.wire_registry.wire_for command.payload_type
|
|
18
|
+
|
|
19
|
+
unless wire
|
|
20
|
+
raise ArgumentError, 'Not capable of handling [%s] commands' % command.payload_type
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
invoke_wire command, wire
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Subscribes this handler to the given command bus for any types that have been wired
|
|
27
|
+
#
|
|
28
|
+
# @param [CommandBus] command_bus
|
|
29
|
+
# @return [undefined]
|
|
30
|
+
def subscribe(command_bus)
|
|
31
|
+
self.wire_registry.each_type do |type|
|
|
32
|
+
command_bus.subscribe type, self
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Unsubscribes this handler from the given command bus for any types that have been wired
|
|
37
|
+
#
|
|
38
|
+
# @param [CommandBus] command_bus
|
|
39
|
+
# @return [undefined]
|
|
40
|
+
def unsubscribe(command_bus)
|
|
41
|
+
self.wire_registry.each_type do |type|
|
|
42
|
+
command_bus.unsubscribe type, self
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module Domain
|
|
3
|
+
# Mixin module for a basic aggregate root
|
|
4
|
+
module AggregateRoot
|
|
5
|
+
# @return [Boolean] True if this aggregate has been marked for deletion
|
|
6
|
+
attr_reader :deleted
|
|
7
|
+
|
|
8
|
+
alias deleted? deleted
|
|
9
|
+
|
|
10
|
+
# @return [Object] The identifier of this aggregate
|
|
11
|
+
attr_reader :id
|
|
12
|
+
|
|
13
|
+
# @return [Integer] The version of this aggregate
|
|
14
|
+
attr_reader :version
|
|
15
|
+
|
|
16
|
+
# Adds a listener that will be notified when this aggregate registers an event to be published
|
|
17
|
+
#
|
|
18
|
+
# If an event registration listener is added after events have already been registered, it
|
|
19
|
+
# will still get a change to process the uncommitted events in this aggregate.
|
|
20
|
+
#
|
|
21
|
+
# @param [#call] listener
|
|
22
|
+
# @return [undefined]
|
|
23
|
+
def add_registration_listener(&listener)
|
|
24
|
+
event_container.add_registration_listener listener
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Marks this aggregate as committed by a repository
|
|
28
|
+
# @return [undefined]
|
|
29
|
+
def mark_committed
|
|
30
|
+
if @event_container
|
|
31
|
+
@last_sequence_number = @event_container.last_sequence_number
|
|
32
|
+
@event_container.mark_committed
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns the number of uncommitted events published by this aggregate
|
|
37
|
+
# @return [Integer]
|
|
38
|
+
def uncommitted_event_count
|
|
39
|
+
unless @event_container
|
|
40
|
+
return 0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
@event_container.size
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns a domain event strema containing any uncommitted events published by this aggregate
|
|
47
|
+
# @return [DomainEventStream]
|
|
48
|
+
def uncommitted_events
|
|
49
|
+
unless @event_container
|
|
50
|
+
return SimpleDomainEventStream.new
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
@event_container.to_stream
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
protected
|
|
57
|
+
|
|
58
|
+
# Publishes a domain event with the given payload and metadata (optional)
|
|
59
|
+
#
|
|
60
|
+
# @param [Object] payload
|
|
61
|
+
# Payload of the message; the actual event object
|
|
62
|
+
#
|
|
63
|
+
# @param [Hash] metadata
|
|
64
|
+
# Metadata associated with the event
|
|
65
|
+
#
|
|
66
|
+
# @return [DomainEventMessage] The event that will be committed
|
|
67
|
+
def publish_event(payload, metadata = nil)
|
|
68
|
+
event_container.register_event payload, metadata
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Initializes the event container with the given sequence number
|
|
72
|
+
#
|
|
73
|
+
# @param [Integer] last_sequence_number
|
|
74
|
+
# The sequence number of the last committed event for this aggregate
|
|
75
|
+
#
|
|
76
|
+
# @return [undefined]
|
|
77
|
+
def initialize_event_container(last_sequence_number)
|
|
78
|
+
event_container.initialize_sequence_number last_sequence_number
|
|
79
|
+
@last_sequence_number = last_sequence_number >= 0 ? last_sequence_number : nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Returns the sequence number of the last committed event
|
|
83
|
+
# @return [Integer]
|
|
84
|
+
def last_committed_sequence_number
|
|
85
|
+
unless @event_container
|
|
86
|
+
return @last_sequence_number
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
@event_container.last_committed_sequence_number
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Marks this aggregate for deletion by its repository
|
|
93
|
+
# @return [undefined]
|
|
94
|
+
def mark_deleted
|
|
95
|
+
@deleted = true
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
# Initializes the uncommitted event container for this aggregate, if not already
|
|
101
|
+
#
|
|
102
|
+
# @raise [AggregateIdentifierNotInitializedError] If identifier not set
|
|
103
|
+
# @return [EventContainer]
|
|
104
|
+
def event_container
|
|
105
|
+
unless @event_container
|
|
106
|
+
unless @id
|
|
107
|
+
raise AggregateIdentifierNotInitializedError
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
@event_container = EventContainer.new @id
|
|
111
|
+
@event_container.initialize_sequence_number @last_sequence_number
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
@event_container
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Raised when an event is published but the aggregate identifier is not set
|
|
119
|
+
class AggregateIdentifierNotInitializedError < NonTransientError; end
|
|
120
|
+
end
|
|
121
|
+
end
|