bdb 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,8 +19,12 @@ class Bdb::Base
19
19
  indexes[field] = opts
20
20
  end
21
21
 
22
+ def path
23
+ config[:path] || Dir.pwd
24
+ end
25
+
22
26
  def environment
23
- @environment ||= Bdb::Environment.new(config[:path], self)
27
+ @environment ||= Bdb::Environment.new(path, self)
24
28
  end
25
29
 
26
30
  def transaction(nested = true, &block)
@@ -32,9 +36,13 @@ class Bdb::Base
32
36
  end
33
37
 
34
38
  def checkpoint(opts = {})
35
- environment.synchronize(opts)
39
+ environment.checkpoint(opts)
36
40
  end
37
-
41
+
42
+ def master?
43
+ environment.master?
44
+ end
45
+
38
46
  private
39
47
 
40
48
  def get_field(field, value)
@@ -10,10 +10,11 @@ class Bdb::Database < Bdb::Base
10
10
  def db(index = nil)
11
11
  if @db.nil?
12
12
  @db = {}
13
+ open_flags = master? ? Bdb::DB_CREATE : Bdb::DB_RDONLY
13
14
  transaction(false) do
14
15
  primary_db = environment.env.db
15
16
  primary_db.pagesize = config[:page_size] if config[:page_size]
16
- primary_db.open(transaction, name, nil, Bdb::Db::BTREE, Bdb::DB_CREATE, 0)
17
+ primary_db.open(transaction, name, nil, Bdb::Db::BTREE, open_flags, 0)
17
18
  @db[:primary_key] = primary_db
18
19
 
19
20
  indexes.each do |field, opts|
@@ -31,13 +32,22 @@ class Bdb::Database < Bdb::Base
31
32
  index_db = environment.env.db
32
33
  index_db.flags = Bdb::DB_DUPSORT unless opts[:unique]
33
34
  index_db.pagesize = config[:page_size] if config[:page_size]
34
- index_db.open(transaction, "#{name}_by_#{field}", nil, Bdb::Db::BTREE, Bdb::DB_CREATE, 0)
35
- primary_db.associate(transaction, index_db, Bdb::DB_CREATE, index_callback)
35
+ index_db.open(transaction, "#{name}_by_#{field}", nil, Bdb::Db::BTREE, open_flags, 0)
36
+ primary_db.associate(transaction, index_db, open_flags, index_callback)
36
37
  @db[field] = index_db
37
38
  end
38
39
  end
39
40
  end
40
41
  @db[index || :primary_key]
42
+ rescue Bdb::DbError => e
43
+ # Retry if the database doesn't exist and we are a replication client.
44
+ if not master? and e.code == Errno::ENOENT::Errno
45
+ close
46
+ sleep 1
47
+ retry
48
+ else
49
+ raise(e)
50
+ end
41
51
  end
42
52
 
43
53
  def close
@@ -48,6 +58,10 @@ class Bdb::Database < Bdb::Base
48
58
  end
49
59
  end
50
60
 
61
+ def close_environment
62
+ environment.close
63
+ end
64
+
51
65
  def count(field, key)
52
66
  with_cursor(db(field)) do |cursor|
53
67
  k, v = cursor.get(Tuple.dump(key), nil, Bdb::DB_SET)
@@ -131,13 +145,18 @@ class Bdb::Database < Bdb::Base
131
145
  set.results
132
146
  end
133
147
 
148
+ def [](key)
149
+ get(key).first
150
+ end
151
+
134
152
  def set(key, value, opts = {})
135
153
  synchronize do
136
154
  key = Tuple.dump(key)
137
155
  value = Marshal.dump(value)
138
156
  flags = opts[:create] ? Bdb::DB_NOOVERWRITE : 0
139
157
  db.put(transaction, key, value, flags)
140
- end
158
+ value
159
+ end
141
160
  end
142
161
 
143
162
  def delete(key)
@@ -154,6 +173,10 @@ class Bdb::Database < Bdb::Base
154
173
  end
155
174
  end
156
175
 
176
+ def sync
177
+ db.sync
178
+ end
179
+
157
180
  private
158
181
 
159
182
  def get_key(key, opts)
@@ -1,12 +1,25 @@
1
+ require 'thread'
2
+ require 'bdb/replication'
3
+
1
4
  class Bdb::Environment
2
5
  @@env = {}
3
6
  def self.new(path, database = nil)
7
+ # Only allow one environment per path.
4
8
  path = File.expand_path(path)
5
9
  @@env[path] ||= super(path)
6
10
  @@env[path].databases << database if database
7
11
  @@env[path]
8
12
  end
9
13
 
14
+ def initialize(path)
15
+ @path = path
16
+ end
17
+ attr_reader :path
18
+
19
+ def self.[](path)
20
+ new(path)
21
+ end
22
+
10
23
  def self.config(config = {})
11
24
  @config ||= {
12
25
  :max_locks => 5000,
@@ -22,10 +35,10 @@ class Bdb::Environment
22
35
  @config.merge!(config)
23
36
  end
24
37
 
25
- def initialize(path)
26
- @path = path
38
+ include Replication
39
+ def self.replicate(path, opts)
40
+ self[path].replicate(opts)
27
41
  end
28
- attr_reader :path
29
42
 
30
43
  def databases
31
44
  @databases ||= []
@@ -40,16 +53,19 @@ class Bdb::Environment
40
53
  else
41
54
  env_flags = Bdb::DB_CREATE | Bdb::DB_INIT_TXN | Bdb::DB_INIT_LOCK |
42
55
  Bdb::DB_REGISTER | Bdb::DB_RECOVER | Bdb::DB_INIT_MPOOL | Bdb::DB_THREAD
43
- end
44
56
 
57
+ env_flags |= Bdb::DB_INIT_REP if replicate?
58
+ end
45
59
  @env.cachesize = config[:cache_size] if config[:cache_size]
46
60
  @env.set_timeout(config[:txn_timeout], Bdb::DB_SET_TXN_TIMEOUT) if config[:txn_timeout]
47
61
  @env.set_timeout(config[:lock_timeout], Bdb::DB_SET_LOCK_TIMEOUT) if config[:lock_timeout]
48
62
  @env.set_lk_max_locks(config[:max_locks]) if config[:max_locks]
49
63
  @env.set_lk_detect(Bdb::DB_LOCK_RANDOM)
50
64
  @env.flags_on = Bdb::DB_TXN_WRITE_NOSYNC | Bdb::DB_TIME_NOTGRANTED
51
- @env.open(path, env_flags, 0)
65
+ init_replication(@env) if replicate?
52
66
 
67
+ @env.open(path, env_flags, 0)
68
+ start_replication(@env) if replicate?
53
69
  @exit_handler ||= at_exit { close }
54
70
  end
55
71
  end
@@ -0,0 +1,68 @@
1
+ require 'socket'
2
+ module Replication
3
+ DEFAULT_PORT = 3463
4
+ NUM_THREADS = 1
5
+
6
+ ACK_POLICY = {
7
+ :all => Bdb::DB_REPMGR_ACKS_ALL,
8
+ :all_peers => Bdb::DB_REPMGR_ACKS_ALL_PEERS,
9
+ :none => Bdb::DB_REPMGR_ACKS_NONE,
10
+ :one => Bdb::DB_REPMGR_ACKS_ONE,
11
+ :one_peer => Bdb::DB_REPMGR_ACKS_ONE_PEER,
12
+ :quorom => Bdb::DB_REPMGR_ACKS_QUORUM,
13
+ }
14
+
15
+ def replicate?
16
+ not @replicate.nil?
17
+ end
18
+
19
+ def master?
20
+ not replicate? or replicate[:master]
21
+ end
22
+
23
+ def replicate(opts = nil)
24
+ return @replicate if opts.nil?
25
+
26
+ master = normalize_host(opts.delete(:from))
27
+ clients = [*opts.delete(:to)].compact.collect {|h| normalize_host(h)}
28
+ local = normalize_host(opts.delete(:host) || ENV['BDB_REPLICATION_HOST'], opts.delete(:port))
29
+ remote = clients + [master] - [local]
30
+
31
+ opts[:master] = (local == master)
32
+ opts[:local] = local
33
+ opts[:remote] = remote
34
+ opts[:num_threads] ||= NUM_THREADS
35
+ @replicate = opts
36
+
37
+ env
38
+ end
39
+
40
+ private
41
+
42
+ def init_replication(env)
43
+ env.set_verbose(Bdb::DB_VERB_REPLICATION, true) if replicate[:verbose]
44
+ env.rep_priority = replicate[:master] ? 1 : 0
45
+ env.repmgr_ack_policy = ACK_POLICY[replicate[:ack_policy]] if replicate[:ack_policy]
46
+ env.repmgr_set_local_site(*replicate[:local])
47
+ replicate[:remote].each do |s|
48
+ env.repmgr_add_remote_site(*s)
49
+ end
50
+ env.rep_nsites = replicate[:remote].size + 1
51
+ end
52
+
53
+ def start_replication(env)
54
+ env.repmgr_start(replicate[:num_threads], replicate[:master] ? Bdb::DB_REP_MASTER : Bdb::DB_REP_CLIENT)
55
+ end
56
+
57
+ def normalize_host(*host)
58
+ host = host.compact.join(':')
59
+ host, port = host.split(':')
60
+ host ||= Socket.gethostname
61
+ port ||= DEFAULT_PORT
62
+ port = port.to_i
63
+
64
+ addr_info = Socket.getaddrinfo(host.strip, port)
65
+ ip = addr_info.detect {|i| i[0] == 'AF_INET'}[3]
66
+ [ip, port]
67
+ end
68
+ end
@@ -0,0 +1,18 @@
1
+ require File.dirname(__FILE__) + '/database_test_helper'
2
+
3
+ TMPDIR = File.dirname(__FILE__) + '/tmp'
4
+ FileUtils.rmtree TMPDIR
5
+ FileUtils.mkdir TMPDIR
6
+
7
+ class DatabaseTest < Test::Unit::TestCase
8
+ include DatabaseTestHelper
9
+
10
+ def setup
11
+ @db = Bdb::Database.new('foo', :path => TMPDIR)
12
+ @db.truncate!
13
+ end
14
+
15
+ def teardown
16
+ @db.close
17
+ end
18
+ end
@@ -0,0 +1,37 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ module DatabaseTestHelper
4
+ def db
5
+ @db
6
+ end
7
+
8
+ def test_set_and_get
9
+ assert_equal nil, db['foo']
10
+ assert_equal [], db.get('foo')
11
+
12
+ db.set('foo', [1,2,3])
13
+ assert_equal [1,2,3], db['foo']
14
+ assert_equal [[1,2,3]], db.get('foo')
15
+ end
16
+
17
+ def test_set_and_get_with_tuples
18
+ assert_equal [], db.get(['foo', 1])
19
+ assert_equal [], db.get(['foo', 2])
20
+
21
+ db.set(['foo', 1], [1,2,3])
22
+ assert_equal [[1,2,3]], db.get(['foo', 1])
23
+
24
+ db.set(['foo', 2], [3,4,5])
25
+ assert_equal [[3,4,5]], db.get(['foo', 2])
26
+
27
+ assert_equal [[1,2,3], [3,4,5]], db.get('foo', :partial => true)
28
+ end
29
+
30
+ def test_get_with_ranges
31
+ 100.times do |i|
32
+ db.set([:foo, i], {:id => i, :type => 'foo'})
33
+ end
34
+
35
+ assert_equal (17..34).to_a, db.get([:foo, 17]..[:foo, 34]).collect {|r| r[:id]}
36
+ end
37
+ end
@@ -0,0 +1,125 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ DIR = '/tmp/bdb_deadlock_test'
4
+ Bdb::Environment.config :path => DIR, :cache_size => 1 * 1024 * 1024, :page_size => 512
5
+
6
+ class DeadlockTest < Test::Unit::TestCase
7
+ def setup
8
+ FileUtils.rmtree DIR
9
+ FileUtils.mkdir DIR
10
+ Bdb::Environment[DIR].close
11
+ @db = Bdb::Database.new('foo')
12
+ end
13
+
14
+ attr_reader :db
15
+
16
+ N = 5000 # total number of records
17
+ R = 10 # number of readers
18
+ W = 10 # number of writers
19
+ T = 20 # reads per transaction
20
+ L = 100 # logging frequency
21
+
22
+ def test_detect_deadlock
23
+ pids = []
24
+
25
+ W.times do |n|
26
+ pids << fork(&writer)
27
+ end
28
+
29
+ sleep(1)
30
+
31
+ R.times do
32
+ pids << fork(&reader)
33
+ end
34
+
35
+ # Make sure that all processes finish with no errors.
36
+ pids.each do |pid|
37
+ Process.wait(pid)
38
+ assert_equal status, $?.exitstatus
39
+ end
40
+ end
41
+
42
+ C = 10
43
+ def test_detect_unclosed_resources
44
+ threads = []
45
+
46
+ threads << Thread.new do
47
+ C.times do
48
+ sleep(10)
49
+
50
+ pid = fork do
51
+ cursor = db.db.cursor(nil, 0)
52
+ cursor.get(nil, nil, Bdb::DB_FIRST)
53
+ exit!(1)
54
+ end
55
+ puts "\n====simulating exit with unclosed resources ===="
56
+ Process.wait(pid)
57
+ assert_equal 1, $?.exitstatus
58
+ end
59
+ end
60
+
61
+ threads << Thread.new do
62
+ C.times do
63
+ pid = fork(&writer(1000))
64
+ Process.wait(pid)
65
+ assert [0,9].include?($?.exitstatus)
66
+ end
67
+ end
68
+
69
+ sleep(3)
70
+
71
+ threads << Thread.new do
72
+ C.times do
73
+ pid = fork(&reader(1000))
74
+ Process.wait(pid)
75
+ assert [0,9].include?($?.exitstatus)
76
+ end
77
+ end
78
+
79
+ threads.each {|t| t.join}
80
+ end
81
+
82
+ def reader(n = N)
83
+ lambda do
84
+ T.times do
85
+ (1...n).to_a.shuffle.each_slice(T) do |ids|
86
+ db.transaction do
87
+ ids.each {|id| db.get(id)}
88
+ end
89
+ log('r')
90
+ end
91
+ end
92
+ db.close_environment
93
+ end
94
+ end
95
+
96
+ def writer(n = N)
97
+ lambda do
98
+ (1...n).to_a.shuffle.each do |id|
99
+ db.transaction do
100
+ begin
101
+ db.set(id, {:bar => "bar" * 1000 + " ayn #{rand}"})
102
+ rescue Bdb::DbError => e
103
+ if e.code == Bdb::DB_KEYEXIST
104
+ db.delete(id)
105
+ retry
106
+ else
107
+ raise(e)
108
+ end
109
+ end
110
+ end
111
+ log('w')
112
+ end
113
+ db.close_environment
114
+ end
115
+ end
116
+
117
+ def log(action)
118
+ @count ||= Hash.new(0)
119
+ if @count[action] % L == 0
120
+ print action.to_s
121
+ $stdout.flush
122
+ end
123
+ @count[action] += 1
124
+ end
125
+ end
@@ -0,0 +1,47 @@
1
+ require File.dirname(__FILE__) + '/database_test_helper'
2
+
3
+ MASTER_DIR = '/tmp/bdb_rep_test_master'
4
+ CLIENT_DIR = '/tmp/bdb_rep_test_client'
5
+ FileUtils.rmtree MASTER_DIR; FileUtils.mkdir MASTER_DIR;
6
+ FileUtils.rmtree CLIENT_DIR; FileUtils.mkdir CLIENT_DIR
7
+
8
+ MASTER = 'localhost:8888'
9
+ CLIENT = 'localhost:8889'
10
+
11
+ N = 100
12
+
13
+ class ReplicationTest < Test::Unit::TestCase
14
+ def setup
15
+ Bdb::Environment.replicate CLIENT_DIR, :from => MASTER, :to => CLIENT, :host => CLIENT #, :verbose => true
16
+
17
+ @pid = Process.fork do
18
+ Bdb::Environment.replicate MASTER_DIR, :from => MASTER, :to => CLIENT, :host => MASTER #, :verbose => true
19
+ db = Bdb::Database.new('foo', :path => MASTER_DIR)
20
+
21
+ N.times do |i|
22
+ db.set(i, i + 1)
23
+ log "m"
24
+ sleep 0.1
25
+ end
26
+ sleep 10
27
+ end
28
+ end
29
+
30
+ def teardown
31
+ Process.kill(9, @pid)
32
+ end
33
+
34
+ def test_single_process_replication
35
+ db = Bdb::Database.new('foo', :path => CLIENT_DIR)
36
+ N.times do |i|
37
+ sleep 0.2
38
+ assert_equal i + 1, db[i]
39
+ log "c"
40
+ end
41
+ end
42
+
43
+ def log(action)
44
+ print action
45
+ $stdout.flush
46
+ end
47
+ end