synapse-core 0.5.6 → 0.6.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 (74) hide show
  1. checksums.yaml +7 -0
  2. data/lib/synapse-core.rb +31 -1
  3. data/lib/synapse/command.rb +1 -0
  4. data/lib/synapse/command/callbacks/future.rb +3 -3
  5. data/lib/synapse/command/callbacks/void.rb +14 -0
  6. data/lib/synapse/command/command_bus.rb +2 -2
  7. data/lib/synapse/command/command_callback.rb +9 -2
  8. data/lib/synapse/command/gateway.rb +1 -1
  9. data/lib/synapse/command/gateway/interval_retry_scheduler.rb +6 -6
  10. data/lib/synapse/command/gateway/retry_scheduler.rb +7 -4
  11. data/lib/synapse/command/interceptor_chain.rb +1 -1
  12. data/lib/synapse/command/interceptors/serialization.rb +2 -1
  13. data/lib/synapse/command/simple_command_bus.rb +23 -34
  14. data/lib/synapse/common.rb +2 -2
  15. data/lib/synapse/common/concurrency/disposable_lock.rb +157 -0
  16. data/lib/synapse/common/concurrency/identifier_lock_manager.rb +164 -0
  17. data/lib/synapse/common/duplication.rb +4 -4
  18. data/lib/synapse/common/errors.rb +5 -0
  19. data/lib/synapse/configuration/container.rb +1 -1
  20. data/lib/synapse/configuration/container_builder.rb +1 -1
  21. data/lib/synapse/configuration/definition.rb +1 -1
  22. data/lib/synapse/domain/aggregate_root.rb +1 -1
  23. data/lib/synapse/domain/simple_stream.rb +3 -5
  24. data/lib/synapse/domain/stream.rb +2 -2
  25. data/lib/synapse/event_bus/event_bus.rb +2 -2
  26. data/lib/synapse/event_bus/simple_event_bus.rb +5 -6
  27. data/lib/synapse/event_sourcing/caching.rb +1 -1
  28. data/lib/synapse/event_sourcing/entity.rb +4 -2
  29. data/lib/synapse/event_sourcing/repository.rb +9 -13
  30. data/lib/synapse/event_sourcing/snapshot/taker.rb +1 -1
  31. data/lib/synapse/mapping/mapping.rb +1 -1
  32. data/lib/synapse/process_manager/correlation.rb +3 -29
  33. data/lib/synapse/process_manager/pessimistic_lock_manager.rb +3 -3
  34. data/lib/synapse/process_manager/process.rb +2 -6
  35. data/lib/synapse/process_manager/process_manager.rb +1 -1
  36. data/lib/synapse/process_manager/process_repository.rb +1 -1
  37. data/lib/synapse/process_manager/repository/in_memory.rb +5 -4
  38. data/lib/synapse/process_manager/simple_process_manager.rb +2 -2
  39. data/lib/synapse/repository/errors.rb +2 -2
  40. data/lib/synapse/repository/locking.rb +48 -0
  41. data/lib/synapse/repository/optimistic_lock_manager.rb +10 -9
  42. data/lib/synapse/repository/pessimistic_lock_manager.rb +5 -4
  43. data/lib/synapse/repository/repository.rb +1 -1
  44. data/lib/synapse/repository/simple_repository.rb +8 -7
  45. data/lib/synapse/serialization/converter.rb +3 -0
  46. data/lib/synapse/serialization/converter_factory.rb +6 -5
  47. data/lib/synapse/serialization/serialized_object.rb +4 -4
  48. data/lib/synapse/serialization/serialized_type.rb +4 -4
  49. data/lib/synapse/uow/listener_collection.rb +12 -9
  50. data/lib/synapse/uow/provider.rb +1 -1
  51. data/lib/synapse/uow/uow.rb +1 -1
  52. data/lib/synapse/upcasting/upcaster_chain.rb +1 -1
  53. data/lib/synapse/version.rb +1 -1
  54. data/test/command/serialization_test.rb +5 -2
  55. data/test/command/simple_command_bus_test.rb +9 -16
  56. data/test/command/validation_test.rb +1 -1
  57. data/test/common/concurrency/identifier_lock_manager_test.rb +137 -0
  58. data/test/configuration/component/serialization/converter_factory_test.rb +2 -2
  59. data/test/event_sourcing/repository_test.rb +18 -0
  60. data/test/repository/simple_repository_test.rb +42 -10
  61. data/test/test_helper.rb +3 -4
  62. metadata +25 -29
  63. data/lib/synapse.rb +0 -34
  64. data/lib/synapse/common/concurrency/identifier_lock.rb +0 -56
  65. data/lib/synapse/common/concurrency/public_lock.rb +0 -95
  66. data/lib/synapse/event_bus/clustering/cluster.rb +0 -10
  67. data/lib/synapse/event_bus/clustering/event_bus.rb +0 -55
  68. data/lib/synapse/event_bus/clustering/selector.rb +0 -14
  69. data/lib/synapse/rails/injection_helper.rb +0 -23
  70. data/lib/synapse/railtie.rb +0 -17
  71. data/test/common/concurrency/identifier_lock_test.rb +0 -25
  72. data/test/common/concurrency/public_lock_test.rb +0 -83
  73. data/test/process_manager/correlation_test.rb +0 -24
  74. data/test/rails/injection_helper_test.rb +0 -27
@@ -12,6 +12,7 @@ module Synapse
12
12
  # before any listeners are allowed to do anything, and log that the commit is finished after
13
13
  # all other listeners have finished.
14
14
  class UnitOfWorkListenerCollection < UnitOfWorkListener
15
+ # @return [undefined]
15
16
  def initialize
16
17
  @listeners = Array.new
17
18
  @logger = Logging.logger[self.class]
@@ -22,17 +23,17 @@ module Synapse
22
23
  # @param [UnitOfWorkListener] listener
23
24
  # @return [undefined]
24
25
  def push(listener)
25
- @logger.debug 'Registering listener [%s]' % listener.class
26
+ @logger.debug "Registering listener {#{listener.class}}"
26
27
  @listeners.push listener
27
28
  end
28
29
 
29
- alias << push
30
+ alias_method :<<, :push
30
31
 
31
32
  # @param [UnitOfWork] unit
32
33
  # @return [undefined]
33
34
  def on_start(unit)
34
35
  @listeners.each do |listener|
35
- @logger.debug 'Notifying [%s] of start' % listener.class
36
+ @logger.debug "Notifying {#{listener.class}} that unit of work is starting"
36
37
  listener.on_start unit
37
38
  end
38
39
  end
@@ -54,7 +55,7 @@ module Synapse
54
55
  # @return [undefined]
55
56
  def on_prepare_commit(unit, aggregates, events)
56
57
  @listeners.each do |listener|
57
- @logger.debug 'Notifying [%s] of commit' % listener.class
58
+ @logger.debug "Notifying {#{listener.class}} that unit of work is preparing for commit"
58
59
  listener.on_prepare_commit unit, aggregates, events
59
60
  end
60
61
  end
@@ -64,7 +65,7 @@ module Synapse
64
65
  # @return [undefined]
65
66
  def on_prepare_transaction_commit(unit, transaction)
66
67
  @listeners.each do |listener|
67
- @logger.debug 'Notifying [%s] of transactional commit' % listener.class
68
+ @logger.debug "Notifying {#{listener.class}} that unit of work is preparing for tx commit"
68
69
  listener.on_prepare_transaction_commit unit, transaction
69
70
  end
70
71
  end
@@ -73,7 +74,7 @@ module Synapse
73
74
  # @return [undefined]
74
75
  def after_commit(unit)
75
76
  @listeners.reverse_each do |listener|
76
- @logger.debug 'Notifying [%s] of finished commit' % listener.class
77
+ @logger.debug "Notifying {#{listener.class}} that unit of work has been committed"
77
78
  listener.after_commit unit
78
79
  end
79
80
  end
@@ -83,7 +84,7 @@ module Synapse
83
84
  # @return [undefined]
84
85
  def on_rollback(unit, cause = nil)
85
86
  @listeners.reverse_each do |listener|
86
- @logger.debug 'Notifying [%s] of rollback' % listener.class
87
+ @logger.debug "Notifying {#{listener.class}} that unit of work is rolling back"
87
88
  listener.on_rollback unit, cause
88
89
  end
89
90
  end
@@ -92,13 +93,15 @@ module Synapse
92
93
  # @return [undefined]
93
94
  def on_cleanup(unit)
94
95
  @listeners.reverse_each do |listener|
95
- @logger.debug 'Notifying [%s] of cleanup' % listener.class
96
+ @logger.debug "Notifying {#{listener.class}} that unit of work is cleaning up"
96
97
 
97
98
  begin
98
99
  listener.on_cleanup unit
99
100
  rescue => exception
100
101
  # Ignore this exception so that we can continue cleaning up
101
- @logger.warn 'Listener raised an exception during cleanup: %s' % exception.inspect
102
+ backtrace = exception.backtrace.join $RS
103
+ @logger.warn "Listener {#{listener.class}} raised exception during cleanup: " +
104
+ "#{exception.inspect} #{backtrace}"
102
105
  end
103
106
  end
104
107
  end
@@ -3,6 +3,7 @@ module Synapse
3
3
  # Entry point for components to access units of work. Components managing transactional
4
4
  # boundaries can register and clear unit of work instances.
5
5
  class UnitOfWorkProvider
6
+ # @return [undefined]
6
7
  def initialize
7
8
  @threads = Hash.new
8
9
  end
@@ -40,7 +41,6 @@ module Synapse
40
41
  stack.last
41
42
  end
42
43
 
43
-
44
44
  # Pushes the given unit of work onto the top of the stack, making it the active unit of work
45
45
  #
46
46
  # If there are other units of work bound to this provider, they will be held until the given
@@ -160,7 +160,7 @@ module Synapse
160
160
  # @return [AggregateRoot] Returns nil if no similar aggregate was found
161
161
  def find_similar_aggregate(aggregate)
162
162
  @aggregates.each_key do |candidate|
163
- if aggregate.class === candidate and aggregate.id == candidate.id
163
+ if aggregate.class === candidate && aggregate.id == candidate.id
164
164
  return candidate
165
165
  end
166
166
  end
@@ -22,7 +22,7 @@ module Synapse
22
22
  @upcasters.push upcaster
23
23
  end
24
24
 
25
- alias << push
25
+ alias_method :<<, :push
26
26
 
27
27
  # @param [SerializedObject] serialized_object
28
28
  # @param [UpcastingContext] upcast_context
@@ -1,3 +1,3 @@
1
1
  module Synapse
2
- VERSION = '0.5.6'
2
+ VERSION = '0.6.0'
3
3
  end
@@ -10,11 +10,14 @@ module Synapse
10
10
  command = CommandMessage.build
11
11
  chain = Object.new
12
12
  unit = Object.new
13
+ result = Object.new
13
14
 
14
- mock(chain).proceed(command)
15
+ mock(chain).proceed(command) do
16
+ result
17
+ end
15
18
  mock(unit).register_listener(is_a(SerializationOptimizingListener))
16
19
 
17
- interceptor.intercept(command, unit, chain)
20
+ assert_same result, interceptor.intercept(command, unit, chain)
18
21
  end
19
22
  end
20
23
 
@@ -108,29 +108,22 @@ module Synapse
108
108
  @command_bus.dispatch_with_callback command, callback
109
109
  end
110
110
 
111
- should 'log when a subscribed handler is replaced' do
112
- handler = Object.new
113
-
114
- mock(@logger).debug(anything).ordered
115
- mock(@logger).info(anything).ordered
111
+ should 'return the previous handler when a subscribed handler is replaced' do
112
+ handler_a = Object.new
113
+ handler_b = Object.new
116
114
 
117
- @command_bus.subscribe TestCommand, handler
118
- @command_bus.subscribe TestCommand, handler
115
+ assert_nil (@command_bus.subscribe TestCommand, handler_a)
116
+ assert_same (@command_bus.subscribe TestCommand, handler_b), handler_a
119
117
  end
120
118
 
121
- should 'log when a handler is unsubscribed' do
119
+ should 'return true when a handler is unsubscribed' do
122
120
  handler_a = Object.new
123
121
  handler_b = Object.new
124
122
 
125
- mock(@logger).info(anything).ordered # not subscribed to anyone
126
- mock(@logger).debug(anything).ordered # now subscribed
127
- mock(@logger).info(anything).ordered # subscribed to different
128
- mock(@logger).debug(anything).ordered # now unsubscribed
129
-
130
- @command_bus.unsubscribe TestCommand, handler_a
123
+ refute @command_bus.unsubscribe TestCommand, handler_a
131
124
  @command_bus.subscribe TestCommand, handler_a
132
- @command_bus.unsubscribe TestCommand, handler_b
133
- @command_bus.unsubscribe TestCommand, handler_a
125
+ refute @command_bus.unsubscribe TestCommand, handler_b
126
+ assert @command_bus.unsubscribe TestCommand, handler_a
134
127
  end
135
128
  end
136
129
 
@@ -11,7 +11,7 @@ module Synapse
11
11
  end
12
12
 
13
13
  filter = ActiveModelValidationFilter.new
14
- filter.filter message
14
+ assert_same message, filter.filter(message)
15
15
  end
16
16
 
17
17
  should 'raise an exception if payload of a command message is invalid' do
@@ -0,0 +1,137 @@
1
+ require 'test_helper'
2
+
3
+ module Synapse
4
+ class IdentifierLockManagerTest < Test::Unit::TestCase
5
+ CountdownLatch = Contender::CountdownLatch
6
+
7
+ should 'dispose locks when they are no longer in use' do
8
+ manager = IdentifierLockManager.new
9
+
10
+ identifier = SecureRandom.uuid
11
+ manager.obtain_lock identifier
12
+ manager.release_lock identifier
13
+
14
+ assert_equal 0, manager.internal_locks.size
15
+ end
16
+
17
+ should 'not dispose locks when they are still in use' do
18
+ manager = IdentifierLockManager.new
19
+
20
+ identifier = SecureRandom.uuid
21
+
22
+ refute manager.owned? identifier
23
+
24
+ manager.obtain_lock identifier
25
+ assert manager.owned? identifier
26
+
27
+ manager.obtain_lock identifier
28
+ assert manager.owned? identifier
29
+
30
+ manager.release_lock identifier
31
+ assert manager.owned? identifier
32
+
33
+ manager.release_lock identifier
34
+ refute manager.owned? identifier
35
+ end
36
+
37
+ should 'detect a deadlock between two threads' do
38
+ manager = IdentifierLockManager.new
39
+
40
+ start_latch = CountdownLatch.new 1
41
+ latch = CountdownLatch.new 1
42
+ deadlock = Atomic.new false
43
+
44
+ lock_a = SecureRandom.uuid
45
+ lock_b = SecureRandom.uuid
46
+
47
+ start_lock_thread start_latch, latch, deadlock, manager, lock_a, manager, lock_b
48
+
49
+ manager.obtain_lock lock_b
50
+
51
+ start_latch.await
52
+ latch.countdown
53
+
54
+ begin
55
+ manager.obtain_lock lock_a
56
+ assert deadlock.get
57
+ rescue DeadlockError
58
+ # This is expected behavior
59
+ end
60
+ end
61
+
62
+ should 'detect a deadlock between two threads across lock managers' do
63
+ manager_a = IdentifierLockManager.new
64
+ manager_b = IdentifierLockManager.new
65
+
66
+ start_latch = CountdownLatch.new 1
67
+ latch = CountdownLatch.new 1
68
+ deadlock = Atomic.new false
69
+
70
+ lock_a = SecureRandom.uuid
71
+ lock_b = SecureRandom.uuid
72
+
73
+ start_lock_thread start_latch, latch, deadlock, manager_a, lock_a, manager_b, lock_a
74
+
75
+ manager_b.obtain_lock lock_a
76
+
77
+ start_latch.await
78
+ latch.countdown
79
+
80
+ begin
81
+ manager_a.obtain_lock lock_a
82
+ assert deadlock.get
83
+ rescue DeadlockError
84
+ # This is expected behavior
85
+ end
86
+ end
87
+
88
+ should 'detect a deadlock between three threads in a vector' do
89
+ manager = IdentifierLockManager.new
90
+
91
+ start_latch = CountdownLatch.new 3
92
+ latch = CountdownLatch.new 1
93
+ deadlock = Atomic.new false
94
+
95
+ lock_a = SecureRandom.uuid
96
+ lock_b = SecureRandom.uuid
97
+ lock_c = SecureRandom.uuid
98
+ lock_d = SecureRandom.uuid
99
+
100
+ start_lock_thread start_latch, latch, deadlock, manager, lock_a, manager, lock_b
101
+ start_lock_thread start_latch, latch, deadlock, manager, lock_b, manager, lock_c
102
+ start_lock_thread start_latch, latch, deadlock, manager, lock_c, manager, lock_d
103
+
104
+ manager.obtain_lock lock_d
105
+
106
+ start_latch.await
107
+ latch.countdown
108
+
109
+ begin
110
+ manager.obtain_lock lock_a
111
+ assert deadlock.get
112
+ rescue DeadlockError
113
+ # This is expected behavior
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ def start_lock_thread(start_latch, latch, deadlock, manager_a, lock_a, manager_b, lock_b)
120
+ Thread.new do
121
+ manager_a.obtain_lock lock_a
122
+ start_latch.countdown
123
+
124
+ begin
125
+ latch.await
126
+
127
+ manager_b.obtain_lock lock_b
128
+ manager_b.release_lock lock_b
129
+ rescue DeadlockError
130
+ deadlock.set true
131
+ ensure
132
+ manager_a.release_lock lock_a
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -33,7 +33,7 @@ module Synapse
33
33
  @builder.converter_factory
34
34
 
35
35
  factory = @container.resolve :converter_factory
36
- factory.converters.at(0).is_a? Serialization::JsonToObjectConverter
36
+ factory.converters.first.is_a? Serialization::JsonToObjectConverter
37
37
 
38
38
  # Customized
39
39
  @builder.converter_factory :alt_factory do
@@ -41,7 +41,7 @@ module Synapse
41
41
  end
42
42
 
43
43
  factory = @container.resolve :alt_factory
44
- factory.converters.at(0).is_a? Serialization::JsonToObjectConverter
44
+ factory.converters.first.is_a? Serialization::JsonToObjectConverter
45
45
  end
46
46
  end
47
47
  end
@@ -70,6 +70,24 @@ module Synapse
70
70
  end
71
71
  end
72
72
 
73
+ should 'raise an exception while saving if lock could not be validated' do
74
+ event = create_event(123, 0, StubCreatedEvent.new(123))
75
+
76
+ mock(@event_store).read_events(@factory.type_identifier, 123) do
77
+ Domain::SimpleDomainEventStream.new event
78
+ end
79
+
80
+ aggregate = @repository.load 123
81
+
82
+ mock(@lock_manager).validate_lock(aggregate) do
83
+ false
84
+ end
85
+
86
+ assert_raise Repository::ConcurrencyError do
87
+ @unit.commit
88
+ end
89
+ end
90
+
73
91
  should 'defer version checking to a conflict resolver if one is set' do
74
92
  @repository.conflict_resolver = AcceptAllConflictResolver.new
75
93
 
@@ -2,12 +2,15 @@ require 'test_helper'
2
2
 
3
3
  module Synapse
4
4
  module Repository
5
+
5
6
  class SimpleRepositoryTest < Test::Unit::TestCase
6
7
  def setup
7
8
  @unit_provider = UnitOfWork::UnitOfWorkProvider.new
8
9
  @unit_factory = UnitOfWork::UnitOfWorkFactory.new @unit_provider
9
10
 
10
- @repository = SimpleRepository.new NullLockManager.new, TestMappedAggregate
11
+ @lock_manager = NullLockManager.new
12
+
13
+ @repository = SimpleRepository.new @lock_manager, TestMappedAggregate
11
14
  @repository.event_bus = EventBus::SimpleEventBus.new
12
15
  @repository.unit_provider = @unit_provider
13
16
  end
@@ -15,43 +18,71 @@ module Synapse
15
18
  should 'load an aggregate using its finder' do
16
19
  unit = @unit_factory.create
17
20
 
18
- aggregate = TestMappedAggregate.new '5677b4f7'
19
- mock(TestMappedAggregate).find(aggregate.id) do
21
+ aggregate_id = SecureRandom.uuid
22
+ aggregate = TestMappedAggregate.new aggregate_id
23
+
24
+ mock(TestMappedAggregate).find(aggregate_id) do
20
25
  aggregate
21
26
  end
22
27
 
23
- loaded = @repository.load aggregate.id
28
+ loaded = @repository.load aggregate_id
24
29
 
25
30
  assert_same loaded, aggregate
26
31
  end
27
32
 
28
33
  should 'raise an exception if the aggregate could not be found' do
29
- mock(TestMappedAggregate).find('5677b4f7')
34
+ aggregate_id = SecureRandom.uuid
35
+
36
+ mock(TestMappedAggregate).find(aggregate_id)
30
37
 
31
38
  assert_raise AggregateNotFoundError do
32
- @repository.load '5677b4f7'
39
+ @repository.load aggregate_id
33
40
  end
34
41
  end
35
42
 
36
43
  should 'raise an exception if the loaded aggregate has an unexpected version' do
37
44
  unit = @unit_factory.create
38
45
 
39
- aggregate = TestMappedAggregate.new '5677b4f7'
46
+ aggregate_id = SecureRandom.uuid
47
+ aggregate = TestMappedAggregate.new aggregate_id
40
48
  aggregate.version = 5
41
49
 
42
- mock(TestMappedAggregate).find(aggregate.id) do
50
+ mock(TestMappedAggregate).find(aggregate_id) do
43
51
  aggregate
44
52
  end
45
53
 
46
54
  assert_raise ConflictingAggregateVersionError do
47
- @repository.load aggregate.id, 4
55
+ @repository.load aggregate_id, 4
56
+ end
57
+ end
58
+
59
+ should 'raise an exception while saving if lock could not be validated' do
60
+ unit = @unit_factory.create
61
+
62
+ aggregate_id = SecureRandom.uuid
63
+ aggregate = TestMappedAggregate.new aggregate_id
64
+ aggregate.version = 5
65
+
66
+ mock(TestMappedAggregate).find(aggregate_id) do
67
+ aggregate
68
+ end
69
+
70
+ @repository.load aggregate_id
71
+
72
+ mock(@lock_manager).validate_lock(aggregate) do
73
+ false
74
+ end
75
+
76
+ assert_raise ConcurrencyError do
77
+ unit.commit
48
78
  end
49
79
  end
50
80
 
51
81
  should 'delete the aggregate if it has been marked for deletion' do
52
82
  unit = @unit_factory.create
53
83
 
54
- aggregate = TestMappedAggregate.new '5677b4f7'
84
+ aggregate_id = SecureRandom.uuid
85
+ aggregate = TestMappedAggregate.new aggregate_id
55
86
  aggregate.delete_this_thing
56
87
 
57
88
  @repository.add aggregate
@@ -75,5 +106,6 @@ module Synapse
75
106
  mark_deleted
76
107
  end
77
108
  end
109
+
78
110
  end
79
111
  end