amqp-failover 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,6 @@
1
+ README.md
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
6
+
data/.gitignore ADDED
@@ -0,0 +1,28 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ *.gem
20
+ .bundle
21
+ Gemfile.lock
22
+ pkg/*
23
+
24
+ ## PROJECT::SPECIFIC
25
+ .yardoc/*
26
+ spec/db/*
27
+ doc/*
28
+
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm gemset use amqp-failover
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in amqp-failover.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Jim Myhrberg & Global Personals, Ltd.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # amqp-failover #
2
+
3
+ Add multi-server support with failover and fallback to the [amqp](https://github.com/ruby-amqp/amqp) gem. Failover is configured by providing multiple servers/configurations to `AMQP.start` or `AMQP.connect`. Both methods will still accept the same options input as they always have, they simply now support additional forms of options which when used, enables the failover features.
4
+
5
+
6
+ ## Basic Usage ##
7
+
8
+ require 'mq'
9
+ require 'amqp/failover'
10
+ opts = [{:port => 5672}, {:port => 5673}]
11
+ AMQP.start(opts) do
12
+ # code...
13
+ end
14
+
15
+ By default the client will connect to `localhost:5672`, but if for any reason it can't connect, or looses connection to that server, it'll attempt to connect to `localhost:5673` instead.
16
+
17
+
18
+ ## Options Formats ##
19
+
20
+ ### Standard Non-Failover ###
21
+
22
+ Hash:
23
+
24
+ opts = {:host => "hostname", :port => 5673}
25
+
26
+ URL:
27
+
28
+ opts = "amqp://user:pass@hostname:5673/"
29
+
30
+ ### With Failover ###
31
+
32
+ URLs
33
+
34
+ opts = "amqp://localhost:5672/,amqp://localhost:5673/"
35
+
36
+ Array of Hashes:
37
+
38
+ opts = [{:port => 5672}, {:port => 5673}]
39
+
40
+ Array of URLs:
41
+
42
+ opts = ["amqp://localhost:5672/", "amqp://localhost:5673/"]
43
+
44
+ Specify AMQP servers and Failover options by passing a Hash containing a `:hosts` key with a value of either of the above three examples:
45
+
46
+ opts = {:hosts => "amqp://localhost:5672/,amqp://localhost:5673/", :fallback => true}
47
+ opts = {:hosts => [{:port => 5672}, {:port => 5673}], :fallback => true}
48
+ opts = {:hosts => ["amqp://localhost:5672/", "amqp://localhost:5673/"], :fallback => true}
49
+
50
+ ## Failover Options ##
51
+
52
+ * `:retry_timeout`, time to wait before retrying a specific AMQP config after failure.
53
+ * `:primary_config`, specify which of the supplied configurations is it the primary one. The default value is 0, the first item in the config array. Use 1 for the second and so on.
54
+ * `:fallback`, check for the return of the primary server, and fallback to it if and when it returns. WARNING: This currently calls `Process.exit` cause I haven't figured out a way to artificially kill the EM connection without the AMQP channels also being closed, which causes nothing to work even after EM connects to the primary server. It works for me cause dead workers are automatically relaunched with their default config.
55
+ * `:fallback_interval`, seconds between each check for original server if :fallback is true.
56
+ * `:selection`, not yet implemented.
57
+
58
+
59
+ ## Notes ##
60
+
61
+ I would recommend you test the failover functionality in your own infrastructure before deploy to production, as this gem is still very much alpha/beta quality, and it does do a little bit of monkey patching to the amqp gem. That said, it there's a number of specs which should ensure things work as advertised, and nothing breaks. We are currently using it at Global Personals without any problems.
62
+
63
+
64
+ ## Todo ##
65
+
66
+ * Figure out a sane way to fallback without having to kill the Ruby process.
67
+ * Better Readme/Documentation.
68
+ * Add option for next server selection on failover to be selected by random rather than next on the list.
69
+ * Convince get failover functionality merged in, or otherwise rewritten/added to the official AMQP gem.
70
+
71
+
72
+ ## Note on Patches/Pull Requests ##
73
+
74
+ * Fork the project.
75
+ * Make your feature addition or bug fix.
76
+ * Add tests for it. This is important so I don't break it in a
77
+ future version unintentionally.
78
+ * Commit, do not mess with rakefile, version, or history.
79
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
80
+ * Send me a pull request. Bonus points for topic branches.
81
+
82
+
83
+ ## Liccense and Copyright ##
84
+
85
+ Copyright (c) 2011 Jim Myhrberg & Global Personals, Ltd.
86
+
87
+ Permission is hereby granted, free of charge, to any person obtaining
88
+ a copy of this software and associated documentation files (the
89
+ "Software"), to deal in the Software without restriction, including
90
+ without limitation the rights to use, copy, modify, merge, publish,
91
+ distribute, sublicense, and/or sell copies of the Software, and to
92
+ permit persons to whom the Software is furnished to do so, subject to
93
+ the following conditions:
94
+
95
+ The above copyright notice and this permission notice shall be
96
+ included in all copies or substantial portions of the Software.
97
+
98
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
99
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
100
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
101
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
102
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
103
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
104
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,56 @@
1
+ $LOAD_PATH.unshift File.expand_path("lib", File.dirname(__FILE__))
2
+
3
+ require 'bundler'
4
+ Bundler::GemHelper.install_tasks
5
+
6
+
7
+ #
8
+ # Rspec
9
+ #
10
+
11
+ require 'rspec/core/rake_task'
12
+
13
+ RSpec::Core::RakeTask.new('spec:all') do |spec|
14
+ spec.pattern = [ 'spec/unit/**/*_spec.rb',
15
+ 'spec/integration/**/*_spec.rb' ]
16
+ end
17
+
18
+ desc "Run unit specs"
19
+ task :spec => ["spec:unit"]
20
+ RSpec::Core::RakeTask.new('spec:unit') do |spec|
21
+ spec.pattern = 'spec/unit/**/*_spec.rb'
22
+ end
23
+
24
+ RSpec::Core::RakeTask.new('spec:integration') do |spec|
25
+ spec.pattern = 'spec/integration/**/*_spec.rb'
26
+ end
27
+
28
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
29
+ spec.pattern = 'spec/**/*_spec.rb'
30
+ spec.rcov = true
31
+ end
32
+
33
+
34
+ #
35
+ # Yard
36
+ #
37
+
38
+ begin
39
+ require 'yard'
40
+ YARD::Rake::YardocTask.new
41
+ rescue LoadError
42
+ task :yardoc do
43
+ abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
44
+ end
45
+ end
46
+
47
+
48
+ #
49
+ # Misc.
50
+ #
51
+
52
+ desc "Start irb with amqp-failover pre-loaded"
53
+ task :console do
54
+ exec "irb -r spec/spec_helper"
55
+ end
56
+ task :c => :console
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "amqp/failover/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "amqp-failover"
7
+ s.version = AMQP::Failover::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Jim Myhrberg"]
10
+ s.email = ["contact@jimeh.me"]
11
+ s.homepage = 'http://github.com/jimeh/amqp-failover'
12
+ s.summary = 'Add multi-server failover and fallback to amqp gem.'
13
+ s.description = 'Add multi-server failover and fallback to amqp gem.'
14
+
15
+ s.rubyforge_project = "amqp-failover"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_runtime_dependency 'amqp', '>= 0.7.0'
23
+
24
+ s.add_development_dependency 'rake', '>= 0.8.7'
25
+ s.add_development_dependency 'rack-test', '>= 0.5.6'
26
+ s.add_development_dependency 'rspec', '>= 2.1.0'
27
+ s.add_development_dependency 'yard', '>= 0.6.3'
28
+ s.add_development_dependency 'json', '>= 1.5.0'
29
+ s.add_development_dependency 'ruby-debug'
30
+ end
@@ -0,0 +1,42 @@
1
+ # encoding: utf-8
2
+
3
+ module AMQP
4
+ class Failover
5
+ class Config < ::Hash
6
+
7
+ attr_accessor :last_fail
8
+
9
+ def initialize(hash = {}, last_fail_date = nil)
10
+ self.replace(symbolize_keys(defaults.merge(hash)))
11
+ self.last_fail = last_fail_date if last_fail_date
12
+ end
13
+
14
+ def defaults
15
+ AMQP.settings
16
+ end
17
+
18
+ def symbolize_keys(hash = {})
19
+ hash.inject({}) do |result, (key, value)|
20
+ result[key.is_a?(String) ? key.to_sym : key] = value
21
+ result
22
+ end
23
+ end
24
+
25
+ # order by latest fail, potentially useful if random config selection is used
26
+ def <=>(other)
27
+ if self.respond_to?(:last_fail) && other.respond_to?(:last_fail)
28
+ if self.last_fail.nil? && other.last_fail.nil?
29
+ return 0
30
+ elsif self.last_fail.nil? && !other.last_fail.nil?
31
+ return 1
32
+ elsif !self.last_fail.nil? && other.last_fail.nil?
33
+ return -1
34
+ end
35
+ return other.last_fail <=> self.last_fail
36
+ end
37
+ return 0
38
+ end
39
+
40
+ end # Config
41
+ end # Failover
42
+ end # AMQP
@@ -0,0 +1,92 @@
1
+ # encoding: utf-8
2
+
3
+ module AMQP
4
+ class Failover
5
+ class Configurations < Array
6
+
7
+ def initialize(confs = nil)
8
+ load(confs)
9
+ end
10
+
11
+ def [](*args)
12
+ if args[0].is_a?(Symbol)
13
+ return primary if args[0] == :primary
14
+ get(args[0])
15
+ else
16
+ super(*args)
17
+ end
18
+ end
19
+
20
+ def []=(*args)
21
+ if args[0].is_a?(Symbol)
22
+ return primary = args.last if args[0] == :primary
23
+ set(args.last, args[0])
24
+ end
25
+ super(*args)
26
+ end
27
+
28
+ def refs
29
+ @refs ||= {}
30
+ end
31
+
32
+ def primary_ref
33
+ @primary_ref ||= 0
34
+ end
35
+
36
+ def primary_ref=(ref)
37
+ @primary_ref = ref
38
+ end
39
+
40
+ def primary
41
+ get(primary_ref) || AMQP.settings
42
+ end
43
+
44
+ def primary=(conf = {})
45
+ set(conf, primary_ref)
46
+ end
47
+
48
+ def get(ref = nil)
49
+ return self[ref] if ref.is_a?(Fixnum)
50
+ self[refs[ref]] if refs[ref]
51
+ end
52
+
53
+ def set(conf = {}, ref = nil)
54
+ conf = Failover::Config.new(conf) if !conf.is_a?(Failover::Config)
55
+ if (index = self.index(conf)).nil?
56
+ self << conf
57
+ else
58
+ conf = self[index]
59
+ end
60
+ refs[ref] = (index || self.index(conf)) if ref
61
+ conf
62
+ end
63
+
64
+ def find_next(conf = {})
65
+ current = self.index(conf)
66
+ self[(current+1 == self.size) ? 0 : current+1] if current
67
+ end
68
+
69
+ def load(conf)
70
+ if conf.is_a?(Array)
71
+ load_array(conf)
72
+ elsif conf.is_a?(Hash)
73
+ load_hash(conf)
74
+ end
75
+ end
76
+
77
+ def load_array(confs = [])
78
+ self.clear
79
+ refs = {}
80
+ confs.each do |conf|
81
+ conf = AMQP::Client.parse_amqp_url(conf) if conf.is_a?(String)
82
+ load_hash(conf)
83
+ end
84
+ end
85
+
86
+ def load_hash(conf = {})
87
+ set(conf)
88
+ end
89
+
90
+ end # Config
91
+ end # Failover
92
+ end # AMQP
@@ -0,0 +1,67 @@
1
+ # encoding: utf-8
2
+
3
+ AMQP.client = AMQP::FailoverClient
4
+
5
+ module AMQP
6
+ module Client
7
+
8
+ class << self
9
+
10
+ # Connect with Failover supports specifying multiple AMQP servers and configurations.
11
+ #
12
+ # Argument Examples:
13
+ # - "amqp://guest:guest@host:5672,amqp://guest:guest@host:5673"
14
+ # - ["amqp://guest:guest@host:5672", "amqp://guest:guest@host:5673"]
15
+ # - [{:host => "host", :port => 5672}, {:host => "host", :port => 5673}]
16
+ # - {:hosts => ["amqp://user:pass@host:5672", "amqp://user:pass@host:5673"]}
17
+ # - {:hosts => [{:host => "host", :port => 5672}, {:host => "host", :port => 5673}]}
18
+ #
19
+ # The last two examples are by far the most flexible, cause they also let you specify
20
+ # failover and fallback specific options. Like so:
21
+ # - {:hosts => ["amqp://localhost:5672"], :fallback => false}
22
+ #
23
+ # Available failover options are:
24
+ # - :retry_timeout, time to wait before retrying a specific AMQP config after failure.
25
+ # - :primary_config, specify which of the supplied configurations is it the primary one. The default
26
+ # value is 0, the first item in the config array. Use 1 for the second and so on.
27
+ # - :fallback, check for the return of the primary server, and fallback to it if and when it returns.
28
+ # - :fallback_interval, seconds between each check for original server if :fallback is true.
29
+ # - :selection, not yet implimented.
30
+ #
31
+ def connect_with_failover(opts = nil)
32
+ opts = parse_amqp_url_or_opts(opts)
33
+ connect_without_failover(opts)
34
+ end
35
+ alias :connect_without_failover :connect
36
+ alias :connect :connect_with_failover
37
+
38
+ def parse_amqp_url_or_opts(opts = nil)
39
+ if opts.is_a?(String) && opts.index(',').nil?
40
+ opts = init_failover(opts.split(','))
41
+ elsif opts.is_a?(Array)
42
+ opts = init_failover(opts)
43
+ elsif opts.is_a?(Hash) && opts[:hosts].is_a?(Array)
44
+ confs = opts.delete(:hosts)
45
+ opts = init_failover(confs, opts)
46
+ end
47
+ opts
48
+ end
49
+
50
+ def init_failover(confs = nil, opts = {})
51
+ if !confs.nil? && confs.size > 0
52
+ failover = Failover.new(confs, opts)
53
+ failover.primary.merge({ :failover => failover })
54
+ end
55
+ end
56
+
57
+ end # << self
58
+
59
+ def disconnected_with_failover
60
+ return failover_switch if @failover
61
+ disconnected_without_failover
62
+ end
63
+ alias :disconnected_without_failover :disconnected
64
+ alias :disconnected :disconnected_with_failover
65
+
66
+ end # Client
67
+ end # AMQP
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+
3
+ module AMQP
4
+ class Failover
5
+ class Logger
6
+
7
+ attr_accessor :enabled
8
+
9
+ def initialize(enabled = nil)
10
+ @enabled = enabled || true
11
+ end
12
+
13
+ def error(*msg)
14
+ msg[0] = "[ERROR]: " + msg[0] if msg[0].is_a?(String)
15
+ write(*msg)
16
+ end
17
+
18
+ def info(*msg)
19
+ write(*msg)
20
+ end
21
+
22
+ private
23
+
24
+ def write(*msg)
25
+ return if !@enabled
26
+ puts *msg
27
+ end
28
+
29
+ end # Logger
30
+ end # Failover
31
+ end # AMQP
@@ -0,0 +1,45 @@
1
+ # encoding: utf-8
2
+
3
+ module AMQP
4
+ class Failover
5
+ class ServerDiscovery < EM::Connection
6
+
7
+ class << self
8
+ attr_accessor :connection
9
+ end
10
+
11
+ def self.monitor(conf = {}, retry_interval = nil, &block)
12
+ if EM.reactor_running?
13
+ start_monitoring(conf, retry_interval, &block)
14
+ else
15
+ EM.run { start_monitoring(conf, retry_interval, &block) }
16
+ end
17
+ end
18
+
19
+ def initialize(args)
20
+ @done = args[:done]
21
+ @timer = args[:timer]
22
+ end
23
+
24
+ def connection_completed
25
+ @done.call
26
+ @timer.cancel
27
+ close_connection
28
+ end
29
+
30
+ def self.start_monitoring(conf = {}, retry_interval = nil, &block)
31
+ conf = conf.clone
32
+ retry_interval ||= 5
33
+ conf[:done] = block
34
+ conf[:timer] = EM::PeriodicTimer.new(retry_interval) do
35
+ @connection = connect(conf)
36
+ end
37
+ end
38
+
39
+ def self.connect(conf)
40
+ EM.connect(conf[:host], conf[:port], self, conf)
41
+ end
42
+
43
+ end # ServerDiscovery
44
+ end # Failover
45
+ end # AMQP
@@ -0,0 +1,7 @@
1
+ # encoding: utf-8
2
+
3
+ module AMQP
4
+ class Failover
5
+ VERSION = "0.0.1"
6
+ end
7
+ end
@@ -0,0 +1,111 @@
1
+ # encoding: utf-8
2
+
3
+ require 'amqp/failover_client'
4
+ require 'amqp/failover/config'
5
+ require 'amqp/failover/configurations'
6
+ require 'amqp/failover/logger'
7
+ require 'amqp/failover/server_discovery'
8
+ require 'amqp/failover/version'
9
+ require 'amqp/failover/ext/amqp/client.rb'
10
+
11
+
12
+ module AMQP
13
+ class Failover
14
+
15
+ attr_reader :latest_failed
16
+ attr_accessor :primary
17
+ attr_accessor :retry_timeout
18
+ attr_accessor :fallback
19
+
20
+ def initialize(confs = nil, opts = {})
21
+ @configs = Failover::Configurations.new(confs)
22
+ @options = default_options.merge(opts)
23
+ @configs.primary_ref = @options[:primary_config]
24
+ end
25
+
26
+ class << self
27
+ # pluggable logger specifically for tracking failover and fallbacks
28
+ def logger
29
+ @logger ||= Logger.new
30
+ end
31
+ attr_writer :logger
32
+ end
33
+
34
+ def default_options
35
+ { :primary_config => 0,
36
+ :retry_timeout => 1,
37
+ :selection => :sequential, #TODO: Implement next server selection algorithm
38
+ :fallback => false, #TODO: Enable by default once a sane implementation is figured out
39
+ :fallback_interval => 10 }
40
+ end
41
+
42
+ def options
43
+ @options ||= {}
44
+ end
45
+
46
+ def fallback_interval
47
+ options[:fallback_interval] ||= default_options[:fallback_interval]
48
+ end
49
+
50
+ def primary
51
+ configs[:primary]
52
+ end
53
+
54
+ def refs
55
+ @refs ||= {}
56
+ end
57
+
58
+ def configs
59
+ @configs ||= Configurations.new
60
+ end
61
+
62
+ def add_config(conf = {}, ref = nil)
63
+ index = configs.index(conf)
64
+ configs.set(conf) if index.nil?
65
+ refs[ref] = (index || configs.index(conf)) if !ref.nil?
66
+ end
67
+
68
+ def failover_from(conf = {}, time = nil)
69
+ failed_with(conf, nil, time)
70
+ next_config
71
+ end
72
+ alias :from :failover_from
73
+
74
+ def failed_with(conf = {}, ref = nil, time = nil)
75
+ time ||= Time.now
76
+ if !(index = configs.index(conf)).nil?
77
+ configs[index].last_fail = time
78
+ @latest_failed = configs[index]
79
+ else
80
+ @latest_failed = configs.set(conf)
81
+ configs.last.last_fail = time
82
+ end
83
+ refs[ref] = (index || configs.index(conf)) if !ref.nil?
84
+ end
85
+
86
+ def next_config(retry_timeout = nil, after = nil)
87
+ return nil if configs.size <= 1
88
+ retry_timeout ||= @options[:retry_timeout]
89
+ after ||= @latest_failed
90
+ index = configs.index(after)
91
+ available = (index > 0) ? configs[index+1..-1] + configs[0..index-1] : configs[1..-1]
92
+ available.each do |conf|
93
+ return conf if conf.last_fail.nil? || (conf.last_fail.to_i + retry_timeout) < Time.now.to_i
94
+ end
95
+ return nil
96
+ end
97
+
98
+ def last_fail_of(match)
99
+ ((match.is_a?(Hash) ? get_by_conf(match) : get_by_ref(match)) || Config::Failed.new).last_fail
100
+ end
101
+
102
+ def get_by_conf(conf = {})
103
+ configs[configs.index(conf)]
104
+ end
105
+
106
+ def get_by_ref(ref = nil)
107
+ configs[refs[ref]] if refs[ref]
108
+ end
109
+
110
+ end # Failover
111
+ end # AMQP