about summary refs log tree commit
diff options
context:
space:
mode:
authorEric Wong <normalperson@yhbt.net>2010-11-23 18:52:08 -0800
committerEric Wong <normalperson@yhbt.net>2010-11-23 18:52:08 -0800
commit0f3c1c14630fda58363ffd7d3a942041ca2419eb (patch)
treedf1340b1702ac2d8a7b234a1c6e426445fc5d1a1
parent2bb7406db018e6902aacaf495e63d69cf9b93174 (diff)
downloadmetropolis-0f3c1c14630fda58363ffd7d3a942041ca2419eb.tar.gz
add plain Hash database support
Useful as a proof-of-concept and for benchmark base.
-rw-r--r--lib/metropolis.rb4
-rw-r--r--lib/metropolis/common.rb8
-rw-r--r--lib/metropolis/hash.rb75
-rw-r--r--test/test_hash.rb40
4 files changed, 127 insertions, 0 deletions
diff --git a/lib/metropolis.rb b/lib/metropolis.rb
index 1bae3d3..a7cfc47 100644
--- a/lib/metropolis.rb
+++ b/lib/metropolis.rb
@@ -4,12 +4,16 @@ require 'uri'
 
 module Metropolis
   autoload :TC, 'metropolis/tc'
+  autoload :Hash, 'metropolis/hash'
 
   def self.new(opts = {})
     opts = opts.dup
     rv = Object.new
     uri = opts[:uri] = URI.parse(opts[:uri])
     case uri.scheme
+    when 'hash'
+      opts[:path] = uri.path if uri.path != '/'
+      rv.extend Metropolis::Hash
     when 'tc'
       opts[:path_pattern] = uri.path
       opts[:query] = Rack::Utils.parse_query(uri.query) if uri.query
diff --git a/lib/metropolis/common.rb b/lib/metropolis/common.rb
index 738a511..13a02d8 100644
--- a/lib/metropolis/common.rb
+++ b/lib/metropolis/common.rb
@@ -37,4 +37,12 @@ module Metropolis::Common
       r(405)
     end
   end
+
+  # generic HEAD implementation, some databases can optimize this by
+  # not retrieving the value
+  def head(key)
+    r = get(key)
+    r[2].clear
+    r
+  end
 end
diff --git a/lib/metropolis/hash.rb b/lib/metropolis/hash.rb
new file mode 100644
index 0000000..698aef0
--- /dev/null
+++ b/lib/metropolis/hash.rb
@@ -0,0 +1,75 @@
+# -*- encoding: binary -*-
+require 'tempfile'
+
+# use a Ruby hash as a plain data store
+# It can unmarshal a hash from disk
+module Metropolis::Hash
+  include Metropolis::Common
+
+  def setup(opts)
+    super
+    if @path = opts[:path]
+      begin
+        @db = Marshal.load(File.open(@path, "rb") { |fp| fp.read })
+        Hash === @db or raise ArgumentError, "#@path is not a marshaled Hash"
+      rescue Errno::ENOENT
+        @db = {}
+      end
+    else
+      @db = {}
+    end
+    if @readonly
+      extend Metropolis::Common::RO
+    else
+      args = [ @db, @path, !!opts[:fsync] ]
+      @clean_proc = Metropolis::Hash.finalizer_callback(args)
+      ObjectSpace.define_finalizer(self, @clean_proc)
+    end
+  end
+
+  def close!
+    unless @readonly
+      @clean_proc.call
+      ObjectSpace.undefine_finalizer(self)
+    end
+    @db = @path = nil
+  end
+
+  def get(key)
+    value = @db[key] or return r(404)
+    [ 200, { 'Content-Length' => value.size.to_s }.merge!(@headers), [ value ] ]
+  end
+
+  def put(key, env)
+    value = env["rack.input"].read
+    case env['HTTP_X_TT_PDMODE']
+    when '1'
+      @db.exists?(key) and r(409)
+      @db[key] = value
+    when '2'
+      (tmp = @db[key] ||= "") << value
+    else
+      @db[key] = value
+    end
+    r(201)
+  end
+
+  def delete(key)
+    r(@db.delete(key) ? 200 : 404)
+  end
+
+  def self.finalizer_callback(data)
+    lambda {
+      db, path, fsync = data
+      dir = File.dirname(path)
+      tmp = Tempfile.new('hash_save', dir)
+      tmp.binmode
+      tmp.sync = true
+      tmp.write(Marshal.dump(db))
+      tmp.fsync if fsync
+      File.rename(tmp.path, path)
+      File.open(dir) { |d| d.fsync } if fsync
+      tmp.close!
+    }
+  end
+end
diff --git a/test/test_hash.rb b/test/test_hash.rb
new file mode 100644
index 0000000..cf11e82
--- /dev/null
+++ b/test/test_hash.rb
@@ -0,0 +1,40 @@
+# -*- encoding: binary -*-
+require './test/rack_read_write.rb'
+$-w = true
+require 'metropolis'
+
+class Test_Hash < Test::Unit::TestCase
+  attr_reader :tmp, :o, :uri
+  include TestRackReadWrite
+
+  def setup
+    @tmp = Tempfile.new('hash')
+    File.unlink(@tmp)
+    @uri = "hash://#{@tmp.path}"
+  end
+
+  def teardown
+    @tmp.close!
+  end
+
+  def test_marshalled
+    File.open(@tmp, "wb") { |fp| fp.write(Marshal.dump({"x" => "y"})) }
+    app = Metropolis.new(:uri => @uri, :readonly => true)
+    o = { :lint => true, :fatal => true }
+    req = Rack::MockRequest.new(app)
+
+    r = req.put("/x", o.merge(:input=>"ASDF"))
+    assert_equal 403, r.status
+
+    r = req.get("/x")
+    assert_equal 200, r.status
+    assert_equal "y", r.body
+
+    r = req.request("HEAD", "/x", {})
+    assert_equal 200, r.status
+    assert_equal "", r.body
+
+    r = req.delete("/x", {})
+    assert_equal 403, r.status
+  end
+end