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.
- data/VERSION +1 -1
- data/examples/replication.rb +29 -0
- data/ext/Makefile +157 -0
- data/ext/bdb.bundle +0 -0
- data/ext/bdb.c +225 -12
- data/ext/bdb.o +0 -0
- data/ext/bdb_aux._c +433 -0
- data/ext/extconf.rb +3 -35
- data/ext/mkmf.log +4766 -0
- data/lib/bdb/base.rb +11 -3
- data/lib/bdb/database.rb +27 -4
- data/lib/bdb/environment.rb +21 -5
- data/lib/bdb/replication.rb +68 -0
- data/test/database_test.rb +18 -0
- data/test/database_test_helper.rb +37 -0
- data/test/deadlock_test.rb +125 -0
- data/test/replication_test.rb +47 -0
- data/test/test_helper.rb +8 -1
- metadata +17 -4
- data/test/simple_test.rb +0 -93
data/lib/bdb/base.rb
CHANGED
|
@@ -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(
|
|
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.
|
|
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)
|
data/lib/bdb/database.rb
CHANGED
|
@@ -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,
|
|
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,
|
|
35
|
-
primary_db.associate(transaction, index_db,
|
|
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
|
-
|
|
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)
|
data/lib/bdb/environment.rb
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
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
|
|
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
|