synapse-core 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/synapse.rb +3 -0
- data/lib/synapse/command/simple_command_bus.rb +2 -2
- data/lib/synapse/common/concurrency/identifier_lock.rb +71 -0
- data/lib/synapse/common/concurrency/public_lock.rb +96 -0
- data/lib/synapse/event_bus/simple_event_bus.rb +1 -1
- data/lib/synapse/event_bus/wiring.rb +0 -4
- data/lib/synapse/event_sourcing/member.rb +0 -4
- data/lib/synapse/event_sourcing/snapshot/count_trigger.rb +2 -2
- data/lib/synapse/event_store.rb +1 -9
- data/lib/synapse/partitioning.rb +0 -2
- data/lib/synapse/process_manager.rb +12 -0
- data/lib/synapse/process_manager/lock_manager.rb +22 -0
- data/lib/synapse/process_manager/pessimistic_lock_manager.rb +23 -0
- data/lib/synapse/process_manager/process.rb +2 -0
- data/lib/synapse/process_manager/process_factory.rb +52 -0
- data/lib/synapse/process_manager/process_manager.rb +170 -0
- data/lib/synapse/process_manager/process_repository.rb +53 -0
- data/lib/synapse/process_manager/repository/in_memory.rb +63 -0
- data/lib/synapse/process_manager/resource_injector.rb +12 -0
- data/lib/synapse/process_manager/simple_process_manager.rb +48 -0
- data/lib/synapse/process_manager/wiring/process.rb +27 -0
- data/lib/synapse/process_manager/wiring/process_manager.rb +72 -0
- data/lib/synapse/repository.rb +1 -0
- data/lib/synapse/repository/locking.rb +1 -1
- data/lib/synapse/repository/optimistic_lock_manager.rb +128 -0
- data/lib/synapse/repository/pessimistic_lock_manager.rb +4 -37
- data/lib/synapse/serialization.rb +1 -1
- data/lib/synapse/serialization/{converter/factory.rb → converter_factory.rb} +0 -0
- data/lib/synapse/serialization/serializer.rb +5 -3
- data/lib/synapse/uow/listener_collection.rb +59 -1
- data/lib/synapse/version.rb +1 -1
- data/lib/synapse/wiring/message_wiring.rb +7 -3
- data/lib/synapse/wiring/wire.rb +7 -2
- data/test/common/concurrency/identifier_lock_test.rb +36 -0
- data/test/common/concurrency/public_lock_test.rb +83 -0
- data/test/partitioning/packing/json_test.rb +2 -1
- data/test/process_manager/in_memory_test.rb +57 -0
- data/test/process_manager/process_factory_test.rb +31 -0
- data/test/process_manager/simple_process_manager_test.rb +130 -0
- data/test/process_manager/wiring/fixtures.rb +42 -0
- data/test/process_manager/wiring/process_manager_test.rb +73 -0
- data/test/process_manager/wiring/process_test.rb +35 -0
- data/test/repository/optimistic_test.rb +41 -0
- data/test/repository/pessimistic_test.rb +20 -0
- data/test/serialization/converter/chain_test.rb +31 -0
- data/test/serialization/lazy_object_test.rb +1 -1
- data/test/serialization/message/serialization_aware_message_test.rb +4 -2
- data/test/serialization/message/serialized_message_builder_test.rb +1 -1
- data/test/serialization/message/serialized_message_test.rb +3 -2
- data/test/serialization/serializer/marshal_test.rb +1 -1
- data/test/serialization/serializer/oj_test.rb +1 -1
- data/test/serialization/serializer/ox_test.rb +1 -1
- data/test/serialization/serializer_test.rb +1 -1
- data/test/test_ext.rb +5 -2
- data/test/wiring/wire_registry_test.rb +10 -10
- data/test/wiring/wire_test.rb +5 -5
- metadata +29 -16
- data/lib/synapse/event_store/mongo.rb +0 -8
- data/lib/synapse/event_store/mongo/cursor_event_stream.rb +0 -63
- data/lib/synapse/event_store/mongo/event_store.rb +0 -86
- data/lib/synapse/event_store/mongo/per_commit_strategy.rb +0 -253
- data/lib/synapse/event_store/mongo/per_event_strategy.rb +0 -143
- data/lib/synapse/event_store/mongo/storage_strategy.rb +0 -113
- data/lib/synapse/event_store/mongo/template.rb +0 -73
- data/lib/synapse/partitioning/amqp.rb +0 -3
- data/lib/synapse/partitioning/amqp/amqp_queue_reader.rb +0 -50
- data/lib/synapse/partitioning/amqp/amqp_queue_writer.rb +0 -31
- data/lib/synapse/partitioning/amqp/key_resolver.rb +0 -26
- data/lib/synapse/serialization/converter/bson.rb +0 -28
@@ -0,0 +1,53 @@
|
|
1
|
+
module Synapse
|
2
|
+
module ProcessManager
|
3
|
+
# Represents a mechanism for storing and loading process instances
|
4
|
+
# @abstract
|
5
|
+
class ProcessRepository
|
6
|
+
# Returns a set of process identifiers for processes of the given type that have been
|
7
|
+
# correlated with the given key value pair
|
8
|
+
#
|
9
|
+
# Processes that have been changed must be committed for changes to take effect
|
10
|
+
#
|
11
|
+
# @abstract
|
12
|
+
# @param [Class] type
|
13
|
+
# @param [Correlation] correlation
|
14
|
+
# @return [Set]
|
15
|
+
def find(type, correlation); end
|
16
|
+
|
17
|
+
# Loads a known process by its unique identifier
|
18
|
+
#
|
19
|
+
# Processes that have been changed must be committed for changes to take effect
|
20
|
+
#
|
21
|
+
# Due to the concurrent nature of processes, it is not unlikely for a process to have
|
22
|
+
# ceased to exist after it has been found based on correlations. Therefore, a repository
|
23
|
+
# should gracefully handle a missing process.
|
24
|
+
#
|
25
|
+
# @abstract
|
26
|
+
# @param [String] id
|
27
|
+
# @return [Process] Returns nil if process could not be found
|
28
|
+
def load(id); end
|
29
|
+
|
30
|
+
# Commits the changes made to the process instance
|
31
|
+
#
|
32
|
+
# If the committed process is marked as inactive, it should delete the process from the
|
33
|
+
# underlying storage and remove all correlations for that process.
|
34
|
+
#
|
35
|
+
# @abstract
|
36
|
+
# @param [Process] process
|
37
|
+
# @return [undefined]
|
38
|
+
def commit(process); end
|
39
|
+
|
40
|
+
# Registers a newly created process with the repository
|
41
|
+
#
|
42
|
+
# Once a process has been registered, it can be found using its correlations or by its
|
43
|
+
# unique identifier.
|
44
|
+
#
|
45
|
+
# Note that if the added process is marked as inactive, it will not be stored.
|
46
|
+
#
|
47
|
+
# @abstract
|
48
|
+
# @param [Process] process
|
49
|
+
# @return [undefined]
|
50
|
+
def add(process); end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Synapse
|
2
|
+
module ProcessManager
|
3
|
+
# Process repository that stores all processes in memory
|
4
|
+
#
|
5
|
+
# This implementation is not thread-safe -- use a lock manager for thread safety
|
6
|
+
class InMemoryProcessRepository < ProcessRepository
|
7
|
+
def initialize
|
8
|
+
@managed_processes = Hash.new
|
9
|
+
@lock = Mutex.new
|
10
|
+
end
|
11
|
+
|
12
|
+
# @param [Class] type
|
13
|
+
# @param [Correlation] correlation
|
14
|
+
# @return [Set]
|
15
|
+
def find(type, correlation)
|
16
|
+
matching = Array.new
|
17
|
+
|
18
|
+
@managed_processes.each_value do |process|
|
19
|
+
if process.correlations.include? correlation
|
20
|
+
matching.push process.id
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
matching
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param [String] id
|
28
|
+
# @return [Process] Returns nil if process could not be found
|
29
|
+
def load(id)
|
30
|
+
if @managed_processes.has_key? id
|
31
|
+
@managed_processes.fetch id
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param [Process] process
|
36
|
+
# @return [undefined]
|
37
|
+
def commit(process)
|
38
|
+
@lock.synchronize do
|
39
|
+
if process.active?
|
40
|
+
@managed_processes.store process.id, process
|
41
|
+
else
|
42
|
+
@managed_processes.delete process.id
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
process.correlations.commit
|
47
|
+
end
|
48
|
+
|
49
|
+
# @param [Process] process
|
50
|
+
# @return [undefined]
|
51
|
+
def add(process)
|
52
|
+
if process.active?
|
53
|
+
commit process
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [Integer] The number of processes managed by this repository
|
58
|
+
def count
|
59
|
+
@managed_processes.count
|
60
|
+
end
|
61
|
+
end # InMemoryProcessRepository
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Synapse
|
2
|
+
module ProcessManager
|
3
|
+
# Represents a mechanism for injecting resources into process instances
|
4
|
+
class ResourceInjector
|
5
|
+
# Injects required resources into the given process instance
|
6
|
+
#
|
7
|
+
# @param [Process] process
|
8
|
+
# @return [undefined]
|
9
|
+
def inject_resources(process); end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Synapse
|
2
|
+
module ProcessManager
|
3
|
+
# Simple implementation of a process manager
|
4
|
+
class SimpleProcessManager < ProcessManager
|
5
|
+
# @return [Array] Types of events that will always result in the creation of a process
|
6
|
+
attr_accessor :always_create_events
|
7
|
+
# @return [Array] Types of events that will result in the creation of a process if one
|
8
|
+
# doesn't already exist
|
9
|
+
attr_accessor :optionally_create_events
|
10
|
+
|
11
|
+
# @param [ProcessRepository] repository
|
12
|
+
# @param [ProcessFactory] factory
|
13
|
+
# @param [LockManager] lock_manager
|
14
|
+
# @param [CorrelationResolver] correlation_resolver
|
15
|
+
# @param [Class...] process_types
|
16
|
+
# @return [undefined]
|
17
|
+
def initialize(repository, factory, lock_manager, correlation_resolver, *process_types)
|
18
|
+
super repository, factory, lock_manager, *process_types
|
19
|
+
|
20
|
+
@correlation_resolver = correlation_resolver
|
21
|
+
@always_create_events = Array.new
|
22
|
+
@optionally_create_events = Array.new
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
# @param [Class] process_type
|
28
|
+
# @param [EventMessage] event
|
29
|
+
# @return [Symbol]
|
30
|
+
def creation_policy_for(process_type, event)
|
31
|
+
if @always_create_events.include? event.payload_type
|
32
|
+
:always
|
33
|
+
elsif @optionally_create_events.include? event.payload_type
|
34
|
+
:if_none_found
|
35
|
+
else
|
36
|
+
:none
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param [Class] process_type
|
41
|
+
# @param [EventMessage] event
|
42
|
+
# @return [Correlation] Returns nil if no correlation could be extracted
|
43
|
+
def extract_correlation(process_type, event)
|
44
|
+
@correlation_resolver.resolve event
|
45
|
+
end
|
46
|
+
end # SimpleProcessManager
|
47
|
+
end # ProcessManager
|
48
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Synapse
|
2
|
+
module ProcessManager
|
3
|
+
# Process that has the wiring DSL built-in
|
4
|
+
#
|
5
|
+
# @example
|
6
|
+
# class OrderProcess < WiringProcess
|
7
|
+
# wire OrderCreatedEvent, correlate: :order_id, start: true, to: :on_create
|
8
|
+
# wire OrderFinishedEvent, correlate: :order_id, finish: true, to: :on_finish
|
9
|
+
# end
|
10
|
+
class WiringProcess < Process
|
11
|
+
include Wiring::MessageWiring
|
12
|
+
|
13
|
+
# @param [EventMessage] event
|
14
|
+
# @return [undefined]
|
15
|
+
def handle(event)
|
16
|
+
return unless @active
|
17
|
+
|
18
|
+
wire = self.wire_registry.wire_for event.payload_type
|
19
|
+
|
20
|
+
if wire
|
21
|
+
invoke_wire event, wire
|
22
|
+
finish if wire.options[:finish]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end # WiringProcess
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Synapse
|
2
|
+
module ProcessManager
|
3
|
+
# Process manager that is aware of processes that use the wiring DSL
|
4
|
+
# @see [WiringProcess]
|
5
|
+
class WiringProcessManager < ProcessManager
|
6
|
+
# @raise [ArgumentError] If a process type is given that doesn't support the wiring DSL
|
7
|
+
# @param [ProcessRepository] repository
|
8
|
+
# @param [ProcessFactory] factory
|
9
|
+
# @param [LockManager] lock_manager
|
10
|
+
# @param [Class...] process_types
|
11
|
+
# @return [undefined]
|
12
|
+
def initialize(repository, factory, lock_manager, *process_types)
|
13
|
+
super
|
14
|
+
|
15
|
+
@process_types.each do |process_type|
|
16
|
+
unless process_type.respond_to? :wire_registry
|
17
|
+
raise ArgumentError, 'Incompatible process type %s' % process_type
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
protected
|
23
|
+
|
24
|
+
# @param [Class] process_type
|
25
|
+
# @param [EventMessage] event
|
26
|
+
# @return [Symbol]
|
27
|
+
def creation_policy_for(process_type, event)
|
28
|
+
wire = process_type.wire_registry.wire_for event.payload_type
|
29
|
+
|
30
|
+
if wire
|
31
|
+
if !wire.options[:start]
|
32
|
+
:none
|
33
|
+
elsif wire.options[:force_new]
|
34
|
+
:always
|
35
|
+
else
|
36
|
+
:if_none_found
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# @param [Class] process_type
|
42
|
+
# @param [EventMessage] event
|
43
|
+
# @return [Correlation] Returns nil if no correlation could be extracted
|
44
|
+
def extract_correlation(process_type, event)
|
45
|
+
wire = process_type.wire_registry.wire_for event.payload_type
|
46
|
+
|
47
|
+
if wire
|
48
|
+
correlation_key = wire.options[:correlate]
|
49
|
+
if correlation_key
|
50
|
+
correlation_value event.payload, correlation_key
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# @param [Object] payload
|
58
|
+
# @param [Symbol] correlation_key
|
59
|
+
# @return [Correlation] Returns nil if correlation value could not be extracted
|
60
|
+
def correlation_value(payload, correlation_key)
|
61
|
+
unless payload.respond_to? correlation_key
|
62
|
+
raise 'Correlation key [%s] is not valid for [%s]' % [correlation_key, payload.class]
|
63
|
+
end
|
64
|
+
|
65
|
+
value = payload.public_send correlation_key
|
66
|
+
if value
|
67
|
+
Correlation.new correlation_key, value
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
data/lib/synapse/repository.rb
CHANGED
@@ -0,0 +1,128 @@
|
|
1
|
+
module Synapse
|
2
|
+
module Repository
|
3
|
+
# Lock manager that uses an optimistic locking strategy
|
4
|
+
#
|
5
|
+
# This implementation uses the sequence number of an aggregate's last committed event to
|
6
|
+
# detect concurrenct access.
|
7
|
+
class OptimisticLockManager < LockManager
|
8
|
+
def initialize
|
9
|
+
@aggregates = Hash.new
|
10
|
+
@lock = Mutex.new
|
11
|
+
end
|
12
|
+
|
13
|
+
# @param [AggregateRoot] aggregate
|
14
|
+
# @return [Boolean]
|
15
|
+
def validate_lock(aggregate)
|
16
|
+
@aggregates.has_key? aggregate.id and @aggregates[aggregate.id].validate aggregate
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param [Object] aggregate_id
|
20
|
+
# @return [undefined]
|
21
|
+
def obtain_lock(aggregate_id)
|
22
|
+
obtained = false
|
23
|
+
until obtained
|
24
|
+
lock = lock_for aggregate_id
|
25
|
+
obtained = lock and lock.lock
|
26
|
+
unless obtained
|
27
|
+
remove_lock aggregate_id, lock
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# @param [Object] aggregate_id
|
33
|
+
# @return [undefined]
|
34
|
+
def release_lock(aggregate_id)
|
35
|
+
lock = @aggregates[aggregate_id]
|
36
|
+
if lock
|
37
|
+
lock.unlock
|
38
|
+
if lock.closed?
|
39
|
+
remove_lock aggregate_id, lock
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# @param [Object] aggregate_id
|
47
|
+
# @param [OptimisticLock] lock
|
48
|
+
# @return [undefined]
|
49
|
+
def remove_lock(aggregate_id, lock)
|
50
|
+
@lock.synchronize do
|
51
|
+
if @aggregates.has_key? aggregate_id and @aggregates[aggregate_id].equal? lock
|
52
|
+
@aggregates.delete aggregate_id
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# @param [Object] aggregate_id
|
58
|
+
# @return [OptimisticLock]
|
59
|
+
def lock_for(aggregate_id)
|
60
|
+
@lock.synchronize do
|
61
|
+
if @aggregates.has_key? aggregate_id
|
62
|
+
@aggregates[aggregate_id]
|
63
|
+
else
|
64
|
+
@aggregates[aggregate_id] = OptimisticLock.new
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Lock that keeps track of an aggregate's version
|
71
|
+
# @api private
|
72
|
+
class OptimisticLock
|
73
|
+
# @return [Boolean] True if this lock can be disposed
|
74
|
+
attr_reader :closed
|
75
|
+
|
76
|
+
alias closed? closed
|
77
|
+
|
78
|
+
# @return [Hash] Hash of threads to the number of times they hold the lock
|
79
|
+
attr_reader :threads
|
80
|
+
|
81
|
+
def initialize
|
82
|
+
@closed = false
|
83
|
+
@threads = Hash.new 0
|
84
|
+
end
|
85
|
+
|
86
|
+
# @param [AggregateRoot] aggregate
|
87
|
+
# @return [Boolean]
|
88
|
+
def validate(aggregate)
|
89
|
+
last_committed = aggregate.version
|
90
|
+
if @version.nil? or @version.eql? last_committed
|
91
|
+
if last_committed.nil?
|
92
|
+
last_committed = 0
|
93
|
+
end
|
94
|
+
|
95
|
+
@version = last_committed + aggregate.uncommitted_event_count
|
96
|
+
|
97
|
+
true
|
98
|
+
else
|
99
|
+
false
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# @return [Boolean] Returns false if lock is closed
|
104
|
+
def lock
|
105
|
+
if @closed
|
106
|
+
false
|
107
|
+
else
|
108
|
+
@threads[Thread.current] = @threads[Thread.current] + 1
|
109
|
+
true
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# @return [undefined]
|
114
|
+
def unlock
|
115
|
+
count = @threads[Thread.current]
|
116
|
+
if count <= 1
|
117
|
+
@threads.delete Thread.current
|
118
|
+
else
|
119
|
+
@threads[Thread.current] = @threads[Thread.current] - 1
|
120
|
+
end
|
121
|
+
|
122
|
+
if @threads.empty?
|
123
|
+
@closed = true
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end # OptimisticLock
|
127
|
+
end # Repository
|
128
|
+
end
|
@@ -3,59 +3,26 @@ module Synapse
|
|
3
3
|
# Rough implementation of a pessimistic lock manager using local locks
|
4
4
|
class PessimisticLockManager < LockManager
|
5
5
|
def initialize
|
6
|
-
@aggregates =
|
7
|
-
@lock = Mutex.new
|
6
|
+
@aggregates = IdentifierLock.new
|
8
7
|
end
|
9
8
|
|
10
|
-
# @todo Check if current thread holds lock, not just if lock is held
|
11
9
|
# @param [AggregateRoot] aggregate
|
12
10
|
# @return [Boolean]
|
13
11
|
def validate_lock(aggregate)
|
14
|
-
@aggregates.
|
12
|
+
@aggregates.owned? aggregate.id
|
15
13
|
end
|
16
14
|
|
17
15
|
# @param [Object] aggregate_id
|
18
16
|
# @return [undefined]
|
19
17
|
def obtain_lock(aggregate_id)
|
20
|
-
|
21
|
-
lock.lock
|
18
|
+
@aggregates.obtain_lock aggregate_id
|
22
19
|
end
|
23
20
|
|
24
21
|
# @param [Object] aggregate_id
|
25
22
|
# @return [undefined]
|
26
23
|
def release_lock(aggregate_id)
|
27
|
-
|
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
|
24
|
+
@aggregates.release_lock aggregate_id
|
46
25
|
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
26
|
end
|
60
27
|
end
|
61
28
|
end
|