synapse-core 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/lib/synapse.rb +3 -0
  2. data/lib/synapse/command/simple_command_bus.rb +2 -2
  3. data/lib/synapse/common/concurrency/identifier_lock.rb +71 -0
  4. data/lib/synapse/common/concurrency/public_lock.rb +96 -0
  5. data/lib/synapse/event_bus/simple_event_bus.rb +1 -1
  6. data/lib/synapse/event_bus/wiring.rb +0 -4
  7. data/lib/synapse/event_sourcing/member.rb +0 -4
  8. data/lib/synapse/event_sourcing/snapshot/count_trigger.rb +2 -2
  9. data/lib/synapse/event_store.rb +1 -9
  10. data/lib/synapse/partitioning.rb +0 -2
  11. data/lib/synapse/process_manager.rb +12 -0
  12. data/lib/synapse/process_manager/lock_manager.rb +22 -0
  13. data/lib/synapse/process_manager/pessimistic_lock_manager.rb +23 -0
  14. data/lib/synapse/process_manager/process.rb +2 -0
  15. data/lib/synapse/process_manager/process_factory.rb +52 -0
  16. data/lib/synapse/process_manager/process_manager.rb +170 -0
  17. data/lib/synapse/process_manager/process_repository.rb +53 -0
  18. data/lib/synapse/process_manager/repository/in_memory.rb +63 -0
  19. data/lib/synapse/process_manager/resource_injector.rb +12 -0
  20. data/lib/synapse/process_manager/simple_process_manager.rb +48 -0
  21. data/lib/synapse/process_manager/wiring/process.rb +27 -0
  22. data/lib/synapse/process_manager/wiring/process_manager.rb +72 -0
  23. data/lib/synapse/repository.rb +1 -0
  24. data/lib/synapse/repository/locking.rb +1 -1
  25. data/lib/synapse/repository/optimistic_lock_manager.rb +128 -0
  26. data/lib/synapse/repository/pessimistic_lock_manager.rb +4 -37
  27. data/lib/synapse/serialization.rb +1 -1
  28. data/lib/synapse/serialization/{converter/factory.rb → converter_factory.rb} +0 -0
  29. data/lib/synapse/serialization/serializer.rb +5 -3
  30. data/lib/synapse/uow/listener_collection.rb +59 -1
  31. data/lib/synapse/version.rb +1 -1
  32. data/lib/synapse/wiring/message_wiring.rb +7 -3
  33. data/lib/synapse/wiring/wire.rb +7 -2
  34. data/test/common/concurrency/identifier_lock_test.rb +36 -0
  35. data/test/common/concurrency/public_lock_test.rb +83 -0
  36. data/test/partitioning/packing/json_test.rb +2 -1
  37. data/test/process_manager/in_memory_test.rb +57 -0
  38. data/test/process_manager/process_factory_test.rb +31 -0
  39. data/test/process_manager/simple_process_manager_test.rb +130 -0
  40. data/test/process_manager/wiring/fixtures.rb +42 -0
  41. data/test/process_manager/wiring/process_manager_test.rb +73 -0
  42. data/test/process_manager/wiring/process_test.rb +35 -0
  43. data/test/repository/optimistic_test.rb +41 -0
  44. data/test/repository/pessimistic_test.rb +20 -0
  45. data/test/serialization/converter/chain_test.rb +31 -0
  46. data/test/serialization/lazy_object_test.rb +1 -1
  47. data/test/serialization/message/serialization_aware_message_test.rb +4 -2
  48. data/test/serialization/message/serialized_message_builder_test.rb +1 -1
  49. data/test/serialization/message/serialized_message_test.rb +3 -2
  50. data/test/serialization/serializer/marshal_test.rb +1 -1
  51. data/test/serialization/serializer/oj_test.rb +1 -1
  52. data/test/serialization/serializer/ox_test.rb +1 -1
  53. data/test/serialization/serializer_test.rb +1 -1
  54. data/test/test_ext.rb +5 -2
  55. data/test/wiring/wire_registry_test.rb +10 -10
  56. data/test/wiring/wire_test.rb +5 -5
  57. metadata +29 -16
  58. data/lib/synapse/event_store/mongo.rb +0 -8
  59. data/lib/synapse/event_store/mongo/cursor_event_stream.rb +0 -63
  60. data/lib/synapse/event_store/mongo/event_store.rb +0 -86
  61. data/lib/synapse/event_store/mongo/per_commit_strategy.rb +0 -253
  62. data/lib/synapse/event_store/mongo/per_event_strategy.rb +0 -143
  63. data/lib/synapse/event_store/mongo/storage_strategy.rb +0 -113
  64. data/lib/synapse/event_store/mongo/template.rb +0 -73
  65. data/lib/synapse/partitioning/amqp.rb +0 -3
  66. data/lib/synapse/partitioning/amqp/amqp_queue_reader.rb +0 -50
  67. data/lib/synapse/partitioning/amqp/amqp_queue_writer.rb +0 -31
  68. data/lib/synapse/partitioning/amqp/key_resolver.rb +0 -26
  69. 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
@@ -1,6 +1,7 @@
1
1
  require 'synapse/repository/errors'
2
2
 
3
3
  require 'synapse/repository/lock_manager'
4
+ require 'synapse/repository/optimistic_lock_manager'
4
5
  require 'synapse/repository/pessimistic_lock_manager'
5
6
 
6
7
  require 'synapse/repository/repository'
@@ -10,7 +10,7 @@ module Synapse
10
10
  # @return [undefined]
11
11
  def initialize(lock_manager)
12
12
  @lock_manager = lock_manager
13
- @logger = Logging.logger.new self.class
13
+ @logger = Logging.logger[self.class]
14
14
  end
15
15
 
16
16
  # @raise [AggregateNotFoundError]
@@ -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 = Hash.new
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.has_key?(aggregate.id) and lock_for(aggregate.id).locked?
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
- lock = lock_for aggregate_id
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
- 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
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