├── .gitignore ├── .rspec ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib └── redis │ ├── elasticache.rb │ └── elasticache │ ├── failover.rb │ └── version.rb ├── redis-elasticache.gemspec └── spec ├── redis └── elasticache_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.4 4 | - 2.5 5 | - 2.6 6 | - 2.7 7 | before_install: gem install bundler 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in redis-elasticache.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Craig McNamara 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated 2 | 3 | This is no longer needed: https://github.com/redis/redis-rb/issues/550#issuecomment-703248932 4 | 5 | ## redis-elasticache 6 | 7 | Adds support for Elasticache failover to the Ruby driver of the redis-rb gem. Elasticache replication groups can transparently promote a new node to master, but TCP connections are persistent. This patch makes READONLY error messages from Redis get handled like a connection error instead of a command error so redis-rb will self heal the connection and run the command against the new master node. 8 | 9 | 10 | ## Installation 11 | 12 | Add this line to your application's Gemfile: 13 | 14 | In your gemfile 15 | 16 | ```ruby 17 | gem 'redis-elasticache' 18 | ``` 19 | 20 | And then execute: 21 | 22 | $ bundle 23 | 24 | Or install it yourself as: 25 | 26 | $ gem install redis-elasticache 27 | 28 | ## Usage 29 | 30 | In an environment that is backed by an Elasticache Replication Group. 31 | 32 | ```ruby 33 | require 'redis/elasticache/failover' 34 | ``` 35 | 36 | ## Contributing 37 | 38 | Bug reports and pull requests are welcome on GitHub at https://github.com/craigmcnamara/redis-elasticache. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 39 | 40 | 41 | ## License 42 | 43 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 44 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "redis/elasticache" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /lib/redis/elasticache.rb: -------------------------------------------------------------------------------- 1 | require "redis/elasticache/version" 2 | 3 | class Redis 4 | module Elasticache 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/redis/elasticache/failover.rb: -------------------------------------------------------------------------------- 1 | require 'redis' 2 | 3 | class Redis 4 | 5 | module Connection 6 | class Ruby 7 | ELASTICACHE_READONLY_ERROR_PREFIX = "READONLY".freeze 8 | ELASTICACHE_LOADING_ERROR_PREFIX = "LOADING".freeze 9 | ELASTICACHE_READONLY_MESSAGE = "A write operation was issued to an ELASTICACHE replica node that is READONLY.".freeze 10 | ELASTICACHE_LOADING_MESSAGE = "A write operation was issued to an ELASTICACHE node that was previously READONLY and is now LOADING.".freeze 11 | 12 | # Amazon Elasticache supports failover, but because it uses DNS magic to 13 | # point to the master node, TCP connections are not disconnected and we 14 | # can issue write operations to a node that is no longer the master. Under 15 | # normal conditions this should be interpreted as a `CommandError`, but with 16 | # Elasticache replication groups, we should consider this a `BaseConnectionError` 17 | # so we terminate the connection, reconnect and retry the operation with the 18 | # correct node as the master accepting writes. 19 | def format_error_reply(line) 20 | error_message = line.strip 21 | if error_message_has_prefix?(ELASTICACHE_READONLY_ERROR_PREFIX, error_message) 22 | raise BaseConnectionError, ELASTICACHE_READONLY_MESSAGE 23 | elsif error_message_has_prefix?(ELASTICACHE_LOADING_ERROR_PREFIX, error_message) 24 | raise BaseConnectionError, ELASTICACHE_LOADING_MESSAGE 25 | else 26 | CommandError.new(error_message) 27 | end 28 | end 29 | 30 | private 31 | def error_message_has_prefix?(prefix, error_message) 32 | (error_message.slice(0, prefix.length) === prefix) 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/redis/elasticache/version.rb: -------------------------------------------------------------------------------- 1 | class Redis 2 | module Elasticache 3 | VERSION = "0.2.0" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /redis-elasticache.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'redis/elasticache/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "redis-elasticache" 8 | spec.version = Redis::Elasticache::VERSION 9 | spec.authors = ["Craig McNamara", "Eddy Kim"] 10 | spec.email = ["craig@caring.com", "eddy.kim@dollarshaveclub.com"] 11 | 12 | spec.summary = %q{Adds missing support for AWS Elasticache to the redis-rb gem.} 13 | spec.description = %q{Enable applications to handle AWS Elasticache cluster failovers without rebooting the app.} 14 | spec.homepage = "https://github.com/craigmcnamara/redis-elasticache" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 18 | spec.bindir = "exe" 19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 20 | spec.require_paths = ["lib"] 21 | 22 | spec.add_dependency "redis", ">= 3.0.0" 23 | 24 | spec.add_development_dependency "bundler", "> 1.10" 25 | spec.add_development_dependency "rake", "> 10" 26 | spec.add_development_dependency "rspec" 27 | end 28 | -------------------------------------------------------------------------------- /spec/redis/elasticache_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Redis::Elasticache do 4 | it 'has a version number' do 5 | expect(Redis::Elasticache::VERSION).not_to be nil 6 | end 7 | 8 | context 'Redis::Connection::Ruby#format_error_reply failover patch' do 9 | 10 | let(:connection) { Redis::Connection::Ruby.new nil } 11 | 12 | let(:read_only_response1) { "READONLY You can't write against a read only replica." } 13 | let(:read_only_response2) { "READONLY You can't write against a read only slave." } 14 | let(:read_only_response3) { "READONLY unkown error message." } 15 | let(:loading_response1) { "LOADING unkown error message." } 16 | 17 | it 'raises `BaseConnectionError` when a write occurs against a replica node' do 18 | expect { connection.format_error_reply read_only_response1 }.to raise_error Redis::BaseConnectionError 19 | end 20 | 21 | it 'raises `BaseConnectionError` when a write occurs against a slave node' do 22 | expect { connection.format_error_reply read_only_response2 }.to raise_error Redis::BaseConnectionError 23 | end 24 | 25 | it 'raises `BaseConnectionError` when error message having the READONLY prefix' do 26 | expect { connection.format_error_reply read_only_response3 }.to raise_error Redis::BaseConnectionError 27 | end 28 | 29 | it 'raises `BaseConnectionError` when error message having the LOADING prefix' do 30 | expect { connection.format_error_reply loading_response1 }.to raise_error Redis::BaseConnectionError 31 | end 32 | 33 | it 'returns a `CommandError` in all other cases' do 34 | expect(connection.format_error_reply 'Derp').to be_a Redis::CommandError 35 | end 36 | 37 | end 38 | 39 | end 40 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'redis/elasticache/failover' 3 | --------------------------------------------------------------------------------