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.
@@ -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.7.5
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-03 00:00:00.000000000 Z
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