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.
Files changed (155) hide show
  1. data/lib/synapse.rb +351 -0
  2. data/lib/synapse/command/command_bus.rb +45 -0
  3. data/lib/synapse/command/command_callback.rb +18 -0
  4. data/lib/synapse/command/command_filter.rb +17 -0
  5. data/lib/synapse/command/command_handler.rb +13 -0
  6. data/lib/synapse/command/dispatch_interceptor.rb +16 -0
  7. data/lib/synapse/command/duplication.rb +43 -0
  8. data/lib/synapse/command/errors.rb +27 -0
  9. data/lib/synapse/command/filters/validation.rb +32 -0
  10. data/lib/synapse/command/gateway.rb +34 -0
  11. data/lib/synapse/command/interceptor_chain.rb +31 -0
  12. data/lib/synapse/command/interceptors/serialization.rb +35 -0
  13. data/lib/synapse/command/message.rb +19 -0
  14. data/lib/synapse/command/rollback_policy.rb +22 -0
  15. data/lib/synapse/command/simple_command_bus.rb +138 -0
  16. data/lib/synapse/command/wiring.rb +47 -0
  17. data/lib/synapse/domain/aggregate_root.rb +121 -0
  18. data/lib/synapse/domain/event_container.rb +127 -0
  19. data/lib/synapse/domain/message.rb +82 -0
  20. data/lib/synapse/domain/message_builder.rb +34 -0
  21. data/lib/synapse/domain/stream.rb +108 -0
  22. data/lib/synapse/duplication.rb +60 -0
  23. data/lib/synapse/errors.rb +13 -0
  24. data/lib/synapse/event_bus/event_bus.rb +40 -0
  25. data/lib/synapse/event_bus/event_listener.rb +16 -0
  26. data/lib/synapse/event_bus/event_listener_proxy.rb +12 -0
  27. data/lib/synapse/event_bus/simple_event_bus.rb +69 -0
  28. data/lib/synapse/event_bus/wiring.rb +23 -0
  29. data/lib/synapse/event_sourcing/aggregate_factory.rb +69 -0
  30. data/lib/synapse/event_sourcing/aggregate_root.rb +104 -0
  31. data/lib/synapse/event_sourcing/conflict_resolver.rb +80 -0
  32. data/lib/synapse/event_sourcing/entity.rb +64 -0
  33. data/lib/synapse/event_sourcing/member.rb +72 -0
  34. data/lib/synapse/event_sourcing/repository.rb +119 -0
  35. data/lib/synapse/event_sourcing/snapshot/count_stream.rb +86 -0
  36. data/lib/synapse/event_sourcing/snapshot/count_trigger.rb +91 -0
  37. data/lib/synapse/event_sourcing/snapshot/taker.rb +73 -0
  38. data/lib/synapse/event_sourcing/storage_listener.rb +34 -0
  39. data/lib/synapse/event_sourcing/stream_decorator.rb +25 -0
  40. data/lib/synapse/event_store/errors.rb +16 -0
  41. data/lib/synapse/event_store/event_store.rb +43 -0
  42. data/lib/synapse/event_store/in_memory.rb +59 -0
  43. data/lib/synapse/event_store/mongo/cursor_event_stream.rb +63 -0
  44. data/lib/synapse/event_store/mongo/event_store.rb +86 -0
  45. data/lib/synapse/event_store/mongo/per_commit_strategy.rb +253 -0
  46. data/lib/synapse/event_store/mongo/per_event_strategy.rb +143 -0
  47. data/lib/synapse/event_store/mongo/storage_strategy.rb +113 -0
  48. data/lib/synapse/event_store/mongo/template.rb +73 -0
  49. data/lib/synapse/identifier.rb +23 -0
  50. data/lib/synapse/message.rb +101 -0
  51. data/lib/synapse/message_builder.rb +38 -0
  52. data/lib/synapse/process_manager/correlation.rb +32 -0
  53. data/lib/synapse/process_manager/correlation_resolver.rb +14 -0
  54. data/lib/synapse/process_manager/correlation_set.rb +58 -0
  55. data/lib/synapse/process_manager/process.rb +71 -0
  56. data/lib/synapse/repository/errors.rb +26 -0
  57. data/lib/synapse/repository/lock_manager.rb +40 -0
  58. data/lib/synapse/repository/locking.rb +97 -0
  59. data/lib/synapse/repository/pessimistic_lock_manager.rb +61 -0
  60. data/lib/synapse/repository/repository.rb +109 -0
  61. data/lib/synapse/serialization/converter.rb +39 -0
  62. data/lib/synapse/serialization/converter/chain.rb +45 -0
  63. data/lib/synapse/serialization/converter/factory.rb +68 -0
  64. data/lib/synapse/serialization/converter/identity.rb +29 -0
  65. data/lib/synapse/serialization/converter/json.rb +31 -0
  66. data/lib/synapse/serialization/converter/ox.rb +31 -0
  67. data/lib/synapse/serialization/errors.rb +12 -0
  68. data/lib/synapse/serialization/lazy_object.rb +61 -0
  69. data/lib/synapse/serialization/message/data.rb +25 -0
  70. data/lib/synapse/serialization/message/metadata.rb +13 -0
  71. data/lib/synapse/serialization/message/serialization_aware.rb +17 -0
  72. data/lib/synapse/serialization/message/serialization_aware_message.rb +66 -0
  73. data/lib/synapse/serialization/message/serialized_message.rb +201 -0
  74. data/lib/synapse/serialization/message/serialized_message_builder.rb +64 -0
  75. data/lib/synapse/serialization/message/serialized_object_cache.rb +50 -0
  76. data/lib/synapse/serialization/message/serializer.rb +47 -0
  77. data/lib/synapse/serialization/revision_resolver.rb +30 -0
  78. data/lib/synapse/serialization/serialized_object.rb +37 -0
  79. data/lib/synapse/serialization/serialized_type.rb +31 -0
  80. data/lib/synapse/serialization/serializer.rb +98 -0
  81. data/lib/synapse/serialization/serializer/marshal.rb +32 -0
  82. data/lib/synapse/serialization/serializer/oj.rb +34 -0
  83. data/lib/synapse/serialization/serializer/ox.rb +31 -0
  84. data/lib/synapse/uow/factory.rb +28 -0
  85. data/lib/synapse/uow/listener.rb +79 -0
  86. data/lib/synapse/uow/listener_collection.rb +93 -0
  87. data/lib/synapse/uow/nesting.rb +262 -0
  88. data/lib/synapse/uow/provider.rb +71 -0
  89. data/lib/synapse/uow/storage_listener.rb +14 -0
  90. data/lib/synapse/uow/transaction_manager.rb +27 -0
  91. data/lib/synapse/uow/uow.rb +178 -0
  92. data/lib/synapse/upcasting/chain.rb +78 -0
  93. data/lib/synapse/upcasting/context.rb +58 -0
  94. data/lib/synapse/upcasting/data.rb +30 -0
  95. data/lib/synapse/upcasting/single_upcaster.rb +57 -0
  96. data/lib/synapse/upcasting/upcaster.rb +55 -0
  97. data/lib/synapse/version.rb +3 -0
  98. data/lib/synapse/wiring/message_wiring.rb +41 -0
  99. data/lib/synapse/wiring/wire.rb +55 -0
  100. data/lib/synapse/wiring/wire_registry.rb +61 -0
  101. data/test/command/duplication_test.rb +54 -0
  102. data/test/command/gateway_test.rb +25 -0
  103. data/test/command/interceptor_chain_test.rb +26 -0
  104. data/test/command/serialization_test.rb +37 -0
  105. data/test/command/simple_command_bus_test.rb +141 -0
  106. data/test/command/validation_test.rb +42 -0
  107. data/test/command/wiring_test.rb +73 -0
  108. data/test/domain/aggregate_root_test.rb +57 -0
  109. data/test/domain/fixtures.rb +31 -0
  110. data/test/domain/message_test.rb +61 -0
  111. data/test/domain/stream_test.rb +35 -0
  112. data/test/duplication_test.rb +40 -0
  113. data/test/event_bus/wiring_test.rb +46 -0
  114. data/test/event_sourcing/aggregate_factory_test.rb +28 -0
  115. data/test/event_sourcing/aggregate_root_test.rb +76 -0
  116. data/test/event_sourcing/entity_test.rb +34 -0
  117. data/test/event_sourcing/fixtures.rb +85 -0
  118. data/test/event_sourcing/repository_test.rb +102 -0
  119. data/test/event_sourcing/snapshot/aggregate_taker_test.rb +39 -0
  120. data/test/event_sourcing/snapshot/deferred_taker_test.rb +19 -0
  121. data/test/event_sourcing/snapshot/integration_test.rb +65 -0
  122. data/test/event_sourcing/storage_listener_test.rb +77 -0
  123. data/test/event_store/in_memory_test.rb +47 -0
  124. data/test/process_manager/correlation_set_test.rb +49 -0
  125. data/test/process_manager/correlation_test.rb +24 -0
  126. data/test/process_manager/process_test.rb +52 -0
  127. data/test/repository/locking_test.rb +101 -0
  128. data/test/serialization/converter/factory_test.rb +33 -0
  129. data/test/serialization/converter/identity_test.rb +17 -0
  130. data/test/serialization/converter/json_test.rb +31 -0
  131. data/test/serialization/converter/ox_test.rb +40 -0
  132. data/test/serialization/fixtures.rb +17 -0
  133. data/test/serialization/lazy_object_test.rb +32 -0
  134. data/test/serialization/message/metadata_test.rb +19 -0
  135. data/test/serialization/message/serialization_aware_message_test.rb +88 -0
  136. data/test/serialization/message/serialized_message_builder_test.rb +41 -0
  137. data/test/serialization/message/serialized_message_test.rb +140 -0
  138. data/test/serialization/message/serializer_test.rb +50 -0
  139. data/test/serialization/revision_resolver_test.rb +12 -0
  140. data/test/serialization/serialized_object_test.rb +36 -0
  141. data/test/serialization/serialized_type_test.rb +27 -0
  142. data/test/serialization/serializer/marshal_test.rb +22 -0
  143. data/test/serialization/serializer/oj_test.rb +24 -0
  144. data/test/serialization/serializer/ox_test.rb +36 -0
  145. data/test/serialization/serializer_test.rb +20 -0
  146. data/test/test_helper.rb +19 -0
  147. data/test/uow/factory_test.rb +23 -0
  148. data/test/uow/outer_commit_listener_test.rb +50 -0
  149. data/test/uow/provider_test.rb +70 -0
  150. data/test/uow/uow_test.rb +337 -0
  151. data/test/upcasting/chain_test.rb +29 -0
  152. data/test/upcasting/fixtures.rb +66 -0
  153. data/test/wiring/wire_registry_test.rb +60 -0
  154. data/test/wiring/wire_test.rb +51 -0
  155. 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