/* bucket.c - The routines for playing with hash buckets. */
/* This file is part of GDBM, the GNU data base manager.
Copyright (C) 1990-2023 Free Software Foundation, Inc.
GDBM is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3, or (at your option)
any later version.
GDBM is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with GDBM. If not, see . */
#include "autoconf.h"
#include "gdbmdefs.h"
#include
#include
#define GDBM_MAX_DIR_SIZE INT32_MAX
#define GDBM_MAX_DIR_HALF (GDBM_MAX_DIR_SIZE / 2)
/* Initializing a new hash buckets sets all bucket entries to -1 hash value. */
void
_gdbm_new_bucket (GDBM_FILE dbf, hash_bucket *bucket, int bits)
{
int index;
/* Initialize the avail block. */
bucket->av_count = 0;
/* Set the information fields first. */
bucket->bucket_bits = bits;
bucket->count = 0;
/* Initialize all bucket elements. */
for (index = 0; index < dbf->header->bucket_elems; index++)
bucket->h_table[index].hash_value = -1;
}
/* Bucket cache table functions */
/* Hash an off_t word into an index of width NBITS. */
static size_t
adrhash (off_t adr, size_t nbits)
{
adr ^= adr >> (GDBM_HASH_BITS + 1 - nbits);
return ((265443576910ul * adr) & 0xffffffff) >> (GDBM_HASH_BITS + 1 - nbits);
}
/*
* Return a pointer to the cache table slot for bucket address ADR.
* Never returns NULL.
*/
static cache_elem **
cache_tab_lookup_slot (GDBM_FILE dbf, off_t adr)
{
cache_elem **cache = dbf->cache;
size_t h = adrhash (adr, dbf->cache_bits);
if (cache[h])
{
if (cache[h]->ca_adr != adr)
{
cache_elem *prev = cache[h], *p = prev->ca_coll;
while (p)
{
if (p->ca_adr == adr)
break;
prev = p;
p = prev->ca_coll;
}
return &prev->ca_coll;
}
}
return &cache[h];
}
/* LRU list management */
/*
* Link ELEM after REF in DBF cache. If REF is NULL, link at head and
* set DBF->bucket to point to the ca_bucket of ELEM.
*/
static void
lru_link_elem (GDBM_FILE dbf, cache_elem *elem, cache_elem *ref)
{
if (!ref)
{
elem->ca_prev = NULL;
elem->ca_next = dbf->cache_mru;
if (dbf->cache_mru)
dbf->cache_mru->ca_prev = elem;
else
dbf->cache_lru = elem;
dbf->cache_mru = elem;
dbf->bucket = dbf->cache_mru->ca_bucket;
}
else
{
cache_elem *x;
elem->ca_prev = ref;
elem->ca_next = ref->ca_next;
if ((x = ref->ca_next))
x->ca_prev = elem;
else
dbf->cache_lru = elem;
ref->ca_next = elem;
}
}
/*
* Unlink ELEM from the list of cache elements in DBF.
* If cache_mru gets updated, update DBF->bucket accordingly.
*/
static void
lru_unlink_elem (GDBM_FILE dbf, cache_elem *elem)
{
cache_elem *x;
if ((x = elem->ca_prev))
x->ca_next = elem->ca_next;
else
{
dbf->cache_mru = elem->ca_next;
dbf->bucket = dbf->cache_mru ? dbf->cache_mru->ca_bucket : NULL;
}
if ((x = elem->ca_next))
x->ca_prev = elem->ca_prev;
else
dbf->cache_lru = elem->ca_prev;
elem->ca_prev = elem->ca_next = NULL;
}
/* Creates and returns new cache element for DBF. The element is initialized,
but not linked to the LRU list.
Return NULL on error.
*/
static cache_elem *
cache_elem_new (GDBM_FILE dbf, off_t adr)
{
cache_elem *elem;
elem = dbf->cache_avail;
if (elem)
{
dbf->cache_avail = elem->ca_next;
}
else
{
elem = calloc (1,
sizeof (*elem) -
sizeof (elem->ca_bucket[0]) +
dbf->header->bucket_size);
if (!elem)
return NULL;
}
elem->ca_adr = adr;
elem->ca_changed = FALSE;
elem->ca_data.hash_val = -1;
elem->ca_data.elem_loc = -1;
elem->ca_prev = elem->ca_next = elem->ca_coll = NULL;
elem->ca_hits = 0;
return elem;
}
/* Frees element ELEM. Unlinks it from the cache tree and LRU list. */
static void
cache_elem_free (GDBM_FILE dbf, cache_elem *elem)
{
size_t h = adrhash (elem->ca_adr, dbf->cache_bits);
cache_elem **pp;
lru_unlink_elem (dbf, elem);
elem->ca_next = dbf->cache_avail;
dbf->cache_avail = elem;
dbf->cache_num--;
pp = &dbf->cache[h];
while (*pp)
{
if (*pp == elem)
{
*pp = (*pp)->ca_coll;
break;
}
pp = &(*pp)->ca_coll;
}
}
/* Free the least recently used cache entry. */
static inline int
cache_lru_free (GDBM_FILE dbf)
{
cache_elem *last = dbf->cache_lru;
if (last->ca_changed)
{
if (_gdbm_write_bucket (dbf, last))
return -1;
}
cache_elem_free (dbf, last);
return 0;
}
/*
* Round up V to the next highest power of 2 and compute log2 of
* it using De Brujin sequences.
* See http://supertech.csail.mit.edu/papers/debruijn.pdf
*/
static unsigned
log2i (unsigned v)
{
static const int dbp[32] =
{
0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8,
31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9
};
v--;
v |= v >> 1;
v |= v >> 2;
v |= v >> 4;
v |= v >> 8;
v |= v >> 16;
v++;
return dbp[(uint32_t)(v * 0x077CB531U) >> 27];
}
static int
cache_tab_resize (GDBM_FILE dbf, int bits)
{
size_t size = 1 << bits;
if (!dbf->cache || size != dbf->cache_size)
{
size_t n = size * sizeof (dbf->cache[0]);
cache_elem **p, *elem;
/* Flush existing cache */
if (_gdbm_cache_flush (dbf))
return -1;
/* Reallocate it */
p = realloc (dbf->cache, n);
if (!p)
{
GDBM_SET_ERRNO (dbf, GDBM_MALLOC_ERROR, FALSE);
return -1;
}
dbf->cache = p;
dbf->cache_size = size;
dbf->cache_bits = bits;
memset (dbf->cache, 0, n);
/* Rehash and free surplus elements */
for (elem = dbf->cache_lru; elem; )
{
cache_elem *prev = elem->ca_prev;
elem->ca_coll = NULL;
if (size < dbf->cache_num)
{
cache_elem_free (dbf, elem);
}
else
{
p = cache_tab_lookup_slot (dbf, elem->ca_adr);
if (*p)
abort ();// shouldn't happen
*p = elem;
}
elem = prev;
}
}
return 0;
}
enum
{
cache_found,
cache_new,
cache_failure
};
static int
cache_lookup (GDBM_FILE dbf, off_t adr, cache_elem *ref, cache_elem **ret_elem)
{
int rc;
cache_elem **elp, *elem;
dbf->cache_access_count++;
elp = cache_tab_lookup_slot (dbf, adr);
if (*elp != NULL)
{
elem = *elp;
elem->ca_hits++;
dbf->cache_hits++;
lru_unlink_elem (dbf, elem);
rc = cache_found;
}
else if ((elem = cache_elem_new (dbf, adr)) == NULL)
return cache_failure;
else
{
rc = cache_new;
if (dbf->cache_num == dbf->cache_size)
{
if (dbf->cache_auto && dbf->cache_bits < dbf->header->dir_bits &&
cache_tab_resize (dbf, dbf->cache_bits + 1) == 0)
{
/* Table has been reallocated, recompute the slot. */
elp = cache_tab_lookup_slot (dbf, adr);
}
else if (cache_lru_free (dbf))
{
rc = cache_failure;
}
}
if (rc == cache_new)
{
*elp = elem;
dbf->cache_num++;
}
}
/*
* If the obtained bucket is not changed and is going to become current,
* flush all changed cache elements. This ensures that changed cache
* elements form a contiguous sequence at the head of the cache list (see
* _gdbm_cache_flush).
*/
if (ref == NULL && !elem->ca_changed)
_gdbm_cache_flush (dbf);
lru_link_elem (dbf, elem, ref);
if (rc != cache_failure)
*ret_elem = elem;
return rc;
}
/*
* Find a bucket for DBF that is pointed to by the bucket directory from
* location DIR_INDEX. The bucket cache is first checked to see if it
* is already in memory. If not, the last recently used bucket may be
* tossed (if the cache is full) to read the new bucket.
*
* On success, the cached entry with the requested bucket is placed at
* the head of the cache list (cache_mru) and the requested bucket becomes
* "current".
*
* On error, the current bucket remains unchanged.
*/
int
_gdbm_get_bucket (GDBM_FILE dbf, int dir_index)
{
int rc;
off_t bucket_adr; /* The address of the correct hash bucket. */
off_t file_pos; /* The return address for lseek. */
hash_bucket *bucket;
cache_elem *elem;
if (!gdbm_dir_entry_valid_p (dbf, dir_index))
{
/* FIXME: negative caching? */
GDBM_SET_ERRNO (dbf, GDBM_BAD_DIR_ENTRY, TRUE);
return -1;
}
/* Initial set up. */
dbf->bucket_dir = dir_index;
bucket_adr = dbf->dir[dir_index];
switch (cache_lookup (dbf, bucket_adr, NULL, &elem))
{
case cache_found:
break;
case cache_new:
/* Position the file pointer */
file_pos = gdbm_file_seek (dbf, bucket_adr, SEEK_SET);
if (file_pos != bucket_adr)
{
GDBM_SET_ERRNO (dbf, GDBM_FILE_SEEK_ERROR, TRUE);
cache_elem_free (dbf, elem);
_gdbm_fatal (dbf, _("lseek error"));
return -1;
}
/* Read the bucket. */
rc = _gdbm_full_read (dbf, elem->ca_bucket, dbf->header->bucket_size);
if (rc)
{
GDBM_DEBUG (GDBM_DEBUG_ERR,
"%s: error reading bucket: %s",
dbf->name, gdbm_db_strerror (dbf));
dbf->need_recovery = TRUE;
cache_elem_free (dbf, elem);
_gdbm_fatal (dbf, gdbm_db_strerror (dbf));
return -1;
}
/* Validate the bucket */
bucket = elem->ca_bucket;
if (!(bucket->count >= 0
&& bucket->count <= dbf->header->bucket_elems
&& bucket->bucket_bits >= 0
&& bucket->bucket_bits <= dbf->header->dir_bits))
{
GDBM_SET_ERRNO (dbf, GDBM_BAD_BUCKET, TRUE);
cache_elem_free (dbf, elem);
return -1;
}
/* Validate bucket_avail table */
if (gdbm_bucket_avail_table_validate (dbf, bucket))
{
cache_elem_free (dbf, elem);
return -1;
}
/* Update the cache */
elem->ca_adr = bucket_adr;
elem->ca_data.elem_loc = -1;
elem->ca_changed = FALSE;
break;
case cache_failure:
return -1;
}
return 0;
}
/* Split the current bucket. This includes moving all items in the bucket to
a new bucket. This doesn't require any disk reads because all hash values
are stored in the buckets. Splitting the current bucket may require
doubling the size of the hash directory. */
int
_gdbm_split_bucket (GDBM_FILE dbf, int next_insert)
{
off_t old_adr[GDBM_HASH_BITS]; /* Address of the old directories. */
int old_size[GDBM_HASH_BITS]; /* Size of the old directories. */
int old_count; /* Number of old directories. */
int index; /* Used in array indexing. */
int index1; /* Used in array indexing. */
/* No directories are yet old. */
old_count = 0;
while (dbf->bucket->count == dbf->header->bucket_elems)
{
int new_bits; /* The number of bits for the new buckets. */
cache_elem *newcache[2]; /* Location in the cache for the buckets. */
off_t adr_0; /* File address of the new bucket 0. */
off_t adr_1; /* File address of the new bucket 1. */
avail_elem old_bucket; /* Avail Struct for the old bucket. */
off_t dir_start0; /* Used in updating the directory. */
off_t dir_start1;
off_t dir_end;
new_bits = dbf->bucket->bucket_bits + 1;
/*
* Allocate two new buckets. They will be populated with the entries
* from the current bucket (cache_mru->bucket), so make sure that
* cache_mru remains unchanged until both buckets are fully formed.
* Newly allocated buckets must be linked right after cache_mru, so
* that all changed buckets form a contiguous sequence at the beginning
* of the cache list (this is needed by _gdbm_cache_flush).
*/
adr_0 = _gdbm_alloc (dbf, dbf->header->bucket_size);
switch (cache_lookup (dbf, adr_0, dbf->cache_mru, &newcache[0]))
{
case cache_new:
break;
case cache_found:
/* should not happen */
GDBM_DEBUG (GDBM_DEBUG_ERR,
"%s: bucket found where it should not",
dbf->name);
GDBM_SET_ERRNO (dbf, GDBM_BUCKET_CACHE_CORRUPTED, TRUE);
return -1;
case cache_failure:
return -1;
}
_gdbm_new_bucket (dbf, newcache[0]->ca_bucket, new_bits);
adr_1 = _gdbm_alloc (dbf, dbf->header->bucket_size);
switch (cache_lookup (dbf, adr_1, newcache[0], &newcache[1]))
{
case cache_new:
break;
case cache_found:
/* should not happen */
GDBM_DEBUG (GDBM_DEBUG_ERR,
"%s: bucket found where it should not",
dbf->name);
GDBM_SET_ERRNO (dbf, GDBM_BUCKET_CACHE_CORRUPTED, TRUE);
return -1;
case cache_failure:
return -1;
}
_gdbm_new_bucket (dbf, newcache[1]->ca_bucket, new_bits);
/* Double the directory size if necessary. */
if (dbf->header->dir_bits == dbf->bucket->bucket_bits)
{
off_t *new_dir; /* Pointer to the new directory. */
int dir_size; /* Size of the new directory. */
off_t dir_adr; /* Address of the new directory. */
if (dbf->header->dir_size >= GDBM_MAX_DIR_HALF)
{
GDBM_SET_ERRNO (dbf, GDBM_DIR_OVERFLOW, TRUE);
_gdbm_fatal (dbf, _("directory overflow"));
return -1;
}
dir_size = dbf->header->dir_size * 2;
dir_adr = _gdbm_alloc (dbf, dir_size);
if (dir_adr == 0)
return -1;
new_dir = malloc (dir_size);
if (new_dir == NULL)
{
GDBM_SET_ERRNO (dbf, GDBM_MALLOC_ERROR, TRUE);
_gdbm_fatal (dbf, _("malloc error"));
return -1;
}
for (index = 0; index < GDBM_DIR_COUNT (dbf); index++)
{
new_dir[2*index] = dbf->dir[index];
new_dir[2*index+1] = dbf->dir[index];
}
/* Update header. */
old_adr[old_count] = dbf->header->dir;
dbf->header->dir = dir_adr;
old_size[old_count] = dbf->header->dir_size;
dbf->header->dir_size = dir_size;
dbf->header->dir_bits = new_bits;
old_count++;
/* Now update dbf. */
dbf->header_changed = TRUE;
dbf->bucket_dir *= 2;
free (dbf->dir);
dbf->dir = new_dir;
}
/* Copy all elements in dbf->bucket into the new buckets. */
for (index = 0; index < dbf->header->bucket_elems; index++)
{
bucket_element *old_el = &dbf->bucket->h_table[index];
hash_bucket *bucket;
int elem_loc;
if (old_el->hash_value < 0)
{
GDBM_SET_ERRNO (dbf, GDBM_BAD_BUCKET, TRUE);
return -1;
}
bucket =
newcache[(old_el->hash_value >> (GDBM_HASH_BITS - new_bits)) & 1]->ca_bucket;
elem_loc = old_el->hash_value % dbf->header->bucket_elems;
while (bucket->h_table[elem_loc].hash_value != -1)
elem_loc = (elem_loc + 1) % dbf->header->bucket_elems;
bucket->h_table[elem_loc] = *old_el;
bucket->count++;
}
/* Allocate avail space for the newcache[1]->ca_bucket. */
newcache[1]->ca_bucket->bucket_avail[0].av_adr
= _gdbm_alloc (dbf, dbf->header->block_size);
if (newcache[1]->ca_bucket->bucket_avail[0].av_adr == 0)
return -1;
newcache[1]->ca_bucket->bucket_avail[0].av_size
= dbf->header->block_size;
newcache[1]->ca_bucket->av_count = 1;
/* Copy the avail elements in dbf->bucket to newcache[0]->ca_bucket. */
newcache[0]->ca_bucket->av_count = dbf->bucket->av_count;
index = 0;
if (newcache[0]->ca_bucket->av_count == BUCKET_AVAIL)
{
/* The avail is full, move the first one to newcache[1]->ca_bucket.*/
_gdbm_put_av_elem (dbf->bucket->bucket_avail[0],
newcache[1]->ca_bucket->bucket_avail,
&newcache[1]->ca_bucket->av_count,
dbf->coalesce_blocks);
index = 1;
newcache[0]->ca_bucket->av_count--;
}
index1 = 0;
for (; index < dbf->bucket->av_count; index++)
{
newcache[0]->ca_bucket->bucket_avail[index1++]
= dbf->bucket->bucket_avail[index];
}
/* Update the directory. We have new file addresses for both buckets. */
dir_start1 = (dbf->bucket_dir >> (dbf->header->dir_bits - new_bits)) | 1;
dir_end = (dir_start1 + 1) << (dbf->header->dir_bits - new_bits);
dir_start1 = dir_start1 << (dbf->header->dir_bits - new_bits);
dir_start0 = dir_start1 - (dir_end - dir_start1);
for (index = dir_start0; index < dir_start1; index++)
dbf->dir[index] = adr_0;
for (index = dir_start1; index < dir_end; index++)
dbf->dir[index] = adr_1;
/* Set changed flags. */
newcache[0]->ca_changed = TRUE;
newcache[1]->ca_changed = TRUE;
dbf->directory_changed = TRUE;
/* Update the cache! */
dbf->bucket_dir = _gdbm_bucket_dir (dbf, next_insert);
/* Invalidate old cache entry. */
old_bucket.av_adr = dbf->cache_mru->ca_adr;
old_bucket.av_size = dbf->header->bucket_size;
cache_elem_free (dbf, dbf->cache_mru);
/* Set dbf->bucket to the proper bucket. */
if (dbf->dir[dbf->bucket_dir] != adr_0)
{
cache_elem *t = newcache[0];
newcache[0] = newcache[1];
newcache[1] = t;
}
_gdbm_put_av_elem (old_bucket,
newcache[1]->ca_bucket->bucket_avail,
&newcache[1]->ca_bucket->av_count,
dbf->coalesce_blocks);
lru_unlink_elem (dbf, newcache[0]);
lru_link_elem (dbf, newcache[0], NULL);
}
/* Get rid of old directories. */
for (index = 0; index < old_count; index++)
if (_gdbm_free (dbf, old_adr[index], old_size[index]))
return -1;
return 0;
}
/* The only place where a bucket is written. CA_ENTRY is the
cache entry containing the bucket to be written. */
int
_gdbm_write_bucket (GDBM_FILE dbf, cache_elem *ca_entry)
{
int rc;
off_t file_pos; /* The return value for lseek. */
file_pos = gdbm_file_seek (dbf, ca_entry->ca_adr, SEEK_SET);
if (file_pos != ca_entry->ca_adr)
{
GDBM_SET_ERRNO (dbf, GDBM_FILE_SEEK_ERROR, TRUE);
_gdbm_fatal (dbf, _("lseek error"));
return -1;
}
rc = _gdbm_full_write (dbf, ca_entry->ca_bucket, dbf->header->bucket_size);
if (rc)
{
GDBM_DEBUG (GDBM_DEBUG_STORE|GDBM_DEBUG_ERR,
"%s: error writing bucket: %s",
dbf->name, gdbm_db_strerror (dbf));
_gdbm_fatal (dbf, gdbm_strerror (rc));
return -1;
}
ca_entry->ca_changed = FALSE;
ca_entry->ca_data.hash_val = -1;
ca_entry->ca_data.elem_loc = -1;
return 0;
}
/* Cache manipulation interface functions. */
#define INIT_CACHE_BITS 9
/* Initialize the bucket cache. */
int
_gdbm_cache_init (GDBM_FILE dbf, size_t size)
{
int bits;
int cache_auto;
if (size == GDBM_CACHE_AUTO)
{
cache_auto = TRUE;
bits = dbf->cache ? dbf->cache_bits : INIT_CACHE_BITS;
}
else if (size > SIZE_T_MAX / sizeof (dbf->cache[0]))
{
GDBM_SET_ERRNO (dbf, GDBM_OPT_BADVAL, FALSE);
return -1;
}
else
{
cache_auto = FALSE;
bits = log2i (size < 4 ? 4 : size);
}
dbf->cache_auto = cache_auto;
return cache_tab_resize (dbf, bits);
}
/* Free the bucket cache */
void
_gdbm_cache_free (GDBM_FILE dbf)
{
cache_elem *elem;
while (dbf->cache_lru)
cache_elem_free (dbf, dbf->cache_lru);
free (dbf->cache);
dbf->cache = NULL;
while ((elem = dbf->cache_avail) != NULL)
{
dbf->cache_avail = elem->ca_next;
free (elem->ca_data.dptr);
free (elem);
}
}
/*
* Flush cache content to disk.
* All cache elements with the changed buckets form a contiguous sequence
* at the head of the cache list (starting with cache_mru).
*/
int
_gdbm_cache_flush (GDBM_FILE dbf)
{
cache_elem *elem;
for (elem = dbf->cache_mru; elem && elem->ca_changed; elem = elem->ca_next)
{
if (_gdbm_write_bucket (dbf, elem))
return -1;
}
return 0;
}
void
gdbm_get_cache_stats (GDBM_FILE dbf,
size_t *access_count,
size_t *cache_hits,
size_t *cache_count,
struct gdbm_cache_stat *bstat,
size_t nstat)
{
if (access_count)
*access_count = dbf->cache_access_count;
if (cache_hits)
*cache_hits = dbf->cache_hits;
if (cache_count)
*cache_count = dbf->cache_num;
if (bstat)
{
size_t i;
cache_elem *elem;
if (nstat > dbf->cache_num)
nstat = dbf->cache_num;
for (i = 0, elem = dbf->cache_mru; i < nstat; i++, elem = elem->ca_next)
{
bstat[i].adr = elem->ca_adr;
bstat[i].hits = elem->ca_hits;
}
}
}