Skip to content
Browse files

Add simple etag based cache

The cache only handles get requests (so in particular RelaxDB.load %w(lots of ids) is not cached), but it does handle all get requests (so views are cached).
The cache uses the etags that couchdb provides to add a If-None-Match header, if the document or view has not changed then couchdb return a 304 response and the data from the cache is returned. This means that there is not a huge gain when loading a single document (other than shifting some work off couchdb) - the cache is most useful for views. This mechanism means that there is no expiry to worry about. Couchdb knows from the etag whether the cached data is up to date or not.

The cache is off by default. To enable it pass a cache store object to RelaxDB.configure eg

RelaxDB.configure ..., :cache_store => RelaxDB::MemoryStore.new(:size => 200)

will tell RelaxDB to use a cache store that stores up to 200 entries in memory.

There is also a simple memcache based store, eg

RelaxDB.configure ..., :cache_store => RelaxDB::MemcacheStore.new('localhost:11211')

which is more useful for a web app environment. All options passed to MemcacheStore.new are passed straight through to memcache-client
  • Loading branch information...
1 parent bf79e22 commit 1d9acfd5f6b3c23da0d275252b6a6e064865440e @fcheung committed Apr 13, 2009
Showing with 235 additions and 19 deletions.
  1. +36 −0 lib/relaxdb/memcache_store.rb
  2. +112 −18 lib/relaxdb/server.rb
  3. +60 −0 spec/memory_store_spec.rb
  4. +27 −1 spec/server_spec.rb
View
36 lib/relaxdb/memcache_store.rb
@@ -0,0 +1,36 @@
+gem 'memcache-client'
+require 'memcache'
+require 'zlib'
+module RelaxDB
+ class MemcacheStore
+ def initialize(*args_for_memcache)
+ @cache = MemCache.new *args_for_memcache
+ end
+
+ def get_etag(cache_key)
+ @cache.get "etag:#{cache_key}"
+ end
+
+ def get_data(cache_key)
+ entry = @cache.get "data:#{cache_key}"
+ if entry[:deflated]
+ Zlib::Inflate.inflate(entry[:data])
+ else
+ entry[:data]
+ end
+ end
+
+ def store(cache_key, data, etag)
+ deflated = false
+ if data.length >= 1024 * 1024
+ data = Zlib::Deflate.deflate(data, 1)
+ deflated = true
+ end
+
+ if data.size < 1024*1024 #memcache cannot store values over 1mb
+ @cache.set( "data:#{cache_key}", {:data => data, :deflated => deflated})
+ @cache.set( "etag:#{cache_key}", etag)
+ end
+ end
+ end
+end
View
130 lib/relaxdb/server.rb
@@ -4,24 +4,103 @@ class HTTP_404 < StandardError; end
class HTTP_409 < StandardError; end
class HTTP_412 < StandardError; end
+ class CacheEntry
+ attr_accessor :data, :etag
+ def initialize(data, etag)
+ self.data = data
+ self.etag = etag
+ end
+ end
+
+ class NilStore
+ def get_etag(cache_key)
+ end
+ def get_data(cache_key)
+ end
+ def store(cache_key, data,etag)
+ end
+ end
+
+ class MemoryStore
+ def initialize(options={})
+ @cache = {}
+ @maximum_entry_size = options[:maximum_entry_size]
+ @size = options[:size] || 100
+ @cache_order = []
+ end
+
+ def get(cache_key)
+ value = @cache[cache_key]
+ push_key_to_top cache_key if value
+ value
+ end
+
+ def get_etag(cache_key)
+ entry = get(cache_key)
+ entry && entry.etag
+ end
+
+ def get_data(cache_key)
+ entry = get(cache_key)
+ entry && entry.data
+ end
+
+ def store(cache_key, data, etag)
+ if @maximum_entry_size.nil? || data.length <= @maximum_entry_size
+ if @cache.size >= @size && !@cache.has_key?(cache_key)
+ first_key = @cache_order.shift
+ @cache.delete first_key
+ end
+ push_key_to_top cache_key
+ @cache[cache_key] = CacheEntry.new(data, etag)
+ end
+ end
+
+ private
+ def push_key_to_top key
+ @cache_order.delete_if {|k| k == key} if @cache.has_key? key
+ @cache_order << key
+ end
+ end
+
class Server
+ attr_reader :cache_store
class Response
- attr_reader :body
- def initialize body
- @body = body
+ attr_reader :status, :body, :etag
+ def initialize status, body, etag
+ @status, @body, @etag = status, body, etag
end
end
- def initialize(host, port)
+ def initialize(host, port, cache_store)
@host, @port = host, port
+ @cache_store = cache_store || NilStore.new
end
def delete(uri)
request(uri, 'delete'){ |c| c.http_delete}
end
def get(uri)
- request(uri, 'get'){ |c| c.http_get}
+ etag = cache_store.get_etag uri
+
+ response = request(uri, 'get') do |c|
+ c.headers['If-None-Match'] = etag
+ c.http_get
+ end
+
+ if etag && response.status == 304
+ data = cache_store.get_data uri
+ if data
+ return Response.new( 200, data, etag)
+ else
+ #we don't have the data in our cache anymore boohoo
+ response = request(uri, 'get') {|c| c.http_get }
+ end
+ end
+
+ cache_store.store uri, response.body, response.etag
+ response
end
def put(uri, json)
@@ -40,27 +119,42 @@ def post(uri, json)
def request(uri, method)
c = Curl::Easy.new "http://#{@host}:#{@port}#{uri}"
+ etag = status_line = nil
+
+ c.on_header do |header_string|
+ if !status_line
+ status_line = header_string
+ elsif header_string.strip =~ /etag:\s*(.*)/i
+ etag = $1
+ end
+ header_string.length
+ end
+
yield c
- if c.response_code < 200 || c.response_code >= 300
- status_line = c.header_str.split('\r\n').first
- msg = "#{c.response_code}:#{status_line}\nMETHOD:#{method}\nURI:#{uri}\n#{c.body_str}"
- begin
- klass = RelaxDB.const_get("HTTP_#{c.response_code}")
- e = klass.new(msg)
- rescue
- e = RuntimeError.new(msg)
- end
-
- raise e
+ if (c.response_code < 200 || c.response_code >= 300) && c.response_code != 304
+ handle_error c.response_code, status_line, method, uri, c.body_str
end
- Response.new c.body_str
+ Response.new c.response_code, c.body_str, etag
end
def to_s
"http://#{@host}:#{@port}/"
end
+
+ protected
+
+ def handle_error status_code, status_line, method, uri, body
+ msg = "#{status_code}:#{status_line}\nMETHOD:#{method}\nURI:#{uri}\n#{body}"
+ begin
+ klass = RelaxDB.const_get("HTTP_#{status_code}")
+ e = klass.new(msg)
+ rescue
+ e = RuntimeError.new(msg)
+ end
+ raise e
+ end
end
@@ -74,7 +168,7 @@ class CouchDB
def initialize(config)
@get_count, @post_count, @put_count = 0, 0, 0
- @server = RelaxDB::Server.new(config[:host], config[:port])
+ @server = RelaxDB::Server.new(config[:host], config[:port], config[:cache_store])
@logger = config[:logger] ? config[:logger] : Logger.new(Tempfile.new('couchdb.log'))
end
View
60 spec/memory_store_spec.rb
@@ -0,0 +1,60 @@
+require File.dirname(__FILE__) + '/spec_helper.rb'
+
+describe RelaxDB::MemoryStore do
+
+ describe "cache" do
+ it "should not store objects bigger than the maximum size" do
+ @store = RelaxDB::MemoryStore.new :maximum_entry_size => 30
+ @store.store 'some_key', 'x' * 30, 'tag'
+
+ @store.get('some_key').should_not be_nil
+
+ @store.store 'some_other_key', 'x' * 31, 'tag'
+ @store.get('some_other_key').should be_nil
+ end
+
+ it "should store and retrieve objects" do
+ @store = RelaxDB::MemoryStore.new
+ @store.store 'some_key', 'x' * 30, 'tag'
+ entry = @store.get 'some_key'
+ entry.data.should == 'x' * 30
+ entry.etag.should == 'tag'
+ end
+
+ it "should remove old items from the cache" do
+ @store = RelaxDB::MemoryStore.new :size => 2
+
+ @store.store 'first_key', 'data', 'tag'
+ @store.store 'second_key', 'data', 'tag'
+ @store.store 'third_key', 'data', 'tag'
+
+ @store.get('second_key').should_not be_nil
+ @store.get('third_key').should_not be_nil
+ @store.get('first_key').should be_nil
+ end
+
+ it "should not remove items from cache when storing an existing key" do
+ @store = RelaxDB::MemoryStore.new :size => 2
+ @store.store 'first_key', 'data', 'tag'
+ @store.store 'second_key', 'data', 'tag'
+ @store.store 'second_key', 'data2', 'tag'
+ @store.store 'second_key', 'data3', 'tag'
+
+ @store.get('second_key').should_not be_nil
+ @store.get('first_key').should_not be_nil
+ end
+
+ it "should push items to the front of the queue" do
+ @store = RelaxDB::MemoryStore.new :size => 2
+ @store.store 'first_key', 'data', 'tag'
+ @store.store 'second_key', 'data', 'tag'
+
+ @store.get('first_key')
+ @store.store 'third_key', 'data', 'tag'
+
+ @store.get('second_key').should be_nil
+ @store.get('third_key').should_not be_nil
+ @store.get('first_key').should_not be_nil
+ end
+ end
+end
View
28 spec/server_spec.rb
@@ -5,7 +5,7 @@
before(:all) do
RelaxDB.configure :host => "localhost", :port => 5984, :design_doc => "spec_doc"
- @server = RelaxDB::Server.new("localhost", 5984)
+ @server = RelaxDB::Server.new("localhost", 5984, RelaxDB::MemoryStore.new)
end
before(:each) do
@@ -27,6 +27,32 @@
end.should raise_error(RuntimeError)
end
+ it "should store results in the cache" do
+ a = Atom.new.save
+
+ response = @server.get "/relaxdb_spec/#{a._id}"
+
+ entry = @server.cache_store.get "/relaxdb_spec/#{a._id}"
+ entry.should_not be_nil
+ entry.etag.should == "\"#{a._rev}\""
+ entry.data.should == response.body
+ end
+
+ it "should fetch results from the cache" do
+ a = Atom.new.save
+ entry = @server.cache_store.store "/relaxdb_spec/#{a._id}", "dummy data", "\"#{a._rev}\""
+
+ response = @server.get "/relaxdb_spec/#{a._id}"
+ response.body.should == "dummy data"
+ end
+
+ it "should not get stale data from the cache" do
+ a = Atom.new.save
+ entry = @server.cache_store.store "/relaxdb_spec/#{a._id}", "dummy data", "\"#{a._rev}\""
+ a.save
+ response = @server.get "/relaxdb_spec/#{a._id}"
+ response.body.should_not == "dummy data"
+ end
end
end

0 comments on commit 1d9acfd

Please sign in to comment.
Something went wrong with that request. Please try again.