summaryrefslogtreecommitdiff
path: root/lib/metropolis/tc/hdb.rb
blob: 6ba6f85c4030eb42915baa3034f177af6e11203c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# -*- 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'
  autoload :EX, 'metropolis/tc/hdb/ex'

  TCHDB = TokyoCabinet::HDB # :nodoc
  include Metropolis::Common

  def setup(opts)
    super
    @rd_flags = TCHDB::OREADER
    @wr_flags = TCHDB::OWRITER

    @optimize = nil
    if @query
      case @query['rdlock']
      when 'true', nil
      when 'false'
        @rd_flags |= TCHDB::ONOLCK
      else
        raise ArgumentError, "'rdlock' must be 'true' or 'false'"
      end

      case @query['wrlock']
      when 'true', nil
      when 'false'
        @wr_flags |= TCHDB::ONOLCK
      else
        raise ArgumentError, "'wrlock' must be 'true' or 'false'"
      end

      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
    @nr_slots = 1 unless @path_pattern
    @dbv = (0...@nr_slots).to_a.map do |slot|
      path = @path_pattern ? sprintf(@path_pattern, slot) : @uri.path
      hdb = TCHDB.new
      unless @readonly
        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
    @multi_hash ||= :digest_sha1
    extend Metropolis::MultiHash
    extend(RO) if @readonly
    extend(EX) if @exclusive
  end

  def ex!(msg, hdb)
    raise "#{msg}: #{hdb.errmsg(hdb.ecode)}"
  end

  def writer(key, &block)
    hdb, path = @dbv[multi_hash(key) % @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[multi_hash(key) % @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 get(key, env)
    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