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.
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