hashtable.c. It uses separate chaining for collision resolution and lazy expiry to clean up TTL keys without a background thread.
Data structures
Two structs carry the entire in-memory state of the database.Entry
hashtable.h
strdup). The next pointer forms the collision chain within a bucket. expires_at stores an absolute Unix timestamp; a value of 0 means the key never expires.
HashTable
hashtable.h
| Field | Description |
|---|---|
buckets | Heap-allocated array of Entry * pointers, one per slot |
size | Current number of buckets (always grows by 2×) |
count | Number of live keys currently stored |
resizes | Cumulative count of resize operations, exposed by INFO |
djb2 hash function
All keys are hashed using djb2, a non-cryptographic hash function developed by Daniel Bernstein:hashtable.c
5381 and multiplier 33 — were chosen empirically. Multiplying by 33 (hash << 5 + hash) distributes bits widely across the output without expensive division. The seed 5381 is an odd prime that avoids clustering at zero for short keys. djb2 is not collision-resistant but has excellent avalanche behaviour for short ASCII strings, which is the dominant key type in a cache workload.
The bucket index is computed as hash(key) % ht->size. Because size is always a power of two after the first resize, this modulo can be computed with a bitwise AND in practice, though the current code uses % for readability.
Separate chaining
When two keys hash to the same bucket index, they coexist in a linked list hanging off that bucket slot. New entries are prepended (O(1)) rather than appended.strcmp until they find a matching key or exhaust the list. Chain length is bounded by the load factor: RadishDB resizes at 0.75, keeping average chain length below 1.
ht_set: writing a key
ht_set handles both inserts and in-place updates.
Check load factor
Before every write, the load factor (
count / size) is computed. If it exceeds 0.75, ht_resize is called to double the table before the write proceeds.hashtable.c
Walk the chain for an existing key
The code walks the linked list at that bucket. If a matching key is found, it frees the old value, assigns the new one, and updates
expires_at. No count increment occurs on an update.hashtable.c
ht_get: passive expiry
ht_get embeds expiry logic directly in the read path. No separate background thread is needed.
hashtable.c
- If
expires_atis non-zero and the current time is at or pastexpires_at, the entry is deleted immediately andNULLis returned — the key appears to never have existed. - Otherwise the value pointer is returned directly. The caller must not free this pointer; it is owned by the entry.
Passive expiry only fires when the key is accessed. Keys that are set with a TTL and then never read again remain in memory until the active sweeper (
expire_sweep) removes them. See TTL expiration for the full sweep algorithm.ht_delete: unlinking an entry
ht_delete must handle two cases depending on where in the chain the target entry sits.
free(entry->key), free(entry->value), and free(entry) are called. ht->count is decremented. The function returns 1 on success and 0 if the key was not found.
ht_resize: rehashing
ht_resize allocates a fresh bucket array and rehashes every live entry into it. Crucially, it reuses the existing Entry nodes — it does not malloc new entries, only malloc the new bucket array.
hashtable.c
entry->next before relinking because entry->next is overwritten during the prepend. After the loop, the old bucket array is freed. ht->resizes is incremented for telemetry.
Memory model
RadishDB applies a strict ownership rule: the hash table owns all strings.| Operation | Memory action |
|---|---|
ht_set (new key) | strdup(key) + strdup(value) |
ht_set (update) | free(old_value) + strdup(new_value); key is unchanged |
ht_get | Returns a raw pointer into the entry — do not free |
ht_delete | free(key) + free(value) + free(entry) |
ht_resize | Reuses Entry nodes; frees old bucket array only |
ht_free | Walks all chains, frees every entry, frees bucket array, frees ht |
strdup is paired with exactly one free.
ht_info: introspection
ht_info returns a snapshot of hash table statistics without modifying state:
hashtable.h
expired_keys by comparing entry->expires_at against time(NULL). max_chain is updated for each bucket by counting chain length inline. The INFO command calls ht_info and formats the result for display.
Performance characteristics
| Operation | Average case | Worst case |
|---|---|---|
ht_get | O(1) | O(n) — all keys in one bucket |
ht_set (update) | O(1) | O(n) |
ht_set (insert, no resize) | O(1) amortized | O(n) |
ht_set (insert, triggers resize) | O(n) | O(n) |
ht_delete | O(1) | O(n) |
ht_resize | O(n) | O(n) |
ht_info | O(n) | O(n) |