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.
@@ -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
- Data_Get_Struct(var, Environment, var_env); \
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
- Data_Get_Struct(var, Database, var_db);
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
- Data_Get_Struct(var, Transaction, var_txn)
44
+ TypedData_Get_Struct(var, Transaction, type, var_txn)
44
45
 
45
- #define CURSOR(var, var_cur) \
46
- Cursor* var_cur; \
47
- Data_Get_Struct(var, Cursor, var_cur); \
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
- const VALUE* argv;
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(Cursor* cursor);
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(Database* database);
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 environment_free(Environment *environment);
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 transaction_free(Transaction* transaction);
178
- static void transaction_mark(Transaction* transaction);
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
- maybe_txn true do
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 block_given?
59
- maybe_txn true do
60
- #env.transaction do
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
- yield rec.first
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 block_given?
93
+ return enum_for :each_value, key unless block
77
94
 
78
- value = get(key) or return
79
- unless dupsort?
80
- yield value
81
- return
82
- end
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
- yield rec.last
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
- maybe_txn true do
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
- v = get(key) or return false
122
- return true if value.nil? or value.to_s == v
123
- return false unless dupsort?
124
-
125
- # warn "checking dupsort value `#{value.inspect}` (#{value.class})"
126
-
127
- ret = false
128
- # read-only txn was having trouble being nested inside a read-write
129
- # maybe_txn(true) { cursor { |c| ret = !!c.set(key, value) } }
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
- ret
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
@@ -1,3 +1,3 @@
1
1
  module LMDB
2
- VERSION = '0.7.5'.freeze
2
+ VERSION = '0.8.1'.freeze
3
3
  end
@@ -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
@@ -27,7 +27,7 @@ end
27
27
  RSpec.configure do |c|
28
28
  c.include LMDB::SpecHelper
29
29
  c.after { FileUtils.rm_rf TEMP_ROOT }
30
- c.expect_with :rspec do |cc|
31
- cc.syntax = :should
32
- end
30
+ # c.expect_with :rspec do |cc|
31
+ # cc.syntax = :should
32
+ # end
33
33
  end