lmdb 0.7.5 → 0.8.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.
- checksums.yaml +4 -4
- 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/version.rb +1 -1
- data/spec/gc_torture_spec.rb +162 -0
- data/spec/helper.rb +3 -3
- data/spec/lmdb_spec.rb +144 -137
- data/spec/pseudo_transactions_spec.rb +237 -0
- metadata +6 -2
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
require 'helper'
|
|
2
|
+
require 'lmdb'
|
|
3
|
+
require 'tmpdir'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
RSpec.describe 'LMDB pseudo-transactions (RO nested inside RW)' do
|
|
7
|
+
let(:path) { Dir.mktmpdir }
|
|
8
|
+
let(:env) { LMDB.new(path, mapsize: 2**20) }
|
|
9
|
+
let(:db) { env.database('test', create: true) }
|
|
10
|
+
|
|
11
|
+
before(:each) { db } # ensure db is opened inside a txn before tests run
|
|
12
|
+
|
|
13
|
+
after(:each) do
|
|
14
|
+
env.close rescue nil
|
|
15
|
+
FileUtils.rm_rf path
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'should not have anything in the readers' do
|
|
19
|
+
expect(env.info[:numreaders]).to eq(0)
|
|
20
|
+
expect(env.reader_check).to eq(0)
|
|
21
|
+
expect(env.reader_list.first).to eq("(no active readers)\n")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# -----------------------------------------------------------------------
|
|
25
|
+
# The core bug: RO transaction nested inside RW must not abort the RW txn
|
|
26
|
+
# when the RO block raises.
|
|
27
|
+
# -----------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
it 'does not abort the outer RW transaction when the inner RO block raises' do
|
|
30
|
+
|
|
31
|
+
# warn env.reader_list.inspect
|
|
32
|
+
|
|
33
|
+
expect {
|
|
34
|
+
env.transaction do
|
|
35
|
+
|
|
36
|
+
db['key'] = 'value'
|
|
37
|
+
|
|
38
|
+
# This inner RO transaction raises. Before the fix this silently
|
|
39
|
+
# aborted the outer RW transaction, causing the subsequent write
|
|
40
|
+
# to produce EINVAL / "Invalid argument".
|
|
41
|
+
begin
|
|
42
|
+
env.transaction(true) do
|
|
43
|
+
# warn env.reader_list.inspect
|
|
44
|
+
raise 'deliberate error inside RO block'
|
|
45
|
+
end
|
|
46
|
+
rescue RuntimeError
|
|
47
|
+
# swallow — the outer RW transaction should survive this
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# This write must succeed. If the outer txn was aborted by the
|
|
51
|
+
# inner RO block's exception, this raises LMDB::Error::BadTxn or
|
|
52
|
+
# produces "Invalid argument".
|
|
53
|
+
db['key2'] = 'value2'
|
|
54
|
+
end
|
|
55
|
+
}.not_to raise_error
|
|
56
|
+
|
|
57
|
+
# And the writes must have actually committed
|
|
58
|
+
env.transaction(true) do
|
|
59
|
+
expect(db['key']).to eq 'value'
|
|
60
|
+
expect(db['key2']).to eq 'value2'
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# -----------------------------------------------------------------------
|
|
65
|
+
# commit/abort on a pseudo-txn are no-ops: the outer RW txn survives
|
|
66
|
+
# -----------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
it 'treats txn.commit inside a pseudo-transaction as a no-op' do
|
|
69
|
+
env.transaction do
|
|
70
|
+
db['before'] = 'yes'
|
|
71
|
+
|
|
72
|
+
env.transaction(true) do |pseudo|
|
|
73
|
+
# explicit commit on a pseudo-txn should be a no-op, not an error,
|
|
74
|
+
# and must not commit the outer RW transaction prematurely
|
|
75
|
+
pseudo.commit
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# outer txn still alive — this must not raise
|
|
79
|
+
db['after'] = 'yes'
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
env.transaction(true) do
|
|
83
|
+
expect(db['before']).to eq 'yes'
|
|
84
|
+
expect(db['after']).to eq 'yes'
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'treats txn.abort inside a pseudo-transaction as a no-op' do
|
|
89
|
+
env.transaction do
|
|
90
|
+
db['key'] = 'written'
|
|
91
|
+
|
|
92
|
+
env.transaction(true) do |pseudo|
|
|
93
|
+
# explicit abort on a pseudo-txn must not roll back the outer txn
|
|
94
|
+
pseudo.abort
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# outer txn still alive
|
|
98
|
+
db['key2'] = 'also written'
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
env.transaction(true) do
|
|
102
|
+
expect(db['key']).to eq 'written'
|
|
103
|
+
expect(db['key2']).to eq 'also written'
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# -----------------------------------------------------------------------
|
|
108
|
+
# pseudo-txn correctly reflects the read view of the enclosing RW txn
|
|
109
|
+
# -----------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
it 'can read writes made in the outer RW transaction via the pseudo-transaction' do
|
|
112
|
+
env.transaction do
|
|
113
|
+
db['visible'] = 'yes'
|
|
114
|
+
|
|
115
|
+
result = env.transaction(true) do |pseudo|
|
|
116
|
+
# writes made in the outer txn should be visible here since
|
|
117
|
+
# we are using the same underlying MDB_txn
|
|
118
|
+
db['visible']
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
expect(result).to eq 'yes'
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# -----------------------------------------------------------------------
|
|
126
|
+
# break from pseudo-txn exits only the inner block; outer RW continues
|
|
127
|
+
# -----------------------------------------------------------------------
|
|
128
|
+
it 'break from a pseudo-transaction exits only the inner block' do
|
|
129
|
+
outer_continued = false
|
|
130
|
+
|
|
131
|
+
env.transaction do
|
|
132
|
+
db['key'] = 'value'
|
|
133
|
+
|
|
134
|
+
env.transaction(true) do
|
|
135
|
+
break
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
outer_continued = true
|
|
139
|
+
db['key2'] = 'value2'
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
expect(outer_continued).to be true
|
|
143
|
+
env.transaction(true) do
|
|
144
|
+
expect(db['key']).to eq 'value'
|
|
145
|
+
expect(db['key2']).to eq 'value2'
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# -----------------------------------------------------------------------
|
|
150
|
+
# pseudo-txn finished? predicate
|
|
151
|
+
# -----------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
it 'marks the pseudo-transaction as finished after the block exits' do
|
|
154
|
+
pseudo_ref = nil
|
|
155
|
+
|
|
156
|
+
env.transaction do
|
|
157
|
+
env.transaction(true) do |pseudo|
|
|
158
|
+
pseudo_ref = pseudo
|
|
159
|
+
expect(pseudo_ref.finished?).to be false
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# After the pseudo block exits, txn should be terminated
|
|
164
|
+
expect(pseudo_ref.finished?).to be true
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# -----------------------------------------------------------------------
|
|
168
|
+
# Nested pseudo-txns (RO inside RO inside RW) also work correctly
|
|
169
|
+
# -----------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
it 'handles doubly-nested pseudo-transactions without corrupting the txn chain' do
|
|
172
|
+
env.transaction do
|
|
173
|
+
db['key'] = 'value'
|
|
174
|
+
|
|
175
|
+
env.transaction(true) do
|
|
176
|
+
env.transaction(true) do
|
|
177
|
+
expect(db['key']).to eq 'value'
|
|
178
|
+
end
|
|
179
|
+
# still inside the first RO pseudo here
|
|
180
|
+
expect(db['key']).to eq 'value'
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# outer RW still alive
|
|
184
|
+
db['key2'] = 'value2'
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
env.transaction(true) do
|
|
188
|
+
expect(db['key']).to eq 'value'
|
|
189
|
+
expect(db['key2']).to eq 'value2'
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# -----------------------------------------------------------------------
|
|
194
|
+
# The original store-digest scenario: exception inside a method that
|
|
195
|
+
# opens its own RO transaction while called from inside a RW transaction.
|
|
196
|
+
# This is the real-world pattern that was failing.
|
|
197
|
+
# -----------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
it 'survives the mark_meta_deleted pattern: RO read inside a RW write block' do
|
|
200
|
+
# Simulate the pattern from store-digest's v1.rb:
|
|
201
|
+
# mark_meta_deleted opens @lmdb.transaction (RW)
|
|
202
|
+
# then calls get_meta which opens @lmdb.transaction(true) (RO)
|
|
203
|
+
# get_meta raises
|
|
204
|
+
# mark_meta_deleted should survive and continue
|
|
205
|
+
|
|
206
|
+
def read_with_possible_raise(env, db, should_raise)
|
|
207
|
+
env.transaction(true) do
|
|
208
|
+
raise 'record not found' if should_raise
|
|
209
|
+
db['key']
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
expect {
|
|
214
|
+
env.transaction do
|
|
215
|
+
db['key'] = 'value'
|
|
216
|
+
|
|
217
|
+
# First inner RO call raises — simulates get_meta failing
|
|
218
|
+
begin
|
|
219
|
+
read_with_possible_raise(env, db, true)
|
|
220
|
+
rescue RuntimeError
|
|
221
|
+
# expected, swallow it
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Second inner RO call succeeds — simulates a subsequent read
|
|
225
|
+
val = read_with_possible_raise(env, db, false)
|
|
226
|
+
expect(val).to eq 'value'
|
|
227
|
+
|
|
228
|
+
db['key2'] = 'also committed'
|
|
229
|
+
end
|
|
230
|
+
}.not_to raise_error
|
|
231
|
+
|
|
232
|
+
env.transaction(true) do
|
|
233
|
+
expect(db['key']).to eq 'value'
|
|
234
|
+
expect(db['key2']).to eq 'also committed'
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lmdb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Mendler
|
|
8
8
|
- Dorian Taylor
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|
|
@@ -91,8 +91,10 @@ files:
|
|
|
91
91
|
- lib/lmdb/database.rb
|
|
92
92
|
- lib/lmdb/version.rb
|
|
93
93
|
- lmdb.gemspec
|
|
94
|
+
- spec/gc_torture_spec.rb
|
|
94
95
|
- spec/helper.rb
|
|
95
96
|
- spec/lmdb_spec.rb
|
|
97
|
+
- spec/pseudo_transactions_spec.rb
|
|
96
98
|
- vendor/liblmdb/VERSION
|
|
97
99
|
- vendor/liblmdb/lmdb.h
|
|
98
100
|
- vendor/liblmdb/mdb.c
|
|
@@ -120,5 +122,7 @@ rubygems_version: 3.6.7
|
|
|
120
122
|
specification_version: 4
|
|
121
123
|
summary: Ruby bindings to Lightning MDB
|
|
122
124
|
test_files:
|
|
125
|
+
- spec/gc_torture_spec.rb
|
|
123
126
|
- spec/helper.rb
|
|
124
127
|
- spec/lmdb_spec.rb
|
|
128
|
+
- spec/pseudo_transactions_spec.rb
|