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