synapse-core 0.5.6 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/synapse-core.rb +31 -1
- data/lib/synapse/command.rb +1 -0
- data/lib/synapse/command/callbacks/future.rb +3 -3
- data/lib/synapse/command/callbacks/void.rb +14 -0
- data/lib/synapse/command/command_bus.rb +2 -2
- data/lib/synapse/command/command_callback.rb +9 -2
- data/lib/synapse/command/gateway.rb +1 -1
- data/lib/synapse/command/gateway/interval_retry_scheduler.rb +6 -6
- data/lib/synapse/command/gateway/retry_scheduler.rb +7 -4
- data/lib/synapse/command/interceptor_chain.rb +1 -1
- data/lib/synapse/command/interceptors/serialization.rb +2 -1
- data/lib/synapse/command/simple_command_bus.rb +23 -34
- data/lib/synapse/common.rb +2 -2
- data/lib/synapse/common/concurrency/disposable_lock.rb +157 -0
- data/lib/synapse/common/concurrency/identifier_lock_manager.rb +164 -0
- data/lib/synapse/common/duplication.rb +4 -4
- data/lib/synapse/common/errors.rb +5 -0
- data/lib/synapse/configuration/container.rb +1 -1
- data/lib/synapse/configuration/container_builder.rb +1 -1
- data/lib/synapse/configuration/definition.rb +1 -1
- data/lib/synapse/domain/aggregate_root.rb +1 -1
- data/lib/synapse/domain/simple_stream.rb +3 -5
- data/lib/synapse/domain/stream.rb +2 -2
- data/lib/synapse/event_bus/event_bus.rb +2 -2
- data/lib/synapse/event_bus/simple_event_bus.rb +5 -6
- data/lib/synapse/event_sourcing/caching.rb +1 -1
- data/lib/synapse/event_sourcing/entity.rb +4 -2
- data/lib/synapse/event_sourcing/repository.rb +9 -13
- data/lib/synapse/event_sourcing/snapshot/taker.rb +1 -1
- data/lib/synapse/mapping/mapping.rb +1 -1
- data/lib/synapse/process_manager/correlation.rb +3 -29
- data/lib/synapse/process_manager/pessimistic_lock_manager.rb +3 -3
- data/lib/synapse/process_manager/process.rb +2 -6
- data/lib/synapse/process_manager/process_manager.rb +1 -1
- data/lib/synapse/process_manager/process_repository.rb +1 -1
- data/lib/synapse/process_manager/repository/in_memory.rb +5 -4
- data/lib/synapse/process_manager/simple_process_manager.rb +2 -2
- data/lib/synapse/repository/errors.rb +2 -2
- data/lib/synapse/repository/locking.rb +48 -0
- data/lib/synapse/repository/optimistic_lock_manager.rb +10 -9
- data/lib/synapse/repository/pessimistic_lock_manager.rb +5 -4
- data/lib/synapse/repository/repository.rb +1 -1
- data/lib/synapse/repository/simple_repository.rb +8 -7
- data/lib/synapse/serialization/converter.rb +3 -0
- data/lib/synapse/serialization/converter_factory.rb +6 -5
- data/lib/synapse/serialization/serialized_object.rb +4 -4
- data/lib/synapse/serialization/serialized_type.rb +4 -4
- data/lib/synapse/uow/listener_collection.rb +12 -9
- data/lib/synapse/uow/provider.rb +1 -1
- data/lib/synapse/uow/uow.rb +1 -1
- data/lib/synapse/upcasting/upcaster_chain.rb +1 -1
- data/lib/synapse/version.rb +1 -1
- data/test/command/serialization_test.rb +5 -2
- data/test/command/simple_command_bus_test.rb +9 -16
- data/test/command/validation_test.rb +1 -1
- data/test/common/concurrency/identifier_lock_manager_test.rb +137 -0
- data/test/configuration/component/serialization/converter_factory_test.rb +2 -2
- data/test/event_sourcing/repository_test.rb +18 -0
- data/test/repository/simple_repository_test.rb +42 -10
- data/test/test_helper.rb +3 -4
- metadata +25 -29
- data/lib/synapse.rb +0 -34
- data/lib/synapse/common/concurrency/identifier_lock.rb +0 -56
- data/lib/synapse/common/concurrency/public_lock.rb +0 -95
- data/lib/synapse/event_bus/clustering/cluster.rb +0 -10
- data/lib/synapse/event_bus/clustering/event_bus.rb +0 -55
- data/lib/synapse/event_bus/clustering/selector.rb +0 -14
- data/lib/synapse/rails/injection_helper.rb +0 -23
- data/lib/synapse/railtie.rb +0 -17
- data/test/common/concurrency/identifier_lock_test.rb +0 -25
- data/test/common/concurrency/public_lock_test.rb +0 -83
- data/test/process_manager/correlation_test.rb +0 -24
- 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
|
26
|
+
@logger.debug "Registering listener {#{listener.class}}"
|
26
27
|
@listeners.push listener
|
27
28
|
end
|
28
29
|
|
29
|
-
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
-
|
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
|
data/lib/synapse/uow/provider.rb
CHANGED
@@ -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
|
data/lib/synapse/uow/uow.rb
CHANGED
@@ -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
|
163
|
+
if aggregate.class === candidate && aggregate.id == candidate.id
|
164
164
|
return candidate
|
165
165
|
end
|
166
166
|
end
|
data/lib/synapse/version.rb
CHANGED
@@ -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 '
|
112
|
-
|
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,
|
118
|
-
@command_bus.subscribe TestCommand,
|
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 '
|
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
|
-
|
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
|
|
@@ -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.
|
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.
|
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
|
-
@
|
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
|
-
|
19
|
-
|
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
|
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
|
-
|
34
|
+
aggregate_id = SecureRandom.uuid
|
35
|
+
|
36
|
+
mock(TestMappedAggregate).find(aggregate_id)
|
30
37
|
|
31
38
|
assert_raise AggregateNotFoundError do
|
32
|
-
@repository.load
|
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
|
-
|
46
|
+
aggregate_id = SecureRandom.uuid
|
47
|
+
aggregate = TestMappedAggregate.new aggregate_id
|
40
48
|
aggregate.version = 5
|
41
49
|
|
42
|
-
mock(TestMappedAggregate).find(
|
50
|
+
mock(TestMappedAggregate).find(aggregate_id) do
|
43
51
|
aggregate
|
44
52
|
end
|
45
53
|
|
46
54
|
assert_raise ConflictingAggregateVersionError do
|
47
|
-
@repository.load
|
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
|
-
|
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
|