lmdb 0.7.5 → 0.8.1
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.
- checksums.yaml +4 -4
- data/CHANGES +22 -0
- data/ext/lmdb_ext/extconf.rb +2 -0
- data/ext/lmdb_ext/lmdb_ext.c +1379 -912
- data/ext/lmdb_ext/lmdb_ext.h +36 -19
- data/lib/lmdb/database.rb +50 -52
- data/lib/lmdb/version.rb +1 -1
- data/spec/gc_torture_spec.rb +162 -0
- data/spec/helper.rb +3 -3
- data/spec/lmdb_spec.rb +154 -137
- data/spec/pseudo_transactions_spec.rb +237 -0
- metadata +6 -2
data/ext/lmdb_ext/lmdb_ext.h
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#ifndef _LMDB_EXT_H
|
|
2
2
|
#define _LMDB_EXT_H
|
|
3
3
|
|
|
4
|
+
#include <errno.h>
|
|
4
5
|
#include "ruby.h"
|
|
5
6
|
#include "lmdb.h"
|
|
6
7
|
|
|
@@ -29,24 +30,28 @@
|
|
|
29
30
|
# define RARRAY_AREF(ary,n) (RARRAY_PTR(ary)[n])
|
|
30
31
|
#endif
|
|
31
32
|
|
|
32
|
-
#define ENVIRONMENT(var, var_env)
|
|
33
|
-
Environment* var_env;
|
|
34
|
-
|
|
33
|
+
#define ENVIRONMENT(var, type, var_env) \
|
|
34
|
+
Environment* var_env; \
|
|
35
|
+
TypedData_Get_Struct(var, Environment, type, var_env); \
|
|
35
36
|
environment_check(var_env)
|
|
36
37
|
|
|
37
|
-
#define DATABASE(var, var_db)
|
|
38
|
-
Database* var_db;
|
|
39
|
-
|
|
38
|
+
#define DATABASE(var, type, var_db) \
|
|
39
|
+
Database* var_db; \
|
|
40
|
+
TypedData_Get_Struct(var, Database, type, var_db)
|
|
40
41
|
|
|
41
|
-
#define TRANSACTION(var, var_txn)
|
|
42
|
+
#define TRANSACTION(var, type, var_txn) \
|
|
42
43
|
Transaction* var_txn; \
|
|
43
|
-
|
|
44
|
+
TypedData_Get_Struct(var, Transaction, type, var_txn)
|
|
44
45
|
|
|
45
|
-
#define CURSOR(var, var_cur)
|
|
46
|
-
Cursor* var_cur;
|
|
47
|
-
|
|
46
|
+
#define CURSOR(var, type, var_cur) \
|
|
47
|
+
Cursor* var_cur; \
|
|
48
|
+
TypedData_Get_Struct(var, Cursor, type, var_cur); \
|
|
48
49
|
cursor_check(var_cur)
|
|
49
50
|
|
|
51
|
+
#define CURSOR_NOCHECK(var, type, var_cur) \
|
|
52
|
+
Cursor* var_cur; \
|
|
53
|
+
TypedData_Get_Struct(var, Cursor, type, var_cur)
|
|
54
|
+
|
|
50
55
|
/*
|
|
51
56
|
hey yo if you can convince hyc to add a function like
|
|
52
57
|
mdb_txn_get_flags or even mdb_txn_is_rdonly, you could probably get
|
|
@@ -92,7 +97,7 @@ typedef struct {
|
|
|
92
97
|
VALUE self;
|
|
93
98
|
const char* name;
|
|
94
99
|
int argc;
|
|
95
|
-
|
|
100
|
+
VALUE* argv;
|
|
96
101
|
} HelperArgs;
|
|
97
102
|
|
|
98
103
|
typedef struct {
|
|
@@ -118,6 +123,17 @@ static VALUE cEnvironment, cDatabase, cTransaction, cCursor, cError;
|
|
|
118
123
|
#include "errors.h"
|
|
119
124
|
#undef ERROR
|
|
120
125
|
|
|
126
|
+
#define GC_MARK(val) \
|
|
127
|
+
do { if ((val) && !NIL_P(val)) rb_gc_mark_movable(val); } while(0)
|
|
128
|
+
|
|
129
|
+
#ifdef HAVE_RB_GC_MARK_MOVABLE
|
|
130
|
+
#define GC_MARK_MOVABLE(val) \
|
|
131
|
+
do { if ((val) && !NIL_P(val)) rb_gc_mark_movable(val); } while(0)
|
|
132
|
+
|
|
133
|
+
#define GC_LOCATION(val) \
|
|
134
|
+
do { if ((val) && !NIL_P(val)) (val) = rb_gc_location(val); } while(0)
|
|
135
|
+
#endif
|
|
136
|
+
|
|
121
137
|
// BEGIN PROTOTYPES
|
|
122
138
|
void Init_lmdb_ext();
|
|
123
139
|
static MDB_txn* active_txn(VALUE self);
|
|
@@ -129,10 +145,10 @@ static VALUE cursor_close(VALUE self);
|
|
|
129
145
|
static VALUE cursor_count(VALUE self);
|
|
130
146
|
static VALUE cursor_delete(int argc, VALUE *argv, VALUE self);
|
|
131
147
|
static VALUE cursor_first(VALUE self);
|
|
132
|
-
static void cursor_free(Cursor* cursor);
|
|
133
148
|
static VALUE cursor_get(VALUE self);
|
|
134
149
|
static VALUE cursor_last(VALUE self);
|
|
135
|
-
static void cursor_mark(
|
|
150
|
+
static void cursor_mark(void* ptr);
|
|
151
|
+
static void cursor_free(void* ptr);
|
|
136
152
|
static VALUE cursor_next(int argc, VALUE* argv, VALUE self);
|
|
137
153
|
static VALUE cursor_prev(VALUE self);
|
|
138
154
|
static VALUE cursor_put(int argc, VALUE* argv, VALUE self);
|
|
@@ -143,7 +159,8 @@ static VALUE database_cursor(VALUE self);
|
|
|
143
159
|
static VALUE database_delete(int argc, VALUE *argv, VALUE self);
|
|
144
160
|
static VALUE database_drop(VALUE self);
|
|
145
161
|
static VALUE database_get(VALUE self, VALUE vkey);
|
|
146
|
-
static void database_mark(
|
|
162
|
+
static void database_mark(void* ptr);
|
|
163
|
+
static void database_free(void* ptr);
|
|
147
164
|
static VALUE database_put(int argc, VALUE *argv, VALUE self);
|
|
148
165
|
static VALUE database_stat(VALUE self);
|
|
149
166
|
static VALUE database_get_flags(VALUE self);
|
|
@@ -158,9 +175,9 @@ static VALUE environment_copy(VALUE self, VALUE path);
|
|
|
158
175
|
static VALUE environment_database(int argc, VALUE *argv, VALUE self);
|
|
159
176
|
static VALUE environment_databases(VALUE self);
|
|
160
177
|
static VALUE environment_flags(VALUE self);
|
|
161
|
-
static void
|
|
178
|
+
static void environment_mark(void* ptr);
|
|
179
|
+
static void environment_free(void* ptr);
|
|
162
180
|
static VALUE environment_info(VALUE self);
|
|
163
|
-
static void environment_mark(Environment* environment);
|
|
164
181
|
static VALUE environment_new(int argc, VALUE *argv, VALUE klass);
|
|
165
182
|
static int environment_options(VALUE key, VALUE value, EnvironmentOptions* options);
|
|
166
183
|
static VALUE environment_path(VALUE self);
|
|
@@ -174,8 +191,8 @@ static VALUE stat2hash(const MDB_stat* stat);
|
|
|
174
191
|
static VALUE transaction_abort(VALUE self);
|
|
175
192
|
static VALUE transaction_commit(VALUE self);
|
|
176
193
|
static void transaction_finish(VALUE self, int commit);
|
|
177
|
-
static void
|
|
178
|
-
static void
|
|
194
|
+
static void transaction_mark(void* ptr);
|
|
195
|
+
static void transaction_free(void* ptr);
|
|
179
196
|
static VALUE with_transaction(VALUE venv, VALUE(*fn)(VALUE), VALUE arg, int flags);
|
|
180
197
|
// END PROTOTYPES
|
|
181
198
|
|
data/lib/lmdb/database.rb
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
module LMDB
|
|
2
|
+
class Environment
|
|
3
|
+
|
|
4
|
+
# Start a new transaction if there isn't already one going.
|
|
5
|
+
#
|
|
6
|
+
# @param readonly [false, true]
|
|
7
|
+
# @param block [Proc] the block to run
|
|
8
|
+
# @yieldparam [LMDB::Transaction] the transaction handle
|
|
9
|
+
# @yieldreturn [Object] your pick
|
|
10
|
+
# @return [Object] whatever the block returns
|
|
11
|
+
#
|
|
12
|
+
def transaction? readonly = false, &block
|
|
13
|
+
raise ArgumentError, 'no block for conditional transaction' unless block
|
|
14
|
+
|
|
15
|
+
t = active_txn
|
|
16
|
+
t ? block.call(t) : transaction(!!readonly, &block)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
2
20
|
class Database
|
|
3
21
|
include Enumerable
|
|
4
22
|
|
|
@@ -10,9 +28,8 @@ module LMDB
|
|
|
10
28
|
# key, value = record
|
|
11
29
|
# puts "at #{key}: #{value}"
|
|
12
30
|
# end
|
|
13
|
-
def each
|
|
14
|
-
|
|
15
|
-
# env.transaction do
|
|
31
|
+
def each &block
|
|
32
|
+
env.transaction? true do
|
|
16
33
|
cursor do |c|
|
|
17
34
|
while i = c.next
|
|
18
35
|
yield(i)
|
|
@@ -55,12 +72,12 @@ module LMDB
|
|
|
55
72
|
# @yield key [String] the next key in the database.
|
|
56
73
|
# @return [Enumerator] in lieu of a block.
|
|
57
74
|
def each_key(&block)
|
|
58
|
-
return enum_for :each_key unless
|
|
59
|
-
|
|
60
|
-
|
|
75
|
+
return enum_for :each_key unless block
|
|
76
|
+
|
|
77
|
+
env.transaction? true do
|
|
61
78
|
cursor do |c|
|
|
62
79
|
while (rec = c.next true)
|
|
63
|
-
|
|
80
|
+
block.call rec.first
|
|
64
81
|
end
|
|
65
82
|
end
|
|
66
83
|
end
|
|
@@ -73,24 +90,25 @@ module LMDB
|
|
|
73
90
|
# @yield value [String] the next value associated with the key.
|
|
74
91
|
# @return [Enumerator] in lieu of a block.
|
|
75
92
|
def each_value(key, &block)
|
|
76
|
-
return enum_for :each_value, key unless
|
|
93
|
+
return enum_for :each_value, key unless block
|
|
77
94
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
95
|
+
op = -> txn do
|
|
96
|
+
value = get(key) or return
|
|
97
|
+
unless dupsort?
|
|
98
|
+
block.call value
|
|
99
|
+
return
|
|
100
|
+
end
|
|
83
101
|
|
|
84
|
-
maybe_txn true do
|
|
85
|
-
# env.transaction do
|
|
86
102
|
cursor do |c|
|
|
87
103
|
rec = c.set key
|
|
88
104
|
while rec
|
|
89
|
-
|
|
105
|
+
block.call rec.last
|
|
90
106
|
rec = c.next_range key
|
|
91
107
|
end
|
|
92
108
|
end
|
|
93
109
|
end
|
|
110
|
+
|
|
111
|
+
env.transaction? true, &op
|
|
94
112
|
end
|
|
95
113
|
|
|
96
114
|
# Return the cardinality (number of duplicates) of a given
|
|
@@ -99,8 +117,7 @@ module LMDB
|
|
|
99
117
|
# @return [Integer] The number of entries under the key.
|
|
100
118
|
def cardinality(key)
|
|
101
119
|
ret = 0
|
|
102
|
-
|
|
103
|
-
# env.transaction do
|
|
120
|
+
env.transaction? true do
|
|
104
121
|
if get key
|
|
105
122
|
if dupsort?
|
|
106
123
|
cursor do |c|
|
|
@@ -118,18 +135,20 @@ module LMDB
|
|
|
118
135
|
# Test if the database has a given key (or, if opened in
|
|
119
136
|
# +:dupsort+, value)
|
|
120
137
|
def has?(key, value = nil)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
env.transaction(true) { cursor { |c| ret = !!c.set(key, value) } }
|
|
138
|
+
env.transaction? true do
|
|
139
|
+
if v = get(key)
|
|
140
|
+
if value.nil? or value.to_s == v
|
|
141
|
+
true
|
|
142
|
+
elsif !dupsort?
|
|
143
|
+
false
|
|
144
|
+
else
|
|
145
|
+
ret = false
|
|
146
|
+
cursor { |c| ret = !!c.set(key, value) }
|
|
131
147
|
|
|
132
|
-
|
|
148
|
+
ret
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
133
152
|
end
|
|
134
153
|
|
|
135
154
|
# Conditionally put a value into the database.
|
|
@@ -148,7 +167,7 @@ module LMDB
|
|
|
148
167
|
|
|
149
168
|
flags = { (dupsort? ? :nodupdata : :nooverwrite) => true }
|
|
150
169
|
|
|
151
|
-
env.transaction do |txn|
|
|
170
|
+
env.transaction? do |txn|
|
|
152
171
|
put(key, value, **options.merge(flags)) unless has?(key, value)
|
|
153
172
|
end
|
|
154
173
|
end
|
|
@@ -164,7 +183,7 @@ module LMDB
|
|
|
164
183
|
# @return [void]
|
|
165
184
|
#
|
|
166
185
|
def delete?(key, value = nil)
|
|
167
|
-
env.transaction { |txn| delete(key, value) if has?(key, value) }
|
|
186
|
+
env.transaction? { |txn| delete(key, value) if has?(key, value) }
|
|
168
187
|
end
|
|
169
188
|
|
|
170
189
|
# Return how many records there are in this database.
|
|
@@ -179,26 +198,5 @@ module LMDB
|
|
|
179
198
|
def empty?
|
|
180
199
|
stat[:entries] == 0
|
|
181
200
|
end
|
|
182
|
-
|
|
183
|
-
private
|
|
184
|
-
|
|
185
|
-
# having trouble with read-only transactions embedded in
|
|
186
|
-
# read-write for some reason; can't pin it down to test it yet so
|
|
187
|
-
# going to do this (djt; 2020-02-10)
|
|
188
|
-
#
|
|
189
|
-
# 2025-11-14: this miiiigght no longer be necessary?? like as of
|
|
190
|
-
# whenever i implemented that short-circuiting code
|
|
191
|
-
#
|
|
192
|
-
def maybe_txn(readonly, &block)
|
|
193
|
-
if t = env.active_txn
|
|
194
|
-
# warn "reusing #{t.readonly? ? 'read-only ' : ''}txn #{t.inspect}"
|
|
195
|
-
yield t
|
|
196
|
-
else
|
|
197
|
-
env.transaction !!readonly do |t|
|
|
198
|
-
# warn "new #{t.readonly? ? 'read-only ' : ''}txn #{t.inspect}"
|
|
199
|
-
yield t
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
end
|
|
203
201
|
end
|
|
204
202
|
end
|
data/lib/lmdb/version.rb
CHANGED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
require 'helper'
|
|
2
|
+
require 'lmdb'
|
|
3
|
+
require 'tmpdir'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
RSpec.describe 'LMDB GC safety' do
|
|
7
|
+
let(:env) { LMDB.new(path, mapsize: 2**24) }
|
|
8
|
+
let(:db) { env.database('test', create: true) }
|
|
9
|
+
|
|
10
|
+
before(:each) { db } # open db before each example
|
|
11
|
+
|
|
12
|
+
after(:each) do
|
|
13
|
+
env.close rescue nil
|
|
14
|
+
FileUtils.rm_rf path
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# -----------------------------------------------------------------------
|
|
18
|
+
# 1. Basic GC.compact safety — no open transactions
|
|
19
|
+
# Verifies compaction callbacks update all VALUE references correctly
|
|
20
|
+
# -----------------------------------------------------------------------
|
|
21
|
+
it 'survives GC.compact with no open transactions' do
|
|
22
|
+
env.transaction { db['key'] = 'value' }
|
|
23
|
+
expect { GC.compact }.not_to raise_error
|
|
24
|
+
env.transaction(true) { expect(db['key']).to eq 'value' }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# -----------------------------------------------------------------------
|
|
28
|
+
# 2. GC.compact during an open read transaction
|
|
29
|
+
# Verifies the transaction object survives compaction
|
|
30
|
+
# -----------------------------------------------------------------------
|
|
31
|
+
it 'survives GC.compact with an open read transaction' do
|
|
32
|
+
env.transaction { db['key'] = 'value' }
|
|
33
|
+
env.transaction(true) do |txn|
|
|
34
|
+
expect { GC.compact }.not_to raise_error
|
|
35
|
+
expect(db['key']).to eq 'value'
|
|
36
|
+
expect(txn.finished?).to be false
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# -----------------------------------------------------------------------
|
|
41
|
+
# 3. GC.compact between consecutive write transactions
|
|
42
|
+
# This is the Store::Digest scenario — write, compact, write again
|
|
43
|
+
# -----------------------------------------------------------------------
|
|
44
|
+
it 'allows a new write transaction after GC.compact' do
|
|
45
|
+
env.transaction { db['key1'] = 'value1' }
|
|
46
|
+
GC.compact
|
|
47
|
+
expect {
|
|
48
|
+
env.transaction { db['key2'] = 'value2' }
|
|
49
|
+
}.not_to raise_error
|
|
50
|
+
env.transaction(true) do
|
|
51
|
+
expect(db['key1']).to eq 'value1'
|
|
52
|
+
expect(db['key2']).to eq 'value2'
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# -----------------------------------------------------------------------
|
|
57
|
+
# 4. GC.compact after multiple write transactions
|
|
58
|
+
# Stress version — compact after several writes
|
|
59
|
+
# -----------------------------------------------------------------------
|
|
60
|
+
it 'allows write transactions after repeated GC.compact calls' do
|
|
61
|
+
5.times do |i|
|
|
62
|
+
env.transaction { db["key#{i}"] = "value#{i}" }
|
|
63
|
+
GC.compact
|
|
64
|
+
end
|
|
65
|
+
env.transaction(true) do
|
|
66
|
+
5.times do |i|
|
|
67
|
+
expect(db["key#{i}"]).to eq "value#{i}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# -----------------------------------------------------------------------
|
|
73
|
+
# 5. GC.compact with a pseudo-transaction (RO nested in RW)
|
|
74
|
+
# Verifies pseudo-transaction compaction safety
|
|
75
|
+
# -----------------------------------------------------------------------
|
|
76
|
+
it 'survives GC.compact after pseudo-transaction use' do
|
|
77
|
+
env.transaction do
|
|
78
|
+
db['key'] = 'value'
|
|
79
|
+
env.transaction(true) do
|
|
80
|
+
expect(db['key']).to eq 'value'
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
GC.compact
|
|
84
|
+
expect {
|
|
85
|
+
env.transaction { db['key2'] = 'value2' }
|
|
86
|
+
}.not_to raise_error
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# -----------------------------------------------------------------------
|
|
90
|
+
# 6. GC.compact with cursor use
|
|
91
|
+
# Verifies cursor compaction safety
|
|
92
|
+
# -----------------------------------------------------------------------
|
|
93
|
+
it 'survives GC.compact after cursor iteration' do
|
|
94
|
+
env.transaction do
|
|
95
|
+
5.times { |i| db["key#{i}"] = "value#{i}" }
|
|
96
|
+
end
|
|
97
|
+
env.transaction(true) do
|
|
98
|
+
db.cursor do |c|
|
|
99
|
+
c.first
|
|
100
|
+
GC.compact
|
|
101
|
+
expect(c.next).not_to be_nil
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
GC.compact
|
|
105
|
+
expect {
|
|
106
|
+
env.transaction { db['new'] = 'write' }
|
|
107
|
+
}.not_to raise_error
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# -----------------------------------------------------------------------
|
|
111
|
+
# 7. GC.start (sweep without compaction) — baseline
|
|
112
|
+
# Should always pass; establishes that non-compacting GC is safe
|
|
113
|
+
# -----------------------------------------------------------------------
|
|
114
|
+
it 'survives GC.start (sweep only) between write transactions' do
|
|
115
|
+
env.transaction { db['key1'] = 'value1' }
|
|
116
|
+
GC.start
|
|
117
|
+
expect {
|
|
118
|
+
env.transaction { db['key2'] = 'value2' }
|
|
119
|
+
}.not_to raise_error
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# -----------------------------------------------------------------------
|
|
123
|
+
# 8. Transaction object survives GC after finishing
|
|
124
|
+
# Verifies that a finished transaction's Ruby object can be
|
|
125
|
+
# collected without corrupting the environment
|
|
126
|
+
# -----------------------------------------------------------------------
|
|
127
|
+
it 'allows GC to collect finished transaction objects safely' do
|
|
128
|
+
10.times do |i|
|
|
129
|
+
env.transaction { db["key#{i}"] = "value#{i}" }
|
|
130
|
+
end
|
|
131
|
+
# force collection of all those transaction objects
|
|
132
|
+
GC.compact
|
|
133
|
+
GC.start
|
|
134
|
+
expect {
|
|
135
|
+
env.transaction { db['final'] = 'write' }
|
|
136
|
+
}.not_to raise_error
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# -----------------------------------------------------------------------
|
|
140
|
+
# 9. Many databases, many transactions, GC between each
|
|
141
|
+
# Closer to the Store::Digest workload profile
|
|
142
|
+
# -----------------------------------------------------------------------
|
|
143
|
+
it 'handles multiple named databases with GC.compact between writes' do
|
|
144
|
+
dbs = %w[alpha beta gamma delta].map do |name|
|
|
145
|
+
env.database(name, create: true)
|
|
146
|
+
end
|
|
147
|
+
GC.compact
|
|
148
|
+
|
|
149
|
+
10.times do |i|
|
|
150
|
+
env.transaction do
|
|
151
|
+
dbs.each { |d| d["key#{i}"] = "value#{i}" }
|
|
152
|
+
end
|
|
153
|
+
GC.compact
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
env.transaction(true) do
|
|
157
|
+
dbs.each do |d|
|
|
158
|
+
10.times { |i| expect(d["key#{i}"]).to eq "value#{i}" }
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
data/spec/helper.rb
CHANGED