amqp-failover 0.0.1

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.
@@ -0,0 +1,83 @@
1
+ # encoding: utf-8
2
+
3
+ module AMQP
4
+ module FailoverClient
5
+ include AMQP::BasicClient
6
+
7
+ attr_accessor :failover
8
+ attr_reader :fallback_monitor
9
+
10
+ attr_accessor :settings
11
+ attr_accessor :on_disconnect
12
+
13
+ def self.extended(base)
14
+ if (base.failover = base.settings.delete(:failover))
15
+ base.on_disconnect = base.method(:disconnected)
16
+ end
17
+ end
18
+
19
+ def failover_switch
20
+ if (new_settings = @failover.from(@settings))
21
+ log_message = "Could not connect to or lost connection to server #{@settings[:host]}:#{@settings[:port]}. " +
22
+ "Attempting connection to: #{new_settings[:host]}:#{new_settings[:port]}"
23
+ logger.error(log_message)
24
+ logger.info(log_message)
25
+
26
+ if @failover.options[:fallback] && @failover.primary == @settings
27
+ fallback(@failover.primary, @failover.fallback_interval)
28
+ end
29
+ @settings = new_settings
30
+ reconnect
31
+ else
32
+ raise Error, "Could not connect to server #{@settings[:host]}:#{@settings[:port]}"
33
+ end
34
+ end
35
+
36
+ def logger
37
+ Failover.logger
38
+ end
39
+
40
+ def configs
41
+ @failover.configs if @failover
42
+ end
43
+
44
+ def clean_exit(msg = nil)
45
+ msg ||= "clean exit"
46
+ logger.info(msg)
47
+ logger.error(msg)
48
+ Process.exit
49
+ end
50
+
51
+ def fallback(conf = {}, retry_interval = nil)
52
+ @fallback_monitor = Failover::ServerDiscovery.monitor(conf, retry_interval) do
53
+ fallback_callback.call(conf, retry_interval)
54
+ end
55
+ end
56
+
57
+ def fallback_callback
58
+ #TODO: Figure out a way to artificially trigger EM to disconnect on fallback without channels being closed.
59
+ @fallback_callback ||= proc { |conf, retry_interval|
60
+ clean_exit("Primary server (#{conf[:host]}:#{conf[:port]}) is back. " +
61
+ "Performing clean exit to be relaunched with primary config.")
62
+ }
63
+ end
64
+ attr_writer :fallback_callback
65
+
66
+ #TODO: Figure out why I originally needed this
67
+ # def process_frame(frame)
68
+ # if mq = channels[frame.channel]
69
+ # mq.process_frame(frame)
70
+ # return
71
+ # end
72
+ #
73
+ # if frame.is_a?(AMQP::Frame::Method) && (method = frame.payload).is_a?(AMQP::Protocol::Connection::Close)
74
+ # if method.reply_text =~ /^NOT_ALLOWED/
75
+ # raise AMQP::Error, "#{method.reply_text} in #{::AMQP::Protocol.classes[method.class_id].methods[method.method_id]}"
76
+ # end
77
+ # end
78
+ # super(frame)
79
+ # end
80
+
81
+ end # FailoverClient
82
+ end # AMQP
83
+
@@ -0,0 +1,59 @@
1
+ # encoding: utf-8
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__))
3
+
4
+ require 'spec_helper'
5
+ require 'mq'
6
+ require 'amqp'
7
+ require 'amqp/server'
8
+ require 'server_helper'
9
+
10
+ describe "Basic AMQP connection with FailoverClient loaded" do
11
+
12
+ after(:each) do
13
+ ServerHelper.clear_logs
14
+ end
15
+
16
+ it "should be using FailoverClient" do
17
+ AMQP.client.should == AMQP::FailoverClient
18
+ end
19
+
20
+ it "should be able to connect" do
21
+ EM.run {
22
+ port = 15672
23
+ timeout = 2
24
+ serv = start_server(port)
25
+ EM.add_timer(1.5) {
26
+ conn = AMQP.connect(:host => 'localhost', :port => 15672)
27
+ EM.add_timer(0.1) {
28
+ conn.should be_connected
29
+ serv.stop
30
+ log = serv.log
31
+ log.size.should == 3
32
+ (0..2).each { |i| log[i]['method'].should == "send" }
33
+ log[0]['class'].should == 'AMQP::Protocol::Connection::Start'
34
+ log[1]['class'].should == 'AMQP::Protocol::Connection::Tune'
35
+ log[2]['class'].should == 'AMQP::Protocol::Connection::OpenOk'
36
+ EM.stop
37
+ }
38
+ }
39
+ }
40
+ end
41
+
42
+ it "should be able to connect and get disconnected" do
43
+ EM.run {
44
+ serv = start_server(25672)
45
+ EM.add_timer(0.1) {
46
+ conn = AMQP.connect(:host => 'localhost', :port => 25672)
47
+ EM.add_timer(0.1) {
48
+ conn.should be_connected
49
+ serv.stop
50
+ EM.add_timer(0.1) {
51
+ conn.should_not be_connected
52
+ EM.stop
53
+ }
54
+ }
55
+ }
56
+ }
57
+ end
58
+
59
+ end
@@ -0,0 +1,141 @@
1
+ # encoding: utf-8
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__))
3
+
4
+ require 'spec_helper'
5
+ require 'amqp/server'
6
+ require 'server_helper'
7
+ require 'logger_helper'
8
+
9
+ describe "Failover support loaded into AMQP gem" do
10
+
11
+ before(:each) do
12
+ @flog = LoggerHelper.new
13
+ AMQP::Failover.logger = @flog
14
+ end
15
+
16
+ after(:each) do
17
+ ServerHelper.clear_logs
18
+ AMQP::Failover.logger = nil
19
+ end
20
+
21
+ it "should be able to connect" do
22
+ port1 = 15672
23
+ EM.run {
24
+ serv = start_server(port1)
25
+ EM.add_timer(0.1) {
26
+ conn = AMQP.connect(:host => 'localhost', :port => port1)
27
+ conn.failover.should be_nil
28
+ EM.add_timer(0.1) {
29
+ conn.should be_connected
30
+ EM.stop
31
+ }
32
+ }
33
+ }
34
+ end
35
+
36
+ it "should be able to connect and failover" do
37
+ port1 = 25672
38
+ port2 = 35672
39
+ EM.run {
40
+ # start mock amqp servers
41
+ serv1 = start_server(port1)
42
+ serv2 = start_server(port2)
43
+ EM.add_timer(0.1) {
44
+ # start amqp client connection and make sure it's picked the right config
45
+ conn = AMQP.connect({:hosts => [{:port => port1}, {:port => port2}]})
46
+ conn.failover.primary[:port].should == port1
47
+ conn.settings[:port].should == port1
48
+ conn.settings.should == conn.failover.primary
49
+ EM.add_timer(0.1) {
50
+ # make sure client connected to the correct server, then kill server
51
+ conn.should be_connected
52
+ serv1.log.should have(3).items
53
+ serv2.log.should have(0).items
54
+ serv1.stop
55
+ EM.add_timer(0.1) {
56
+ # make sure client performed a failover when primary server died
57
+ conn.should be_connected
58
+ [:error, :info].each do |i|
59
+ @flog.send("#{i}_log").should have(1).item
60
+ @flog.send("#{i}_log")[0][0].should match(/connect to or lost connection.+#{port1}.+attempting connection.+#{port2}/i)
61
+ end
62
+ conn.settings[:port].should == port2
63
+ serv1.log.should have(3).items
64
+ serv2.log.should have(3).items
65
+ conn.close
66
+ EM.add_timer(0.1) {
67
+ serv2.stop
68
+ EM.stop
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+ end
75
+
76
+ it "should be able to fallback when primary server returns" do
77
+ port1 = 45672
78
+ port2 = 55672
79
+ lambda {
80
+ EM.run {
81
+ # start mock amqp servers
82
+ serv1 = start_server(port1)
83
+ serv2 = start_server(port2)
84
+ EM.add_timer(0.1) {
85
+ # start amqp client connection and make sure it's picked the right config
86
+ conn = AMQP.connect({:hosts => [{:port => port1}, {:port => port2}], :fallback => true, :fallback_interval => 0.1})
87
+ conn.failover.primary[:port].should == port1
88
+ conn.settings[:port].should == port1
89
+ conn.settings.should == conn.failover.primary
90
+ EM.add_timer(0.1) {
91
+ # make sure client connected to the correct server, then kill server
92
+ conn.should be_connected
93
+ serv1.log.should have(3).items
94
+ serv2.log.should have(0).items
95
+ serv1.stop
96
+ EM.add_timer(0.1) {
97
+ # make sure client performed a failover when primary server died
98
+ conn.should be_connected
99
+ [:error, :info].each do |i|
100
+ @flog.send("#{i}_log").should have(1).item
101
+ @flog.send("#{i}_log")[0][0].should match(/connect to or lost connection.+#{port1}.+attempting connection.+#{port2}/i)
102
+ end
103
+ conn.settings[:port].should == port2
104
+ serv1.log.should have(3).items
105
+ serv2.log.should have(3).items
106
+ serv3 = start_server(port1)
107
+ EM.add_timer(0.2) {
108
+ # by this point client should have raised a SystemExit exception
109
+ serv2.stop
110
+ EM.stop
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }.should raise_error(SystemExit, "exit")
117
+ [:error, :info].each do |i|
118
+ @flog.send("#{i}_log").should have(2).item
119
+ @flog.send("#{i}_log")[1][0].should match(/primary server.+45672.+performing clean exit/i)
120
+ end
121
+ end
122
+
123
+ it "should abide to :primary_config option" do
124
+ port1 = 75672
125
+ port2 = 65672
126
+ EM.run {
127
+ serv = start_server(port1)
128
+ EM.add_timer(0.1) {
129
+ conn = AMQP.connect({:hosts => [{:port => port1}, {:port => port2}], :primary_config => 1})
130
+ conn.failover.primary[:port].should == port2
131
+ conn.settings[:port].should == port2
132
+ conn.settings.should == conn.failover.primary
133
+ EM.add_timer(0.1) {
134
+ conn.should be_connected
135
+ EM.stop
136
+ }
137
+ }
138
+ }
139
+ end
140
+
141
+ end
@@ -0,0 +1,18 @@
1
+ # encoding: utf-8
2
+
3
+ class LoggerHelper
4
+
5
+ attr_accessor :error_log
6
+ attr_accessor :info_log
7
+
8
+ def info(*args)
9
+ @info_log ||= []
10
+ @info_log << args
11
+ end
12
+
13
+ def error(*args)
14
+ @error_log ||= []
15
+ @error_log << args
16
+ end
17
+
18
+ end
@@ -0,0 +1,77 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'mq'
5
+ require 'amqp'
6
+ require 'amqp/server'
7
+ require 'json'
8
+
9
+ class ServerHelper
10
+
11
+ attr_accessor :stdin
12
+ attr_accessor :stdout
13
+ attr_accessor :stderr
14
+ attr_accessor :pid
15
+
16
+
17
+ def initialize(port = nil, timeout = nil)
18
+ @port = port
19
+ @timout = timeout
20
+ File.open(log_file, 'w') {}
21
+ @pid = start(port, timeout)
22
+ end
23
+
24
+ def self.clear_logs
25
+ Dir.glob(File.expand_path('server_helper*.log', File.dirname(__FILE__))).each do |file|
26
+ File.delete(file)
27
+ end
28
+ end
29
+
30
+ def start(port = nil, timeout = nil)
31
+ port ||= 15672
32
+ timeout ||= 2
33
+ EM.fork_reactor {
34
+ $PORT = port
35
+ EM.start_server('localhost', port, AmqpServer)
36
+ EM.add_timer(timeout) { EM.stop }
37
+ }
38
+ end
39
+
40
+ def stop
41
+ Process.kill('TERM', @pid)
42
+ end
43
+
44
+ def kill
45
+ Process.kill('KILL', @pid)
46
+ end
47
+
48
+ def log
49
+ File.open(log_file).to_a.map{ |l| JSON.parse(l) }
50
+ end
51
+
52
+ def log_file
53
+ File.expand_path("server_helper-port#{@port}.log", File.dirname(__FILE__))
54
+ end
55
+
56
+ end
57
+
58
+ module AmqpServer
59
+ include AMQP::Server
60
+
61
+ # customize log output
62
+ def log(*args)
63
+ args = {:method => args[0], :class => args[1].payload.class, :pid => Process.pid}
64
+ filename = File.expand_path("server_helper-port#{$PORT}.log", File.dirname(__FILE__))
65
+ File.open(filename, 'a') do |f|
66
+ f.write("#{args.to_json}\n")
67
+ end
68
+ end
69
+ end
70
+
71
+ #
72
+ # Helper methods
73
+ #
74
+
75
+ def start_server(port = nil, timeout = nil)
76
+ ServerHelper.new(port, timeout)
77
+ end
@@ -0,0 +1,49 @@
1
+ # encoding: utf-8
2
+
3
+ # add project-relative load paths
4
+ $LOAD_PATH.unshift File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
5
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__))
6
+
7
+ # require stuff
8
+ require 'rubygems'
9
+
10
+ begin
11
+ require 'mq'
12
+ rescue LoadError => e
13
+ require 'amqp'
14
+ end
15
+ require 'amqp/failover'
16
+
17
+ require 'rspec'
18
+ require 'rspec/autorun'
19
+
20
+
21
+ #
22
+ # Helper methods
23
+ #
24
+
25
+ def wait_while(timeout = 10, retry_interval = 0.1, &block)
26
+ start = Time.now
27
+ while block.call
28
+ break if (Time.now - start).to_i >= timeout
29
+ sleep(retry_interval)
30
+ end
31
+ end
32
+
33
+ # stolen from Pid::running? from daemons gem
34
+ def pid_running?(pid)
35
+ return false unless pid
36
+
37
+ # Check if process is in existence
38
+ # The simplest way to do this is to send signal '0'
39
+ # (which is a single system call) that doesn't actually
40
+ # send a signal
41
+ begin
42
+ Process.kill(0, pid)
43
+ return true
44
+ rescue Errno::ESRCH
45
+ return false
46
+ rescue ::Exception # for example on EPERM (process exists but does not belong to us)
47
+ return true
48
+ end
49
+ end
@@ -0,0 +1,67 @@
1
+ # encoding: utf-8
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__))
3
+
4
+ require 'spec_helper'
5
+
6
+ describe 'AMQP::Failover::Config' do
7
+
8
+ before(:each) do
9
+ configs = [
10
+ {:host => 'rabbit0.local'},
11
+ {:host => 'rabbit1.local'},
12
+ {:host => 'rabbit2.local', :port => 5673}
13
+ ]
14
+ @configs = configs.map { |conf| AMQP.settings.merge(conf) }
15
+ @fail = AMQP::Failover.new(@configs)
16
+ end
17
+
18
+ it "should initialize" do
19
+ fail = AMQP::Failover::Config.new(@configs[0])
20
+ fail.should == @configs[0]
21
+ fail.last_fail.should be_nil
22
+
23
+ now = Time.now
24
+ fail = AMQP::Failover::Config.new(@configs[1], now)
25
+ fail.should == @configs[1]
26
+ fail.last_fail.should == now
27
+ end
28
+
29
+ it "should order properly with #<=>" do
30
+ one_hour_ago = (Time.now - 3600)
31
+ two_hours_ago = (Time.now - 7200)
32
+
33
+ fail = [ AMQP::Failover::Config.new(@configs[0]),
34
+ AMQP::Failover::Config.new(@configs[1], one_hour_ago),
35
+ AMQP::Failover::Config.new(@configs[2], two_hours_ago) ]
36
+
37
+ (fail[1] <=> fail[0]).should == -1
38
+ (fail[0] <=> fail[0]).should == 0
39
+ (fail[0] <=> fail[1]).should == 1
40
+
41
+ (fail[1] <=> fail[2]).should == -1
42
+ (fail[1] <=> fail[1]).should == 0
43
+ (fail[2] <=> fail[1]).should == 1
44
+
45
+ fail.sort[0].last_fail.should == one_hour_ago
46
+ fail.sort[1].last_fail.should == two_hours_ago
47
+ fail.sort[2].last_fail.should == nil
48
+ end
49
+
50
+ it "should be ordered by last_fail" do
51
+ result = [ AMQP::Failover::Config.new(@configs[1], (Time.now - 60)),
52
+ AMQP::Failover::Config.new(@configs[2], (Time.now - (60*25))),
53
+ AMQP::Failover::Config.new(@configs[0], (Time.now - 3600)) ]
54
+
55
+ origin = [ AMQP::Failover::Config.new(@configs[0], (Time.now - 3600)),
56
+ AMQP::Failover::Config.new(@configs[1], (Time.now - 60)),
57
+ AMQP::Failover::Config.new(@configs[2], (Time.now - (60*25))) ]
58
+ origin.sort.should == result
59
+
60
+ origin = [ AMQP::Failover::Config.new(@configs[0]),
61
+ AMQP::Failover::Config.new(@configs[1], (Time.now - 60)),
62
+ AMQP::Failover::Config.new(@configs[2], (Time.now - (60*25))) ]
63
+ origin.sort.should == result
64
+ end
65
+
66
+ end
67
+
@@ -0,0 +1,79 @@
1
+ # encoding: utf-8
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__))
3
+
4
+ require 'spec_helper'
5
+
6
+ describe 'AMQP::Failover::Configurations' do
7
+
8
+ before(:each) do
9
+ @conf = AMQP::Failover::Configurations.new
10
+ @raw_configs = [
11
+ {:host => 'rabbit0.local'},
12
+ {:host => 'rabbit1.local'},
13
+ {:host => 'rabbit2.local', :port => 5673}
14
+ ]
15
+ @configs = @raw_configs.map { |conf| AMQP.settings.merge(conf) }
16
+ end
17
+
18
+ it "should initialize" do
19
+ confs = AMQP::Failover::Configurations.new(@raw_configs)
20
+ confs.each_with_index do |conf, i|
21
+ conf.should be_a(AMQP::Failover::Config)
22
+ conf.should == @configs[i]
23
+ end
24
+ end
25
+
26
+ it "should set and get configs" do
27
+ @conf.primary_ref.should == 0
28
+ @conf.should have(0).items
29
+
30
+ @conf.set(@raw_configs[0])
31
+ @conf.should have(1).items
32
+ @conf.get(0).should == @configs[0]
33
+ @conf[0].should == @configs[0]
34
+
35
+ @conf.set(@raw_configs[1])
36
+ @conf.should have(2).items
37
+ @conf.get(1).should == @configs[1]
38
+ @conf[1].should == @configs[1]
39
+
40
+ # should just create a ref, as config exists
41
+ @conf.set(@raw_configs[1], :the_one)
42
+ @conf.should have(2).items
43
+ @conf.get(1).should == @configs[1]
44
+ @conf[:the_one].should == @configs[1]
45
+
46
+ @conf.load_array(@raw_configs)
47
+ @conf.should have(3).items
48
+ @conf.primary.should == @configs[0]
49
+ @conf.primary_ref = 1
50
+ @conf.primary.should == @configs[1]
51
+ @conf[:primary].should == @configs[1]
52
+ end
53
+
54
+ it "should #find_next" do
55
+ @conf.load(@raw_configs)
56
+ @conf.should have(3).items
57
+ @conf.find_next(@configs[0]).should == @configs[1]
58
+ @conf.find_next(@configs[1]).should == @configs[2]
59
+ @conf.find_next(@configs[2]).should == @configs[0]
60
+ end
61
+
62
+ it "should #load_hash" do
63
+ @conf.should have(0).items
64
+ @conf.load_hash(@raw_configs[0])
65
+ @conf.should have(1).items
66
+ @conf.primary.should == @configs[0]
67
+ end
68
+
69
+ it "should #load_array" do
70
+ @conf.load_hash(:host => 'rabbid-rabbit')
71
+ @conf.should have(1).items
72
+ @conf.load_array(@raw_configs)
73
+ @conf.should have(3).items
74
+ @conf.should == @configs
75
+ @conf.primary.should == @configs[0]
76
+ end
77
+
78
+ end
79
+
@@ -0,0 +1,31 @@
1
+ class ServerDiscoveryHelper < AMQP::Failover::ServerDiscovery
2
+
3
+ class << self
4
+ alias :real_start_monitoring :start_monitoring
5
+ def start_monitoring(*args, &block)
6
+ $called << :start_monitoring
7
+ real_start_monitoring(*args, &block)
8
+ end
9
+ end
10
+
11
+ alias :real_initialize :initialize
12
+ def initialize(*args)
13
+ $called << :initialize
14
+ EM.start_server('127.0.0.1', 9999) if $start_count == 2
15
+ $start_count += 1
16
+ real_initialize(*args)
17
+ end
18
+
19
+ alias :real_connection_completed :connection_completed
20
+ def connection_completed
21
+ $called << :connection_completed
22
+ real_connection_completed
23
+ end
24
+
25
+ alias :real_close_connection :close_connection
26
+ def close_connection
27
+ $called << :close_connection
28
+ real_close_connection
29
+ end
30
+
31
+ end
@@ -0,0 +1,56 @@
1
+ # encoding: utf-8
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__))
3
+
4
+ require 'spec_helper'
5
+ require 'server_discovery_helper'
6
+
7
+ describe 'AMQP::Failover::ServerDiscovery' do
8
+
9
+ before(:each) do
10
+ $called = []
11
+ $start_count = 0
12
+ @args = { :host => 'localhost', :port => 9999 }
13
+ @retry_interval = 0.01
14
+ end
15
+
16
+ after(:all) do
17
+ $called = nil
18
+ $start_count = nil
19
+ end
20
+
21
+ it "should initialize" do
22
+ EM.run {
23
+ EM.start_server('127.0.0.1', 9999)
24
+ @mon = ServerDiscoveryHelper.monitor(@args, @retry_interval) do
25
+ $called << :done_block
26
+ EM.stop_event_loop
27
+ end
28
+ }
29
+ $start_count.should == 1
30
+ $called.should have(5).items
31
+ $called.uniq.should have(5).items
32
+ $called.should include(:start_monitoring)
33
+ $called.should include(:initialize)
34
+ $called.should include(:connection_completed)
35
+ $called.should include(:close_connection)
36
+ $called.should include(:done_block)
37
+ end
38
+
39
+ it "should retry on error" do
40
+ EM.run {
41
+ @mon = ServerDiscoveryHelper.monitor(@args, @retry_interval) do
42
+ $called << :done_block
43
+ EM.stop_event_loop
44
+ end
45
+ }
46
+ $start_count.should >= 3
47
+ $called.should have($start_count + 4).items
48
+ $called.uniq.should have(5).items
49
+ $called.should include(:start_monitoring)
50
+ $called.should include(:initialize)
51
+ $called.should include(:connection_completed)
52
+ $called.should include(:close_connection)
53
+ $called.should include(:done_block)
54
+ end
55
+
56
+ end