<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array">
    <added>
      <filename>lib/relaxdb/memcache_store.rb</filename>
    </added>
    <added>
      <filename>spec/memory_store_spec.rb</filename>
    </added>
  </added>
  <modified type="array">
    <modified>
      <diff>@@ -4,16 +4,77 @@ module RelaxDB
   class HTTP_409 &lt; StandardError; end
   class HTTP_412 &lt; 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 &amp;&amp; entry.etag
+    end
+    
+    def get_data(cache_key)
+      entry = get(cache_key)
+      entry &amp;&amp; entry.data
+    end
+    
+    def store(cache_key, data, etag)
+      if @maximum_entry_size.nil? || data.length &lt;= @maximum_entry_size
+        if @cache.size &gt;= @size &amp;&amp; !@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 &lt;&lt; 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)
@@ -21,7 +82,25 @@ module RelaxDB
     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 &amp;&amp; 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 @@ module RelaxDB
 
     def request(uri, method)
       c = Curl::Easy.new &quot;http://#{@host}:#{@port}#{uri}&quot;
+      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 &lt; 200 || c.response_code &gt;= 300
-        status_line = c.header_str.split('\r\n').first
-        msg = &quot;#{c.response_code}:#{status_line}\nMETHOD:#{method}\nURI:#{uri}\n#{c.body_str}&quot;
-        begin
-          klass = RelaxDB.const_get(&quot;HTTP_#{c.response_code}&quot;)
-          e = klass.new(msg)
-        rescue
-          e = RuntimeError.new(msg)
-        end
-
-        raise e
+      if (c.response_code &lt; 200 || c.response_code &gt;= 300) &amp;&amp; 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
       &quot;http://#{@host}:#{@port}/&quot;
     end
+    
+    protected
+    
+    def handle_error status_code, status_line, method, uri, body
+      msg = &quot;#{status_code}:#{status_line}\nMETHOD:#{method}\nURI:#{uri}\n#{body}&quot;
+      begin
+        klass = RelaxDB.const_get(&quot;HTTP_#{status_code}&quot;)
+        e = klass.new(msg)
+      rescue
+        e = RuntimeError.new(msg)
+      end
 
+      raise e
+    end
   end
   
       
@@ -74,7 +168,7 @@ module RelaxDB
         
     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
     </diff>
      <filename>lib/relaxdb/server.rb</filename>
    </modified>
    <modified>
      <diff>@@ -5,7 +5,7 @@ describe RelaxDB do
 
   before(:all) do
     RelaxDB.configure :host =&gt; &quot;localhost&quot;, :port =&gt; 5984, :design_doc =&gt; &quot;spec_doc&quot;
-    @server = RelaxDB::Server.new(&quot;localhost&quot;, 5984)
+    @server = RelaxDB::Server.new(&quot;localhost&quot;, 5984, RelaxDB::MemoryStore.new)
   end
 
   before(:each) do
@@ -27,6 +27,32 @@ describe RelaxDB do
       end.should raise_error(RuntimeError)
     end
     
+    it &quot;should store results in the cache&quot; do
+      a = Atom.new.save
+
+      response = @server.get &quot;/relaxdb_spec/#{a._id}&quot;
+      
+      entry = @server.cache_store.get &quot;/relaxdb_spec/#{a._id}&quot;
+      entry.should_not be_nil
+      entry.etag.should == &quot;\&quot;#{a._rev}\&quot;&quot;
+      entry.data.should == response.body
+    end
+    
+    it &quot;should fetch results from the cache&quot; do
+      a = Atom.new.save
+      entry = @server.cache_store.store &quot;/relaxdb_spec/#{a._id}&quot;, &quot;dummy data&quot;, &quot;\&quot;#{a._rev}\&quot;&quot;
+      
+      response = @server.get &quot;/relaxdb_spec/#{a._id}&quot;
+      response.body.should == &quot;dummy data&quot;
+    end
+    
+    it &quot;should not get stale data from the cache&quot; do
+      a = Atom.new.save
+      entry = @server.cache_store.store &quot;/relaxdb_spec/#{a._id}&quot;, &quot;dummy data&quot;, &quot;\&quot;#{a._rev}\&quot;&quot;
+      a.save
+      response = @server.get &quot;/relaxdb_spec/#{a._id}&quot;
+      response.body.should_not == &quot;dummy data&quot;
+    end
   end
   
 end</diff>
      <filename>spec/server_spec.rb</filename>
    </modified>
  </modified>
  <removed type="array"/>
  <parents type="array">
    <parent>
      <id>bf79e221bcf3b9c1cdf0a7ea99937968668d0053</id>
    </parent>
  </parents>
  <author>
    <name>Frederick Cheung</name>
    <email>frederick.cheung@gmail.com</email>
  </author>
  <url>http://github.com/fcheung/relaxdb/commit/1d9acfd5f6b3c23da0d275252b6a6e064865440e</url>
  <id>1d9acfd5f6b3c23da0d275252b6a6e064865440e</id>
  <committed-date>2009-04-14T04:04:07-07:00</committed-date>
  <authored-date>2009-04-13T15:44:07-07:00</authored-date>
  <message>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 =&gt; RelaxDB::MemoryStore.new(:size =&gt; 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 =&gt; 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</message>
  <tree>df77ce5cf3162e665118fb9d2551bf406ea0d482</tree>
  <committer>
    <name>Frederick Cheung</name>
    <email>frederick.cheung@gmail.com</email>
  </committer>
</commit>
