about summary refs log tree commit
path: root/lib/metropolis/tc/hdb.rb
diff options
context:
space:
mode:
authorEric Wong <normalperson@yhbt.net>2010-11-23 12:09:56 -0800
committerEric Wong <normalperson@yhbt.net>2010-11-23 12:09:56 -0800
commitdf91c57c312bee97a16bced1035bd704e518ac38 (patch)
tree0678c58830c09e9a8e0de92a33fa078f2445ee4e /lib/metropolis/tc/hdb.rb
parent51d43461adca99482154ddbd337658a70d5ffc3d (diff)
downloadmetropolis-df91c57c312bee97a16bced1035bd704e518ac38.tar.gz
rename internal "TokyoCabinet" => "TC"
It's less typing and less likely to clash on both
the eyes and the interpreter.
Diffstat (limited to 'lib/metropolis/tc/hdb.rb')
-rw-r--r--lib/metropolis/tc/hdb.rb160
1 files changed, 160 insertions, 0 deletions
diff --git a/lib/metropolis/tc/hdb.rb b/lib/metropolis/tc/hdb.rb
new file mode 100644
index 0000000..ec387b4
--- /dev/null
+++ b/lib/metropolis/tc/hdb.rb
@@ -0,0 +1,160 @@
+# -*- encoding: binary -*-
+
+# this module is NOT thread-safe, all performance is dependent on the
+# local machine so there is never anything that needs yielding to threads.
+module Metropolis::TC::HDB
+  autoload :RO, 'metropolis/tc/hdb/ro'
+
+  TCHDB = TokyoCabinet::HDB # :nodoc
+  include Rack::Utils # unescape
+
+  def r(code)
+    body = "#{HTTP_STATUS_CODES[code]}\n"
+    [ code,
+      { 'Content-Length' => body.size.to_s, 'Content-Type' => 'text/plain' },
+      [ body ] ]
+  end
+
+  def setup(opts)
+    @headers = { 'Content-Type' => 'application/octet-stream' }
+    @headers.merge!(opts[:response_headers] || {})
+    @nr_slots = opts[:nr_slots] || 3
+    path_pattern = opts[:path_pattern]
+    path_pattern.scan(/%\d*x/).size == 1 or
+      raise ArgumentError, "only one '/%\d*x/' may appear in #{path_pattern}"
+    @optimize = nil
+    if query = opts[:query]
+      flags = 0
+      @optimize = %w(bnum apow fpow).map do |x|
+        v = query[x]
+        v ? v.to_i : nil
+      end
+      case large = query['large']
+      when 'false', nil
+      when 'true'
+        flags |= TCHDB::TLARGE
+      else
+        raise ArgumentError, "invalid 'large' value: #{large}"
+      end
+      case compress = query['compress']
+      when nil
+      when 'deflate', 'bzip', 'tcbs'
+        flags |= TCHDB.const_get("T#{compress.upcase}")
+      else
+        raise ArgumentError, "invalid 'compress' value: #{compress}"
+      end
+      @optimize << flags
+    end
+    @dbv = (0...@nr_slots).to_a.map do |slot|
+      path = sprintf(path_pattern, slot)
+      hdb = TCHDB.new
+      unless opts[:read_only]
+        hdb.open(path, TCHDB::OWRITER | TCHDB::OCREAT) or ex!(:open, hdb)
+        if @optimize
+          hdb.optimize(*@optimize) or ex!(:optimize, hdb)
+        end
+        hdb.close or ex!(:close, hdb)
+      end
+      [ hdb, path ]
+    end
+    @rd_flags = TCHDB::OREADER
+    @wr_flags = TCHDB::OWRITER
+    if opts[:read_only]
+      extend(RO)
+    end
+  end
+
+  def call(env)
+    if %r{\A/(.*)\z} =~ env["PATH_INFO"]
+      key = unescape($1)
+      case env["REQUEST_METHOD"]
+      when "GET"
+        get(key)
+      when "HEAD"
+        head(key)
+      when "DELETE"
+        delete(key)
+      when "PUT"
+        put(key, env)
+      else
+        [ 405, {}, [] ]
+      end
+    else # OPTIONS
+      [ 405, {}, [] ]
+    end
+  end
+
+  def ex!(msg, hdb)
+    raise "#{msg}: #{hdb.errmsg(hdb.ecode)}"
+  end
+
+  def writer(key, &block)
+    hdb, path = @dbv[key.hash % @nr_slots]
+    hdb.open(path, @wr_flags) or ex!(:open, hdb)
+    yield hdb
+    ensure
+      hdb.close or ex!(:close, hdb)
+  end
+
+  def reader(key)
+    hdb, path = @dbv[key.hash % @nr_slots]
+    hdb.open(path, @rd_flags) or ex!(:open, hdb)
+    yield hdb
+    ensure
+      hdb.close or ex!(:close, hdb)
+  end
+
+  def put(key, env)
+    value = env["rack.input"].read
+    writer(key) do |hdb|
+      case env['HTTP_X_TT_PDMODE']
+      when '1'
+        unless hdb.putkeep(key, value)
+          TCHDB::EKEEP == hdb.ecode and return r(409)
+          ex!(:putkeep, hdb)
+        end
+      when '2'
+        hdb.putcat(key, value) or ex!(:putcat, hdb)
+      else
+        # ttserver does not care for other PDMODE values, so we don't, either
+        hdb.put(key, value) or ex!(:put, hdb)
+      end
+    end
+    r(201)
+  end
+
+  def delete(key)
+    writer(key) do |hdb|
+      unless hdb.delete(key)
+        TCHDB::ENOREC == hdb.ecode and return r(404)
+        ex!(:delete, hdb)
+      end
+    end
+    r(200)
+  end
+
+  def head(key)
+    size = reader(key) { |hdb| hdb.vsiz(key) or ex!(:vsiz, hdb) }
+    0 > size and return r(404)
+    [ 200, {
+        'Content-Length' => size.to_s,
+      }.merge!(@headers), [] ]
+  end
+
+  def get(key)
+    value = nil
+    reader(key) do |hdb|
+      unless value = hdb.get(key)
+        TCHDB::ENOREC == hdb.ecode and return r(404)
+        ex!(:get, hdb)
+      end
+    end
+    [ 200, {
+        'Content-Length' => value.size.to_s,
+      }.merge!(@headers), [ value ] ]
+  end
+
+  def close!
+    @dbv.each { |(hdb,_)| hdb.close }
+  end
+end