Skip to content

Commit

Permalink
Fix: allocate C memory for MDB_val in readonly Txn
Browse files Browse the repository at this point in the history
Use C.malloc to allocate memory for MDB_val in readonly Txn.
CGo does not allow using new() for this.

Fixes #28.
  • Loading branch information
wojas committed Dec 7, 2023
1 parent 2b2b787 commit 9e9ad32
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 6 deletions.
31 changes: 25 additions & 6 deletions lmdb/txn.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ type Txn struct {
// reset/renewed
id uintptr

// Pointer to scratch space for key and val in readonly transactions
cbuf unsafe.Pointer

env *Env
_txn *C.MDB_txn
key *C.MDB_val
Expand All @@ -87,12 +90,20 @@ func beginTxn(env *Env, parent *Txn, flags uint) (*Txn, error) {
txn.key = env.ckey
txn.val = env.cval
} else {
// It is not easy to share C.MDB_val values in this scenario unless
// there is a synchronized pool involved, which will increase
// overhead. Further, allocating these values with C will add
// overhead both here and when the values are freed.
txn.key = new(C.MDB_val)
txn.val = new(C.MDB_val)
// Readonly transaction.
// We cannot share global C.MDB_val values in this scenario, because
// there can be multiple simultaneous read transactions.
// Allocate C memory for two values in one call.
// This is freed in clearTxn(), which is always called at the end
// of a transaction through Commit() or Abort().
if C.sizeof_MDB_val == 0 {
panic("zero C.sizeof_MDB_val") // should never happen
}
txn.cbuf = C.malloc(2 * C.sizeof_MDB_val)
txn.key = (*C.MDB_val)(txn.cbuf)
txn.val = (*C.MDB_val)(unsafe.Pointer(
uintptr(txn.cbuf) + uintptr(C.sizeof_MDB_val),
))
}
} else {
// Because parent Txn objects cannot be used while a sub-Txn is active
Expand Down Expand Up @@ -240,6 +251,14 @@ func (txn *Txn) clearTxn() {
// sure the value returned for an invalid Txn is more or less consistent
// for people familiar with the C semantics.
txn.resetID()

// Release C allocated buffer, if used
if txn.cbuf != nil {
txn.key = nil
txn.val = nil
C.free(txn.cbuf)
txn.cbuf = nil
}
}

// resetID has to be called anytime the value of Txn.getID() may change
Expand Down
48 changes: 48 additions & 0 deletions lmdb/txn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,54 @@ func TestTxn_Reset_writeTxn(t *testing.T) {
}
}

// This test demonstrates that in a readonly transaction C memory is allocated
// for the values, and freed during a Reset.
func TestTxn_Reset_readonly_C_free(t *testing.T) {
env := setup(t)
path, err := env.Path()
if err != nil {
env.Close()
t.Error(err)
return
}
defer os.RemoveAll(path)
defer env.Close()

runtime.LockOSThread()
defer runtime.UnlockOSThread()

txn, err := env.BeginTxn(nil, Readonly)
if err != nil {
t.Error(err)
return
}
defer txn.Abort()

// Since this is a readonly transaction, the global Env key/val cannot be
// reused and new C memory must be allocated.
if txn.cbuf == nil {
t.Error("cbuf expected to not be nil when opening a readonly Txn")
}

// Reset must not free the buffer
txn.Reset()
if txn.cbuf == nil {
t.Error("cbuf must not be nil after Reset")
}

// Abort must free the buffer
txn.Abort()
if txn.cbuf != nil {
t.Error("cbuf expected to be nil, C memory not freed")
}
if txn.key != nil || txn.val != nil {
t.Error("key and val expected to be nil after C memory free")
}

// A second Abort must not panic
txn.Abort()
}

func TestTxn_UpdateLocked(t *testing.T) {
env := setup(t)
path, err := env.Path()
Expand Down

0 comments on commit 9e9ad32

Please sign in to comment.