mirror of https://github.com/etcd-io/bbolt.git
1607 lines
43 KiB
Go
1607 lines
43 KiB
Go
package bolt
|
|
|
|
var TransactionExistingChildError = &Error{"txn already has a child", nil}
|
|
var TransactionReadOnlyChildError = &Error{"read-only txn cannot create a child", nil}
|
|
|
|
const (
|
|
txnb_dirty = 0x01 /**< DB was modified or is DUPSORT data */
|
|
txnb_stale = 0x02 /**< Named-DB record is older than txnID */
|
|
txnb_new = 0x04 /**< Named-DB handle opened in this txn */
|
|
txnb_valid = 0x08 /**< DB handle is valid, see also #MDB_VALID */
|
|
)
|
|
|
|
const (
|
|
ps_modify = 1
|
|
ps_rootonly = 2
|
|
ps_first = 4
|
|
ps_last = 8
|
|
)
|
|
|
|
type Transaction struct {
|
|
id int
|
|
db *DB
|
|
writable bool
|
|
dirty bool
|
|
spilled bool
|
|
err error
|
|
parent *Transaction
|
|
child *Transaction
|
|
buckets []*txnbucket
|
|
|
|
pgno int
|
|
freePages []pgno
|
|
spillPages []pgno
|
|
dirtyList []pgno
|
|
reader *reader
|
|
// TODO: bucketxs []*bucketx
|
|
// Implicit from slices? TODO: MDB_dbi mt_numdbs;
|
|
dirty_room int
|
|
pagestate pagestate
|
|
}
|
|
|
|
type txnbucket struct {
|
|
bucket *Bucket
|
|
cursor *Cursor
|
|
flags int
|
|
}
|
|
|
|
// Transaction begins a nested child transaction. Child transactions are only
|
|
// available to writable transactions and a transaction can only have one child
|
|
// at a time.
|
|
func (t *Transaction) Transaction() (*Transaction, error) {
|
|
// Exit if parent already has a child transaction.
|
|
if t.child != nil {
|
|
return nil, TransactionExistingChildError
|
|
}
|
|
// Exit if using parent for read-only transaction.
|
|
if !t.writable {
|
|
return nil, TransactionReadOnlyChildError
|
|
}
|
|
// TODO: Exit if parent is in an error state.
|
|
|
|
// Create the child transaction and attach the parent.
|
|
child := &Transaction{
|
|
id: t.id,
|
|
db: t.db,
|
|
parent: t,
|
|
writable: true,
|
|
pgno: t.pgno,
|
|
pagestate: t.db.pagestate,
|
|
}
|
|
copy(child.buckets, t.buckets)
|
|
|
|
// TODO: Remove DB_NEW flag.
|
|
t.child = child
|
|
|
|
// TODO: wtf?
|
|
// if (env->me_pghead) {
|
|
// size = MDB_IDL_SIZEOF(env->me_pghead);
|
|
// env->me_pghead = mdb_midl_alloc(env->me_pghead[0]);
|
|
// if (env->me_pghead)
|
|
// memcpy(env->me_pghead, ntxn->mnt_pgstate.mf_pghead, size);
|
|
// else
|
|
// rc = ENOMEM;
|
|
// }
|
|
|
|
// TODO: Back up parent transaction's cursors.
|
|
// if t.shadow(child); err != nil {
|
|
// child.reset0()
|
|
// return err
|
|
// }
|
|
|
|
return child, nil
|
|
}
|
|
|
|
func (t *Transaction) Cursor(b *Bucket) (*Cursor, error) {
|
|
// TODO: if !(txn->mt_dbflags[dbi] & DB_VALID) return InvalidBucketError
|
|
// TODO: if (txn->mt_flags & MDB_TXN_ERROR) return BadTransactionError
|
|
|
|
// Allow read access to the freelist
|
|
// TODO: if (!dbi && !F_ISSET(txn->mt_flags, MDB_TXN_RDONLY))
|
|
|
|
/*
|
|
MDB_cursor *mc;
|
|
size_t size = sizeof(MDB_cursor);
|
|
|
|
// Allow read access to the freelist
|
|
if (!dbi && !F_ISSET(txn->mt_flags, MDB_TXN_RDONLY))
|
|
return EINVAL;
|
|
|
|
if ((mc = malloc(size)) != NULL) {
|
|
mdb_cursor_init(mc, txn, dbi, (MDB_xcursor *)(mc + 1));
|
|
if (txn->mt_cursors) {
|
|
mc->mc_next = txn->mt_cursors[dbi];
|
|
txn->mt_cursors[dbi] = mc;
|
|
mc->mc_flags |= C_UNTRACK;
|
|
}
|
|
} else {
|
|
return ENOMEM;
|
|
}
|
|
|
|
*ret = mc;
|
|
|
|
return MDB_SUCCESS;
|
|
*/
|
|
return nil, nil
|
|
}
|
|
|
|
// //
|
|
// //
|
|
// //
|
|
// //
|
|
// //
|
|
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CONVERTED ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //
|
|
// //
|
|
// //
|
|
// //
|
|
// //
|
|
// //
|
|
|
|
func (t *Transaction) allocPage(num int) *page {
|
|
/*
|
|
MDB_env *env = txn->mt_env;
|
|
MDB_page *ret = env->me_dpages;
|
|
size_t psize = env->me_psize, sz = psize, off;
|
|
// For ! #MDB_NOMEMINIT, psize counts how much to init.
|
|
// For a single page alloc, we init everything after the page header.
|
|
// For multi-page, we init the final page; if the caller needed that
|
|
// many pages they will be filling in at least up to the last page.
|
|
if (num == 1) {
|
|
if (ret) {
|
|
VGMEMP_ALLOC(env, ret, sz);
|
|
VGMEMP_DEFINED(ret, sizeof(ret->mp_next));
|
|
env->me_dpages = ret->mp_next;
|
|
return ret;
|
|
}
|
|
psize -= off = PAGEHDRSZ;
|
|
} else {
|
|
sz *= num;
|
|
off = sz - psize;
|
|
}
|
|
if ((ret = malloc(sz)) != NULL) {
|
|
VGMEMP_ALLOC(env, ret, sz);
|
|
if (!(env->me_flags & MDB_NOMEMINIT)) {
|
|
memset((char *)ret + off, 0, psize);
|
|
ret->mp_pad = 0;
|
|
}
|
|
} else {
|
|
txn->mt_flags |= MDB_TXN_ERROR;
|
|
}
|
|
return ret;
|
|
*/
|
|
return nil
|
|
}
|
|
|
|
// Find oldest txnid still referenced. Expects txn->mt_txnid > 0.
|
|
func (t *Transaction) oldest() int {
|
|
/*
|
|
int i;
|
|
txnid_t mr, oldest = txn->mt_txnid - 1;
|
|
if (txn->mt_env->me_txns) {
|
|
MDB_reader *r = txn->mt_env->me_txns->mti_readers;
|
|
for (i = txn->mt_env->me_txns->mti_numreaders; --i >= 0; ) {
|
|
if (r[i].mr_pid) {
|
|
mr = r[i].mr_txnid;
|
|
if (oldest > mr)
|
|
oldest = mr;
|
|
}
|
|
}
|
|
}
|
|
return oldest;
|
|
*/
|
|
return 0
|
|
}
|
|
|
|
// Add a page to the txn's dirty list
|
|
func (t *Transaction) addDirtyPage(p *page) {
|
|
/*
|
|
MDB_ID2 mid;
|
|
int rc, (*insert)(MDB_ID2L, MDB_ID2 *);
|
|
|
|
if (txn->mt_env->me_flags & MDB_WRITEMAP) {
|
|
insert = mdb_mid2l_append;
|
|
} else {
|
|
insert = mdb_mid2l_insert;
|
|
}
|
|
mid.mid = mp->mp_pgno;
|
|
mid.mptr = mp;
|
|
rc = insert(txn->mt_u.dirty_list, &mid);
|
|
mdb_tassert(txn, rc == 0);
|
|
txn->mt_dirty_room--;
|
|
*/
|
|
}
|
|
|
|
// Pull a page off the txn's spill list, if present.
|
|
// If a page being referenced was spilled to disk in this txn, bring
|
|
// it back and make it dirty/writable again.
|
|
// @param[in] txn the transaction handle.
|
|
// @param[in] mp the page being referenced. It must not be dirty.
|
|
// @param[out] ret the writable page, if any. ret is unchanged if
|
|
// mp wasn't spilled.
|
|
func (t *Transaction) unspill(p *page) *page {
|
|
/*
|
|
MDB_env *env = txn->mt_env;
|
|
const MDB_txn *tx2;
|
|
unsigned x;
|
|
pgno_t pgno = mp->mp_pgno, pn = pgno << 1;
|
|
|
|
for (tx2 = txn; tx2; tx2=tx2->mt_parent) {
|
|
if (!tx2->mt_spill_pgs)
|
|
continue;
|
|
x = mdb_midl_search(tx2->mt_spill_pgs, pn);
|
|
if (x <= tx2->mt_spill_pgs[0] && tx2->mt_spill_pgs[x] == pn) {
|
|
MDB_page *np;
|
|
int num;
|
|
if (txn->mt_dirty_room == 0)
|
|
return MDB_TXN_FULL;
|
|
if (IS_OVERFLOW(mp))
|
|
num = mp->mp_pages;
|
|
else
|
|
num = 1;
|
|
if (env->me_flags & MDB_WRITEMAP) {
|
|
np = mp;
|
|
} else {
|
|
np = mdb_page_malloc(txn, num);
|
|
if (!np)
|
|
return ENOMEM;
|
|
if (num > 1)
|
|
memcpy(np, mp, num * env->me_psize);
|
|
else
|
|
mdb_page_copy(np, mp, env->me_psize);
|
|
}
|
|
if (tx2 == txn) {
|
|
// If in current txn, this page is no longer spilled.
|
|
// If it happens to be the last page, truncate the spill list.
|
|
// Otherwise mark it as deleted by setting the LSB.
|
|
if (x == txn->mt_spill_pgs[0])
|
|
txn->mt_spill_pgs[0]--;
|
|
else
|
|
txn->mt_spill_pgs[x] |= 1;
|
|
} // otherwise, if belonging to a parent txn, the
|
|
// page remains spilled until child commits
|
|
|
|
mdb_page_dirty(txn, np);
|
|
np->mp_flags |= P_DIRTY;
|
|
*ret = np;
|
|
break;
|
|
}
|
|
}
|
|
return MDB_SUCCESS;
|
|
*/
|
|
return nil
|
|
}
|
|
|
|
// Back up parent txn's cursors, then grab the originals for tracking
|
|
func (t *Transaction) shadow(dst *Transaction) error {
|
|
/*
|
|
MDB_cursor *mc, *bk;
|
|
MDB_xcursor *mx;
|
|
size_t size;
|
|
int i;
|
|
|
|
for (i = src->mt_numdbs; --i >= 0; ) {
|
|
if ((mc = src->mt_cursors[i]) != NULL) {
|
|
size = sizeof(MDB_cursor);
|
|
if (mc->mc_xcursor)
|
|
size += sizeof(MDB_xcursor);
|
|
for (; mc; mc = bk->mc_next) {
|
|
bk = malloc(size);
|
|
if (!bk)
|
|
return ENOMEM;
|
|
*bk = *mc;
|
|
mc->mc_backup = bk;
|
|
mc->mc_db = &dst->mt_dbs[i];
|
|
// Kill pointers into src - and dst to reduce abuse: The
|
|
// user may not use mc until dst ends. Otherwise we'd...
|
|
mc->mc_txn = NULL; // ...set this to dst
|
|
mc->mc_dbflag = NULL; // ...and &dst->mt_dbflags[i]
|
|
if ((mx = mc->mc_xcursor) != NULL) {
|
|
*(MDB_xcursor *)(bk+1) = *mx;
|
|
mx->mx_cursor.mc_txn = NULL; // ...and dst.
|
|
}
|
|
mc->mc_next = dst->mt_cursors[i];
|
|
dst->mt_cursors[i] = mc;
|
|
}
|
|
}
|
|
}
|
|
return MDB_SUCCESS;
|
|
*/
|
|
return nil
|
|
}
|
|
|
|
// Close this write txn's cursors, give parent txn's cursors back to parent.
|
|
// @param[in] txn the transaction handle.
|
|
// @param[in] merge true to keep changes to parent cursors, false to revert.
|
|
// @return 0 on success, non-zero on failure.
|
|
func (t *Transaction) closeCursors(merge bool) {
|
|
/*
|
|
MDB_cursor **cursors = txn->mt_cursors, *mc, *next, *bk;
|
|
MDB_xcursor *mx;
|
|
int i;
|
|
|
|
for (i = txn->mt_numdbs; --i >= 0; ) {
|
|
for (mc = cursors[i]; mc; mc = next) {
|
|
next = mc->mc_next;
|
|
if ((bk = mc->mc_backup) != NULL) {
|
|
if (merge) {
|
|
// Commit changes to parent txn
|
|
mc->mc_next = bk->mc_next;
|
|
mc->mc_backup = bk->mc_backup;
|
|
mc->mc_txn = bk->mc_txn;
|
|
mc->mc_db = bk->mc_db;
|
|
mc->mc_dbflag = bk->mc_dbflag;
|
|
if ((mx = mc->mc_xcursor) != NULL)
|
|
mx->mx_cursor.mc_txn = bk->mc_txn;
|
|
} else {
|
|
// Abort nested txn
|
|
*mc = *bk;
|
|
if ((mx = mc->mc_xcursor) != NULL)
|
|
*mx = *(MDB_xcursor *)(bk+1);
|
|
}
|
|
mc = bk;
|
|
}
|
|
// Only malloced cursors are permanently tracked.
|
|
free(mc);
|
|
}
|
|
cursors[i] = NULL;
|
|
}
|
|
*/
|
|
}
|
|
|
|
// Common code for #mdb_txn_begin() and #mdb_txn_renew().
|
|
// @param[in] txn the transaction handle to initialize
|
|
// @return 0 on success, non-zero on failure.
|
|
func (t *Transaction) renew() error {
|
|
/*
|
|
MDB_env *env = txn->mt_env;
|
|
MDB_txninfo *ti = env->me_txns;
|
|
MDB_meta *meta;
|
|
unsigned int i, nr;
|
|
uint16_t x;
|
|
int rc, new_notls = 0;
|
|
|
|
// Setup db info
|
|
txn->mt_numdbs = env->me_numdbs;
|
|
txn->mt_dbxs = env->me_dbxs; // mostly static anyway
|
|
|
|
if (txn->mt_flags & MDB_TXN_RDONLY) {
|
|
if (!ti) {
|
|
meta = env->me_metas[ mdb_env_pick_meta(env) ];
|
|
txn->mt_txnid = meta->mm_txnid;
|
|
txn->mt_u.reader = NULL;
|
|
} else {
|
|
MDB_reader *r = (env->me_flags & MDB_NOTLS) ? txn->mt_u.reader :
|
|
pthread_getspecific(env->me_txkey);
|
|
if (r) {
|
|
if (r->mr_pid != env->me_pid || r->mr_txnid != (txnid_t)-1)
|
|
return MDB_BAD_RSLOT;
|
|
} else {
|
|
MDB_PID_T pid = env->me_pid;
|
|
pthread_t tid = pthread_self();
|
|
|
|
if (!(env->me_flags & MDB_LIVE_READER)) {
|
|
rc = mdb_reader_pid(env, Pidset, pid);
|
|
if (rc)
|
|
return rc;
|
|
env->me_flags |= MDB_LIVE_READER;
|
|
}
|
|
|
|
LOCK_MUTEX_R(env);
|
|
nr = ti->mti_numreaders;
|
|
for (i=0; i<nr; i++)
|
|
if (ti->mti_readers[i].mr_pid == 0)
|
|
break;
|
|
if (i == env->me_maxreaders) {
|
|
UNLOCK_MUTEX_R(env);
|
|
return MDB_READERS_FULL;
|
|
}
|
|
ti->mti_readers[i].mr_pid = pid;
|
|
ti->mti_readers[i].mr_tid = tid;
|
|
if (i == nr)
|
|
ti->mti_numreaders = ++nr;
|
|
// Save numreaders for un-mutexed mdb_env_close()
|
|
env->me_numreaders = nr;
|
|
UNLOCK_MUTEX_R(env);
|
|
|
|
r = &ti->mti_readers[i];
|
|
new_notls = (env->me_flags & MDB_NOTLS);
|
|
if (!new_notls && (rc=pthread_setspecific(env->me_txkey, r))) {
|
|
r->mr_pid = 0;
|
|
return rc;
|
|
}
|
|
}
|
|
txn->mt_txnid = r->mr_txnid = ti->mti_txnid;
|
|
txn->mt_u.reader = r;
|
|
meta = env->me_metas[txn->mt_txnid & 1];
|
|
}
|
|
} else {
|
|
if (ti) {
|
|
LOCK_MUTEX_W(env);
|
|
|
|
txn->mt_txnid = ti->mti_txnid;
|
|
meta = env->me_metas[txn->mt_txnid & 1];
|
|
} else {
|
|
meta = env->me_metas[ mdb_env_pick_meta(env) ];
|
|
txn->mt_txnid = meta->mm_txnid;
|
|
}
|
|
txn->mt_txnid++;
|
|
#if MDB_DEBUG
|
|
if (txn->mt_txnid == mdb_debug_start)
|
|
mdb_debug = 1;
|
|
#endif
|
|
txn->mt_dirty_room = MDB_IDL_UM_MAX;
|
|
txn->mt_u.dirty_list = env->me_dirty_list;
|
|
txn->mt_u.dirty_list[0].mid = 0;
|
|
txn->mt_free_pgs = env->me_free_pgs;
|
|
txn->mt_free_pgs[0] = 0;
|
|
txn->mt_spill_pgs = NULL;
|
|
env->me_txn = txn;
|
|
}
|
|
|
|
// Copy the DB info and flags
|
|
memcpy(txn->mt_dbs, meta->mm_dbs, 2 * sizeof(MDB_db));
|
|
|
|
// Moved to here to avoid a data race in read TXNs
|
|
txn->mt_next_pgno = meta->mm_last_pg+1;
|
|
|
|
for (i=2; i<txn->mt_numdbs; i++) {
|
|
x = env->me_dbflags[i];
|
|
txn->mt_dbs[i].md_flags = x & PERSISTENT_FLAGS;
|
|
txn->mt_dbflags[i] = (x & MDB_VALID) ? DB_VALID|DB_STALE : 0;
|
|
}
|
|
txn->mt_dbflags[0] = txn->mt_dbflags[1] = DB_VALID;
|
|
|
|
if (env->me_maxpg < txn->mt_next_pgno) {
|
|
mdb_txn_reset0(txn, "renew0-mapfail");
|
|
if (new_notls) {
|
|
txn->mt_u.reader->mr_pid = 0;
|
|
txn->mt_u.reader = NULL;
|
|
}
|
|
return MDB_MAP_RESIZED;
|
|
}
|
|
|
|
return MDB_SUCCESS;
|
|
*/
|
|
return nil
|
|
}
|
|
|
|
func (t *Transaction) Renew() error {
|
|
/*
|
|
int rc;
|
|
|
|
if (!txn || txn->mt_dbxs) // A reset txn has mt_dbxs==NULL
|
|
return EINVAL;
|
|
|
|
if (txn->mt_env->me_flags & MDB_FATAL_ERROR) {
|
|
DPUTS("environment had fatal error, must shutdown!");
|
|
return MDB_PANIC;
|
|
}
|
|
|
|
rc = mdb_txn_renew0(txn);
|
|
if (rc == MDB_SUCCESS) {
|
|
DPRINTF(("renew txn %"Z"u%c %p on mdbenv %p, root page %"Z"u",
|
|
txn->mt_txnid, (txn->mt_flags & MDB_TXN_RDONLY) ? 'r' : 'w',
|
|
(void *)txn, (void *)txn->mt_env, txn->mt_dbs[MAIN_DBI].md_root));
|
|
}
|
|
return rc;
|
|
*/
|
|
return nil
|
|
}
|
|
|
|
func (t *Transaction) DB() *DB {
|
|
return t.db
|
|
}
|
|
|
|
// Export or close DBI handles opened in this txn.
|
|
func (t *Transaction) updateBuckets(keep bool) {
|
|
/*
|
|
int i;
|
|
MDB_dbi n = txn->mt_numdbs;
|
|
MDB_env *env = txn->mt_env;
|
|
unsigned char *tdbflags = txn->mt_dbflags;
|
|
|
|
for (i = n; --i >= 2;) {
|
|
if (tdbflags[i] & DB_NEW) {
|
|
if (keep) {
|
|
env->me_dbflags[i] = txn->mt_dbs[i].md_flags | MDB_VALID;
|
|
} else {
|
|
char *ptr = env->me_dbxs[i].md_name.mv_data;
|
|
env->me_dbxs[i].md_name.mv_data = NULL;
|
|
env->me_dbxs[i].md_name.mv_size = 0;
|
|
env->me_dbflags[i] = 0;
|
|
free(ptr);
|
|
}
|
|
}
|
|
}
|
|
if (keep && env->me_numdbs < n)
|
|
env->me_numdbs = n;
|
|
*/
|
|
}
|
|
|
|
// Common code for #mdb_txn_reset() and #mdb_txn_abort().
|
|
// May be called twice for readonly txns: First reset it, then abort.
|
|
// @param[in] txn the transaction handle to reset
|
|
// @param[in] act why the transaction is being reset
|
|
func (t *Transaction) reset(act string) {
|
|
/*
|
|
MDB_env *env = txn->mt_env;
|
|
|
|
// Close any DBI handles opened in this txn
|
|
mdb_dbis_update(txn, 0);
|
|
|
|
DPRINTF(("%s txn %"Z"u%c %p on mdbenv %p, root page %"Z"u",
|
|
act, txn->mt_txnid, (txn->mt_flags & MDB_TXN_RDONLY) ? 'r' : 'w',
|
|
(void *) txn, (void *)env, txn->mt_dbs[MAIN_DBI].md_root));
|
|
|
|
if (F_ISSET(txn->mt_flags, MDB_TXN_RDONLY)) {
|
|
if (txn->mt_u.reader) {
|
|
txn->mt_u.reader->mr_txnid = (txnid_t)-1;
|
|
if (!(env->me_flags & MDB_NOTLS))
|
|
txn->mt_u.reader = NULL; // txn does not own reader
|
|
}
|
|
txn->mt_numdbs = 0; // close nothing if called again
|
|
txn->mt_dbxs = NULL; // mark txn as reset
|
|
} else {
|
|
mdb_cursors_close(txn, 0);
|
|
|
|
if (!(env->me_flags & MDB_WRITEMAP)) {
|
|
mdb_dlist_free(txn);
|
|
}
|
|
mdb_midl_free(env->me_pghead);
|
|
|
|
if (txn->mt_parent) {
|
|
txn->mt_parent->mt_child = NULL;
|
|
env->me_pgstate = ((MDB_ntxn *)txn)->mnt_pgstate;
|
|
mdb_midl_free(txn->mt_free_pgs);
|
|
mdb_midl_free(txn->mt_spill_pgs);
|
|
free(txn->mt_u.dirty_list);
|
|
return;
|
|
}
|
|
|
|
if (mdb_midl_shrink(&txn->mt_free_pgs))
|
|
env->me_free_pgs = txn->mt_free_pgs;
|
|
env->me_pghead = NULL;
|
|
env->me_pglast = 0;
|
|
|
|
env->me_txn = NULL;
|
|
// The writer mutex was locked in mdb_txn_begin.
|
|
if (env->me_txns)
|
|
UNLOCK_MUTEX_W(env);
|
|
}
|
|
*/
|
|
}
|
|
|
|
func (t *Transaction) Reset() {
|
|
/*
|
|
if (txn == NULL)
|
|
return;
|
|
|
|
// This call is only valid for read-only txns
|
|
if (!(txn->mt_flags & MDB_TXN_RDONLY))
|
|
return;
|
|
|
|
mdb_txn_reset0(txn, "reset");
|
|
*/
|
|
}
|
|
|
|
func (t *Transaction) Abort() {
|
|
/*
|
|
if (txn == NULL)
|
|
return;
|
|
|
|
if (txn->mt_child)
|
|
mdb_txn_abort(txn->mt_child);
|
|
|
|
mdb_txn_reset0(txn, "abort");
|
|
// Free reader slot tied to this txn (if MDB_NOTLS && writable FS)
|
|
if ((txn->mt_flags & MDB_TXN_RDONLY) && txn->mt_u.reader)
|
|
txn->mt_u.reader->mr_pid = 0;
|
|
|
|
free(txn);
|
|
*/
|
|
}
|
|
|
|
// Save the freelist as of this transaction to the freeDB.
|
|
// This changes the freelist. Keep trying until it stabilizes.
|
|
func (t *Transaction) saveFreelist() error {
|
|
/*
|
|
// env->me_pghead[] can grow and shrink during this call.
|
|
// env->me_pglast and txn->mt_free_pgs[] can only grow.
|
|
// Page numbers cannot disappear from txn->mt_free_pgs[].
|
|
MDB_cursor mc;
|
|
MDB_env *env = txn->mt_env;
|
|
int rc, maxfree_1pg = env->me_maxfree_1pg, more = 1;
|
|
txnid_t pglast = 0, head_id = 0;
|
|
pgno_t freecnt = 0, *free_pgs, *mop;
|
|
ssize_t head_room = 0, total_room = 0, mop_len, clean_limit;
|
|
|
|
mdb_cursor_init(&mc, txn, FREE_DBI, NULL);
|
|
|
|
if (env->me_pghead) {
|
|
// Make sure first page of freeDB is touched and on freelist
|
|
rc = mdb_page_search(&mc, NULL, MDB_PS_FIRST|MDB_PS_MODIFY);
|
|
if (rc && rc != MDB_NOTFOUND)
|
|
return rc;
|
|
}
|
|
|
|
// MDB_RESERVE cancels meminit in ovpage malloc (when no WRITEMAP)
|
|
clean_limit = (env->me_flags & (MDB_NOMEMINIT|MDB_WRITEMAP))
|
|
? SSIZE_MAX : maxfree_1pg;
|
|
|
|
for (;;) {
|
|
// Come back here after each Put() in case freelist changed
|
|
MDB_val key, data;
|
|
pgno_t *pgs;
|
|
ssize_t j;
|
|
|
|
// If using records from freeDB which we have not yet
|
|
// deleted, delete them and any we reserved for me_pghead.
|
|
while (pglast < env->me_pglast) {
|
|
rc = mdb_cursor_first(&mc, &key, NULL);
|
|
if (rc)
|
|
return rc;
|
|
pglast = head_id = *(txnid_t *)key.mv_data;
|
|
total_room = head_room = 0;
|
|
mdb_tassert(txn, pglast <= env->me_pglast);
|
|
rc = mdb_cursor_del(&mc, 0);
|
|
if (rc)
|
|
return rc;
|
|
}
|
|
|
|
// Save the IDL of pages freed by this txn, to a single record
|
|
if (freecnt < txn->mt_free_pgs[0]) {
|
|
if (!freecnt) {
|
|
// Make sure last page of freeDB is touched and on freelist
|
|
rc = mdb_page_search(&mc, NULL, MDB_PS_LAST|MDB_PS_MODIFY);
|
|
if (rc && rc != MDB_NOTFOUND)
|
|
return rc;
|
|
}
|
|
free_pgs = txn->mt_free_pgs;
|
|
// Write to last page of freeDB
|
|
key.mv_size = sizeof(txn->mt_txnid);
|
|
key.mv_data = &txn->mt_txnid;
|
|
do {
|
|
freecnt = free_pgs[0];
|
|
data.mv_size = MDB_IDL_SIZEOF(free_pgs);
|
|
rc = mdb_cursor_put(&mc, &key, &data, MDB_RESERVE);
|
|
if (rc)
|
|
return rc;
|
|
// Retry if mt_free_pgs[] grew during the Put()
|
|
free_pgs = txn->mt_free_pgs;
|
|
} while (freecnt < free_pgs[0]);
|
|
mdb_midl_sort(free_pgs);
|
|
memcpy(data.mv_data, free_pgs, data.mv_size);
|
|
#if (MDB_DEBUG) > 1
|
|
{
|
|
unsigned int i = free_pgs[0];
|
|
DPRINTF(("IDL write txn %"Z"u root %"Z"u num %u",
|
|
txn->mt_txnid, txn->mt_dbs[FREE_DBI].md_root, i));
|
|
for (; i; i--)
|
|
DPRINTF(("IDL %"Z"u", free_pgs[i]));
|
|
}
|
|
#endif
|
|
continue;
|
|
}
|
|
|
|
mop = env->me_pghead;
|
|
mop_len = mop ? mop[0] : 0;
|
|
|
|
// Reserve records for me_pghead[]. Split it if multi-page,
|
|
// to avoid searching freeDB for a page range. Use keys in
|
|
// range [1,me_pglast]: Smaller than txnid of oldest reader.
|
|
if (total_room >= mop_len) {
|
|
if (total_room == mop_len || --more < 0)
|
|
break;
|
|
} else if (head_room >= maxfree_1pg && head_id > 1) {
|
|
// Keep current record (overflow page), add a new one
|
|
head_id--;
|
|
head_room = 0;
|
|
}
|
|
// (Re)write {key = head_id, IDL length = head_room}
|
|
total_room -= head_room;
|
|
head_room = mop_len - total_room;
|
|
if (head_room > maxfree_1pg && head_id > 1) {
|
|
// Overflow multi-page for part of me_pghead
|
|
head_room /= head_id; // amortize page sizes
|
|
head_room += maxfree_1pg - head_room % (maxfree_1pg + 1);
|
|
} else if (head_room < 0) {
|
|
// Rare case, not bothering to delete this record
|
|
head_room = 0;
|
|
}
|
|
key.mv_size = sizeof(head_id);
|
|
key.mv_data = &head_id;
|
|
data.mv_size = (head_room + 1) * sizeof(pgno_t);
|
|
rc = mdb_cursor_put(&mc, &key, &data, MDB_RESERVE);
|
|
if (rc)
|
|
return rc;
|
|
// IDL is initially empty, zero out at least the length
|
|
pgs = (pgno_t *)data.mv_data;
|
|
j = head_room > clean_limit ? head_room : 0;
|
|
do {
|
|
pgs[j] = 0;
|
|
} while (--j >= 0);
|
|
total_room += head_room;
|
|
}
|
|
|
|
// Fill in the reserved me_pghead records
|
|
rc = MDB_SUCCESS;
|
|
if (mop_len) {
|
|
MDB_val key, data;
|
|
|
|
mop += mop_len;
|
|
rc = mdb_cursor_first(&mc, &key, &data);
|
|
for (; !rc; rc = mdb_cursor_next(&mc, &key, &data, MDB_NEXT)) {
|
|
unsigned flags = MDB_CURRENT;
|
|
txnid_t id = *(txnid_t *)key.mv_data;
|
|
ssize_t len = (ssize_t)(data.mv_size / sizeof(MDB_ID)) - 1;
|
|
MDB_ID save;
|
|
|
|
mdb_tassert(txn, len >= 0 && id <= env->me_pglast);
|
|
key.mv_data = &id;
|
|
if (len > mop_len) {
|
|
len = mop_len;
|
|
data.mv_size = (len + 1) * sizeof(MDB_ID);
|
|
flags = 0;
|
|
}
|
|
data.mv_data = mop -= len;
|
|
save = mop[0];
|
|
mop[0] = len;
|
|
rc = mdb_cursor_put(&mc, &key, &data, flags);
|
|
mop[0] = save;
|
|
if (rc || !(mop_len -= len))
|
|
break;
|
|
}
|
|
}
|
|
return rc;
|
|
*/
|
|
return nil
|
|
}
|
|
|
|
// Flush (some) dirty pages to the map, after clearing their dirty flag.
|
|
// @param[in] txn the transaction that's being committed
|
|
// @param[in] keep number of initial pages in dirty_list to keep dirty.
|
|
// @return 0 on success, non-zero on failure.
|
|
func (t *Transaction) flush(keep bool) error {
|
|
/*
|
|
MDB_env *env = txn->mt_env;
|
|
MDB_ID2L dl = txn->mt_u.dirty_list;
|
|
unsigned psize = env->me_psize, j;
|
|
int i, pagecount = dl[0].mid, rc;
|
|
size_t size = 0, pos = 0;
|
|
pgno_t pgno = 0;
|
|
MDB_page *dp = NULL;
|
|
#ifdef _WIN32
|
|
OVERLAPPED ov;
|
|
#else
|
|
struct iovec iov[MDB_COMMIT_PAGES];
|
|
ssize_t wpos = 0, wsize = 0, wres;
|
|
size_t next_pos = 1; // impossible pos, so pos != next_pos
|
|
int n = 0;
|
|
#endif
|
|
|
|
j = i = keep;
|
|
|
|
if (env->me_flags & MDB_WRITEMAP) {
|
|
// Clear dirty flags
|
|
while (++i <= pagecount) {
|
|
dp = dl[i].mptr;
|
|
// Don't flush this page yet
|
|
if (dp->mp_flags & P_KEEP) {
|
|
dp->mp_flags ^= P_KEEP;
|
|
dl[++j] = dl[i];
|
|
continue;
|
|
}
|
|
dp->mp_flags &= ~P_DIRTY;
|
|
}
|
|
goto done;
|
|
}
|
|
|
|
// Write the pages
|
|
for (;;) {
|
|
if (++i <= pagecount) {
|
|
dp = dl[i].mptr;
|
|
// Don't flush this page yet
|
|
if (dp->mp_flags & P_KEEP) {
|
|
dp->mp_flags ^= P_KEEP;
|
|
dl[i].mid = 0;
|
|
continue;
|
|
}
|
|
pgno = dl[i].mid;
|
|
// clear dirty flag
|
|
dp->mp_flags &= ~P_DIRTY;
|
|
pos = pgno * psize;
|
|
size = psize;
|
|
if (IS_OVERFLOW(dp)) size *= dp->mp_pages;
|
|
}
|
|
#ifdef _WIN32
|
|
else break;
|
|
|
|
// Windows actually supports scatter/gather I/O, but only on
|
|
// unbuffered file handles. Since we're relying on the OS page
|
|
// cache for all our data, that's self-defeating. So we just
|
|
// write pages one at a time. We use the ov structure to set
|
|
// the write offset, to at least save the overhead of a Seek
|
|
// system call.
|
|
DPRINTF(("committing page %"Z"u", pgno));
|
|
memset(&ov, 0, sizeof(ov));
|
|
ov.Offset = pos & 0xffffffff;
|
|
ov.OffsetHigh = pos >> 16 >> 16;
|
|
if (!WriteFile(env->me_fd, dp, size, NULL, &ov)) {
|
|
rc = ErrCode();
|
|
DPRINTF(("WriteFile: %d", rc));
|
|
return rc;
|
|
}
|
|
#else
|
|
// Write up to MDB_COMMIT_PAGES dirty pages at a time.
|
|
if (pos!=next_pos || n==MDB_COMMIT_PAGES || wsize+size>MAX_WRITE) {
|
|
if (n) {
|
|
// Write previous page(s)
|
|
#ifdef MDB_USE_PWRITEV
|
|
wres = pwritev(env->me_fd, iov, n, wpos);
|
|
#else
|
|
if (n == 1) {
|
|
wres = pwrite(env->me_fd, iov[0].iov_base, wsize, wpos);
|
|
} else {
|
|
if (lseek(env->me_fd, wpos, SEEK_SET) == -1) {
|
|
rc = ErrCode();
|
|
DPRINTF(("lseek: %s", strerror(rc)));
|
|
return rc;
|
|
}
|
|
wres = writev(env->me_fd, iov, n);
|
|
}
|
|
#endif
|
|
if (wres != wsize) {
|
|
if (wres < 0) {
|
|
rc = ErrCode();
|
|
DPRINTF(("Write error: %s", strerror(rc)));
|
|
} else {
|
|
rc = EIO; // TODO: Use which error code?
|
|
DPUTS("short write, filesystem full?");
|
|
}
|
|
return rc;
|
|
}
|
|
n = 0;
|
|
}
|
|
if (i > pagecount)
|
|
break;
|
|
wpos = pos;
|
|
wsize = 0;
|
|
}
|
|
DPRINTF(("committing page %"Z"u", pgno));
|
|
next_pos = pos + size;
|
|
iov[n].iov_len = size;
|
|
iov[n].iov_base = (char *)dp;
|
|
wsize += size;
|
|
n++;
|
|
#endif // _WIN32
|
|
}
|
|
|
|
for (i = keep; ++i <= pagecount; ) {
|
|
dp = dl[i].mptr;
|
|
// This is a page we skipped above
|
|
if (!dl[i].mid) {
|
|
dl[++j] = dl[i];
|
|
dl[j].mid = dp->mp_pgno;
|
|
continue;
|
|
}
|
|
mdb_dpage_free(env, dp);
|
|
}
|
|
|
|
done:
|
|
i--;
|
|
txn->mt_dirty_room += i - j;
|
|
dl[0].mid = j;
|
|
return MDB_SUCCESS;
|
|
}
|
|
|
|
int
|
|
mdb_txn_commit(MDB_txn *txn)
|
|
{
|
|
int rc;
|
|
unsigned int i;
|
|
MDB_env *env;
|
|
|
|
if (txn == NULL || txn->mt_env == NULL)
|
|
return EINVAL;
|
|
|
|
if (txn->mt_child) {
|
|
rc = mdb_txn_commit(txn->mt_child);
|
|
txn->mt_child = NULL;
|
|
if (rc)
|
|
goto fail;
|
|
}
|
|
|
|
env = txn->mt_env;
|
|
|
|
if (F_ISSET(txn->mt_flags, MDB_TXN_RDONLY)) {
|
|
mdb_dbis_update(txn, 1);
|
|
txn->mt_numdbs = 2; // so txn_abort() doesn't close any new handles
|
|
mdb_txn_abort(txn);
|
|
return MDB_SUCCESS;
|
|
}
|
|
|
|
if (F_ISSET(txn->mt_flags, MDB_TXN_ERROR)) {
|
|
DPUTS("error flag is set, can't commit");
|
|
if (txn->mt_parent)
|
|
txn->mt_parent->mt_flags |= MDB_TXN_ERROR;
|
|
rc = MDB_BAD_TXN;
|
|
goto fail;
|
|
}
|
|
|
|
if (txn->mt_parent) {
|
|
MDB_txn *parent = txn->mt_parent;
|
|
MDB_ID2L dst, src;
|
|
MDB_IDL pspill;
|
|
unsigned x, y, len, ps_len;
|
|
|
|
// Append our free list to parent's
|
|
rc = mdb_midl_append_list(&parent->mt_free_pgs, txn->mt_free_pgs);
|
|
if (rc)
|
|
goto fail;
|
|
mdb_midl_free(txn->mt_free_pgs);
|
|
// Failures after this must either undo the changes
|
|
// to the parent or set MDB_TXN_ERROR in the parent.
|
|
|
|
parent->mt_next_pgno = txn->mt_next_pgno;
|
|
parent->mt_flags = txn->mt_flags;
|
|
|
|
// Merge our cursors into parent's and close them
|
|
mdb_cursors_close(txn, 1);
|
|
|
|
// Update parent's DB table.
|
|
memcpy(parent->mt_dbs, txn->mt_dbs, txn->mt_numdbs * sizeof(MDB_db));
|
|
parent->mt_numdbs = txn->mt_numdbs;
|
|
parent->mt_dbflags[0] = txn->mt_dbflags[0];
|
|
parent->mt_dbflags[1] = txn->mt_dbflags[1];
|
|
for (i=2; i<txn->mt_numdbs; i++) {
|
|
// preserve parent's DB_NEW status
|
|
x = parent->mt_dbflags[i] & DB_NEW;
|
|
parent->mt_dbflags[i] = txn->mt_dbflags[i] | x;
|
|
}
|
|
|
|
dst = parent->mt_u.dirty_list;
|
|
src = txn->mt_u.dirty_list;
|
|
// Remove anything in our dirty list from parent's spill list
|
|
if ((pspill = parent->mt_spill_pgs) && (ps_len = pspill[0])) {
|
|
x = y = ps_len;
|
|
pspill[0] = (pgno_t)-1;
|
|
// Mark our dirty pages as deleted in parent spill list
|
|
for (i=0, len=src[0].mid; ++i <= len; ) {
|
|
MDB_ID pn = src[i].mid << 1;
|
|
while (pn > pspill[x])
|
|
x--;
|
|
if (pn == pspill[x]) {
|
|
pspill[x] = 1;
|
|
y = --x;
|
|
}
|
|
}
|
|
// Squash deleted pagenums if we deleted any
|
|
for (x=y; ++x <= ps_len; )
|
|
if (!(pspill[x] & 1))
|
|
pspill[++y] = pspill[x];
|
|
pspill[0] = y;
|
|
}
|
|
|
|
// Find len = length of merging our dirty list with parent's
|
|
x = dst[0].mid;
|
|
dst[0].mid = 0; // simplify loops
|
|
if (parent->mt_parent) {
|
|
len = x + src[0].mid;
|
|
y = mdb_mid2l_search(src, dst[x].mid + 1) - 1;
|
|
for (i = x; y && i; y--) {
|
|
pgno_t yp = src[y].mid;
|
|
while (yp < dst[i].mid)
|
|
i--;
|
|
if (yp == dst[i].mid) {
|
|
i--;
|
|
len--;
|
|
}
|
|
}
|
|
} else { // Simplify the above for single-ancestor case
|
|
len = MDB_IDL_UM_MAX - txn->mt_dirty_room;
|
|
}
|
|
// Merge our dirty list with parent's
|
|
y = src[0].mid;
|
|
for (i = len; y; dst[i--] = src[y--]) {
|
|
pgno_t yp = src[y].mid;
|
|
while (yp < dst[x].mid)
|
|
dst[i--] = dst[x--];
|
|
if (yp == dst[x].mid)
|
|
free(dst[x--].mptr);
|
|
}
|
|
mdb_tassert(txn, i == x);
|
|
dst[0].mid = len;
|
|
free(txn->mt_u.dirty_list);
|
|
parent->mt_dirty_room = txn->mt_dirty_room;
|
|
if (txn->mt_spill_pgs) {
|
|
if (parent->mt_spill_pgs) {
|
|
// TODO: Prevent failure here, so parent does not fail
|
|
rc = mdb_midl_append_list(&parent->mt_spill_pgs, txn->mt_spill_pgs);
|
|
if (rc)
|
|
parent->mt_flags |= MDB_TXN_ERROR;
|
|
mdb_midl_free(txn->mt_spill_pgs);
|
|
mdb_midl_sort(parent->mt_spill_pgs);
|
|
} else {
|
|
parent->mt_spill_pgs = txn->mt_spill_pgs;
|
|
}
|
|
}
|
|
|
|
parent->mt_child = NULL;
|
|
mdb_midl_free(((MDB_ntxn *)txn)->mnt_pgstate.mf_pghead);
|
|
free(txn);
|
|
return rc;
|
|
}
|
|
|
|
if (txn != env->me_txn) {
|
|
DPUTS("attempt to commit unknown transaction");
|
|
rc = EINVAL;
|
|
goto fail;
|
|
}
|
|
|
|
mdb_cursors_close(txn, 0);
|
|
|
|
if (!txn->mt_u.dirty_list[0].mid &&
|
|
!(txn->mt_flags & (MDB_TXN_DIRTY|MDB_TXN_SPILLS)))
|
|
goto done;
|
|
|
|
DPRINTF(("committing txn %"Z"u %p on mdbenv %p, root page %"Z"u",
|
|
txn->mt_txnid, (void*)txn, (void*)env, txn->mt_dbs[MAIN_DBI].md_root));
|
|
|
|
// Update DB root pointers
|
|
if (txn->mt_numdbs > 2) {
|
|
MDB_cursor mc;
|
|
MDB_dbi i;
|
|
MDB_val data;
|
|
data.mv_size = sizeof(MDB_db);
|
|
|
|
mdb_cursor_init(&mc, txn, MAIN_DBI, NULL);
|
|
for (i = 2; i < txn->mt_numdbs; i++) {
|
|
if (txn->mt_dbflags[i] & DB_DIRTY) {
|
|
data.mv_data = &txn->mt_dbs[i];
|
|
rc = mdb_cursor_put(&mc, &txn->mt_dbxs[i].md_name, &data, 0);
|
|
if (rc)
|
|
goto fail;
|
|
}
|
|
}
|
|
}
|
|
|
|
rc = mdb_freelist_save(txn);
|
|
if (rc)
|
|
goto fail;
|
|
|
|
mdb_midl_free(env->me_pghead);
|
|
env->me_pghead = NULL;
|
|
if (mdb_midl_shrink(&txn->mt_free_pgs))
|
|
env->me_free_pgs = txn->mt_free_pgs;
|
|
|
|
#if (MDB_DEBUG) > 2
|
|
mdb_audit(txn);
|
|
#endif
|
|
|
|
if ((rc = mdb_page_flush(txn, 0)) ||
|
|
(rc = mdb_env_sync(env, 0)) ||
|
|
(rc = mdb_env_write_meta(txn)))
|
|
goto fail;
|
|
|
|
done:
|
|
env->me_pglast = 0;
|
|
env->me_txn = NULL;
|
|
mdb_dbis_update(txn, 1);
|
|
|
|
if (env->me_txns)
|
|
UNLOCK_MUTEX_W(env);
|
|
free(txn);
|
|
|
|
return MDB_SUCCESS;
|
|
|
|
fail:
|
|
mdb_txn_abort(txn);
|
|
return rc;
|
|
*/
|
|
return nil
|
|
}
|
|
|
|
// Update the environment info to commit a transaction.
|
|
// @param[in] txn the transaction that's being committed
|
|
// @return 0 on success, non-zero on failure.
|
|
func (t *Transaction) writeMeta() error {
|
|
/*
|
|
MDB_env *env;
|
|
MDB_meta meta, metab, *mp;
|
|
off_t off;
|
|
int rc, len, toggle;
|
|
char *ptr;
|
|
HANDLE mfd;
|
|
#ifdef _WIN32
|
|
OVERLAPPED ov;
|
|
#else
|
|
int r2;
|
|
#endif
|
|
|
|
toggle = txn->mt_txnid & 1;
|
|
DPRINTF(("writing meta page %d for root page %"Z"u",
|
|
toggle, txn->mt_dbs[MAIN_DBI].md_root));
|
|
|
|
env = txn->mt_env;
|
|
mp = env->me_metas[toggle];
|
|
|
|
if (env->me_flags & MDB_WRITEMAP) {
|
|
// Persist any increases of mapsize config
|
|
if (env->me_mapsize > mp->mm_mapsize)
|
|
mp->mm_mapsize = env->me_mapsize;
|
|
mp->mm_dbs[0] = txn->mt_dbs[0];
|
|
mp->mm_dbs[1] = txn->mt_dbs[1];
|
|
mp->mm_last_pg = txn->mt_next_pgno - 1;
|
|
mp->mm_txnid = txn->mt_txnid;
|
|
if (!(env->me_flags & (MDB_NOMETASYNC|MDB_NOSYNC))) {
|
|
unsigned meta_size = env->me_psize;
|
|
rc = (env->me_flags & MDB_MAPASYNC) ? MS_ASYNC : MS_SYNC;
|
|
ptr = env->me_map;
|
|
if (toggle) {
|
|
#ifndef _WIN32 // POSIX msync() requires ptr = start of OS page
|
|
if (meta_size < env->me_os_psize)
|
|
meta_size += meta_size;
|
|
else
|
|
#endif
|
|
ptr += meta_size;
|
|
}
|
|
if (MDB_MSYNC(ptr, meta_size, rc)) {
|
|
rc = ErrCode();
|
|
goto fail;
|
|
}
|
|
}
|
|
goto done;
|
|
}
|
|
metab.mm_txnid = env->me_metas[toggle]->mm_txnid;
|
|
metab.mm_last_pg = env->me_metas[toggle]->mm_last_pg;
|
|
|
|
ptr = (char *)&meta;
|
|
if (env->me_mapsize > mp->mm_mapsize) {
|
|
// Persist any increases of mapsize config
|
|
meta.mm_mapsize = env->me_mapsize;
|
|
off = offsetof(MDB_meta, mm_mapsize);
|
|
} else {
|
|
off = offsetof(MDB_meta, mm_dbs[0].md_depth);
|
|
}
|
|
len = sizeof(MDB_meta) - off;
|
|
|
|
ptr += off;
|
|
meta.mm_dbs[0] = txn->mt_dbs[0];
|
|
meta.mm_dbs[1] = txn->mt_dbs[1];
|
|
meta.mm_last_pg = txn->mt_next_pgno - 1;
|
|
meta.mm_txnid = txn->mt_txnid;
|
|
|
|
if (toggle)
|
|
off += env->me_psize;
|
|
off += PAGEHDRSZ;
|
|
|
|
// Write to the SYNC fd
|
|
mfd = env->me_flags & (MDB_NOSYNC|MDB_NOMETASYNC) ?
|
|
env->me_fd : env->me_mfd;
|
|
#ifdef _WIN32
|
|
{
|
|
memset(&ov, 0, sizeof(ov));
|
|
ov.Offset = off;
|
|
if (!WriteFile(mfd, ptr, len, (DWORD *)&rc, &ov))
|
|
rc = -1;
|
|
}
|
|
#else
|
|
rc = pwrite(mfd, ptr, len, off);
|
|
#endif
|
|
if (rc != len) {
|
|
rc = rc < 0 ? ErrCode() : EIO;
|
|
DPUTS("write failed, disk error?");
|
|
// On a failure, the pagecache still contains the new data.
|
|
// Write some old data back, to prevent it from being used.
|
|
// Use the non-SYNC fd; we know it will fail anyway.
|
|
meta.mm_last_pg = metab.mm_last_pg;
|
|
meta.mm_txnid = metab.mm_txnid;
|
|
#ifdef _WIN32
|
|
memset(&ov, 0, sizeof(ov));
|
|
ov.Offset = off;
|
|
WriteFile(env->me_fd, ptr, len, NULL, &ov);
|
|
#else
|
|
r2 = pwrite(env->me_fd, ptr, len, off);
|
|
(void)r2; // Silence warnings. We don't care about pwrite's return value
|
|
#endif
|
|
fail:
|
|
env->me_flags |= MDB_FATAL_ERROR;
|
|
return rc;
|
|
}
|
|
done:
|
|
// Memory ordering issues are irrelevant; since the entire writer
|
|
// is wrapped by wmutex, all of these changes will become visible
|
|
// after the wmutex is unlocked. Since the DB is multi-version,
|
|
// readers will get consistent data regardless of how fresh or
|
|
// how stale their view of these values is.
|
|
if (env->me_txns)
|
|
env->me_txns->mti_txnid = txn->mt_txnid;
|
|
|
|
return MDB_SUCCESS;
|
|
*/
|
|
return nil
|
|
}
|
|
|
|
// Find the address of the page corresponding to a given page number.
|
|
// @param[in] txn the transaction for this access.
|
|
// @param[in] pgno the page number for the page to retrieve.
|
|
// @param[out] ret address of a pointer where the page's address will be stored.
|
|
// @param[out] lvl dirty_list inheritance level of found page. 1=current txn, 0=mapped page.
|
|
// @return 0 on success, non-zero on failure.
|
|
func (t *Transaction) getPage(id int) (*page, int, error) {
|
|
/*
|
|
MDB_env *env = txn->mt_env;
|
|
MDB_page *p = NULL;
|
|
int level;
|
|
|
|
if (!((txn->mt_flags & MDB_TXN_RDONLY) | (env->me_flags & MDB_WRITEMAP))) {
|
|
MDB_txn *tx2 = txn;
|
|
level = 1;
|
|
do {
|
|
MDB_ID2L dl = tx2->mt_u.dirty_list;
|
|
unsigned x;
|
|
// Spilled pages were dirtied in this txn and flushed
|
|
// because the dirty list got full. Bring this page
|
|
// back in from the map (but don't unspill it here,
|
|
// leave that unless page_touch happens again).
|
|
if (tx2->mt_spill_pgs) {
|
|
MDB_ID pn = pgno << 1;
|
|
x = mdb_midl_search(tx2->mt_spill_pgs, pn);
|
|
if (x <= tx2->mt_spill_pgs[0] && tx2->mt_spill_pgs[x] == pn) {
|
|
p = (MDB_page *)(env->me_map + env->me_psize * pgno);
|
|
goto done;
|
|
}
|
|
}
|
|
if (dl[0].mid) {
|
|
unsigned x = mdb_mid2l_search(dl, pgno);
|
|
if (x <= dl[0].mid && dl[x].mid == pgno) {
|
|
p = dl[x].mptr;
|
|
goto done;
|
|
}
|
|
}
|
|
level++;
|
|
} while ((tx2 = tx2->mt_parent) != NULL);
|
|
}
|
|
|
|
if (pgno < txn->mt_next_pgno) {
|
|
level = 0;
|
|
p = (MDB_page *)(env->me_map + env->me_psize * pgno);
|
|
} else {
|
|
DPRINTF(("page %"Z"u not found", pgno));
|
|
txn->mt_flags |= MDB_TXN_ERROR;
|
|
return MDB_PAGE_NOTFOUND;
|
|
}
|
|
|
|
done:
|
|
*ret = p;
|
|
if (lvl)
|
|
*lvl = level;
|
|
return MDB_SUCCESS;
|
|
*/
|
|
|
|
return nil, 0, nil
|
|
}
|
|
|
|
// Return the data associated with a given node.
|
|
// @param[in] txn The transaction for this operation.
|
|
// @param[in] leaf The node being read.
|
|
// @param[out] data Updated to point to the node's data.
|
|
// @return 0 on success, non-zero on failure.
|
|
func (t *Transaction) readNode(leaf *node, data []byte) error {
|
|
/*
|
|
MDB_page *omp; // overflow page
|
|
pgno_t pgno;
|
|
int rc;
|
|
|
|
if (!F_ISSET(leaf->mn_flags, F_BIGDATA)) {
|
|
data->mv_size = NODEDSZ(leaf);
|
|
data->mv_data = NODEDATA(leaf);
|
|
return MDB_SUCCESS;
|
|
}
|
|
|
|
// Read overflow data.
|
|
data->mv_size = NODEDSZ(leaf);
|
|
memcpy(&pgno, NODEDATA(leaf), sizeof(pgno));
|
|
if ((rc = mdb_page_get(txn, pgno, &omp, NULL)) != 0) {
|
|
DPRINTF(("read overflow page %"Z"u failed", pgno));
|
|
return rc;
|
|
}
|
|
data->mv_data = METADATA(omp);
|
|
|
|
return MDB_SUCCESS;
|
|
*/
|
|
return nil
|
|
}
|
|
|
|
func (t *Transaction) Get(bucket Bucket, key []byte) ([]byte, error) {
|
|
/*
|
|
MDB_cursor mc;
|
|
MDB_xcursor mx;
|
|
int exact = 0;
|
|
DKBUF;
|
|
|
|
if (key == NULL || data == NULL)
|
|
return EINVAL;
|
|
|
|
DPRINTF(("===> get db %u key [%s]", dbi, DKEY(key)));
|
|
|
|
if (txn == NULL || !dbi || dbi >= txn->mt_numdbs || !(txn->mt_dbflags[dbi] & DB_VALID))
|
|
return EINVAL;
|
|
|
|
if (txn->mt_flags & MDB_TXN_ERROR)
|
|
return MDB_BAD_TXN;
|
|
|
|
mdb_cursor_init(&mc, txn, dbi, &mx);
|
|
return mdb_cursor_set(&mc, key, data, MDB_SET, &exact);
|
|
*/
|
|
return nil, nil
|
|
}
|
|
|
|
func (t *Transaction) Renew1(c Cursor) error {
|
|
/*
|
|
if (txn == NULL || mc == NULL || mc->mc_dbi >= txn->mt_numdbs)
|
|
return EINVAL;
|
|
|
|
if ((mc->mc_flags & C_UNTRACK) || txn->mt_cursors)
|
|
return EINVAL;
|
|
|
|
mdb_cursor_init(mc, txn, mc->mc_dbi, mc->mc_xcursor);
|
|
return MDB_SUCCESS;
|
|
*/
|
|
return nil
|
|
}
|
|
|
|
func (t *Transaction) Delete(b *Bucket, key []byte, data []byte) error {
|
|
/*
|
|
MDB_cursor mc;
|
|
MDB_xcursor mx;
|
|
MDB_cursor_op op;
|
|
MDB_val rdata, *xdata;
|
|
int rc, exact;
|
|
DKBUF;
|
|
|
|
if (key == NULL)
|
|
return EINVAL;
|
|
|
|
DPRINTF(("====> delete db %u key [%s]", dbi, DKEY(key)));
|
|
|
|
if (txn == NULL || !dbi || dbi >= txn->mt_numdbs || !(txn->mt_dbflags[dbi] & DB_VALID))
|
|
return EINVAL;
|
|
|
|
if (txn->mt_flags & (MDB_TXN_RDONLY|MDB_TXN_ERROR))
|
|
return (txn->mt_flags & MDB_TXN_RDONLY) ? EACCES : MDB_BAD_TXN;
|
|
|
|
mdb_cursor_init(&mc, txn, dbi, &mx);
|
|
|
|
exact = 0;
|
|
if (!F_ISSET(txn->mt_dbs[dbi].md_flags, MDB_DUPSORT)) {
|
|
// must ignore any data
|
|
data = NULL;
|
|
}
|
|
if (data) {
|
|
op = MDB_GET_BOTH;
|
|
rdata = *data;
|
|
xdata = &rdata;
|
|
} else {
|
|
op = MDB_SET;
|
|
xdata = NULL;
|
|
}
|
|
rc = mdb_cursor_set(&mc, key, xdata, op, &exact);
|
|
if (rc == 0) {
|
|
// let mdb_page_split know about this cursor if needed:
|
|
// delete will trigger a rebalance; if it needs to move
|
|
// a node from one page to another, it will have to
|
|
// update the parent's separator key(s). If the new sepkey
|
|
// is larger than the current one, the parent page may
|
|
// run out of space, triggering a split. We need this
|
|
// cursor to be consistent until the end of the rebalance.
|
|
mc.mc_flags |= C_UNTRACK;
|
|
mc.mc_next = txn->mt_cursors[dbi];
|
|
txn->mt_cursors[dbi] = &mc;
|
|
rc = mdb_cursor_del(&mc, data ? 0 : MDB_NODUPDATA);
|
|
txn->mt_cursors[dbi] = mc.mc_next;
|
|
}
|
|
return rc;
|
|
*/
|
|
return nil
|
|
}
|
|
|
|
func (t *Transaction) Put(b Bucket, key []byte, data []byte, flags int) error {
|
|
/*
|
|
MDB_cursor mc;
|
|
MDB_xcursor mx;
|
|
|
|
if (key == NULL || data == NULL)
|
|
return EINVAL;
|
|
|
|
if (txn == NULL || !dbi || dbi >= txn->mt_numdbs || !(txn->mt_dbflags[dbi] & DB_VALID))
|
|
return EINVAL;
|
|
|
|
if ((flags & (MDB_NOOVERWRITE|MDB_NODUPDATA|MDB_RESERVE|MDB_APPEND|MDB_APPENDDUP)) != flags)
|
|
return EINVAL;
|
|
|
|
mdb_cursor_init(&mc, txn, dbi, &mx);
|
|
return mdb_cursor_put(&mc, key, data, flags);
|
|
*/
|
|
return nil
|
|
}
|
|
|
|
func (t *Transaction) Bucket(name string, flags int) (*Bucket, error) {
|
|
/*
|
|
MDB_val key, data;
|
|
MDB_dbi i;
|
|
MDB_cursor mc;
|
|
int rc, dbflag, exact;
|
|
unsigned int unused = 0;
|
|
size_t len;
|
|
|
|
if (txn->mt_dbxs[FREE_DBI].md_cmp == NULL) {
|
|
mdb_default_cmp(txn, FREE_DBI);
|
|
}
|
|
|
|
if ((flags & VALID_FLAGS) != flags)
|
|
return EINVAL;
|
|
if (txn->mt_flags & MDB_TXN_ERROR)
|
|
return MDB_BAD_TXN;
|
|
|
|
// main DB?
|
|
if (!name) {
|
|
*dbi = MAIN_DBI;
|
|
if (flags & PERSISTENT_FLAGS) {
|
|
uint16_t f2 = flags & PERSISTENT_FLAGS;
|
|
// make sure flag changes get committed
|
|
if ((txn->mt_dbs[MAIN_DBI].md_flags | f2) != txn->mt_dbs[MAIN_DBI].md_flags) {
|
|
txn->mt_dbs[MAIN_DBI].md_flags |= f2;
|
|
txn->mt_flags |= MDB_TXN_DIRTY;
|
|
}
|
|
}
|
|
mdb_default_cmp(txn, MAIN_DBI);
|
|
return MDB_SUCCESS;
|
|
}
|
|
|
|
if (txn->mt_dbxs[MAIN_DBI].md_cmp == NULL) {
|
|
mdb_default_cmp(txn, MAIN_DBI);
|
|
}
|
|
|
|
// Is the DB already open?
|
|
len = strlen(name);
|
|
for (i=2; i<txn->mt_numdbs; i++) {
|
|
if (!txn->mt_dbxs[i].md_name.mv_size) {
|
|
// Remember this free slot
|
|
if (!unused) unused = i;
|
|
continue;
|
|
}
|
|
if (len == txn->mt_dbxs[i].md_name.mv_size &&
|
|
!strncmp(name, txn->mt_dbxs[i].md_name.mv_data, len)) {
|
|
*dbi = i;
|
|
return MDB_SUCCESS;
|
|
}
|
|
}
|
|
|
|
// If no free slot and max hit, fail
|
|
if (!unused && txn->mt_numdbs >= txn->mt_env->me_maxdbs)
|
|
return MDB_DBS_FULL;
|
|
|
|
// Cannot mix named databases with some mainDB flags
|
|
if (txn->mt_dbs[MAIN_DBI].md_flags & (MDB_DUPSORT|MDB_INTEGERKEY))
|
|
return (flags & MDB_CREATE) ? MDB_INCOMPATIBLE : MDB_NOTFOUND;
|
|
|
|
// Find the DB info
|
|
dbflag = DB_NEW|DB_VALID;
|
|
exact = 0;
|
|
key.mv_size = len;
|
|
key.mv_data = (void *)name;
|
|
mdb_cursor_init(&mc, txn, MAIN_DBI, NULL);
|
|
rc = mdb_cursor_set(&mc, &key, &data, MDB_SET, &exact);
|
|
if (rc == MDB_SUCCESS) {
|
|
// make sure this is actually a DB
|
|
MDB_node *node = NODEPTR(mc.mc_pg[mc.mc_top], mc.mc_ki[mc.mc_top]);
|
|
if (!(node->mn_flags & F_SUBDATA))
|
|
return MDB_INCOMPATIBLE;
|
|
} else if (rc == MDB_NOTFOUND && (flags & MDB_CREATE)) {
|
|
// Create if requested
|
|
MDB_db dummy;
|
|
data.mv_size = sizeof(MDB_db);
|
|
data.mv_data = &dummy;
|
|
memset(&dummy, 0, sizeof(dummy));
|
|
dummy.md_root = P_INVALID;
|
|
dummy.md_flags = flags & PERSISTENT_FLAGS;
|
|
rc = mdb_cursor_put(&mc, &key, &data, F_SUBDATA);
|
|
dbflag |= DB_DIRTY;
|
|
}
|
|
|
|
// OK, got info, add to table
|
|
if (rc == MDB_SUCCESS) {
|
|
unsigned int slot = unused ? unused : txn->mt_numdbs;
|
|
txn->mt_dbxs[slot].md_name.mv_data = strdup(name);
|
|
txn->mt_dbxs[slot].md_name.mv_size = len;
|
|
txn->mt_dbxs[slot].md_rel = NULL;
|
|
txn->mt_dbflags[slot] = dbflag;
|
|
memcpy(&txn->mt_dbs[slot], data.mv_data, sizeof(MDB_db));
|
|
*dbi = slot;
|
|
mdb_default_cmp(txn, slot);
|
|
if (!unused) {
|
|
txn->mt_numdbs++;
|
|
}
|
|
}
|
|
|
|
return rc;
|
|
*/
|
|
return nil, nil
|
|
}
|
|
|
|
func (t *Transaction) Stat(b Bucket) *stat {
|
|
/*
|
|
if (txn == NULL || arg == NULL || dbi >= txn->mt_numdbs)
|
|
return EINVAL;
|
|
|
|
if (txn->mt_dbflags[dbi] & DB_STALE) {
|
|
MDB_cursor mc;
|
|
MDB_xcursor mx;
|
|
// Stale, must read the DB's root. cursor_init does it for us.
|
|
mdb_cursor_init(&mc, txn, dbi, &mx);
|
|
}
|
|
return mdb_stat0(txn->mt_env, &txn->mt_dbs[dbi], arg);
|
|
*/
|
|
return nil
|
|
}
|
|
|
|
func (t *Transaction) BucketFlags(b Bucket) (int, error) {
|
|
/*
|
|
// We could return the flags for the FREE_DBI too but what's the point?
|
|
if (txn == NULL || dbi < MAIN_DBI || dbi >= txn->mt_numdbs)
|
|
return EINVAL;
|
|
*flags = txn->mt_dbs[dbi].md_flags & PERSISTENT_FLAGS;
|
|
return MDB_SUCCESS;
|
|
*/
|
|
return 0, nil
|
|
}
|
|
|
|
func (t *Transaction) Drop(b *Bucket, del int) error {
|
|
/*
|
|
MDB_cursor *mc, *m2;
|
|
int rc;
|
|
|
|
if (!txn || !dbi || dbi >= txn->mt_numdbs || (unsigned)del > 1 || !(txn->mt_dbflags[dbi] & DB_VALID))
|
|
return EINVAL;
|
|
|
|
if (F_ISSET(txn->mt_flags, MDB_TXN_RDONLY))
|
|
return EACCES;
|
|
|
|
rc = mdb_cursor_open(txn, dbi, &mc);
|
|
if (rc)
|
|
return rc;
|
|
|
|
rc = mdb_drop0(mc, mc->mc_db->md_flags & MDB_DUPSORT);
|
|
// Invalidate the dropped DB's cursors
|
|
for (m2 = txn->mt_cursors[dbi]; m2; m2 = m2->mc_next)
|
|
m2->mc_flags &= ~(C_INITIALIZED|C_EOF);
|
|
if (rc)
|
|
goto leave;
|
|
|
|
// Can't delete the main DB
|
|
if (del && dbi > MAIN_DBI) {
|
|
rc = mdb_del(txn, MAIN_DBI, &mc->mc_dbx->md_name, NULL);
|
|
if (!rc) {
|
|
txn->mt_dbflags[dbi] = DB_STALE;
|
|
mdb_dbi_close(txn->mt_env, dbi);
|
|
}
|
|
} else {
|
|
// reset the DB record, mark it dirty
|
|
txn->mt_dbflags[dbi] |= DB_DIRTY;
|
|
txn->mt_dbs[dbi].md_depth = 0;
|
|
txn->mt_dbs[dbi].md_branch_pages = 0;
|
|
txn->mt_dbs[dbi].md_leaf_pages = 0;
|
|
txn->mt_dbs[dbi].md_overflow_pages = 0;
|
|
txn->mt_dbs[dbi].md_entries = 0;
|
|
txn->mt_dbs[dbi].md_root = P_INVALID;
|
|
|
|
txn->mt_flags |= MDB_TXN_DIRTY;
|
|
}
|
|
leave:
|
|
mdb_cursor_close(mc);
|
|
return rc;
|
|
*/
|
|
return nil
|
|
}
|