Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: allocate C memory for MDB_val in readonly Txn #29

Merged
merged 4 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions lmdb/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,13 @@ func (env *Env) FD() (uintptr, error) {
// to avoid constant value overflow errors at compile time.
const fdInvalid = ^uintptr(0)

mf := new(C.mdb_filehandle_t)
ret := C.mdb_env_get_fd(env._env, mf)
var mf C.mdb_filehandle_t
ret := C.mdb_env_get_fd(env._env, &mf)
err := operrno("mdb_env_get_fd", ret)
if err != nil {
return 0, err
}
fd := uintptr(*mf)
fd := uintptr(mf)

if fd == fdInvalid {
return 0, errNotOpen
Expand Down
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
63 changes: 63 additions & 0 deletions lmdb/txn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,69 @@ 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.key == env.ckey || txn.val == env.cval {
t.Error("env.ckey and cval must not be used in a readonly Txn")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}
if txn.cbuf == nil {
t.Error("cbuf expected to not be nil when opening a readonly Txn")
}
wojas marked this conversation as resolved.
Show resolved Hide resolved

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

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

// 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