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
data/lib/synapse.rb CHANGED
@@ -10,6 +10,9 @@ require 'synapse/common/identifier'
10
10
  require 'synapse/common/message'
11
11
  require 'synapse/common/message_builder'
12
12
 
13
+ require 'synapse/common/concurrency/identifier_lock'
14
+ require 'synapse/common/concurrency/public_lock'
15
+
13
16
  module Synapse
14
17
  extend ActiveSupport::Autoload
15
18
 
@@ -18,7 +18,7 @@ module Synapse
18
18
  @handlers = Hash.new
19
19
  @filters = Array.new
20
20
  @interceptors = Array.new
21
- @logger = Logging.logger.new self.class
21
+ @logger = Logging.logger[self.class]
22
22
  @rollback_policy = RollbackOnAnyExceptionPolicy.new
23
23
  @unit_factory = unit_factory
24
24
  end
@@ -100,7 +100,7 @@ module Synapse
100
100
  chain = InterceptorChain.new unit, @interceptors, handler
101
101
 
102
102
  begin
103
- @logger.debug 'Dispatching command [%s] [%s] to handler [%s]' %
103
+ @logger.info 'Dispatching command [%s] [%s] to handler [%s]' %
104
104
  [command.id, command.payload_type, handler.class]
105
105
 
106
106
  result = chain.proceed command
@@ -0,0 +1,71 @@
1
+ module Synapse
2
+ # @todo Deadlock detection
3
+ class IdentifierLock
4
+ # @return [undefined]
5
+ def initialize
6
+ @identifiers = Hash.new
7
+ @lock = Mutex.new
8
+ end
9
+
10
+ # Returns true if the calling thread holds the lock for the given identifier
11
+ #
12
+ # @param [Object] identifier
13
+ # @return [Boolean]
14
+ def owned?(identifier)
15
+ lock_available?(identifier) and lock_for(identifier).owned?
16
+ end
17
+
18
+ # Obtains a lock for the given identifier, blocking until the lock is obtained
19
+ #
20
+ # @param [Object] identifier
21
+ # @return [undefined]
22
+ def obtain_lock(identifier)
23
+ lock_for(identifier).lock
24
+ end
25
+
26
+ # Releases a lock for the given identifier
27
+ #
28
+ # @raise [ThreadError] If no lock was ever obtained for the identifier
29
+ # @param [Object] identifier
30
+ # @return [undefined]
31
+ def release_lock(identifier)
32
+ unless lock_available? identifier
33
+ raise ThreadError, 'No lock for this identifier was ever obtained'
34
+ end
35
+
36
+ lock_for(identifier).unlock
37
+ dispose_if_unused(identifier)
38
+ end
39
+
40
+ private
41
+
42
+ def lock_for(identifier)
43
+ @lock.synchronize do
44
+ if @identifiers.has_key? identifier
45
+ @identifiers[identifier]
46
+ else
47
+ @identifiers[identifier] = PublicLock.new
48
+ end
49
+ end
50
+ end
51
+
52
+ def lock_available?(identifier)
53
+ @identifiers.has_key? identifier
54
+ end
55
+
56
+ # Disposes of the lock for the given identifier if it has no threads waiting for it
57
+ #
58
+ # @param [String] identifier
59
+ # @return [undefined]
60
+ def dispose_if_unused(identifier)
61
+ lock = lock_for identifier
62
+ if lock.try_lock
63
+ @lock.synchronize do
64
+ @identifiers.delete identifier
65
+ end
66
+
67
+ lock.unlock
68
+ end
69
+ end
70
+ end # IdentifierLock
71
+ end # Synapse
@@ -0,0 +1,96 @@
1
+ require 'monitor'
2
+
3
+ module Synapse
4
+ class PublicLock
5
+ # @return [Thread] The current owner of the thread, if any
6
+ attr_reader :owner
7
+
8
+ # @return [Array] The list of threads waiting for this lock
9
+ attr_reader :waiting
10
+
11
+ # @return [undefined]
12
+ def initialize
13
+ @mutex = Mutex.new
14
+ @condition = ConditionVariable.new
15
+ @waiting = Array.new
16
+ end
17
+
18
+ # Returns true if the calling thread owns this lock
19
+ #
20
+ # @see Mutex#owned?
21
+ # @return [Boolean]
22
+ def owned?
23
+ @owner == Thread.current
24
+ end
25
+
26
+ # Returns true if the given thread owns this lock
27
+ # @return [Boolean]
28
+ def owned_by?(thread)
29
+ @owner == thread
30
+ end
31
+
32
+ # @see Mutex#synchronize
33
+ # @return [undefined]
34
+ def synchronize
35
+ lock
36
+
37
+ begin
38
+ yield
39
+ ensure
40
+ unlock rescue nil
41
+ end
42
+ end
43
+
44
+ # @see Mutex#lock
45
+ # @return [undefined]
46
+ def lock
47
+ @mutex.synchronize do
48
+ if @owner == Thread.current
49
+ raise ThreadError, 'Lock is already owned by the current thread'
50
+ end
51
+
52
+ while @owner
53
+ begin
54
+ @waiting.push Thread.current
55
+ @condition.wait @mutex
56
+ ensure
57
+ @waiting.delete Thread.current
58
+ end
59
+ end
60
+
61
+ @owner = Thread.current
62
+ end
63
+ end
64
+
65
+ # @see Mutex#unlock
66
+ # @return [undefined]
67
+ def unlock
68
+ @mutex.synchronize do
69
+ if @owner == Thread.current
70
+ @owner = nil
71
+ @condition.signal
72
+ else
73
+ raise ThreadError, 'Lock is not owned by the current thread'
74
+ end
75
+ end
76
+ end
77
+
78
+ # @see Mutex#try_lock
79
+ # @return [Boolean]]
80
+ def try_lock
81
+ @mutex.synchronize do
82
+ if @owner == Thread.current
83
+ raise ThreadError, 'Lock is already owned by the current thread'
84
+ end
85
+
86
+ if @owner
87
+ return false
88
+ end
89
+
90
+ @owner = Thread.current
91
+ end
92
+
93
+ true
94
+ end
95
+ end
96
+ end
@@ -5,7 +5,7 @@ module Synapse
5
5
  class SimpleEventBus < EventBus
6
6
  def initialize
7
7
  @listeners = Set.new
8
- @logger = Logging.logger.new self.class
8
+ @logger = Logging.logger[self.class]
9
9
  end
10
10
 
11
11
  # @param [EventMessage...] events
@@ -6,10 +6,6 @@ module Synapse
6
6
  include EventListener
7
7
  include Wiring::MessageWiring
8
8
 
9
- included do
10
- self.wire_registry = Wiring::WireRegistry.new true
11
- end
12
-
13
9
  # @param [EventMessage] event
14
10
  # @return [undefined]
15
11
  def notify(event)
@@ -6,10 +6,6 @@ module Synapse
6
6
  extend ActiveSupport::Concern
7
7
  include Wiring::MessageWiring
8
8
 
9
- included do
10
- self.wire_registry = Wiring::WireRegistry.new true
11
- end
12
-
13
9
  module ClassMethods
14
10
  # Registers an instance variable as a child entity
15
11
  #
@@ -24,7 +24,7 @@ module Synapse
24
24
  def initialize(snapshot_taker, unit_provider)
25
25
  @counters = Hash.new
26
26
  @lock = Mutex.new
27
- @logger = Logging.logger.new self.class
27
+ @logger = Logging.logger[self.class]
28
28
  @threshold = DEFAULT_THRESHOLD
29
29
 
30
30
  @snapshot_taker = snapshot_taker
@@ -40,7 +40,7 @@ module Synapse
40
40
  # @return [undefined]
41
41
  def trigger_snapshot(type_identifier, aggregate_id, counter)
42
42
  if counter.value > @threshold
43
- @logger.debug 'Snapshot threshold reached for [%s] [%s]' % [type_identifier, aggregate_id]
43
+ @logger.info 'Snapshot threshold reached for [%s] [%s]' % [type_identifier, aggregate_id]
44
44
 
45
45
  @snapshot_taker.schedule_snapshot type_identifier, aggregate_id
46
46
  counter.value = 1
@@ -1,11 +1,3 @@
1
- module Synapse
2
- module EventStore
3
- extend ActiveSupport::Autoload
4
-
5
- autoload :InMemoryEventStore, 'synapse/event_store/in_memory'
6
- autoload :Mongo
7
- end
8
- end
9
-
10
1
  require 'synapse/event_store/errors'
11
2
  require 'synapse/event_store/event_store'
3
+ require 'synapse/event_store/in_memory'
@@ -3,8 +3,6 @@ module Synapse
3
3
  extend ActiveSupport::Autoload
4
4
 
5
5
  # Optional queues
6
- autoload :AMQP
7
-
8
6
  autoload :MemoryQueueReader
9
7
  autoload :MemoryQueueWriter
10
8
 
@@ -1,4 +1,16 @@
1
1
  require 'synapse/process_manager/correlation'
2
2
  require 'synapse/process_manager/correlation_resolver'
3
3
  require 'synapse/process_manager/correlation_set'
4
+ require 'synapse/process_manager/lock_manager'
5
+ require 'synapse/process_manager/pessimistic_lock_manager'
4
6
  require 'synapse/process_manager/process'
7
+ require 'synapse/process_manager/process_factory'
8
+ require 'synapse/process_manager/process_manager'
9
+ require 'synapse/process_manager/process_repository'
10
+ require 'synapse/process_manager/resource_injector'
11
+ require 'synapse/process_manager/simple_process_manager'
12
+
13
+ require 'synapse/process_manager/wiring/process'
14
+ require 'synapse/process_manager/wiring/process_manager'
15
+
16
+ require 'synapse/process_manager/repository/in_memory'
@@ -0,0 +1,22 @@
1
+ module Synapse
2
+ module ProcessManager
3
+ # Represents a mechanism for synchronizing access to processes
4
+ #
5
+ # This base implementation does no locking; it can be used if processes are thread safe
6
+ # and don't need any additional synchronization.
7
+ class LockManager
8
+ # Obtains a lock for a process with the given identifier, blocking if necessary
9
+ #
10
+ # @param [String] process_id
11
+ # @return [undefined]
12
+ def obtain_lock(process_id); end
13
+
14
+ # Releases the lock for a process with the given identifier
15
+ #
16
+ # @raise [ThreadError] If thread didn't previously hold the lock
17
+ # @param [String] process_id
18
+ # @return [undefined]
19
+ def release_lock(process_id); end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ module Synapse
2
+ module ProcessManager
3
+ # Lock manager that does pessimistic locking for processes
4
+ class PessimisticLockManager
5
+ def initialize
6
+ @lock = IdentifierLock.new
7
+ end
8
+
9
+ # @param [String] process_id
10
+ # @return [undefined]
11
+ def obtain_lock(process_id)
12
+ @lock.obtain_lock process_id
13
+ end
14
+
15
+ # @raise [ThreadError] If thread didn't previously hold the lock
16
+ # @param [String] process_id
17
+ # @return [undefined]
18
+ def release_lock(process_id)
19
+ @lock.release_lock process_id
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,5 +1,7 @@
1
1
  module Synapse
2
2
  module ProcessManager
3
+ # Processes are used to maintain the state of long-running business transactions
4
+ #
3
5
  # The term process is used in Enterprise Integration Patterns to describe a mechanism used to
4
6
  # "maintain the state of the sequence and determine the next processing step based on
5
7
  # intermediate results" (Hohpe 279). Processes are also called sagas in some CQRS frameworks.
@@ -0,0 +1,52 @@
1
+ module Synapse
2
+ module ProcessManager
3
+ # Represents a mechanism for create instances of processes
4
+ # @abstract
5
+ class ProcessFactory
6
+ # Creates a new instance of a process of a given type
7
+ #
8
+ # The returned process will be fully initialized and any resources required will be
9
+ # provided through dependency injection.
10
+ #
11
+ # @abstract
12
+ # @param [Class] process_type
13
+ # @return [Process]
14
+ def create(process_type); end
15
+
16
+ # Returns true if processes of the given type can be created by this factory
17
+ #
18
+ # @abstract
19
+ # @param [Class] process_type
20
+ # @return [Boolean]
21
+ def supports(process_type); end
22
+ end
23
+
24
+ # Generic implementation of a process factory that supports any process implementations that
25
+ # have a no-argument constructor
26
+ class GenericProcessFactory < ProcessFactory
27
+ # @return [ResourceInjector]
28
+ attr_accessor :resource_injector
29
+
30
+ # @return [undefined]
31
+ def initialize
32
+ @resource_injector = ResourceInjector.new
33
+ end
34
+
35
+ # @param [Class] process_type
36
+ # @return [Process]
37
+ def create(process_type)
38
+ process = process_type.new
39
+ process.tap do
40
+ @resource_injector.inject_resources process
41
+ end
42
+ end
43
+
44
+ # @param [Class] process_type
45
+ # @return [Boolean]
46
+ def supports(process_type)
47
+ ctor = process_type.instance_method :initialize
48
+ ctor.arity <= 0
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,170 @@
1
+ module Synapse
2
+ module ProcessManager
3
+ # Represents a mechanism for managing the lifeycle and notification of process instances
4
+ # @abstract
5
+ class ProcessManager
6
+ include EventBus::EventListener
7
+
8
+ # @return [Boolean] Returns true if exceptions will be logged silently
9
+ attr_accessor :suppress_exceptions
10
+
11
+ # @param [ProcessRepository] repository
12
+ # @param [ProcessFactory] factory
13
+ # @param [LockManager] lock_manager
14
+ # @param [Class...] process_types
15
+ # @return [undefined]
16
+ def initialize(repository, factory, lock_manager, *process_types)
17
+ @repository = repository
18
+ @factory = factory
19
+ @lock_manager = lock_manager
20
+ @process_types = process_types.flatten
21
+
22
+ @logger = Logging.logger[self.class]
23
+ @suppress_exceptions = true
24
+ end
25
+
26
+ # @param [EventMessage] event
27
+ # @return [undefined]
28
+ def notify(event)
29
+ @process_types.each do |process_type|
30
+ correlation = extract_correlation process_type, event
31
+ if correlation
32
+ current = notify_current_processes process_type, event, correlation
33
+ if should_start_new_process process_type, event, current
34
+ start_new_process process_type, event, correlation
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ protected
41
+
42
+ # @abstract
43
+ # @param [Class] process_type
44
+ # @param [EventMessage] event
45
+ # @return [Symbol]
46
+ def creation_policy_for(process_type, event); end
47
+
48
+ # @abstract
49
+ # @param [Class] process_type
50
+ # @param [EventMessage] event
51
+ # @return [Correlation] Returns nil if no correlation could be extracted
52
+ def extract_correlation(process_type, event); end
53
+
54
+ # Determines whether or not a new process should be started, based off of existing processes
55
+ # and the creation policy for the event and process
56
+ #
57
+ # @param [Class] process_type
58
+ # @param [EventMessage] event
59
+ # @param [Boolean] current_processes True if there are existing processes
60
+ # @return [Boolean]
61
+ def should_start_new_process(process_type, event, current_processes)
62
+ creation_policy = creation_policy_for process_type, event
63
+
64
+ if :always == creation_policy
65
+ true
66
+ elsif :if_none_found == creation_policy
67
+ !current_processes
68
+ else
69
+ false
70
+ end
71
+ end
72
+
73
+ # Notifies existing processes of the given type and correlation of the given event
74
+ #
75
+ # @param [Class] process_type
76
+ # @param [EventMessage] event
77
+ # @param [Correlation] correlation
78
+ # @return [Boolean] Returns true if any current processes were found and notified
79
+ def notify_current_processes(process_type, event, correlation)
80
+ processes = @repository.find process_type, correlation
81
+
82
+ process_invoked = false
83
+ processes.each do |process_id|
84
+ @lock_manager.obtain_lock process_id
85
+ begin
86
+ loaded_process = notify_current_process process_id, event, correlation
87
+ if loaded_process
88
+ process_invoked = true
89
+ end
90
+ ensure
91
+ @lock_manager.release_lock process_id
92
+ end
93
+ end
94
+
95
+ process_invoked
96
+ end
97
+
98
+ # Loads and notifies the process with the given identifier of the given event
99
+ #
100
+ # @param [String] process_id
101
+ # @param [EventMessage] event
102
+ # @param [Correlation] correlation
103
+ # @return [Process]
104
+ def notify_current_process(process_id, event, correlation)
105
+ process = @repository.load process_id
106
+
107
+ unless process and process.active and process.correlations.include? correlation
108
+ # Process has changed or was deleted between the time of the selection query and the
109
+ # actual loading and locking of the process
110
+ return
111
+ end
112
+
113
+ begin
114
+ notify_process process, event
115
+ ensure
116
+ @repository.commit process
117
+ end
118
+
119
+ process
120
+ end
121
+
122
+ # Creates a new process of the given type with the given correlation
123
+ #
124
+ # After the process has been created, it is notified of the given event and then is
125
+ # committed to the process repository.
126
+ #
127
+ # @param [Class] process_type
128
+ # @param [EventMessage] event
129
+ # @param [Correlation] correlation
130
+ # @return [undefined]
131
+ def start_new_process(process_type, event, correlation)
132
+ process = @factory.create process_type
133
+ process.correlations.add correlation
134
+
135
+ @lock_manager.obtain_lock process.id
136
+
137
+ begin
138
+ notify_process process, event
139
+ ensure
140
+ begin
141
+ @repository.add process
142
+ ensure
143
+ @lock_manager.release_lock process.id
144
+ end
145
+ end
146
+ end
147
+
148
+ # Notifies the given process with of the given event
149
+ #
150
+ # @raise [Exception] If an error occurs while notifying the process and exception
151
+ # suppression is disabled
152
+ # @param [Process] process
153
+ # @param [EventMessage] event
154
+ # @return [undefined]
155
+ def notify_process(process, event)
156
+ begin
157
+ process.handle event
158
+ rescue => exception
159
+ if @suppress_exceptions
160
+ backtrace = exception.backtrace.join $/
161
+ @logger.error 'Exception occured while invoking process [%s] [%s] with [%s]: %s %s' %
162
+ [process.class, process.id, event.payload_type, exception.inspect, backtrace]
163
+ else
164
+ raise
165
+ end
166
+ end
167
+ end
168
+ end # ProcessManager
169
+ end # ProcessManager
170
+ end