Skip to content
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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use $MEMCACHE_SERVERS as MemCacheStore fallback #40420

Merged
merged 4 commits into from Oct 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 16 additions & 0 deletions activesupport/CHANGELOG.md
@@ -1,3 +1,19 @@
* `ActiveSupport::Cache::MemCacheStore` now checks `ENV["MEMCACHE_SERVERS"]` before falling back to `"localhost:11211"` if configured without any addresses.

```ruby
config.cache_store = :mem_cache_store

# is now equivalent to

config.cache_store = :mem_cache_store, ENV["MEMCACHE_SERVERS"] || "localhost:11211"

# instead of

config.cache_store = :mem_cache_store, "localhost:11211" # ignores ENV["MEMCACHE_SERVERS"]
```

*Sam Bostock*

* `ActiveSupport::Subscriber#attach_to` now accepts an `inherit_all:` argument. When set to true,
it allows a subscriber to receive events for methods defined in the subscriber's ancestor class(es).

Expand Down
12 changes: 7 additions & 5 deletions activesupport/lib/active_support/cache/mem_cache_store.rb
Expand Up @@ -53,16 +53,18 @@ def self.supports_cache_versioning?
ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/n

# Creates a new Dalli::Client instance with specified addresses and options.
# By default address is equal localhost:11211.
# If no addresses are provided, we give nil to Dalli::Client, so it uses its fallbacks:
# - ENV["MEMCACHE_SERVERS"] (if defined)
# - "127.0.0.1:11211" (otherwise)
#
# ActiveSupport::Cache::MemCacheStore.build_mem_cache
# # => #<Dalli::Client:0x007f98a47d2028 @servers=["localhost:11211"], @options={}, @ring=nil>
# # => #<Dalli::Client:0x007f98a47d2028 @servers=["127.0.0.1:11211"], @options={}, @ring=nil>
# ActiveSupport::Cache::MemCacheStore.build_mem_cache('localhost:10290')
# # => #<Dalli::Client:0x007f98a47b3a60 @servers=["localhost:10290"], @options={}, @ring=nil>
def self.build_mem_cache(*addresses) # :nodoc:
addresses = addresses.flatten
options = addresses.extract_options!
addresses = ["localhost:11211"] if addresses.empty?
addresses = nil if addresses.empty?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users of :dalli_store may have been passing an explicit nil parameter for the servers:

config.cache_store = :dalli_cache, nil, { expires_in: 2.hour, compress: true }

If they simply changed :dalli_cache and :mem_cache_store, this code passes addresses = [nil] to Dalli (instead of nil, which cause exceptions when people try to access the cache.

NoMethodError: undefined method `match' for nil:NilClass

I've seen at least 2 cases within repos where this was the case, which confused hapless developers who were just trying to migrate in order to clear the deprecation warning (causing them to revert instead of debugging the issue).

Perhaps we could do something like add a .compact:

addresses = nil if addresses.compact.empty?

This would unify the interface between the two stores.

Or perhaps raise an exception that explained what they were doing wrong / the difference in interface between :dalli_store and :mem_cache_store.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to explain, but I think what needs to explain how the migration should be done is the dalli_store project.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of some relevance, I opened petergoldstein/dalli#764 a while back to document the change in implicit ENV variable handling.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think the compact version is fine. @movermeyer want to open a PR?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rafaelfranca It would be my pleasure!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pool_options = retrieve_pool_options(options)

if pool_options.empty?
Expand All @@ -79,8 +81,8 @@ def self.build_mem_cache(*addresses) # :nodoc:
#
# ActiveSupport::Cache::MemCacheStore.new("localhost", "server-downstairs.localnetwork:8229")
#
# If no addresses are specified, then MemCacheStore will connect to
# localhost port 11211 (the default memcached port).
# If no addresses are provided, but ENV['MEMCACHE_SERVERS'] is defined, it will be used instead. Otherwise,
# MemCacheStore will connect to localhost:11211 (the default memcached port).
def initialize(*addresses)
addresses = addresses.flatten
options = addresses.extract_options!
Expand Down
59 changes: 53 additions & 6 deletions activesupport/test/cache/stores/mem_cache_store_test.rb
Expand Up @@ -45,7 +45,6 @@ def setup
@namespace = "test-#{SecureRandom.hex}"
@cache = lookup_store(expires_in: 60)
@peek = lookup_store
@data = @cache.instance_variable_get(:@data)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't find anywhere this was used in this file or any of the included modules, and this file's tests continued to pass for me locally. 馃し

@cache.silence!
@cache.logger = ActiveSupport::Logger.new(File::NULL)
end
Expand All @@ -64,7 +63,6 @@ def setup
# Overrides test from LocalCacheBehavior in order to stub out the cache clear
# and replace it with a delete.
def test_clear_also_clears_local_cache
client = @cache.instance_variable_get(:@data)
key = "#{@namespace}:foo"
client.stub(:flush_all, -> { client.delete(key) }) do
super
Expand Down Expand Up @@ -102,14 +100,14 @@ def test_local_cache_raw_values

def test_increment_expires_in
cache = lookup_store(raw: true, namespace: nil)
assert_called_with cache.instance_variable_get(:@data), :incr, [ "foo", 1, 60 ] do
assert_called_with client(cache), :incr, [ "foo", 1, 60 ] do
cache.increment("foo", 1, expires_in: 60)
end
end

def test_decrement_expires_in
cache = lookup_store(raw: true, namespace: nil)
assert_called_with cache.instance_variable_get(:@data), :decr, [ "foo", 1, 60 ] do
assert_called_with client(cache), :decr, [ "foo", 1, 60 ] do
cache.decrement("foo", 1, expires_in: 60)
end
end
Expand Down Expand Up @@ -164,18 +162,47 @@ def test_no_multiple_compress

def test_unless_exist_expires_when_configured
cache = ActiveSupport::Cache.lookup_store(:mem_cache_store)
assert_called_with cache.instance_variable_get(:@data), :add, [ "foo", ActiveSupport::Cache::Entry, 1, Hash ] do
assert_called_with client(cache), :add, [ "foo", ActiveSupport::Cache::Entry, 1, Hash ] do
cache.write("foo", "bar", expires_in: 1, unless_exist: true)
end
end

def test_uses_provided_dalli_client_if_present
cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, Dalli::Client.new("custom_host"))

assert_equal ["custom_host"], servers(cache)
end

def test_forwards_string_addresses_if_present
expected_addresses = ["first", "second"]
cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, expected_addresses)

assert_equal expected_addresses, servers(cache)
end

def test_falls_back_to_localhost_if_no_address_provided_and_memcache_servers_undefined
with_memcache_servers_environment_variable(nil) do
cache = ActiveSupport::Cache.lookup_store(:mem_cache_store)

assert_equal ["127.0.0.1:11211"], servers(cache)
end
end

def test_falls_back_to_localhost_if_no_address_provided_and_memcache_servers_defined
with_memcache_servers_environment_variable("custom_host") do
cache = ActiveSupport::Cache.lookup_store(:mem_cache_store)

assert_equal ["custom_host"], servers(cache)
end
end

private
def random_string(length)
(0...length).map { (65 + rand(26)).chr }.join
end

def store
[:mem_cache_store, ENV["MEMCACHE_SERVERS"] || "localhost:11211"]
[:mem_cache_store]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seemed redundant not to leverage the code I just added 馃槄

end

def emulating_latency
Expand All @@ -197,4 +224,24 @@ def emulating_unavailability
Dalli.send(:remove_const, :Server)
Dalli.const_set(:Server, old_server)
end

def servers(cache = @cache)
client(cache).instance_variable_get(:@servers)
end

def client(cache = @cache)
cache.instance_variable_get(:@data)
end
Comment on lines +228 to +234
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't feel great about doing this, but neither of these are publicly accessible, and we were already using cache.instance_variable_get(:@data) in a couple tests.


def with_memcache_servers_environment_variable(value)
original_value = ENV["MEMCACHE_SERVERS"]
ENV["MEMCACHE_SERVERS"] = value
yield
ensure
if original_value.nil?
ENV.delete("MEMCACHE_SERVERS")
Comment on lines +241 to +242
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might not be needed, but I figured it would make sense to remove it as a key entirely if it wasn't there originally.

else
ENV["MEMCACHE_SERVERS"] = original_value
end
end
end
4 changes: 4 additions & 0 deletions guides/CHANGELOG.md
@@ -1,3 +1,7 @@
* Updated `ActiveSupport::Cache::MemCacheStore` docs to reflect support for `$MEMCACHE_SERVERS`.

*Sam Bostock*

Comment on lines +1 to +4
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is required. I couldn't find anything describing what changes to the Guides require CHANGELOG entries.

* Use Bookstore as a unified use-case for all examples in Active Record Query Interface Guide.

*Ashley Engelund*, *Vipul A M*
Expand Down
17 changes: 11 additions & 6 deletions guides/source/caching_with_rails.md
Expand Up @@ -453,17 +453,22 @@ no explicit `config.cache_store` is supplied.

This cache store uses Danga's `memcached` server to provide a centralized cache for your application. Rails uses the bundled `dalli` gem by default. This is currently the most popular cache store for production websites. It can be used to provide a single, shared cache cluster with very high performance and redundancy.

When initializing the cache, you need to specify the addresses for all
memcached servers in your cluster. If none are specified, it will assume
memcached is running on localhost on the default port, but this is not an ideal
setup for larger sites.

The `write` and `fetch` methods on this cache accept two additional options that take advantage of features specific to memcached. You can specify `:raw` to send a value directly to the server with no serialization. The value must be a string or number. You can use memcached direct operations like `increment` and `decrement` only on raw values. You can also specify `:unless_exist` if you don't want memcached to overwrite an existing entry.
When initializing the cache, you should specify the addresses for all memcached servers in your cluster, or ensure the `MEMCACHE_SERVERS` environment variable has been set appropriately.

```ruby
config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com"
```

If neither are specified, it will assume memcached is running on localhost on the default port (`127.0.0.1:11211`), but this is not an ideal setup for larger sites.

```ruby
config.cache_store = :mem_cache_store # Will fallback to $MEMCACHE_SERVERS, then 127.0.0.1:11211
```

See the [`Dalli::Client` documentation](https://www.rubydoc.info/github/mperham/dalli/Dalli%2FClient:initialize) for supported address types.

The `write` and `fetch` methods on this cache accept two additional options that take advantage of features specific to memcached. You can specify `:raw` to send a value directly to the server with no serialization. The value must be a string or number. You can use memcached direct operations like `increment` and `decrement` only on raw values. You can also specify `:unless_exist` if you don't want memcached to overwrite an existing entry.

### ActiveSupport::Cache::RedisCacheStore

The Redis cache store takes advantage of Redis support for automatic eviction
Expand Down