MaxScale/server/core/hashtable.cc
2017-03-24 09:21:20 +02:00

927 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 = (HASHTABLE*)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
{
MXS_ERROR("Failed to write hashtable item count: %d, %s", errno,
mxs_strerror(errno));
}
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)
{
MXS_ERROR("Failed to write hashtable item count: %d, %s", errno,
mxs_strerror(errno));
}
}
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;
}