promiscuous 0.53.1 → 0.90.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.
- data/lib/promiscuous.rb +25 -28
- data/lib/promiscuous/amqp.rb +27 -8
- data/lib/promiscuous/amqp/bunny.rb +131 -16
- data/lib/promiscuous/amqp/fake.rb +52 -0
- data/lib/promiscuous/amqp/hot_bunnies.rb +56 -0
- data/lib/promiscuous/amqp/null.rb +6 -6
- data/lib/promiscuous/cli.rb +108 -24
- data/lib/promiscuous/config.rb +73 -12
- data/lib/promiscuous/convenience.rb +18 -0
- data/lib/promiscuous/dependency.rb +59 -0
- data/lib/promiscuous/dsl.rb +36 -0
- data/lib/promiscuous/error.rb +3 -1
- data/lib/promiscuous/error/already_processed.rb +5 -0
- data/lib/promiscuous/error/base.rb +1 -0
- data/lib/promiscuous/error/connection.rb +7 -5
- data/lib/promiscuous/error/dependency.rb +111 -0
- data/lib/promiscuous/error/lock_unavailable.rb +12 -0
- data/lib/promiscuous/error/lost_lock.rb +12 -0
- data/lib/promiscuous/error/missing_context.rb +29 -0
- data/lib/promiscuous/error/publisher.rb +5 -15
- data/lib/promiscuous/error/recovery.rb +7 -0
- data/lib/promiscuous/error/subscriber.rb +2 -4
- data/lib/promiscuous/key.rb +36 -0
- data/lib/promiscuous/loader.rb +12 -16
- data/lib/promiscuous/middleware.rb +112 -0
- data/lib/promiscuous/publisher.rb +7 -4
- data/lib/promiscuous/publisher/context.rb +92 -0
- data/lib/promiscuous/publisher/mock_generator.rb +72 -0
- data/lib/promiscuous/publisher/model.rb +3 -86
- data/lib/promiscuous/publisher/model/active_record.rb +8 -15
- data/lib/promiscuous/publisher/model/base.rb +136 -0
- data/lib/promiscuous/publisher/model/ephemeral.rb +69 -0
- data/lib/promiscuous/publisher/model/mock.rb +61 -0
- data/lib/promiscuous/publisher/model/mongoid.rb +57 -100
- data/lib/promiscuous/{common/lint.rb → publisher/operation.rb} +1 -1
- data/lib/promiscuous/publisher/operation/base.rb +707 -0
- data/lib/promiscuous/publisher/operation/mongoid.rb +370 -0
- data/lib/promiscuous/publisher/worker.rb +22 -0
- data/lib/promiscuous/railtie.rb +21 -3
- data/lib/promiscuous/redis.rb +132 -40
- data/lib/promiscuous/resque.rb +12 -0
- data/lib/promiscuous/sidekiq.rb +15 -0
- data/lib/promiscuous/subscriber.rb +9 -20
- data/lib/promiscuous/subscriber/model.rb +4 -104
- data/lib/promiscuous/subscriber/model/active_record.rb +10 -0
- data/lib/promiscuous/subscriber/model/base.rb +96 -0
- data/lib/promiscuous/subscriber/model/mongoid.rb +86 -0
- data/lib/promiscuous/subscriber/model/observer.rb +37 -0
- data/lib/promiscuous/subscriber/operation.rb +167 -0
- data/lib/promiscuous/subscriber/payload.rb +34 -0
- data/lib/promiscuous/subscriber/worker.rb +22 -18
- data/lib/promiscuous/subscriber/worker/message.rb +48 -25
- data/lib/promiscuous/subscriber/worker/message_synchronizer.rb +273 -181
- data/lib/promiscuous/subscriber/worker/pump.rb +17 -43
- data/lib/promiscuous/subscriber/worker/recorder.rb +24 -0
- data/lib/promiscuous/subscriber/worker/runner.rb +24 -3
- data/lib/promiscuous/subscriber/worker/stats.rb +62 -0
- data/lib/promiscuous/timer.rb +38 -0
- data/lib/promiscuous/version.rb +1 -1
- metadata +98 -143
- data/README.md +0 -33
- data/lib/promiscuous/amqp/ruby_amqp.rb +0 -140
- data/lib/promiscuous/common.rb +0 -4
- data/lib/promiscuous/common/class_helpers.rb +0 -12
- data/lib/promiscuous/common/lint/base.rb +0 -24
- data/lib/promiscuous/common/options.rb +0 -51
- data/lib/promiscuous/ephemeral.rb +0 -14
- data/lib/promiscuous/error/recover.rb +0 -1
- data/lib/promiscuous/observer.rb +0 -5
- data/lib/promiscuous/publisher/active_record.rb +0 -7
- data/lib/promiscuous/publisher/amqp.rb +0 -18
- data/lib/promiscuous/publisher/attributes.rb +0 -32
- data/lib/promiscuous/publisher/base.rb +0 -23
- data/lib/promiscuous/publisher/class.rb +0 -36
- data/lib/promiscuous/publisher/envelope.rb +0 -7
- data/lib/promiscuous/publisher/ephemeral.rb +0 -9
- data/lib/promiscuous/publisher/lint.rb +0 -35
- data/lib/promiscuous/publisher/lint/amqp.rb +0 -14
- data/lib/promiscuous/publisher/lint/attributes.rb +0 -12
- data/lib/promiscuous/publisher/lint/base.rb +0 -5
- data/lib/promiscuous/publisher/lint/class.rb +0 -15
- data/lib/promiscuous/publisher/lint/polymorphic.rb +0 -22
- data/lib/promiscuous/publisher/mock.rb +0 -79
- data/lib/promiscuous/publisher/mongoid.rb +0 -33
- data/lib/promiscuous/publisher/mongoid/embedded.rb +0 -27
- data/lib/promiscuous/publisher/mongoid/embedded_many.rb +0 -12
- data/lib/promiscuous/publisher/polymorphic.rb +0 -8
- data/lib/promiscuous/subscriber/active_record.rb +0 -11
- data/lib/promiscuous/subscriber/amqp.rb +0 -25
- data/lib/promiscuous/subscriber/attributes.rb +0 -35
- data/lib/promiscuous/subscriber/base.rb +0 -29
- data/lib/promiscuous/subscriber/class.rb +0 -29
- data/lib/promiscuous/subscriber/dummy.rb +0 -19
- data/lib/promiscuous/subscriber/envelope.rb +0 -18
- data/lib/promiscuous/subscriber/lint.rb +0 -30
- data/lib/promiscuous/subscriber/lint/amqp.rb +0 -21
- data/lib/promiscuous/subscriber/lint/attributes.rb +0 -21
- data/lib/promiscuous/subscriber/lint/base.rb +0 -14
- data/lib/promiscuous/subscriber/lint/class.rb +0 -13
- data/lib/promiscuous/subscriber/lint/polymorphic.rb +0 -39
- data/lib/promiscuous/subscriber/mongoid.rb +0 -27
- data/lib/promiscuous/subscriber/mongoid/embedded.rb +0 -17
- data/lib/promiscuous/subscriber/mongoid/embedded_many.rb +0 -44
- data/lib/promiscuous/subscriber/observer.rb +0 -26
- data/lib/promiscuous/subscriber/polymorphic.rb +0 -36
- data/lib/promiscuous/subscriber/upsert.rb +0 -12
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
class Promiscuous::Subscriber::Payload
|
|
2
|
+
attr_accessor :message, :id, :operation, :attributes, :model
|
|
3
|
+
|
|
4
|
+
def initialize(payload, message=nil)
|
|
5
|
+
self.message = message
|
|
6
|
+
|
|
7
|
+
if payload.is_a?(Hash) && payload['__amqp__']
|
|
8
|
+
self.id = payload['id']
|
|
9
|
+
self.operation = payload['operation'].try(:to_sym)
|
|
10
|
+
self.attributes = payload['payload'] # TODO payload payload... not great.
|
|
11
|
+
self.model = self.class.get_subscribed_model(payload)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.get_subscribed_model(payload)
|
|
16
|
+
# TODO test the regexp source
|
|
17
|
+
mapping = Promiscuous::Subscriber::Model.mapping
|
|
18
|
+
model = mapping.select { |from| payload['__amqp__'] =~ from }.values.first
|
|
19
|
+
model = get_subscribed_subclass(model, payload) if model
|
|
20
|
+
model
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.get_subscribed_subclass(root_model, payload)
|
|
24
|
+
# TODO remove 'type' (backward compatibility)
|
|
25
|
+
received_ancestors = [payload['ancestors'] || payload['type']].flatten.compact
|
|
26
|
+
# TODO test the ancestor chain
|
|
27
|
+
subscriber_subclasses = [root_model] + root_model.descendants
|
|
28
|
+
received_ancestors.each do |ancestor|
|
|
29
|
+
model = subscriber_subclasses.select { |klass| klass.subscribe_as == ancestor }.first
|
|
30
|
+
return model if model
|
|
31
|
+
end
|
|
32
|
+
root_model
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -1,23 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
require 'celluloid/io'
|
|
3
|
-
|
|
4
|
-
class Promiscuous::Subscriber::Worker < Celluloid::SupervisionGroup
|
|
1
|
+
class Promiscuous::Subscriber::Worker
|
|
5
2
|
extend Promiscuous::Autoload
|
|
6
|
-
autoload :Message, :Pump, :MessageSynchronizer, :Runner
|
|
3
|
+
autoload :Message, :Pump, :MessageSynchronizer, :Runner, :Stats, :Recorder
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
supervise MessageSynchronizer, :as => :message_synchronizer
|
|
10
|
-
supervise Pump, :as => :pump
|
|
5
|
+
attr_accessor :message_synchronizer, :pump, :runner, :stats
|
|
11
6
|
|
|
12
|
-
def
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
7
|
+
def initialize
|
|
8
|
+
@message_synchronizer = MessageSynchronizer.new(self)
|
|
9
|
+
@pump = Pump.new(self)
|
|
10
|
+
@runner = Runner.new(self)
|
|
11
|
+
@stats = Stats.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def start
|
|
15
|
+
@message_synchronizer.connect
|
|
16
|
+
@pump.connect
|
|
17
|
+
@runner.start
|
|
18
|
+
@stats.connect
|
|
18
19
|
end
|
|
19
|
-
end
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
def stop
|
|
22
|
+
@stats.disconnect
|
|
23
|
+
@runner.stop
|
|
24
|
+
@pump.disconnect
|
|
25
|
+
@message_synchronizer.disconnect
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -1,41 +1,62 @@
|
|
|
1
1
|
class Promiscuous::Subscriber::Worker::Message
|
|
2
|
-
attr_accessor :
|
|
2
|
+
attr_accessor :payload, :parsed_payload
|
|
3
3
|
|
|
4
|
-
def initialize(
|
|
5
|
-
self.metadata = metadata
|
|
4
|
+
def initialize(payload, options={})
|
|
6
5
|
self.payload = payload
|
|
6
|
+
@metadata = options[:metadata]
|
|
7
|
+
@root_worker = options[:root_worker]
|
|
7
8
|
end
|
|
8
9
|
|
|
9
10
|
def parsed_payload
|
|
10
|
-
@parsed_payload ||=
|
|
11
|
+
@parsed_payload ||= MultiJson.load(payload)
|
|
11
12
|
end
|
|
12
13
|
|
|
13
|
-
def
|
|
14
|
+
def endpoint
|
|
14
15
|
parsed_payload['__amqp__']
|
|
15
16
|
end
|
|
16
17
|
|
|
17
|
-
def
|
|
18
|
-
|
|
19
|
-
@version ||= parsed_payload['version'].try(:symbolize_keys)
|
|
18
|
+
def timestamp
|
|
19
|
+
parsed_payload['timestamp'].to_i
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def
|
|
23
|
-
|
|
22
|
+
def dependencies
|
|
23
|
+
return @dependencies if @dependencies
|
|
24
|
+
@dependencies = parsed_payload['dependencies'].try(:symbolize_keys) || {}
|
|
25
|
+
@dependencies[:read] ||= []
|
|
26
|
+
@dependencies[:write] ||= []
|
|
27
|
+
@dependencies[:read].map! { |dep| Promiscuous::Dependency.parse(dep) }
|
|
28
|
+
@dependencies[:write].map! { |dep| Promiscuous::Dependency.parse(dep) }
|
|
29
|
+
@dependencies
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def happens_before_dependencies
|
|
33
|
+
return @happens_before_dependencies if @happens_before_dependencies
|
|
34
|
+
|
|
35
|
+
deps = []
|
|
36
|
+
deps += dependencies[:read]
|
|
37
|
+
deps += dependencies[:write].map { |dep| dep.dup.tap { |d| d.version -= 1 } }
|
|
38
|
+
|
|
39
|
+
# We return the most difficult condition to satisfy first
|
|
40
|
+
@happens_before_dependencies = deps.uniq.reverse
|
|
24
41
|
end
|
|
25
42
|
|
|
26
43
|
def has_dependencies?
|
|
27
44
|
return false if Promiscuous::Config.bareback
|
|
28
|
-
|
|
45
|
+
dependencies[:read].present? || dependencies[:write].present?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def to_s
|
|
49
|
+
"#{endpoint} -> #{happens_before_dependencies.join(', ')}"
|
|
29
50
|
end
|
|
30
51
|
|
|
31
52
|
def ack
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
53
|
+
time = Time.now
|
|
54
|
+
@metadata.ack
|
|
55
|
+
@root_worker.stats.notify_processed_message(self, time)
|
|
56
|
+
rescue Exception => e
|
|
57
|
+
# We don't care if we fail, the message will be redelivered at some point
|
|
58
|
+
Promiscuous.warn "[receive] Some exception happened, but it's okay: #{e}\n#{e.backtrace.join("\n")}"
|
|
59
|
+
Promiscuous::Config.error_notifier.try(:call, e)
|
|
39
60
|
end
|
|
40
61
|
|
|
41
62
|
def unit_of_work(type, &block)
|
|
@@ -53,16 +74,18 @@ class Promiscuous::Subscriber::Worker::Message
|
|
|
53
74
|
end
|
|
54
75
|
|
|
55
76
|
def process
|
|
56
|
-
#return if worker.stopped?
|
|
57
|
-
|
|
58
77
|
Promiscuous.debug "[receive] #{payload}"
|
|
59
|
-
unit_of_work(
|
|
60
|
-
Promiscuous::Subscriber.
|
|
78
|
+
unit_of_work(endpoint) do
|
|
79
|
+
payload = Promiscuous::Subscriber::Payload.new(parsed_payload, self)
|
|
80
|
+
Promiscuous::Subscriber::Operation.new(payload).commit
|
|
61
81
|
end
|
|
62
|
-
|
|
63
82
|
ack
|
|
64
|
-
rescue
|
|
65
|
-
e = Promiscuous::Error::Subscriber.new(
|
|
83
|
+
rescue Promiscuous::Error::AlreadyProcessed => orig_e
|
|
84
|
+
e = Promiscuous::Error::Subscriber.new(orig_e, :payload => payload)
|
|
85
|
+
Promiscuous.debug "[receive] #{e}"
|
|
86
|
+
ack
|
|
87
|
+
rescue Exception => orig_e
|
|
88
|
+
e = Promiscuous::Error::Subscriber.new(orig_e, :payload => payload)
|
|
66
89
|
Promiscuous.warn "[receive] #{e} #{e.backtrace.join("\n")}"
|
|
67
90
|
Promiscuous::Config.error_notifier.try(:call, e)
|
|
68
91
|
end
|
|
@@ -2,262 +2,354 @@ module ::Containers; end
|
|
|
2
2
|
require 'containers/priority_queue'
|
|
3
3
|
|
|
4
4
|
class Promiscuous::Subscriber::Worker::MessageSynchronizer
|
|
5
|
-
|
|
5
|
+
RECONNECT_INTERVAL = 2
|
|
6
|
+
CLEANUP_INTERVAL = 100 # messages
|
|
7
|
+
QUEUE_MAX_AGE = 100 # messages
|
|
6
8
|
|
|
7
|
-
attr_accessor :redis
|
|
9
|
+
attr_accessor :redis, :node_synchronizers, :num_processed_messages, :num_queued_messages
|
|
8
10
|
|
|
9
|
-
def initialize
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
def initialize(root)
|
|
12
|
+
@root = root
|
|
13
|
+
@node_synchronizers = {}
|
|
14
|
+
@lock = Mutex.new
|
|
15
|
+
@reconnect_timer = Promiscuous::Timer.new
|
|
12
16
|
end
|
|
13
17
|
|
|
14
|
-
def
|
|
15
|
-
|
|
18
|
+
def connected?
|
|
19
|
+
!!@redis
|
|
16
20
|
end
|
|
17
21
|
|
|
18
|
-
def
|
|
19
|
-
|
|
22
|
+
def connect
|
|
23
|
+
@lock.synchronize do
|
|
24
|
+
return unless !connected?
|
|
25
|
+
|
|
26
|
+
@num_processed_messages = 0
|
|
27
|
+
@num_queued_messages = 0
|
|
28
|
+
redis = Promiscuous::Redis.new_blocking_connection
|
|
29
|
+
redis.nodes.each { |node| @node_synchronizers[node] = NodeSynchronizer.new(self, node) }
|
|
30
|
+
@redis = redis
|
|
31
|
+
end
|
|
32
|
+
@root.pump.recover
|
|
20
33
|
end
|
|
21
34
|
|
|
22
|
-
def
|
|
23
|
-
@
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
def disconnect
|
|
36
|
+
@lock.synchronize do
|
|
37
|
+
return unless connected?
|
|
38
|
+
|
|
39
|
+
@redis, redis = nil, @redis
|
|
40
|
+
@node_synchronizers.values.each { |node_synchronizer| node_synchronizer.stop_main_loop }
|
|
41
|
+
@node_synchronizers.clear
|
|
42
|
+
redis.quit
|
|
43
|
+
end
|
|
44
|
+
rescue Exception
|
|
26
45
|
end
|
|
27
46
|
|
|
28
|
-
def
|
|
29
|
-
|
|
47
|
+
def reconnect
|
|
48
|
+
self.disconnect
|
|
49
|
+
self.connect
|
|
50
|
+
@reconnect_timer.reset
|
|
51
|
+
Promiscuous.warn "[redis] Reconnected"
|
|
30
52
|
end
|
|
31
53
|
|
|
32
54
|
def rescue_connection
|
|
33
|
-
disconnect
|
|
34
55
|
e = Promiscuous::Redis.lost_connection_exception
|
|
35
56
|
|
|
36
57
|
Promiscuous.warn "[redis] #{e}. Reconnecting..."
|
|
37
58
|
Promiscuous::Config.error_notifier.try(:call, e)
|
|
38
59
|
|
|
39
60
|
# TODO stop the pump to unack all messages
|
|
40
|
-
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def disconnect
|
|
44
|
-
self.redis.client.connection.disconnect if connected?
|
|
45
|
-
rescue
|
|
46
|
-
ensure
|
|
47
|
-
self.redis = nil
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def reconnect
|
|
51
|
-
@reconnect_timer.try(:reset)
|
|
52
|
-
@reconnect_timer = nil
|
|
53
|
-
|
|
54
|
-
unless connected?
|
|
55
|
-
self.connect
|
|
56
|
-
main_loop!
|
|
57
|
-
|
|
58
|
-
Promiscuous.warn "[redis] Reconnected"
|
|
59
|
-
Celluloid::Actor[:pump].recover
|
|
60
|
-
end
|
|
61
|
-
rescue
|
|
62
|
-
reconnect_later
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def reconnect_later
|
|
66
|
-
@reconnect_timer ||= after(2.seconds) { reconnect }
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def main_loop
|
|
70
|
-
redis_client = self.redis.client
|
|
71
|
-
loop do
|
|
72
|
-
reply = redis_client.read
|
|
73
|
-
raise reply if reply.is_a?(Redis::CommandError)
|
|
74
|
-
type, subscription, arg = reply
|
|
75
|
-
|
|
76
|
-
case type
|
|
77
|
-
when 'subscribe'
|
|
78
|
-
find_subscription(subscription).finalize_subscription
|
|
79
|
-
when 'unsubscribe'
|
|
80
|
-
when 'message'
|
|
81
|
-
find_subscription(subscription).signal_version(arg)
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
rescue EOFError
|
|
85
|
-
# Unwanted disconnection
|
|
86
|
-
rescue_connection
|
|
87
|
-
rescue IOError => e
|
|
88
|
-
unless redis_client == self.redis.client
|
|
89
|
-
# We were told to disconnect
|
|
90
|
-
else
|
|
91
|
-
raise e
|
|
92
|
-
end
|
|
93
|
-
rescue Celluloid::Task::TerminatedError
|
|
94
|
-
rescue Exception => e
|
|
95
|
-
Promiscuous.warn "[redis] #{e} #{e.backtrace.join("\n")}"
|
|
96
|
-
|
|
97
|
-
#Promiscuous::Worker.stop TODO
|
|
98
|
-
Promiscuous::Config.error_notifier.try(:call, e)
|
|
61
|
+
@reconnect_timer.run_every(RECONNECT_INTERVAL) { reconnect }
|
|
99
62
|
end
|
|
100
63
|
|
|
101
64
|
# process_when_ready() is called by the AMQP pump. This is what happens:
|
|
102
65
|
# 1. First, we subscribe to redis and wait for the confirmation.
|
|
103
66
|
# 2. Then we check if the version in redis is old enough to process the message.
|
|
104
67
|
# If not we bail out and rely on the subscription to kick the processing.
|
|
105
|
-
# Because we subscribed in advanced, we will not miss the notification
|
|
106
|
-
# extra care needs to be taken to avoid processing the message twice (see
|
|
107
|
-
# perform()).
|
|
68
|
+
# Because we subscribed in advanced, we will not miss the notification.
|
|
108
69
|
def process_when_ready(msg)
|
|
109
|
-
# Dropped messages will be redelivered as we
|
|
110
|
-
# when calling worker.pump.start
|
|
70
|
+
# Dropped messages will be redelivered as we (re)connect
|
|
111
71
|
return unless self.redis
|
|
112
72
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
73
|
+
@lock.synchronize do
|
|
74
|
+
@num_queued_messages += 1
|
|
75
|
+
end
|
|
116
76
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
77
|
+
if msg.has_dependencies?
|
|
78
|
+
process_message_proc = proc { process_message!(msg) }
|
|
79
|
+
msg.happens_before_dependencies.reduce(process_message_proc) do |chain, dep|
|
|
80
|
+
get_redis = dep.redis_node
|
|
81
|
+
subscriber_redis = dep.redis_node(@redis)
|
|
82
|
+
|
|
83
|
+
key = dep.key(:sub).join('rw').to_s
|
|
84
|
+
version = dep.version
|
|
85
|
+
node_synchronizer = @node_synchronizers[subscriber_redis]
|
|
86
|
+
proc { node_synchronizer.on_version(subscriber_redis, get_redis, key, version, msg) { chain.call } }
|
|
87
|
+
end.call
|
|
88
|
+
else
|
|
120
89
|
process_message!(msg)
|
|
121
90
|
end
|
|
122
91
|
end
|
|
123
92
|
|
|
124
|
-
def
|
|
125
|
-
@
|
|
126
|
-
|
|
93
|
+
def process_message!(msg)
|
|
94
|
+
@root.runner.messages_to_process << msg
|
|
95
|
+
|
|
96
|
+
cleanup = false
|
|
97
|
+
@lock.synchronize do
|
|
98
|
+
@num_queued_messages -= 1
|
|
99
|
+
@num_processed_messages += 1
|
|
100
|
+
cleanup = @num_processed_messages % CLEANUP_INTERVAL == 0
|
|
101
|
+
end
|
|
102
|
+
@node_synchronizers.values.each(&:cleanup_if_old) if @node_synchronizers && cleanup
|
|
127
103
|
end
|
|
128
104
|
|
|
129
105
|
def maybe_recover
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if @queued_messages == Promiscuous::Config.prefetch
|
|
106
|
+
if @num_queued_messages == Promiscuous::Config.prefetch
|
|
133
107
|
# We've reached the amount of messages the amqp queue is willing to give us.
|
|
134
|
-
# We also know that we are not processing messages (@
|
|
135
|
-
# decremented before we send the message to the runners)
|
|
108
|
+
# We also know that we are not processing messages (@num_queued_messages is
|
|
109
|
+
# decremented before we send the message to the runners), and we are called
|
|
110
|
+
# after adding a pending callback.
|
|
136
111
|
recover
|
|
137
112
|
end
|
|
138
113
|
end
|
|
139
114
|
|
|
140
115
|
def recover
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
Promiscuous::Redis.publish(global_key, version_to_allow_progress)
|
|
169
|
-
end
|
|
116
|
+
# XXX This recovery mechanism only works with one worker.
|
|
117
|
+
# We are taking the earliest message to unblock, but in reality we should
|
|
118
|
+
# do the DAG of the happens before dependencies, take root nodes
|
|
119
|
+
# of the disconnected graphs, and sort by timestamps if needed.
|
|
120
|
+
msg = blocked_messages.first
|
|
121
|
+
|
|
122
|
+
versions_to_skip = msg.happens_before_dependencies.map do |dep|
|
|
123
|
+
key = dep.key(:sub).join('rw').to_s
|
|
124
|
+
to_skip = dep.version - dep.redis_node.get(key).to_i
|
|
125
|
+
[dep, key, to_skip] if to_skip > 0
|
|
126
|
+
end.compact
|
|
127
|
+
|
|
128
|
+
return not_recovering if versions_to_skip.blank?
|
|
129
|
+
|
|
130
|
+
recovery_msg = "Skipping "
|
|
131
|
+
recovery_msg += versions_to_skip.map do |dep, key, to_skip|
|
|
132
|
+
dep.redis_node.set(key, dep.version)
|
|
133
|
+
dep.redis_node.publish(key, dep.version)
|
|
134
|
+
|
|
135
|
+
# Note: the skipped message would have a write dependency with dep.to_s
|
|
136
|
+
"#{to_skip} message(s) on #{dep}"
|
|
137
|
+
end.join(", ")
|
|
138
|
+
|
|
139
|
+
e = Promiscuous::Error::Recovery.new(recovery_msg)
|
|
140
|
+
Promiscuous.error "[synchronization recovery] #{e}"
|
|
141
|
+
# TODO Don't report when doing the initial sync
|
|
142
|
+
Promiscuous::Config.error_notifier.try(:call, e)
|
|
170
143
|
end
|
|
171
144
|
|
|
172
|
-
def
|
|
173
|
-
@
|
|
174
|
-
|
|
145
|
+
def blocked_messages
|
|
146
|
+
@node_synchronizers.values
|
|
147
|
+
.map { |node_synchronizer| node_synchronizer.blocked_messages }
|
|
148
|
+
.flatten
|
|
149
|
+
.uniq
|
|
150
|
+
.sort_by { |msg| msg.timestamp }
|
|
175
151
|
end
|
|
176
152
|
|
|
177
|
-
def
|
|
178
|
-
|
|
179
|
-
sub = get_subscription(key).subscribe
|
|
180
|
-
sub.add_callback(Subscription::Callback.new(version, callback))
|
|
181
|
-
sub.signal_version(Promiscuous::Redis.get(key))
|
|
153
|
+
def not_recovering
|
|
154
|
+
Promiscuous.warn "[synchronization recovery] Nothing to recover from"
|
|
182
155
|
end
|
|
183
156
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
@subscriptions[key]
|
|
187
|
-
end
|
|
157
|
+
class NodeSynchronizer
|
|
158
|
+
attr_accessor :node, :subscriptions, :root_synchronizer
|
|
188
159
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
160
|
+
def initialize(root_synchronizer, node)
|
|
161
|
+
@root_synchronizer = root_synchronizer
|
|
162
|
+
@node = node
|
|
163
|
+
@subscriptions = {}
|
|
164
|
+
@subscriptions_lock = Mutex.new
|
|
165
|
+
@thread = Thread.new { main_loop }
|
|
166
|
+
end
|
|
192
167
|
|
|
193
|
-
|
|
194
|
-
|
|
168
|
+
def main_loop
|
|
169
|
+
redis_client = @node.client
|
|
195
170
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
171
|
+
loop do
|
|
172
|
+
reply = redis_client.read
|
|
173
|
+
raise reply if reply.is_a?(Redis::CommandError)
|
|
174
|
+
type, subscription, arg = reply
|
|
175
|
+
|
|
176
|
+
case type
|
|
177
|
+
when 'subscribe'
|
|
178
|
+
notify_subscription(subscription)
|
|
179
|
+
when 'unsubscribe'
|
|
180
|
+
when 'message'
|
|
181
|
+
notify_key_change(subscription, arg)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
rescue EOFError, Errno::ECONNRESET
|
|
185
|
+
# Unwanted disconnection
|
|
186
|
+
@root_synchronizer.rescue_connection unless @stop
|
|
187
|
+
rescue IOError => e
|
|
188
|
+
raise e unless @stop
|
|
189
|
+
rescue Exception => e
|
|
190
|
+
Promiscuous.warn "[redis] #{e.class} #{e.message}"
|
|
191
|
+
Promiscuous.warn "[redis] #{e} #{e.backtrace.join("\n")}"
|
|
199
192
|
|
|
200
|
-
|
|
201
|
-
@subscribed_to_redis = false
|
|
202
|
-
# We use a priority queue that returns the smallest value first
|
|
203
|
-
@callbacks = Containers::PriorityQueue.new { |x, y| x < y }
|
|
193
|
+
Promiscuous::Config.error_notifier.try(:call, e)
|
|
204
194
|
end
|
|
205
195
|
|
|
206
|
-
def
|
|
207
|
-
|
|
196
|
+
def stop_main_loop
|
|
197
|
+
@stop = true
|
|
198
|
+
@thread.kill
|
|
199
|
+
end
|
|
208
200
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
201
|
+
def on_version(subscriber_redis, get_redis, key, version, message, &callback)
|
|
202
|
+
# subscriber_redis and get_redis are different connections to the
|
|
203
|
+
# same node.
|
|
204
|
+
if version == 0
|
|
205
|
+
callback.call
|
|
206
|
+
else
|
|
207
|
+
sub = get_subscription(subscriber_redis, get_redis, key)
|
|
208
|
+
sub.subscribe
|
|
209
|
+
sub.add_callback(Subscription::Callback.new(version, callback, message))
|
|
212
210
|
end
|
|
213
|
-
self
|
|
214
211
|
end
|
|
215
212
|
|
|
216
|
-
def
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
213
|
+
def blocked_messages
|
|
214
|
+
@subscriptions_lock.synchronize do
|
|
215
|
+
@subscriptions.values
|
|
216
|
+
.map(&:callbacks)
|
|
217
|
+
.map(&:next)
|
|
218
|
+
.compact
|
|
219
|
+
.map(&:message)
|
|
220
|
+
end
|
|
220
221
|
end
|
|
221
222
|
|
|
222
|
-
def
|
|
223
|
-
|
|
224
|
-
parent.signal :subscription
|
|
223
|
+
def notify_subscription(key)
|
|
224
|
+
find_subscription(key).try(:finalize_subscription)
|
|
225
225
|
end
|
|
226
226
|
|
|
227
|
-
def
|
|
228
|
-
|
|
227
|
+
def notify_key_change(key, version)
|
|
228
|
+
find_subscription(key).try(:signal_version, version)
|
|
229
229
|
end
|
|
230
230
|
|
|
231
|
-
def
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
231
|
+
def remove_subscription(key)
|
|
232
|
+
@subscriptions_lock.synchronize do
|
|
233
|
+
@subscriptions.delete(key)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
236
|
|
|
237
|
-
|
|
238
|
-
|
|
237
|
+
def find_subscription(key)
|
|
238
|
+
@subscriptions_lock.synchronize do
|
|
239
|
+
@subscriptions[key]
|
|
239
240
|
end
|
|
240
241
|
end
|
|
241
242
|
|
|
242
|
-
def
|
|
243
|
-
|
|
244
|
-
|
|
243
|
+
def get_subscription(subscriber_redis, get_redis, key)
|
|
244
|
+
@subscriptions_lock.synchronize do
|
|
245
|
+
@subscriptions[key] ||= Subscription.new(self, subscriber_redis, get_redis, key)
|
|
246
|
+
end
|
|
245
247
|
end
|
|
246
248
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
+
def cleanup_if_old
|
|
250
|
+
@subscriptions_lock.synchronize do
|
|
251
|
+
@subscriptions.values.each(&:cleanup_if_old)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
249
254
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
255
|
+
class Subscription
|
|
256
|
+
attr_accessor :node_synchronizer, :subscriber_redis, :get_redis, :key, :callbacks, :last_version
|
|
257
|
+
|
|
258
|
+
def initialize(node_synchronizer, subscriber_redis, get_redis, key)
|
|
259
|
+
self.node_synchronizer = node_synchronizer
|
|
260
|
+
self.subscriber_redis = subscriber_redis
|
|
261
|
+
self.get_redis = get_redis
|
|
262
|
+
self.key = key
|
|
263
|
+
|
|
264
|
+
@subscription_requested = false
|
|
265
|
+
# We use a priority queue that returns the smallest value first
|
|
266
|
+
@callbacks = Containers::PriorityQueue.new { |x, y| x < y }
|
|
267
|
+
@last_version = 0
|
|
268
|
+
@lock = Mutex.new
|
|
269
|
+
|
|
270
|
+
refresh_activity
|
|
253
271
|
end
|
|
254
272
|
|
|
255
|
-
def
|
|
256
|
-
|
|
273
|
+
def total_num_processed_messages
|
|
274
|
+
node_synchronizer.root_synchronizer.num_processed_messages
|
|
257
275
|
end
|
|
258
276
|
|
|
259
|
-
def
|
|
260
|
-
|
|
277
|
+
def refresh_activity
|
|
278
|
+
@last_activity_at = total_num_processed_messages
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def is_old?
|
|
282
|
+
delta = total_num_processed_messages - @last_activity_at
|
|
283
|
+
@callbacks.empty? && delta >= QUEUE_MAX_AGE
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def cleanup_if_old
|
|
287
|
+
if is_old?
|
|
288
|
+
subscriber_redis.client.process([[:unsubscribe, key]])
|
|
289
|
+
node_synchronizer.subscriptions.delete(key) # lock is already held
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def subscribe
|
|
294
|
+
@lock.synchronize do
|
|
295
|
+
return if @subscription_requested
|
|
296
|
+
@subscription_requested = true
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
subscriber_redis.client.process([[:subscribe, key]])
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def finalize_subscription
|
|
303
|
+
signal_version(get_redis.get(key))
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def signal_version(current_version)
|
|
307
|
+
current_version = current_version.to_i
|
|
308
|
+
@lock.synchronize do
|
|
309
|
+
return if current_version < @last_version
|
|
310
|
+
@last_version = current_version
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
loop do
|
|
314
|
+
next_cb = nil
|
|
315
|
+
@lock.synchronize do
|
|
316
|
+
next_cb = @callbacks.next
|
|
317
|
+
return unless next_cb && next_cb.can_perform?(@last_version)
|
|
318
|
+
@callbacks.pop
|
|
319
|
+
end
|
|
320
|
+
next_cb.perform
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def add_callback(callback)
|
|
325
|
+
refresh_activity
|
|
326
|
+
|
|
327
|
+
can_perform_immediately = false
|
|
328
|
+
@lock.synchronize do
|
|
329
|
+
if callback.can_perform?(@last_version)
|
|
330
|
+
can_perform_immediately = true
|
|
331
|
+
else
|
|
332
|
+
@callbacks.push(callback, callback.version)
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
if can_perform_immediately
|
|
337
|
+
callback.perform
|
|
338
|
+
else
|
|
339
|
+
node_synchronizer.root_synchronizer.maybe_recover if Promiscuous::Config.recovery
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
class Callback < Struct.new(:version, :callback, :message)
|
|
344
|
+
# message is just here for debugging, not used in the happy path
|
|
345
|
+
def can_perform?(current_version)
|
|
346
|
+
# The message synchronizer takes care of happens before dependencies.
|
|
347
|
+
current_version >= self.version
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def perform
|
|
351
|
+
callback.call
|
|
352
|
+
end
|
|
261
353
|
end
|
|
262
354
|
end
|
|
263
355
|
end
|