New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Built-in Redis cache store #31134

Merged
merged 1 commit into from Nov 14, 2017

Conversation

Projects
None yet
6 participants
@jeremy
Member

jeremy commented Nov 12, 2017

  • Supports vanilla Redis, hiredis, and Redis::Distributed.
  • Supports Memcached-like sharding across Redises with Redis::Distributed.
  • Fault tolerant. If the Redis server is unavailable, no exceptions are raised. Cache fetches are treated as misses and writes are dropped.
  • Local cache. Hot in-memory primary cache within block/middleware scope.
  • read_/write_multi support for Redis mget/mset. Use Redis::Distributed 4.0.1+ for distributed mget support.
  • delete_matched support for Redis KEYS globs.
      # Defaults to `redis://localhost:6379/0`
      config.cache_store = :redis_cache_store

      # Supports all common cache store options (:namespace, :compress,
      # :compress_threshold, :expires_in, :race_condition_tool) and all
      # Redis options.
      cache_password = Rails.application.secrets.redis_cache_password
      config.cache_store = :redis_cache_store, driver: :hiredis,
        namespace: 'myapp-cache', compress: true, timeout: 1,
        url: "redis://:#{cache_password}@myapp-cache-1:6379/0"

      # Supports Redis::Distributed with multiple hosts
      config.cache_store = :redis_cache_store, driver: :hiredis
        namespace: 'myapp-cache', compress: true,
        url: %w[
          redis://myapp-cache-1:6379/0
          redis://myapp-cache-1:6380/0
          redis://myapp-cache-2:6379/0
          redis://myapp-cache-2:6380/0
          redis://myapp-cache-3:6379/0
          redis://myapp-cache-3:6380/0
        ]

      # Or pass a builder block
      config.cache_store = :redis_cache_store,
        namespace: 'myapp-cache', compress: true,
        redis: -> { Redis.new … }

Deployment note: Take care to use a dedicated Redis cache rather than pointing this at your existing Redis server. It won't cope well with mixed usage patterns and it won't expire cache entries by default.

Redis cache server setup guide: https://redis.io/topics/lru-cache

Show outdated Hide outdated .travis.yml Outdated
"#{namespace}:#{key}"
else
key
end
end

This comment has been minimized.

@jeremy

jeremy Nov 13, 2017

Member

Separated this method from normalize_key so they may be overridden and called independently by subclasses.

The Redis cache store hashes cache keys larger than 1kB and it namespaces deleted_matched globs, which aren't keys to be normalized.

@jeremy

jeremy Nov 13, 2017

Member

Separated this method from normalize_key so they may be overridden and called independently by subclasses.

The Redis cache store hashes cache keys larger than 1kB and it namespaces deleted_matched globs, which aren't keys to be normalized.

This comment has been minimized.

@rafaelfranca

rafaelfranca Nov 13, 2017

Member

This method was removed from public API in Rails 5.0. #22215 for context.

@rafaelfranca

rafaelfranca Nov 13, 2017

Member

This method was removed from public API in Rails 5.0. #22215 for context.

This comment has been minimized.

@jeremy

jeremy Nov 13, 2017

Member

I'll call it namespace_key instead, matching verb tense with normalize_key. (It's a different method than the old namespaced_key which expanded, sanitized, namespaced, etc.)

@jeremy

jeremy Nov 13, 2017

Member

I'll call it namespace_key instead, matching verb tense with normalize_key. (It's a different method than the old namespaced_key which expanded, sanitized, namespaced, etc.)

This comment has been minimized.

@rafaelfranca
@rafaelfranca

rafaelfranca Nov 13, 2017

Member

👍

Show outdated Hide outdated activesupport/lib/active_support/cache/redis_cache_store.rb Outdated
begin
require "redis/connection/hiredis"
rescue LoadError
end

This comment has been minimized.

@jeremy

jeremy Nov 13, 2017

Member

Maybe it's dubious. Maybe it's good practice. Maybe it's …

@jeremy

jeremy Nov 13, 2017

Member

Maybe it's dubious. Maybe it's good practice. Maybe it's …

Show outdated Hide outdated activesupport/lib/active_support/cache/redis_cache_store.rb Outdated
Show outdated Hide outdated activesupport/lib/active_support/cache/redis_cache_store.rb Outdated
else
key
end
end

This comment has been minimized.

@jeremy

jeremy Nov 13, 2017

Member

Worth hoisting this out too.

@jeremy

jeremy Nov 13, 2017

Member

Worth hoisting this out too.

def failsafe(method, returning: nil)
yield
rescue ::Redis::BaseConnectionError => e

This comment has been minimized.

@jeremy

jeremy Nov 13, 2017

Member

May need to expand the set of failsafe exceptions, and allow Rails users to do it.

@jeremy

jeremy Nov 13, 2017

Member

May need to expand the set of failsafe exceptions, and allow Rails users to do it.

end
rescue => failsafe
warn "RedisCacheStore ignored exception in handle_exception: #{failsafe.class}: #{failsafe.message}\n #{failsafe.backtrace.join("\n ")}"
end

This comment has been minimized.

@jeremy

jeremy Nov 13, 2017

Member

Exceptions are swallowed by default. Apps provide an error_handler to report on them.

@jeremy

jeremy Nov 13, 2017

Member

Exceptions are swallowed by default. Apps provide an error_handler to report on them.

test "write failure returns nil" do
end
end

This comment has been minimized.

@jeremy

jeremy Nov 13, 2017

Member

TBD

@jeremy

jeremy Nov 13, 2017

Member

TBD

Deployment note: Take care to use a *dedicated Redis cache* rather
than pointing this at your existing Redis server. It won't cope well
with mixed usage patterns and it won't expire cache entries by default.

This comment has been minimized.

@matthewd

matthewd Nov 13, 2017

Member

Is this advice consistent with it defaulting to redis://localhost:6379/0? 🤔

@matthewd

matthewd Nov 13, 2017

Member

Is this advice consistent with it defaulting to redis://localhost:6379/0? 🤔

This comment has been minimized.

@jeremy

jeremy Nov 13, 2017

Member

I initially set this up without a default for that reason, but it felt needlessly fussy for dev/test. I think clearly, repeatedly directing users to deployment guidance is the important ingredient.

@jeremy

jeremy Nov 13, 2017

Member

I initially set this up without a default for that reason, but it felt needlessly fussy for dev/test. I think clearly, repeatedly directing users to deployment guidance is the important ingredient.

This comment has been minimized.

@matthewd

matthewd Nov 13, 2017

Member

Maybe we should default to some non-nil expiry, and then people heeding the "how to use this at its best" guidance can turn that off at the same time they point to a dedicated instance?

@matthewd

matthewd Nov 13, 2017

Member

Maybe we should default to some non-nil expiry, and then people heeding the "how to use this at its best" guidance can turn that off at the same time they point to a dedicated instance?

This comment has been minimized.

@jeremy

jeremy Nov 13, 2017

Member

I like that direction since it require an informed choice of Redis usage mode.

Thing is, by default, Redis doesn't expire keys when it hits max mem—it returns errors. For expiry alone to work, we'd need to use volatile-lru/ttl max-mem policy instead of the default noeviction.

That suggests that we configure a volatile/allkeys policy on the cache store as well. Then we can inspect maxmemory-policy to satisfy ourselves that the server is on board with the user's intent.

@jeremy

jeremy Nov 13, 2017

Member

I like that direction since it require an informed choice of Redis usage mode.

Thing is, by default, Redis doesn't expire keys when it hits max mem—it returns errors. For expiry alone to work, we'd need to use volatile-lru/ttl max-mem policy instead of the default noeviction.

That suggests that we configure a volatile/allkeys policy on the cache store as well. Then we can inspect maxmemory-policy to satisfy ourselves that the server is on board with the user's intent.

Show outdated Hide outdated activesupport/lib/active_support/cache/redis_cache_store.rb Outdated
Show outdated Hide outdated activesupport/lib/active_support/cache/redis_cache_store.rb Outdated
raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}"
end
end
end

This comment has been minimized.

@matthewd

matthewd Nov 13, 2017

Member

Seems fine, but that would imply this method isn't "Cache Store API implementation".

@matthewd

matthewd Nov 13, 2017

Member

Seems fine, but that would imply this method isn't "Cache Store API implementation".

Show outdated Hide outdated activesupport/lib/active_support/cache/redis_cache_store.rb Outdated
Show outdated Hide outdated activesupport/lib/active_support/cache/redis_cache_store.rb Outdated
# Compression is enabled by default with a 1kB threshold, so cached
# values larger than 1kB are automatically compressed. Disable by
# passing `cache: false` or change the threshold by passing
# `compress_threshold: 4.kilobytes`.

This comment has been minimized.

@matthewd

matthewd Nov 13, 2017

Member

This does sound like a good idea, but I wonder whether it's up to the redis store to have that opinion. Seems more like it should be the global default (and then overridden for the in-memory store)?

@matthewd

matthewd Nov 13, 2017

Member

This does sound like a good idea, but I wonder whether it's up to the redis store to have that opinion. Seems more like it should be the global default (and then overridden for the in-memory store)?

Show outdated Hide outdated .travis.yml Outdated
"#{namespace}:#{key}"
else
key
end
end

This comment has been minimized.

@rafaelfranca

rafaelfranca Nov 13, 2017

Member

This method was removed from public API in Rails 5.0. #22215 for context.

@rafaelfranca

rafaelfranca Nov 13, 2017

Member

This method was removed from public API in Rails 5.0. #22215 for context.

Show outdated Hide outdated activesupport/lib/active_support/cache/redis_cache_store.rb Outdated
"#{namespace}:#{key}"
else
key
end
end

This comment has been minimized.

@rafaelfranca
@rafaelfranca

rafaelfranca Nov 13, 2017

Member

👍

Built-in Redis cache store
* Supports vanilla Redis, hiredis, and Redis::Distributed.
* Supports Memcached-like sharding across Redises with Redis::Distributed.
* Fault tolerant. If the Redis server is unavailable, no exceptions are
  raised. Cache fetches are treated as misses and writes are dropped.
* Local cache. Hot in-memory primary cache within block/middleware scope.
* `read_/write_multi` support for Redis mget/mset. Use Redis::Distributed
  4.0.1+ for distributed mget support.
* `delete_matched` support for Redis KEYS globs.

@jeremy jeremy merged commit 9f8ec35 into rails:master Nov 14, 2017

2 checks passed

codeclimate All good!
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details

@jeremy jeremy deleted the jeremy:redis-cache-store branch Nov 14, 2017

@jeremy jeremy added this to the 5.2.0 milestone Nov 14, 2017

@jeremy

This comment has been minimized.

Show comment
Hide comment
@jeremy

jeremy Nov 14, 2017

Member

Thanks for review, @matthewd @rafaelfranca 🖖🏼

Member

jeremy commented Nov 14, 2017

Thanks for review, @matthewd @rafaelfranca 🖖🏼

@sorentwo

This comment has been minimized.

Show comment
Hide comment
@sorentwo

sorentwo Dec 8, 2017

Contributor

I'm a bit disappointed to see this from-scratch implementation considering all the effort that was put into https://github.com/sorentwo/readthis. It already supports every feature this implements, aside from distributed redis and local cache support.

Porting Readthis would have been a great way to get something that is well documented, tested, and has been used in production for years into Rails.

Contributor

sorentwo commented Dec 8, 2017

I'm a bit disappointed to see this from-scratch implementation considering all the effort that was put into https://github.com/sorentwo/readthis. It already supports every feature this implements, aside from distributed redis and local cache support.

Porting Readthis would have been a great way to get something that is well documented, tested, and has been used in production for years into Rails.

@jkburges

This comment has been minimized.

Show comment
Hide comment
@jkburges

jkburges Jan 10, 2018

I have a question about the syntax of the configuration, raised at https://stackoverflow.com/questions/48177606/ruby-syntax-error-unexpected-tlabel

I didn't think it was appropriate to discuss the question directly here, but hopefully ok to link to it, in case someone here can shed some light on it :-)

jkburges commented Jan 10, 2018

I have a question about the syntax of the configuration, raised at https://stackoverflow.com/questions/48177606/ruby-syntax-error-unexpected-tlabel

I didn't think it was appropriate to discuss the question directly here, but hopefully ok to link to it, in case someone here can shed some light on it :-)

@steakknife

This comment has been minimized.

Show comment
Hide comment
@steakknife

steakknife May 4, 2018

Contributor

@jkburges No biggie: it's not a function but an assignment. Surround the keys with braces. Should work.

Contributor

steakknife commented May 4, 2018

@jkburges No biggie: it's not a function but an assignment. Surround the keys with braces. Should work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment