Skip to content

Commit

Permalink
Use memcache-client lib unless memcached already required
Browse files Browse the repository at this point in the history
  • Loading branch information
rtomayko committed Mar 31, 2009
1 parent 522dc31 commit dc350d8
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 84 deletions.
6 changes: 6 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 0.5.0 / unreleased

* Added meta and entity store implementations based on the
memcache-client library. These are the default unless the memcached
library has already been required.

## 0.4.0 / March 2009

* Ruby 1.9.1 / Rack 1.0 compatible.
Expand Down
18 changes: 8 additions & 10 deletions TODO
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
## 0.5

- Work with both memcache and memcached gems (memcached hasn't built on
MacOS for some time now).
- Support multiple memcache servers.
- Explicit expiration/invalidation based on response headers or via an
object interface passed in the rack env.

## Backlog

- Move breakers.rb configuration file into rack-contrib as a middleware
component.
- Sample apps: Rack, Rails, Sinatra, Merb, etc.
- Use Bacon instead of test/spec
- Work with both memcache and memcached gems (memcached hasn't built on MacOS
for some time now).
- Fast path pass processing. We do a lot more than necessary just to determine
that the response should be passed through untouched.
- Don't purge/remove cache entries when invalidating. The entries should be
marked as stale and be forced revalidated on the next request instead of
being removed entirely.
- Add missing Expires header if we have a max-age.
- Purge/invalidate everything
- Invalidate at the URI of the Location or Content-Location response header
on POST, PUT, or DELETE that results in a redirect.
- Maximum size of cached entity
- Last-Modified factor: requests that have a Last-Modified header but no Expires
header have a TTL assigned based on the last modified age of the response:
TTL = (Age * Factor), or, 1h = (10h * 0.1)
- Run under multiple-threads with an option to lock before making requests
to the backend. The idea is to be able to serve requests from cache in
separate threads. This should probably be implemented as a separate
middleware component.
- Consider implementing ESI (http://www.w3.org/TR/esi-lang). This should
probably be implemented as a separate middleware component.
- Sqlite3 (meta store)
Expand Down
120 changes: 77 additions & 43 deletions lib/rack/cache/entitystore.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def slurp(body)
yield part
end
body.close if body.respond_to? :close
[ digest.hexdigest, size ]
[digest.hexdigest, size]
end

if ''.respond_to?(:bytesize)
Expand Down Expand Up @@ -168,44 +168,97 @@ def self.resolve(uri)
DISK = Disk
FILE = Disk

# Stores entity bodies in memcached.
class MemCache < EntityStore

# Base class for memcached entity stores.
class MemCacheBase < EntityStore
# The underlying Memcached instance used to communicate with the
# memcahced daemon.
# memcached daemon.
attr_reader :cache

extend Rack::Utils

def open(key)
data = read(key)
data && [data]
end

def self.resolve(uri)
server = "#{uri.host}:#{uri.port || '11211'}"
options = parse_query(uri.query)
options.keys.each do |key|
value =
case value = options.delete(key)
when 'true' ; true
when 'false' ; false
else value.to_sym
end
options[k.to_sym] = value
end
options[:namespace] = uri.path.sub(/^\//, '')
new server, options
end
end

# Uses the memcache-client ruby library. This is the default unless
# the memcached library has already been required.
class MemCache < MemCacheBase
def initialize(server="localhost:11211", options={})
@cache =
if server.respond_to?(:stats)
server
else
require 'memcache'
::MemCache.new(server, options)
end
end

def exist?(key)
!cache.get(key).nil?
end

def read(key)
cache.get(key)
end

def write(body)
buf = StringIO.new
key, size = slurp(body){|part| buf.write(part) }
[key, size] if cache.set(key, buf.string)
end

def purge(key)
cache.delete(key)
nil
end
end

# Uses the memcached client library. The ruby based memcache-client is used
# in preference to this store unless the memcached library has already been
# required.
class MemCached < MemCacheBase
def initialize(server="localhost:11211", options={})
options[:prefix_key] ||= options.delete(:namespace) if options.key?(:namespace)
@cache =
if server.respond_to?(:stats)
server
else
require 'memcached'
Memcached.new(server, options)
::Memcached.new(server, options)
end
end

def exist?(key)
cache.append(key, '')
true
rescue Memcached::NotStored
rescue ::Memcached::NotStored
false
end

def read(key)
cache.get(key, false)
rescue Memcached::NotFound
rescue ::Memcached::NotFound
nil
end

def open(key)
if data = read(key)
[data]
else
nil
end
end

def write(body)
buf = StringIO.new
key, size = slurp(body){|part| buf.write(part) }
Expand All @@ -216,38 +269,19 @@ def write(body)
def purge(key)
cache.delete(key)
nil
rescue Memcached::NotFound
rescue ::Memcached::NotFound
nil
end
end

extend Rack::Utils

# Create MemCache store for the given URI. The URI must specify
# a host and may specify a port, namespace, and options:
#
# memcached://example.com:11211/namespace?opt1=val1&opt2=val2
#
# Query parameter names and values are documented with the memcached
# library: http://tinyurl.com/4upqnd
def self.resolve(uri)
server = "#{uri.host}:#{uri.port || '11211'}"
options = parse_query(uri.query)
options.keys.each do |key|
value =
case value = options.delete(key)
when 'true' ; true
when 'false' ; false
else value.to_sym
end
options[k.to_sym] = value
end
options[:namespace] = uri.path.sub(/^\//, '')
new server, options
MEMCACHE =
if defined?(::Memcached)
MemCached
else
MemCache
end
end

MEMCACHE = MemCache
MEMCACHED = MemCache
MEMCACHED = MEMCACHE
end

end
91 changes: 64 additions & 27 deletions lib/rack/cache/metastore.rb
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,65 @@ def self.resolve(uri)
# Stores request/response pairs in memcached. Keys are not stored
# directly since memcached has a 250-byte limit on key names. Instead,
# the SHA1 hexdigest of the key is used.
class MemCache < MetaStore
class MemCacheBase < MetaStore
extend Rack::Utils

# The MemCache object used to communicated with the memcached
# daemon.
attr_reader :cache

# Create MemCache store for the given URI. The URI must specify
# a host and may specify a port, namespace, and options:
#
# memcached://example.com:11211/namespace?opt1=val1&opt2=val2
#
# Query parameter names and values are documented with the memcached
# library: http://tinyurl.com/4upqnd
def self.resolve(uri)
server = "#{uri.host}:#{uri.port || '11211'}"
options = parse_query(uri.query)
options.keys.each do |key|
value =
case value = options.delete(key)
when 'true' ; true
when 'false' ; false
else value.to_sym
end
options[k.to_sym] = value
end
options[:namespace] = uri.path.sub(/^\//, '')
new server, options
end
end

class MemCache < MemCacheBase
def initialize(server="localhost:11211", options={})
@cache =
if server.respond_to?(:stats)
server
else
require 'memcache'
::MemCache.new(server, options)
end
end

def read(key)
key = hexdigest(key)
cache.get(key) || []
end

def write(key, entries)
key = hexdigest(key)
cache.set(key, entries)
end

def purge(key)
cache.delete(hexdigest(key))
nil
end
end

class MemCached < MemCacheBase
# The Memcached instance used to communicated with the memcached
# daemon.
attr_reader :cache
Expand Down Expand Up @@ -294,34 +351,14 @@ def purge(key)
rescue Memcached::NotFound
nil
end

extend Rack::Utils

# Create MemCache store for the given URI. The URI must specify
# a host and may specify a port, namespace, and options:
#
# memcached://example.com:11211/namespace?opt1=val1&opt2=val2
#
# Query parameter names and values are documented with the memcached
# library: http://tinyurl.com/4upqnd
def self.resolve(uri)
server = "#{uri.host}:#{uri.port || '11211'}"
options = parse_query(uri.query)
options.keys.each do |key|
value =
case value = options.delete(key)
when 'true' ; true
when 'false' ; false
else value.to_sym
end
options[k.to_sym] = value
end
options[:namespace] = uri.path.sub(/^\//, '')
new server, options
end
end

MEMCACHE = MemCache
MEMCACHE =
if defined?(::Memcached)
MemCached
else
MemCache
end
MEMCACHED = MemCache
end

Expand Down
16 changes: 15 additions & 1 deletion test/entitystore_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,24 @@ def sha_like?
end

need_memcached 'entity store tests' do
describe 'MemCached' do
it_should_behave_like 'A Rack::Cache::EntityStore Implementation'
before do
@store = Rack::Cache::EntityStore::MemCached.new($memcached)
end
after do
@store = nil
end
end
end


need_memcache 'entity store tests' do
describe 'MemCache' do
it_should_behave_like 'A Rack::Cache::EntityStore Implementation'
before do
@store = Rack::Cache::EntityStore::MemCache.new($memcached)
$memcache.flush_all
@store = Rack::Cache::EntityStore::MemCache.new($memcache)
end
after do
@store = nil
Expand Down
16 changes: 14 additions & 2 deletions test/metastore_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -250,12 +250,24 @@ def self.call(request); request.path_info.reverse end
end

need_memcached 'metastore tests' do
describe 'MemCache' do
describe 'MemCached' do
it_should_behave_like 'A Rack::Cache::MetaStore Implementation'
before :each do
@temp_dir = create_temp_directory
$memcached.flush
@store = Rack::Cache::MetaStore::MemCache.new($memcached)
@store = Rack::Cache::MetaStore::MemCached.new($memcached)
@entity_store = Rack::Cache::EntityStore::Heap.new
end
end
end

need_memcache 'metastore tests' do
describe 'MemCache' do
it_should_behave_like 'A Rack::Cache::MetaStore Implementation'
before :each do
@temp_dir = create_temp_directory
$memcache.flush_all
@store = Rack::Cache::MetaStore::MemCache.new($memcache)
@entity_store = Rack::Cache::EntityStore::Heap.new
end
end
Expand Down
Loading

0 comments on commit dc350d8

Please sign in to comment.