MaxScale/server/core/hashtable.c
Johan Wikman bf4d00d6e3 Add temporary fixes to hashtable
These fixes prevent loops turning into eternal loops when
optimizations have been turned on.

The right remedy is to remove the internal locks from hashtable
and use external locks instead.
2017-02-27 13:36:51 +02:00

929 lines
23 KiB
C

/*
* Copyright (c) 2016 MariaDB Corporation Ab
*
* Use of this software is governed by the Business Source License included
* in the LICENSE.TXT file and at www.mariadb.com/bsl11.
*
* Change Date: 2019-07-01
*
* On the date above, in accordance with the Business Source License, use
* of this software will be governed by version 2 or later of the General
* Public License.
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <maxscale/alloc.h>
#include <maxscale/atomic.h>
#include <maxscale/hashtable.h>
/**
* @file hashtable.c General purpose hashtable routines
*
* The hashtable can be create with a custom number of hash buckets,
* a hash function and optional functions to call make copies of the key
* and value and to free them.
*
* The hashtable is arrange as a set of linked lists, the number of linked
* lists being the hashsize as requested by the user. Entries are hashed by
* calling the hash function that is passed in by the user, this is used as
* an index into the array of linked lists, usign modulo hashsize.
*
* The linked lists are searched using the key comparison function that is
* passed into the hash table creation routine.
*
* By default the hash table keeps the original pointers that are passed in
* for the keys and values, however two functions can be supplied to copy these
* a copy function and a free function. Please note the same function is used for
* the key and the value, if the actions required are different the called functions
* must understand how to differenate the key and value.
*
* The hash table implements a single write, multiple reader locking policy by
* using a pair of counters and a spinlock. The spinlock is used to protect the
* number of readers and writers counters when taking out locks. Releasing of
* locks uses pure atomic actions and thus does not require spinlock protection.
*
* @verbatim
* Revision History
*
* Date Who Description
* 23/06/2013 Mark Riddoch Initial implementation
* 23/07/2013 Mark Riddoch Addition of hashtable iterator
* 08/01/2014 Massimiliano Pinto Added copy and free funtion pointers for keys and values:
* it's possible to copy and free different data types via
* kcopyfn/kfreefn, vcopyfn/vfreefn
* 06/02/2015 Mark Riddoch Addition of hashtable_save and hashtable_load
*
* @endverbatim
*/
static void hashtable_read_lock(HASHTABLE *table);
static void hashtable_read_unlock(HASHTABLE *table);
static void hashtable_write_lock(HASHTABLE *table);
static void hashtable_write_unlock(HASHTABLE *table);
static HASHTABLE *hashtable_alloc_real(HASHTABLE* target,
int size,
HASHHASHFN hashfn,
HASHCMPFN cmpfn);
/**
* Special identity function used as default key/value copy function in the hashtable
* implementation. This avoids having to special case the code that manipulates
* the keys and values
*
* @param data The data pointer
* @return Return the value we were called with
*/
static void *
identityfn(const void *data)
{
return (void*)data;
}
/**
* Special null function used as default free function in the hashtable
* implementation. This avoids having to special case the code that manipulates
* the keys and values
*
* @param data The data pointer
*/
static void
nullfn(void *data)
{
}
/**
* Allocate a new hash table.
*
* The hashtable must have a size of at least one, however to be of any
* practical use a larger size sould be chosen as the size relates to the number
* of has buckets in the table.
*
* @param size The size of the hash table, msut be > 0
* @param hashfn The user supplied hash function
* @param cmpfn The user supplied key comparison function
* @return The hashtable table
*/
HASHTABLE *
hashtable_alloc(int size, HASHHASHFN hashfn, HASHCMPFN cmpfn)
{
return hashtable_alloc_real(NULL, size, hashfn, cmpfn);
}
HASHTABLE* hashtable_alloc_flat(HASHTABLE* target,
int size,
HASHHASHFN hashfn,
HASHCMPFN cmpfn)
{
return hashtable_alloc_real(target, size, hashfn, cmpfn);
}
static HASHTABLE *
hashtable_alloc_real(HASHTABLE* target,
int size,
HASHHASHFN hashfn,
HASHCMPFN cmpfn)
{
HASHTABLE *rval;
if (target == NULL)
{
if ((rval = MXS_MALLOC(sizeof(HASHTABLE))) == NULL)
{
return NULL;
}
rval->ht_isflat = false;
}
else
{
rval = target;
rval->ht_isflat = true;
}
#if defined(SS_DEBUG)
rval->ht_chk_top = CHK_NUM_HASHTABLE;
rval->ht_chk_tail = CHK_NUM_HASHTABLE;
#endif
rval->hashsize = size > 0 ? size : 1;
rval->hashfn = hashfn;
rval->cmpfn = cmpfn;
rval->kcopyfn = identityfn;
rval->vcopyfn = identityfn;
rval->kfreefn = nullfn;
rval->vfreefn = nullfn;
rval->n_readers = 0;
rval->writelock = 0;
rval->n_elements = 0;
spinlock_init(&rval->spin);
if ((rval->entries = (HASHENTRIES **)MXS_CALLOC(rval->hashsize, sizeof(HASHENTRIES *))) == NULL)
{
MXS_FREE(rval);
return NULL;
}
memset(rval->entries, 0, rval->hashsize * sizeof(HASHENTRIES *));
return rval;
}
/**
* Delete an entire hash table
*
* @param table The hash table to delete
*/
void
hashtable_free(HASHTABLE *table)
{
int i;
HASHENTRIES *entry, *ptr;
if (table == NULL)
{
return;
}
hashtable_write_lock(table);
for (i = 0; i < table->hashsize; i++)
{
entry = table->entries[i];
while (entry)
{
ptr = entry->next;
table->kfreefn(entry->key);
table->vfreefn(entry->value);
MXS_FREE(entry);
entry = ptr;
}
}
MXS_FREE(table->entries);
hashtable_write_unlock(table);
if (!table->ht_isflat)
{
MXS_FREE(table);
}
}
/**
* Provide memory management functions to the hash table. This allows
* function pointers to be registered that can make copies of the
* key and value and free them as well.
*
* @param table The hash table
* @param kcopyfn The copy function for the key
* @param vcopyfn The copy function for the value
* @param kfreefn The free function for the key
* @param vfreefn The free function for the value
*/
void
hashtable_memory_fns(HASHTABLE *table,
HASHCOPYFN kcopyfn,
HASHCOPYFN vcopyfn,
HASHFREEFN kfreefn,
HASHFREEFN vfreefn)
{
if (kcopyfn != NULL)
{
table->kcopyfn = kcopyfn;
}
if (vcopyfn != NULL)
{
table->vcopyfn = vcopyfn;
}
if (kfreefn != NULL)
{
table->kfreefn = kfreefn;
}
if (vfreefn != NULL)
{
table->vfreefn = vfreefn;
}
}
/**
* Add an item to the hash table.
*
* @param table The hash table to which to add the item
* @param key The key of the item
* @param value The value for the item
* @return Return the number of items added
*/
int
hashtable_add(HASHTABLE *table, void *key, void *value)
{
unsigned int hashkey;
HASHENTRIES *entry;
if (table == NULL || key == NULL || value == NULL)
{
return 0;
}
if (table->hashsize <= 0)
{
return 0;
}
else
{
hashkey = table->hashfn(key) % table->hashsize;
}
hashtable_write_lock(table);
entry = table->entries[hashkey % table->hashsize];
while (entry && table->cmpfn(key, entry->key) != 0)
{
entry = entry->next;
}
if (entry && table->cmpfn(key, entry->key) == 0)
{
/* Duplicate key value */
hashtable_write_unlock(table);
return 0;
}
else
{
HASHENTRIES *ptr = (HASHENTRIES *)MXS_MALLOC(sizeof(HASHENTRIES));
if (ptr == NULL)
{
hashtable_write_unlock(table);
return 0;
}
/* copy the key */
ptr->key = table->kcopyfn(key);
/* check succesfull key copy */
if (ptr->key == NULL)
{
MXS_FREE(ptr);
hashtable_write_unlock(table);
return 0;
}
/* copy the value */
ptr->value = table->vcopyfn(value);
/* check succesfull value copy */
if (ptr->value == NULL)
{
/* remove the key ! */
table->kfreefn(ptr->key);
MXS_FREE(ptr);
/* value not copied, return */
hashtable_write_unlock(table);
return 0;
}
ptr->next = table->entries[hashkey % table->hashsize];
table->entries[hashkey % table->hashsize] = ptr;
}
table->n_elements++;
hashtable_write_unlock(table);
return 1;
}
/**
* Delete an item from the hash table that has a given key
*
* @param table The hash table to delete from
* @param key The key value of the item to remove
* @return Return the number of items deleted
*/
int
hashtable_delete(HASHTABLE *table, void *key)
{
unsigned int hashkey;
HASHENTRIES *entry, *ptr;
if (table == NULL || key == NULL)
{
return 0;
}
hashkey = table->hashfn(key) % table->hashsize;
hashtable_write_lock(table);
entry = table->entries[hashkey % table->hashsize];
while (entry && entry->key && table->cmpfn(key, entry->key) != 0)
{
entry = entry->next;
}
if (entry == NULL)
{
/* Not found */
hashtable_write_unlock(table);
return 0;
}
if (entry == table->entries[hashkey % table->hashsize])
{
/* We are removing from the first entry */
table->entries[hashkey % table->hashsize] = entry->next;
table->kfreefn(entry->key);
table->vfreefn(entry->value);
if (entry->next != NULL)
{
entry->key = entry->next->key;
entry->value = entry->next->value;
}
else
{
entry->key = NULL;
entry->value = NULL;
}
MXS_FREE(entry);
}
else
{
ptr = table->entries[hashkey % table->hashsize];
while (ptr && ptr->next != entry)
{
ptr = ptr->next;
}
if (ptr == NULL)
{
hashtable_write_unlock(table);
return 0; /* This should never happen */
}
ptr->next = entry->next;
table->kfreefn(entry->key);
table->vfreefn(entry->value);
MXS_FREE(entry);
}
table->n_elements--;
assert(table->n_elements >= 0);
hashtable_write_unlock(table);
return 1;
}
/**
* Fetch an item with a given key value from the hash table
*
* @param table The hash table
* @param key The key value
* @return The item or NULL if the item was not found
*/
void *
hashtable_fetch(HASHTABLE *table, void *key)
{
unsigned int hashkey;
HASHENTRIES *entry;
if (table == NULL || key == NULL || 0 == table->hashsize)
{
return NULL;
}
hashkey = table->hashfn(key) % table->hashsize;
hashtable_read_lock(table);
entry = table->entries[hashkey % table->hashsize];
while (entry && entry->key && table->cmpfn(key, entry->key) != 0)
{
entry = entry->next;
}
if (entry == NULL)
{
hashtable_read_unlock(table);
return NULL;
}
else
{
hashtable_read_unlock(table);
return entry->value;
}
}
/**
* Print hash table statistics to the standard output
*
* @param table The hash table
*/
void
hashtable_stats(HASHTABLE *table)
{
int total, longest, i, j;
HASHENTRIES *entries;
if (table == NULL)
{
return;
}
printf("Hashtable: %p, size %d\n", table, table->hashsize);
total = 0;
longest = 0;
hashtable_read_lock(table);
for (i = 0; i < table->hashsize; i++)
{
j = 0;
entries = table->entries[i];
while (entries)
{
j++;
entries = entries->next;
}
total += j;
if (j > longest)
{
longest = j;
}
}
hashtable_read_unlock(table);
printf("\tNo. of entries: %d\n", total);
printf("\tAverage chain length: %.1f\n", (float)total / table->hashsize);
printf("\tLongest chain length: %d\n", longest);
}
/**
* Produces stat output about hashtable
*
* Parameters:
* @param table - <usage>
* <description>
*
* @param hashsize - <usage>
* <description>
*
* @param nelems - <usage>
* <description>
*
* @param longest - <usage>
* <description>
*
* @return void
*
*
*/
void hashtable_get_stats(void* table,
int* hashsize,
int* nelems,
int* longest)
{
HASHTABLE* ht;
HASHENTRIES* entries;
int i;
int j;
*nelems = 0;
*longest = 0;
*hashsize = 0;
if (table != NULL)
{
ht = (HASHTABLE *)table;
CHK_HASHTABLE(ht);
hashtable_read_lock(ht);
for (i = 0; i < ht->hashsize; i++)
{
j = 0;
entries = ht->entries[i];
while (entries)
{
j++;
entries = entries->next;
}
*nelems += j;
if (j > *longest)
{
*longest = j;
}
}
*hashsize = ht->hashsize;
hashtable_read_unlock(ht);
}
}
/**
* Take a read lock on the hashtable.
*
* The hashtable support multiple readers and a single writer,
* we have a spinlock to protect the two counts, n_readers and
* writelock.
*
* We take the hashtable spinlock and then check that writelock
* is set to zero. If not we release the spinlock and do dirty
* reads of writelock until it goes to 0. Once it is zero we
* acquire the spinlock again and test that writelock is still
* 0.
*
* With writelock set to zero we increment n_readers with the
* spinlock still held.
*
* @param table The hashtable to lock.
*/
static void
hashtable_read_lock(HASHTABLE *table)
{
spinlock_acquire(&table->spin);
while (table->writelock)
{
spinlock_release(&table->spin);
while (atomic_add(&table->writelock, 1) != 0)
{
atomic_add(&table->writelock, -1);
}
atomic_add(&table->writelock, -1);
spinlock_acquire(&table->spin);
}
atomic_add(&table->n_readers, 1);
spinlock_release(&table->spin);
}
/**
* Release a previously obtained readlock.
*
* Simply decrement the n_readers value for the hash table
*
* @param table The hash table to unlock
*/
static void
hashtable_read_unlock(HASHTABLE *table)
{
atomic_add(&table->n_readers, -1);
}
/**
* Obtain an exclusive write lock for the hash table.
*
* We acquire the hashtable spinlock, check for the number of
* readers beign zero. If it is not we hold the spinlock and
* loop waiting for the n_readers to reach zero. This will prevent
* any new readers beign granted access but will not prevent current
* readers releasing the read lock.
*
* Once we have no readers we increment writelock and test if we are
* the only writelock holder, if not we repeat the process. We hold
* the spinlock throughout the process since both read and write
* locks do not require the spinlock to be acquired.
*
* @param table The table to lock for updates
*/
static void
hashtable_write_lock(HASHTABLE *table)
{
int available;
spinlock_acquire(&table->spin);
do
{
while (atomic_add(&table->n_readers, 1) != 0)
{
atomic_add(&table->n_readers, -1);
}
atomic_add(&table->n_readers, -1);
available = atomic_add(&table->writelock, 1);
if (available != 0)
{
atomic_add(&table->writelock, -1);
}
}
while (available != 0);
spinlock_release(&table->spin);
}
/**
* Release the write lock on the hash table.
*
* @param table The hash table to unlock
*/
static void
hashtable_write_unlock(HASHTABLE *table)
{
atomic_add(&table->writelock, -1);
}
/**
* Create an iterator on a hash table
*
* @param table The table to ceate an iterator on
* @return An iterator to use in future calls
*/
HASHITERATOR *
hashtable_iterator(HASHTABLE *table)
{
HASHITERATOR *rval = (HASHITERATOR *)MXS_MALLOC(sizeof(HASHITERATOR));
if (rval)
{
rval->table = table;
rval->chain = 0;
rval->depth = -1;
}
return rval;
}
/**
* Return the next key for a hashtable iterator
*
* @param iter The hashtable iterator
* @return The next key value or NULL
*/
void *
hashtable_next(HASHITERATOR *iter)
{
int i;
HASHENTRIES *entries;
if (iter == NULL)
{
return NULL;
}
iter->depth++;
while (iter->chain < iter->table->hashsize)
{
hashtable_read_lock(iter->table);
if ((entries = iter->table->entries[iter->chain]) != NULL)
{
i = 0;
while (entries && i < iter->depth)
{
entries = entries->next;
i++;
}
hashtable_read_unlock(iter->table);
if (entries)
{
return entries->key;
}
}
else
{
hashtable_read_unlock(iter->table);
}
iter->depth = 0;
iter->chain++;
}
return NULL;
}
/**
* Free a hashtable iterator
*
* @param iter The iterator to free
*/
void
hashtable_iterator_free(HASHITERATOR *iter)
{
MXS_FREE(iter);
}
/**
* Save a hashtable to disk
*
* @param table Hashtable to save
* @param filename Filename to write hashtable into
* @param keywrite Pointer to function that writes a single key
* @param valuewrite Pointer to function that writes a single value
* @return Number of entries written or -1 on error
*/
int
hashtable_save(HASHTABLE *table, const char *filename,
int (*keywrite)(int, void*),
int (*valuewrite)(int, void*))
{
int fd, rval = 0;
HASHITERATOR *iter;
void *key, *value;
if ((fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666)) == -1)
{
return -1;
}
if (write(fd, "HASHTABLE", 7) != 7) // Magic number
{
close(fd);
return -1;
}
if (write(fd, &rval, sizeof(rval)) == -1) // Write zero counter, will be overrwriten at end
{
char err[MXS_STRERROR_BUFLEN];
MXS_ERROR("Failed to write hashtable item count: %d, %s", errno,
strerror_r(errno, err, sizeof(err)));
}
if ((iter = hashtable_iterator(table)) != NULL)
{
while ((key = hashtable_next(iter)) != NULL)
{
if (!(*keywrite)(fd, key))
{
close(fd);
hashtable_iterator_free(iter);
return -1;
}
if ((value = hashtable_fetch(table, key)) == NULL ||
(*valuewrite)(fd, value) == 0)
{
close(fd);
hashtable_iterator_free(iter);
return -1;
}
rval++;
}
}
/* Now go back and write the count of entries */
if (lseek(fd, 7L, SEEK_SET) != -1)
{
if (write(fd, &rval, sizeof(rval)) == -1)
{
char err[MXS_STRERROR_BUFLEN];
MXS_ERROR("Failed to write hashtable item count: %d, %s", errno,
strerror_r(errno, err, sizeof(err)));
}
}
close(fd);
hashtable_iterator_free(iter);
return rval;
}
/**
* Load a hashtable from disk
*
* @param table Hashtable to load
* @param filename Filename to read hashtable from
* @param keyread Pointer to function that reads a single key
* @param valueread Pointer to function that reads a single value
* @return Number of entries read or -1 on error
*/
int
hashtable_load(HASHTABLE *table, const char *filename,
void *(*keyread)(int),
void *(*valueread)(int))
{
int fd, count, rval = 0;
void *key, *value;
char buf[40];
if ((fd = open(filename, O_RDONLY)) == -1)
{
return -1;
}
if (read(fd, buf, 7) != 7)
{
close(fd);
return -1;
}
if (strncmp(buf, "HASHTABLE", 7) != 0)
{
close(fd);
return -1;
}
if (read(fd, &count, sizeof(count)) != sizeof(count))
{
close(fd);
return -1;
}
while (count--)
{
key = keyread(fd);
value = valueread(fd);
if (key == NULL || value == NULL)
{
break;
}
hashtable_add(table, key, value);
rval++;
}
close(fd);
return rval;
}
/**
* Return the number of elements added to the hashtable
* @param table Hashtable to measure
* @return Number of inserted elements or 0 if table is NULL
*/
int hashtable_size(HASHTABLE *table)
{
assert(table);
spinlock_acquire(&table->spin);
int rval = table->n_elements;
spinlock_release(&table->spin);
return rval;
}
/**
* Frees memory assumed to have been allocated using one of the MaxScale
* allocation functions. Intended to be used together with a hashtable
* copy function that merely makes a straight memory copy of the key/value,
* e.g. hashtable_item_strdup.
* @param data The memory to be freed.
*/
void hashtable_item_free(void *data)
{
MXS_FREE(data);
}
/**
* Convenience function intended for use as the comparison function of a hashtable,
* when the key is a NULL terminated string. Behaves as strcasecmp.
* @param str1 Pointer to string.
* @param str2 Pointer to string.
* @return Same as strcasecmp.
*/
int hashtable_item_strcasecmp(const void *str1, const void *str2)
{
return strcasecmp((const char*)str1, (const char*)str2);
}
/**
* Convenience function intended for use as the comparison function of a hashtable,
* when the key is a NULL terminated string. Behaves as strcmp.
* @param str1 Pointer to string.
* @param str2 Pointer to string.
* @return Same as strcmp.
*/
int hashtable_item_strcmp(const void *str1, const void *str2)
{
return strcmp((const char*)str1, (const char*)str2);
}
/**
* Convenience function intended for use as the copy function of a hashtable,
* when the key/value is a NULL terminated string.
* @param data A pointer to a NULL terminated string.
* @return A copy of the provided string or NULL if memory
* allocation fails.
*/
void* hashtable_item_strdup(const void* data)
{
return MXS_STRDUP((const char*)data);
}
/**
* Convenience function intended for use as the hash function of a hashtable,
* when the key is a NULL terminated string.
* @param data A pointer to a NULL terminated string.
* @return A hash of the string.
*/
int hashtable_item_strhash(const void* data)
{
int hash = 0;
if (data)
{
const char* key = (const char*)data;
int c;
while ((c = *key++))
{
hash = c + (hash << 6) + (hash << 16) - hash;
}
}
return hash;
}