redis_failover-rails 0.1.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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +45 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +21 -0
  5. data/README.md +100 -0
  6. data/Rakefile +22 -0
  7. data/config/initializers/passenger.rb +12 -0
  8. data/config/redis.yml +33 -0
  9. data/lib/active_support/cache/redis_cache_store.rb +167 -0
  10. data/lib/redis_factory.rb +113 -0
  11. data/lib/redis_failover-rails/version.rb +3 -0
  12. data/redis_failover-rails.gemspec +38 -0
  13. data/test/dummy/README.rdoc +28 -0
  14. data/test/dummy/Rakefile +6 -0
  15. data/test/dummy/app/assets/images/.keep +0 -0
  16. data/test/dummy/app/assets/javascripts/application.js +13 -0
  17. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  18. data/test/dummy/app/controllers/application_controller.rb +5 -0
  19. data/test/dummy/app/controllers/concerns/.keep +0 -0
  20. data/test/dummy/app/helpers/application_helper.rb +2 -0
  21. data/test/dummy/app/mailers/.keep +0 -0
  22. data/test/dummy/app/models/.keep +0 -0
  23. data/test/dummy/app/models/concerns/.keep +0 -0
  24. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  25. data/test/dummy/bin/bundle +3 -0
  26. data/test/dummy/bin/rails +4 -0
  27. data/test/dummy/bin/rake +4 -0
  28. data/test/dummy/config.ru +4 -0
  29. data/test/dummy/config/application.rb +26 -0
  30. data/test/dummy/config/boot.rb +5 -0
  31. data/test/dummy/config/database.yml +25 -0
  32. data/test/dummy/config/environment.rb +5 -0
  33. data/test/dummy/config/environments/development.rb +37 -0
  34. data/test/dummy/config/environments/production.rb +83 -0
  35. data/test/dummy/config/environments/test.rb +45 -0
  36. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  37. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  38. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  39. data/test/dummy/config/initializers/inflections.rb +16 -0
  40. data/test/dummy/config/initializers/mime_types.rb +4 -0
  41. data/test/dummy/config/initializers/session_store.rb +3 -0
  42. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  43. data/test/dummy/config/locales/en.yml +23 -0
  44. data/test/dummy/config/routes.rb +56 -0
  45. data/test/dummy/config/secrets.yml +22 -0
  46. data/test/dummy/lib/assets/.keep +0 -0
  47. data/test/dummy/log/.keep +0 -0
  48. data/test/dummy/public/404.html +67 -0
  49. data/test/dummy/public/422.html +67 -0
  50. data/test/dummy/public/500.html +66 -0
  51. data/test/dummy/public/favicon.ico +0 -0
  52. data/test/test_helper.rb +62 -0
  53. data/test/unit/active_support/cache/redis_cache_store_test.rb +178 -0
  54. data/test/unit/redis_factory_test.rb +111 -0
  55. metadata +301 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 477b89feb234392eeeb44fb9dabd817f590bf5db
4
+ data.tar.gz: 3ef6dff5c7175ec11c21ae7fdee9220b77c18daf
5
+ SHA512:
6
+ metadata.gz: edb014657c027a0f07d41026a6fe837ae9bcde3461eb7d9e80774be2548b8729dfc65080d8b5bdb848697df1fcb3775f93a5e4b6f3ceabfbe1240d6895d46218
7
+ data.tar.gz: 4e43aa4f71e251fcc9e799473bd54497062ce3dea4a4cc699f21e9802b8ce70c8be63a89f5b469d93296154675a722dd64b42fc8c8f5b1aa7ce7fdb8121de53b
@@ -0,0 +1,45 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /test/tmp/
9
+ /test/version_tmp/
10
+ /tmp/
11
+ Gemfile.lock
12
+
13
+ ## Specific to RubyMotion:
14
+ .dat*
15
+ .repl_history
16
+ build/
17
+
18
+ ## Documentation cache and generated files:
19
+ /.yardoc/
20
+ /_yardoc/
21
+ /doc/
22
+ /rdoc/
23
+
24
+ ## Environment normalisation:
25
+ /.bundle/
26
+ /lib/bundler/man/
27
+
28
+ # for a library or gem, you might want to ignore these files since the code is
29
+ # intended to run in multiple environments; otherwise, check them in:
30
+
31
+ # .ruby-version
32
+ # .ruby-gemset
33
+
34
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
35
+ .rvmrc
36
+
37
+ log/*.log
38
+ test/dummy/db/*.sqlite3
39
+ test/dummy/db/*.sqlite3-journal
40
+ test/dummy/log/*.log
41
+ test/dummy/tmp/
42
+ test/dummy/.sass-cache
43
+
44
+ dump.rdb
45
+ redis.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ gem "codeclimate-test-reporter", group: :test, require: nil
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Surfdome Shop Limited
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 all
13
+ 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 THE
21
+ SOFTWARE.
@@ -0,0 +1,100 @@
1
+ # redis_failover-rails
2
+
3
+ [ ![Codeship Status for surfdome/redis_failover-rails](https://codeship.io/projects/1f022be0-c7b8-0131-24ea-6eafa0062d3a/status?branch=master)](https://codeship.io/projects/22198)
4
+ [![Code Climate](https://codeclimate.com/github/surfdome/redis_failover-rails.png)](https://codeclimate.com/github/surfdome/redis_failover-rails)
5
+
6
+ An ActiveSupport::Cache store using redis_failover (a zookeeper based implementation of HA redis) for Rails apps.
7
+
8
+ This is based on the work done by [@wr0ngway](https://github.com/wr0ngway) (Matt Conway), available on [redis_failover_example](https://github.com/wr0ngway/redis_failover_example).
9
+
10
+ Also, this gem utilizes redis_failover, a gem that creates a Redis HA implementation, using zookeeper for mantaining the available nodes.
11
+ This can be found on [redis_failover](https://github.com/ryanlecompte/redis_failover).
12
+
13
+ ## Requirements
14
+ In order to work, this requires a working [redis_failover](https://github.com/ryanlecompte/redis_failover) implementation. Please follow the instructions on the gem and ask your favourite Sysadmin/DevOps to implement in your environment.
15
+
16
+ ## Considerations
17
+ In case you don't have a redis_failover implementation working, and for helping in the development process, this also works with a single node redis server.
18
+ Just don't specify zkservers in the config file.
19
+
20
+ ## Installation
21
+
22
+ Just add the dependency to your Gemfile...
23
+
24
+ # Gemfile
25
+ gem 'redis_failover-rails', github: 'surfdome/redis_failover-rails' # Not in rubygems.org, yet...
26
+
27
+ and run bundle install.
28
+
29
+ bundle install
30
+
31
+ ## Usage
32
+ In your application.rb (or environment), you should use something like:
33
+
34
+ config.cache_store = :redis_cache_store, :cache
35
+ config.cache_classes = true
36
+ config.action_controller.perform_caching = true
37
+
38
+ ## Testing
39
+ In order to make your tests work, you need a working redis_failover installation, with Redis, Zookeeper and Node Manager running on the ports specified in the default config file.
40
+
41
+ Also you need a single redis server running on port 6379.
42
+
43
+ For running the tests, just:
44
+
45
+ bundle exec rake
46
+
47
+ ## Configuration
48
+ In order to define your redis and redis_failover configurations, you just need to use the `redis.yml` config file.
49
+
50
+ This follows a format like this:
51
+
52
+ environment:
53
+ instance:
54
+ parameter1:
55
+ parameter2:
56
+
57
+ This gem automatically selects a redis_failover client if you define `:zkservers` in the config file. If not, it just uses the standard redis client.
58
+
59
+ This are the options available if you configure a redis_failover client:
60
+
61
+ :zk - an existing ZK client instance
62
+ :zkservers - comma-separated ZooKeeper host:port pairs
63
+ :znode_path - the Znode path override for redis server list (optional)
64
+ :password - password for redis nodes (optional)
65
+ :db - db to use for redis nodes (optional)
66
+ :namespace - namespace for redis nodes (optional)
67
+ :logger - logger override (optional)
68
+ :retry_failure - indicate if failures should be retried (default true)
69
+ :max_retries - max retries for a failure (default 3)
70
+ :safe_mode - indicates if safe mode is used or not (default true)
71
+ :master_only - indicates if only redis master is used (default false)
72
+ :verify_role - verify the actual role of a redis node before every command (default true)
73
+
74
+ Typical configuration parameters for standard redis clients are:
75
+
76
+ :host - Redis server host
77
+ :port - Redis server port
78
+ :db - Redis database to use
79
+ :password - Redis connection password
80
+
81
+ Please keep in mind that this options are not validated, just passed to the client configuration.
82
+
83
+ ## License
84
+ This gem is released with MIT licensing. Please see [LICENSE](https://github.com/surfdome/redis_failover-rails/blob/master/LICENSE) for details.
85
+
86
+ ## Author
87
+ Initially created by Jose Pettoruti - [@jepettoruti](https://github.com/jepettoruti) for Surfdome.com
88
+
89
+ ## Acknowledgements
90
+ This couldn't have been achieved without the work of [@ryanlecompte](https://github.com/ryanlecompte/) for making the amazing redis_failover gem.
91
+
92
+ Special thanks to [@wr0ngway](https://github.com/wr0ngway) for his example on how to implement this on Rubber.
93
+
94
+ ## Contributing
95
+
96
+ 1. Fork it
97
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
98
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
99
+ 4. Push to the branch (`git push origin my-new-feature`)
100
+ 5. Create new Pull Request
@@ -0,0 +1,22 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rake'
8
+ require 'bundler/gem_tasks'
9
+
10
+ Bundler::GemHelper.install_tasks
11
+
12
+ require 'rake/testtask'
13
+
14
+ Rake::TestTask.new(:test) do |t|
15
+ t.libs << 'lib'
16
+ t.libs << 'test'
17
+ t.pattern = 'test/**/*_test.rb'
18
+ t.verbose = true
19
+ end
20
+
21
+ desc 'Default: run unit tests.'
22
+ task :default => :test
@@ -0,0 +1,12 @@
1
+ if defined?(PhusionPassenger)
2
+ PhusionPassenger.on_event(:starting_worker_process) do |forked|
3
+ if forked
4
+ # We're in smart spawning mode.
5
+
6
+ # Reset redis failover clients
7
+ RedisFactory.reconnect
8
+ else
9
+ # We're in conservative spawning mode. We don't need to do anything.
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,33 @@
1
+ development:
2
+ cache:
3
+ host: localhost
4
+ port: 6380
5
+ thread_safe: true
6
+ zkservers: localhost:2181, localhost:2182, localhost:2183
7
+ znode_path: /redis_failover/nodes
8
+ db: 1
9
+
10
+ production:
11
+ cache:
12
+ thread_safe: true
13
+ zkservers: localhost:2181, localhost:2182, localhost:2183
14
+ znode_path: /redis_failover
15
+ db: 0
16
+
17
+ test:
18
+ hacache:
19
+ zkservers: localhost:2181
20
+ db: 8
21
+ thread_safe: true
22
+ myredisdb:
23
+ db: 1
24
+ host: localhost
25
+ port: 6379
26
+ thread_safe: true
27
+ cache:
28
+ db: 2
29
+ host: localhost
30
+ port: 6379
31
+ thread_safe: true
32
+
33
+
@@ -0,0 +1,167 @@
1
+ # require "backupify/logger_support"
2
+
3
+ module ActiveSupport
4
+ module Cache
5
+
6
+ class RedisCacheStore < ActiveSupport::Cache::Store
7
+
8
+ # include Backupify::LoggerSupport
9
+
10
+ # Creates a new RedisStore object, with the given redis server
11
+ # address. Each address is a valid redis url string. For example:
12
+ #
13
+ # ActiveSupport::Cache::RedisStore.new("redis://127.0.0.1:6379/0")
14
+ #
15
+ # Instead of addresses one can pass in a redis-like object. For example:
16
+ #
17
+ # require 'redis' # gem install redis; uses C bindings to libredis
18
+ # ActiveSupport::Cache::RedisStore.new(Redis.connect("localhost"))
19
+ def initialize(*connections)
20
+ options = connections.extract_options!
21
+ super(options)
22
+
23
+ raise ArgumentError, "Only a single connection must be provided" if connections.size > 1
24
+ connection = connections.first || options
25
+
26
+ if connection.respond_to?(:get)
27
+ @data = connection
28
+ elsif connection.is_a?(Symbol)
29
+ require 'redis_factory'
30
+ @data = RedisFactory.connect(connection)
31
+ else
32
+ redis_options = {}
33
+ if connection.is_a?(String)
34
+ redis_options[:url] = connection
35
+ elsif connection.is_a?(Hash)
36
+ redis_options = connection
37
+ ActiveSupport::Cache::UNIVERSAL_OPTIONS.each{|name| redis_options.delete(name)}
38
+ else
39
+ raise ArgumentError, "Need a url or hash for a redis connection"
40
+ end
41
+
42
+ @data = ::Redis.connect(redis_options)
43
+ end
44
+
45
+ extend ActiveSupport::Cache::Strategy::LocalCache
46
+ end
47
+
48
+ # Reads multiple values from the cache using a single call to the
49
+ # servers for all keys. Options can be passed in the last argument.
50
+ def read_multi(*names)
51
+ options = names.extract_options!
52
+ options = merged_options(options)
53
+ keys_to_names = Hash[names.map{|name| [namespaced_key(name, options), name]}]
54
+ raw_values = @data.mget(keys_to_names.keys)
55
+ values = {}
56
+ raw_values.each do |key, value|
57
+ entry = deserialize_entry(value)
58
+ values[keys_to_names[key]] = entry.value unless entry.expired?
59
+ end
60
+ values
61
+ end
62
+
63
+ # Delete objects for matched keys.
64
+ #
65
+ # Example:
66
+ # cache.del_matched "rab*"
67
+ def delete_matched(matcher, options = nil) # :nodoc:
68
+ options = merged_options(options)
69
+ response = instrument(:delete_matched, matcher.inspect) do
70
+ matcher = key_matcher(matcher, options)
71
+ @data.keys(matcher).each { |key| @data.del key }
72
+ end
73
+ rescue => e
74
+ logger.error("Error calling delete_matched on redis: #{e}") if logger
75
+ nil
76
+ end
77
+
78
+ # Increment a cached value. This method uses the redis incrby atomic
79
+ # operator
80
+ def increment(name, amount = 1, options = nil) # :nodoc:
81
+ options = merged_options(options)
82
+ response = instrument(:increment, name, :amount => amount) do
83
+ @data.incrby(namespaced_key(name, options), amount)
84
+ end
85
+ rescue => e
86
+ logger.error("Error incrementing cache entry in redis: #{e}") if logger
87
+ nil
88
+ end
89
+
90
+ # Decrement a cached value. This method uses the redis decrby atomic
91
+ # operator
92
+ def decrement(name, amount = 1, options = nil) # :nodoc:
93
+ options = merged_options(options)
94
+ response = instrument(:decrement, name, :amount => amount) do
95
+ @data.decrby(namespaced_key(name, options), amount)
96
+ end
97
+ rescue => e
98
+ logger.error("Error decrementing cache entry in redis: #{e}") if logger
99
+ nil
100
+ end
101
+
102
+ # Clear the entire cache on all redis servers. This method should
103
+ # be used with care when shared cache is being used.
104
+ def clear(options = nil)
105
+ @data.flushdb
106
+ end
107
+
108
+ # Get the statistics from the redis servers.
109
+ def stats
110
+ @data.info
111
+ end
112
+
113
+ protected
114
+
115
+ # Read an entry from the cache.
116
+ def read_entry(key, options) # :nodoc:
117
+ deserialize_entry(@data.get(key))
118
+ rescue => e
119
+ logger.error("Error reading cache entry from redis: #{e}") if logger
120
+ nil
121
+ end
122
+
123
+ # Write an entry to the cache.
124
+ def write_entry(key, entry, options) # :nodoc:
125
+ value = serialize_entry(entry)
126
+ if (options && options[:expires_in])
127
+ expires_in = options[:expires_in].to_i
128
+ response = @data.setex(key, expires_in, value)
129
+ else
130
+ response = @data.set(key, value)
131
+ end
132
+ rescue => e
133
+ logger.error("Error writing cache entry to redis: #{e}") if logger
134
+ false
135
+ end
136
+
137
+ # Delete an entry from the cache.
138
+ def delete_entry(key, options) # :nodoc:
139
+ response = @data.del(key)
140
+ rescue => e
141
+ logger.error("Error deleting cache entry from redis: #{e}") if logger
142
+ false
143
+ end
144
+
145
+ private
146
+
147
+ def deserialize_entry(raw_value)
148
+ if raw_value
149
+ entry = Marshal.load(raw_value) rescue raw_value
150
+ entry.is_a?(ActiveSupport::Cache::Entry) ? entry : ActiveSupport::Cache::Entry.new(entry)
151
+ else
152
+ nil
153
+ end
154
+ end
155
+
156
+ def serialize_entry(entry)
157
+ if entry
158
+ Marshal.dump(entry)
159
+ else
160
+ nil
161
+ end
162
+ end
163
+
164
+ end
165
+
166
+ end
167
+ end
@@ -0,0 +1,113 @@
1
+ require 'redis_failover'
2
+
3
+ # A factory class for creating redis connections, and reconnecting them after a fork
4
+ #
5
+ # It uses RedisFailover as the client if config/redis.yml contains a
6
+ # zkservers setting for the named connection, otherwise vanilla redis client
7
+ class RedisFactory
8
+ extend MonitorMixin
9
+
10
+ @@configuration = nil
11
+ @@clients = {}
12
+
13
+ def self.logger
14
+ Rails.logger
15
+ end
16
+
17
+ # Creates a redis client for the given named configuration
18
+ #
19
+ # @param [String] name The name of the redis configuration
20
+ # (config/redis.yml) )to use
21
+ # @return [RedisClient] A redis client object
22
+ # (may be a failover capable proxy)
23
+ def self.connect(name)
24
+ conf = configuration[name].symbolize_keys!
25
+ raise "No redis configuration for #{Rails.env} environment in redis.yml for #{name}" unless conf
26
+ synchronize do
27
+ # if conf[:zkservers]
28
+ # conf[:logger] = logger
29
+ # @@clients[name] ||= ::RedisFailover::Client.new(
30
+ # :zkservers => 'localhost:2181,localhost:2182,localhost:2183',
31
+ # :znode_path => '/redis_failover',
32
+ # :db => '1')
33
+ # else
34
+ # @@clients[name] ||= ::Redis.new()
35
+ # end
36
+ if conf[:zkservers]
37
+ conf[:logger] = logger
38
+ @@clients[name] ||= ::RedisFailover::Client.new(conf)
39
+ else
40
+ @@clients[name] ||= ::Redis.new(conf)
41
+ end
42
+ end
43
+ @@clients[name]
44
+ end
45
+
46
+ def self.disconnect(key = nil)
47
+ logger.debug "RedisFactory.disconnect start"
48
+ synchronize do
49
+ @@clients.clone.each do |name, client|
50
+ next if key && name != key
51
+ client = @@clients.delete(name)
52
+ if client
53
+ begin
54
+ if client.instance_of?(::RedisFailover::Client)
55
+ logger.debug "Disconnecting RedisFailover client: #{client}"
56
+ client.shutdown
57
+ elsif client.instance_of?(::Redis)
58
+ logger.debug "Disconnecting Redis client: #{client}"
59
+ client.quit
60
+ else
61
+ logger.warn("Couldn't reconnect unknown redis client type: #{client.class}")
62
+ end
63
+ rescue => e
64
+ logger.warn("Exception while disconnecting: #{e}")
65
+ end
66
+ end
67
+ end
68
+ end
69
+ logger.debug "RedisFactory.disconnect complete"
70
+ end
71
+
72
+ def self.reconnect(key = nil)
73
+ logger.debug "RedisFactory.reconnect start"
74
+ synchronize do
75
+ @@clients.each do |name, client|
76
+ next if key && name != key
77
+ if client.instance_of?(::RedisFailover::Client)
78
+ logger.debug "Reconnecting RedisFailover client: #{client}"
79
+ client.reconnect
80
+ elsif client.instance_of?(::Redis)
81
+ logger.debug "Reconnecting Redis client: #{client}"
82
+ client.client.reconnect
83
+ else
84
+ logger.warn("Couldn't reconnect unknown redis client type: #{client.class}")
85
+ end
86
+ end
87
+ end
88
+ logger.debug "RedisFactory.reconnect complete"
89
+ end
90
+
91
+ def self.configuration
92
+ synchronize do
93
+ @@configuration ||= begin
94
+ require 'erb'
95
+ # config = YAML::load(ERB.new(IO.read("../config/redis.yml")).result)
96
+ config = YAML.load(ERB.new(File.read('./config/redis.yml')).result)
97
+ # self.symbolize(config[ENV['RAILS_ENV']])
98
+ config = config[ENV['RAILS_ENV']].symbolize_keys!
99
+ end
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ def self.symbolize(hash)
106
+ hash.inject({}) do |options, (key, value)|
107
+ value = self.symbolize(value) if value.kind_of?(Hash)
108
+ options[key.to_sym || key] = value
109
+ options
110
+ end
111
+ end
112
+
113
+ end