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,38 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
# Builder that is used to easily create and populate messages
|
|
3
|
+
class MessageBuilder
|
|
4
|
+
# @return [String]
|
|
5
|
+
attr_accessor :id
|
|
6
|
+
|
|
7
|
+
# @return [Hash]
|
|
8
|
+
attr_accessor :metadata
|
|
9
|
+
|
|
10
|
+
# @return [Object]
|
|
11
|
+
attr_accessor :payload
|
|
12
|
+
|
|
13
|
+
# Convenience method that yields a new builder, populates defaults and returns the newly
|
|
14
|
+
# built message instance
|
|
15
|
+
#
|
|
16
|
+
# @yield [MessageBuilder]
|
|
17
|
+
# @return [Message]
|
|
18
|
+
def self.build
|
|
19
|
+
builder = self.new
|
|
20
|
+
|
|
21
|
+
yield builder if block_given?
|
|
22
|
+
|
|
23
|
+
builder.populate_defaults
|
|
24
|
+
builder.build
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [Message]
|
|
28
|
+
def build
|
|
29
|
+
Message.new @id, @metadata, @payload
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [undefined]
|
|
33
|
+
def populate_defaults
|
|
34
|
+
@id ||= IdentifierFactory.instance.generate
|
|
35
|
+
@metadata ||= Hash.new
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module ProcessManager
|
|
3
|
+
# Combination key and value that is used to correlate incoming events with process instances
|
|
4
|
+
class Correlation
|
|
5
|
+
# @return [Symbol]
|
|
6
|
+
attr_reader :key
|
|
7
|
+
|
|
8
|
+
# @return [String]
|
|
9
|
+
attr_reader :value
|
|
10
|
+
|
|
11
|
+
# @param [Symbol] key
|
|
12
|
+
# @param [String] value
|
|
13
|
+
# @return [undefined]
|
|
14
|
+
def initialize(key, value)
|
|
15
|
+
@key = key.to_sym
|
|
16
|
+
@value = value.to_s
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ==(other)
|
|
20
|
+
self.class === other and
|
|
21
|
+
other.key == @key and
|
|
22
|
+
other.value == @value
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
alias eql? ==
|
|
26
|
+
|
|
27
|
+
def hash
|
|
28
|
+
@key.hash ^ @value.hash
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module ProcessManager
|
|
3
|
+
# Represents a mechanism for determining correlations between events and process instances
|
|
4
|
+
# @abstract
|
|
5
|
+
class CorrelationResolver
|
|
6
|
+
# Resolves a correlation from the given event
|
|
7
|
+
#
|
|
8
|
+
# @abstract
|
|
9
|
+
# @param [EventMessage] event
|
|
10
|
+
# @return [Correlation]
|
|
11
|
+
def resolve(event); end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module ProcessManager
|
|
3
|
+
# Container that tracks additions and deletions of correlations for a process instance
|
|
4
|
+
class CorrelationSet
|
|
5
|
+
extend Forwardable
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
# @return [Set]
|
|
9
|
+
attr_reader :correlations
|
|
10
|
+
|
|
11
|
+
# @return [Set]
|
|
12
|
+
attr_reader :additions
|
|
13
|
+
|
|
14
|
+
# @return [Set]
|
|
15
|
+
attr_reader :deletions
|
|
16
|
+
|
|
17
|
+
def initialize
|
|
18
|
+
@correlations = Set.new
|
|
19
|
+
@additions = Set.new
|
|
20
|
+
@deletions = Set.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Resets the tracked changes
|
|
24
|
+
# @return [undefined]
|
|
25
|
+
def commit
|
|
26
|
+
@additions.clear
|
|
27
|
+
@deletions.clear
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Adds the given correlation to this set, if not previously added
|
|
31
|
+
#
|
|
32
|
+
# @param [Correlation] correlation
|
|
33
|
+
# @return [Boolean]
|
|
34
|
+
def add(correlation)
|
|
35
|
+
if @correlations.add? correlation
|
|
36
|
+
unless @deletions.delete? correlation
|
|
37
|
+
@additions.add correlation
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Removes the given correlation from this set, if previously added
|
|
43
|
+
#
|
|
44
|
+
# @param [Correlation] correlation
|
|
45
|
+
# @return [Boolean]
|
|
46
|
+
def delete(correlation)
|
|
47
|
+
if @correlations.delete? correlation
|
|
48
|
+
unless @additions.delete? correlation
|
|
49
|
+
@deletions.add correlation
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Delegates enumeration to the backing correlation set
|
|
55
|
+
def_delegators :@correlations, :each, :size
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module ProcessManager
|
|
3
|
+
# The term process is used in Enterprise Integration Patterns to describe a mechanism used to
|
|
4
|
+
# "maintain the state of the sequence and determine the next processing step based on
|
|
5
|
+
# intermediate results" (Hohpe 279). Processes are also called sagas in some CQRS frameworks.
|
|
6
|
+
#
|
|
7
|
+
# @abstract
|
|
8
|
+
class Process
|
|
9
|
+
# @return [String] The unique identifier of this process
|
|
10
|
+
attr_reader :id
|
|
11
|
+
|
|
12
|
+
# @return [CorrelationSet] The correlations for this process
|
|
13
|
+
attr_reader :correlations
|
|
14
|
+
|
|
15
|
+
# @return [Boolean] True if this process is active
|
|
16
|
+
attr_reader :active
|
|
17
|
+
|
|
18
|
+
alias active? active
|
|
19
|
+
|
|
20
|
+
# @param [String] id
|
|
21
|
+
# @return [undefined]
|
|
22
|
+
def initialize(id = nil)
|
|
23
|
+
unless id
|
|
24
|
+
id = IdentifierFactory.instance.generate
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
@id = id
|
|
28
|
+
@correlations = CorrelationSet.new
|
|
29
|
+
@active = true
|
|
30
|
+
|
|
31
|
+
correlate_with :process_id, id
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Handles the given event
|
|
35
|
+
#
|
|
36
|
+
# The actual result of the processing depends on the implementation of the process.
|
|
37
|
+
# Implementations are highly discouraged from throwing exceptions.
|
|
38
|
+
#
|
|
39
|
+
# @abstract
|
|
40
|
+
# @param [EventMessage] event
|
|
41
|
+
# @return [undefined]
|
|
42
|
+
def handle(event); end
|
|
43
|
+
|
|
44
|
+
protected
|
|
45
|
+
|
|
46
|
+
# Correlates this process instance with the given key and value
|
|
47
|
+
#
|
|
48
|
+
# @param [Symbol] key
|
|
49
|
+
# @param [String] value
|
|
50
|
+
# @return [undefined]
|
|
51
|
+
def correlate_with(key, value)
|
|
52
|
+
@correlations.add(Correlation.new(key, value))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Dissociates this process instance from the given key and value
|
|
56
|
+
#
|
|
57
|
+
# @param [Symbol] key
|
|
58
|
+
# @param [String] value
|
|
59
|
+
# @return [undefined]
|
|
60
|
+
def dissociate_from(key, value)
|
|
61
|
+
@correlations.delete(Correlation.new(key, value))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Marks this process as finished
|
|
65
|
+
# @return [undefined]
|
|
66
|
+
def finish
|
|
67
|
+
@active = false
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module Repository
|
|
3
|
+
# Raised when an aggregate could not be found by a repository
|
|
4
|
+
class AggregateNotFoundError < NonTransientError; end
|
|
5
|
+
|
|
6
|
+
# Raised when concurrent access to a repository was detected; the cause is most likely
|
|
7
|
+
# that two threads were modifying the same aggregate.
|
|
8
|
+
class ConcurrencyError < TransientError; end
|
|
9
|
+
|
|
10
|
+
# Raised when conflicting concurrent modifications are detected
|
|
11
|
+
class ConflictingModificationError < NonTransientError; end
|
|
12
|
+
|
|
13
|
+
# Raised when the version number of the aggregate being loaded didn't match the expected
|
|
14
|
+
# version number given. This typically means that the aggregate has been modified by another
|
|
15
|
+
# thread between the moment the data was queried and the command modifying the aggregate
|
|
16
|
+
# was handled.
|
|
17
|
+
class ConflictingAggregateVersionError < ConflictingModificationError
|
|
18
|
+
# @param [AggregateRoot] aggregate
|
|
19
|
+
# @param [Integer] expected_version
|
|
20
|
+
# @return [undefined]
|
|
21
|
+
def initialize(aggregate, expected_version)
|
|
22
|
+
super 'Aggregate [%s] has version %s, expected %s' % [aggregate.id, aggregate.version, expected_version]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module Repository
|
|
3
|
+
# Represents a mechanism for locking aggregates for modification
|
|
4
|
+
# @abstract
|
|
5
|
+
class LockManager
|
|
6
|
+
# Ensures that the current thread holds a valid lock for the given aggregate
|
|
7
|
+
#
|
|
8
|
+
# @abstract
|
|
9
|
+
# @param [AggregateRoot] aggregate
|
|
10
|
+
# @return [Boolean]
|
|
11
|
+
def validate_lock(aggregate); end
|
|
12
|
+
|
|
13
|
+
# Obtains a lock for an aggregate with the given aggregate identifier. Depending on
|
|
14
|
+
# the strategy, this method may return immediately or block until a lock is held.
|
|
15
|
+
#
|
|
16
|
+
# @abstract
|
|
17
|
+
# @param [Object] aggregate_id
|
|
18
|
+
# @return [undefined]
|
|
19
|
+
def obtain_lock(aggregate_id); end
|
|
20
|
+
|
|
21
|
+
# Releases the lock held for an aggregate with the given aggregate identifier. The caller
|
|
22
|
+
# of this method must ensure a valid lock was requested using {#obtain_lock}. If no lock
|
|
23
|
+
# was successfully obtained, the behavior of this method is undefined.
|
|
24
|
+
#
|
|
25
|
+
# @abstract
|
|
26
|
+
# @param [Object] aggregate_id
|
|
27
|
+
# @return [undefined]
|
|
28
|
+
def release_lock(aggregate_id); end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Implementation of a lock manager that does no locking
|
|
32
|
+
class NullLockManager < LockManager
|
|
33
|
+
# @param [AggregateRoot] aggregate
|
|
34
|
+
# @return [Boolean]
|
|
35
|
+
def validate_lock(aggregate)
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module Repository
|
|
3
|
+
# Partial implementation of a repository that handles integration with a lock manager
|
|
4
|
+
# @abstract
|
|
5
|
+
class LockingRepository < Repository
|
|
6
|
+
# @return [LockManager]
|
|
7
|
+
attr_reader :lock_manager
|
|
8
|
+
|
|
9
|
+
# @param [LockManager] lock_manager
|
|
10
|
+
# @return [undefined]
|
|
11
|
+
def initialize(lock_manager)
|
|
12
|
+
@lock_manager = lock_manager
|
|
13
|
+
@logger = Logging.logger.new self.class
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @raise [AggregateNotFoundError]
|
|
17
|
+
# If the aggregate with the given identifier could not be found
|
|
18
|
+
# @raise [ConflictingModificationError]
|
|
19
|
+
# If the expected version doesn't match the aggregate's actual version
|
|
20
|
+
# @param [Object] aggregate_id
|
|
21
|
+
# @param [Integer] expected_version If this is nil, no version validation is performed
|
|
22
|
+
# @return [AggregateRoot]
|
|
23
|
+
def load(aggregate_id, expected_version = nil)
|
|
24
|
+
@lock_manager.obtain_lock aggregate_id
|
|
25
|
+
|
|
26
|
+
begin
|
|
27
|
+
aggregate = perform_load aggregate_id, expected_version
|
|
28
|
+
|
|
29
|
+
aggregate = register_aggregate aggregate
|
|
30
|
+
register_listener LockCleaningUnitOfWorkListener.new aggregate_id, @lock_manager
|
|
31
|
+
|
|
32
|
+
aggregate
|
|
33
|
+
rescue
|
|
34
|
+
@logger.debug 'Excepton raised while loading an aggregate, releasing lock'
|
|
35
|
+
|
|
36
|
+
@lock_manager.release_lock aggregate_id
|
|
37
|
+
raise
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @raise [ArgumentError] If the version of the aggregate is not null
|
|
42
|
+
# @param [AggregateRoot] aggregate
|
|
43
|
+
# @return [undefined]
|
|
44
|
+
def add(aggregate)
|
|
45
|
+
@lock_manager.obtain_lock aggregate.id
|
|
46
|
+
|
|
47
|
+
begin
|
|
48
|
+
assert_compatible aggregate
|
|
49
|
+
|
|
50
|
+
register_aggregate aggregate
|
|
51
|
+
register_listener LockCleaningUnitOfWorkListener.new aggregate.id, @lock_manager
|
|
52
|
+
rescue
|
|
53
|
+
@logger.debug 'Exception raised while adding an aggregate, releasing lock'
|
|
54
|
+
|
|
55
|
+
@lock_manager.release_lock aggregate.id
|
|
56
|
+
raise
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
protected
|
|
61
|
+
|
|
62
|
+
# Fetches the aggregate with the given identifier from the underlying aggregate store
|
|
63
|
+
#
|
|
64
|
+
# @abstract
|
|
65
|
+
# @raise [AggregateNotFoundError]
|
|
66
|
+
# If the aggregate with the given identifier could not be found
|
|
67
|
+
# @raise [ConflictingModificationError]
|
|
68
|
+
# If the expected version doesn't match the aggregate's actual version
|
|
69
|
+
# @param [Object] aggregate_id
|
|
70
|
+
# @param [Integer] expected_version
|
|
71
|
+
# @return [AggregateRoot]
|
|
72
|
+
def perform_load(aggregate_id, expected_version); end
|
|
73
|
+
|
|
74
|
+
def logger
|
|
75
|
+
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Unit of work listener that releases the lock on an aggregate when the unit of work
|
|
80
|
+
# is cleaning up
|
|
81
|
+
class LockCleaningUnitOfWorkListener < UnitOfWork::UnitOfWorkListener
|
|
82
|
+
# @param [Object] aggregate_id
|
|
83
|
+
# @param [LockManager] lock_manager
|
|
84
|
+
# @return [undefined]
|
|
85
|
+
def initialize(aggregate_id, lock_manager)
|
|
86
|
+
@aggregate_id = aggregate_id
|
|
87
|
+
@lock_manager = lock_manager
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @param [UnitOfWork] unit
|
|
91
|
+
# @return [undefined]
|
|
92
|
+
def on_cleanup(unit)
|
|
93
|
+
@lock_manager.release_lock @aggregate_id
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Synapse
|
|
2
|
+
module Repository
|
|
3
|
+
# Rough implementation of a pessimistic lock manager using local locks
|
|
4
|
+
class PessimisticLockManager < LockManager
|
|
5
|
+
def initialize
|
|
6
|
+
@aggregates = Hash.new
|
|
7
|
+
@lock = Mutex.new
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# @todo Check if current thread holds lock, not just if lock is held
|
|
11
|
+
# @param [AggregateRoot] aggregate
|
|
12
|
+
# @return [Boolean]
|
|
13
|
+
def validate_lock(aggregate)
|
|
14
|
+
@aggregates.has_key?(aggregate.id) and lock_for(aggregate.id).locked?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @param [Object] aggregate_id
|
|
18
|
+
# @return [undefined]
|
|
19
|
+
def obtain_lock(aggregate_id)
|
|
20
|
+
lock = lock_for aggregate_id
|
|
21
|
+
lock.lock
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param [Object] aggregate_id
|
|
25
|
+
# @return [undefined]
|
|
26
|
+
def release_lock(aggregate_id)
|
|
27
|
+
unless @aggregates.has_key? aggregate_id
|
|
28
|
+
raise 'No lock for this identifier was ever obtained'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
lock = lock_for aggregate_id
|
|
32
|
+
lock.unlock
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# @param [Object] aggregate_id
|
|
38
|
+
# @return [Mutex]
|
|
39
|
+
def lock_for(aggregate_id)
|
|
40
|
+
lock = @aggregates[aggregate_id]
|
|
41
|
+
until lock
|
|
42
|
+
put_if_absent aggregate_id, Mutex.new
|
|
43
|
+
lock = @aggregates[aggregate_id]
|
|
44
|
+
end
|
|
45
|
+
lock
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @param [Object] aggregate_id
|
|
49
|
+
# @param [Mutex] lock
|
|
50
|
+
# @return [undefined]
|
|
51
|
+
def put_if_absent(aggregate_id, lock)
|
|
52
|
+
@lock.synchronize do
|
|
53
|
+
unless @aggregates.has_key? aggregate_id
|
|
54
|
+
@aggregates.store aggregate_id, lock
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|