libxtc 0.4.0
Async concurrency for C: Tokio + Seastar + BEAM, in one library
Loading...
Searching...
No Matches
xtc_chash.h
1/*-
2 * Copyright (c) 2026, The XTC Project
3 * Use of this source code is governed by the ISC License.
4 *
5 * src/inc/xtc_chash.h
6 * RCU-protected concurrent hash table. M13a's primary xtc_rcu
7 * consumer: chained buckets, an array of per-bucket mutexes for
8 * writers (insert/remove/put serialize only within their own
9 * bucket -- disjoint buckets proceed fully in parallel), and
10 * xtc_rcu for readers, so xtc_chash_get takes NO lock at all --
11 * it is wait-free, bounded only by the chain length it walks.
12 *
13 * Storage convention: caller-owned `void *` key and value,
14 * same as xtc_pdict / xtc_chan (the store keeps the pointers
15 * verbatim, no deep copy). A caller-supplied comparator and
16 * hash function make the table key-type-agnostic (the qsort(3)
17 * convention: cmp returns <0/0/>0, but chash only tests for
18 * equality via == 0).
19 *
20 * Concurrency model:
21 * - Bucket array is RCU-protected: a resize (grow always-on;
22 * shrink opt-in) allocates a new array of the new size,
23 * rehashes every live node into it, publishes the new array
24 * pointer, and retires the old array via xtc_rcu_retire. A
25 * reader that loaded the old array pointer before the swap
26 * keeps working against a fully valid table (larger or smaller
27 * than the new one) until it leaves its read-side; nothing
28 * under it moves.
29 * - Node removal: unlinked from its bucket chain under that
30 * bucket's mutex, then handed to xtc_rcu_retire -- NEVER
31 * freed directly. A concurrent reader that already read a
32 * pointer to the node (chased into the chain before the
33 * unlink) keeps a valid node until it leaves its read-side;
34 * RCU reclaims the node only after every such reader has
35 * drained.
36 * - Node insertion: publish-then-link. The new node is fully
37 * initialized, then linked with a single release-ordered
38 * store of its bucket's head pointer (or the previous node's
39 * `next`) -- a concurrent reader either sees the old chain or
40 * the new one with the new node fully formed, never a torn
41 * write.
42 *
43 * xtc_chash_get returns a value pointer valid only inside the
44 * caller's OWN xtc_rcu_read_lock/xtc_rcu_read_unlock bracket,
45 * exactly like every other xtc_rcu consumer: the table does not
46 * take the read-side for you (nesting is supported, see
47 * xtc_rcu.h, so a caller already inside a read-side may call get
48 * freely) because a caller usually wants to do more than one
49 * lookup, or read the value's contents, inside one critical
50 * section. xtc_chash_insert / _remove take the read-side
51 * internally for their own traversal and enter/leave it around
52 * the mutable part; callers do not need an outer read-side for
53 * them.
54 *
55 * Resize: xtc_chash grows automatically past a 75% load factor
56 * and, IF auto-shrink is enabled (xtc_chash_set_auto_shrink,
57 * default OFF), shrinks automatically once the load factor drops
58 * below 18.75% -- never below the initial/minimum bucket count.
59 * Grow is always-on because memory then tracks the true working-
60 * set peak monotonically; shrink is opt-in because it adds a
61 * rehash to the remove path and reclaims memory a user may have
62 * wanted stable. Enable it if your workload has large delete
63 * phases and you want memory reclaimed. The wide gap between the
64 * grow (0.75) and shrink (0.1875) triggers is deliberate
65 * hysteresis: a table hovering near a boundary cannot thrash
66 * grow<->shrink (a shrink halves the array, doubling the load
67 * factor to <0.375, so re-growing needs a 2x count increase).
68 * Both directions use the identical RCU-swap machinery.
69 */
70
71#ifndef XTC_CHASH_H
72#define XTC_CHASH_H
73
74#include "xtc_export.h"
75
76#include <stddef.h>
77#include <stdint.h>
78
79#include "xtc.h"
80
81typedef struct xtc_chash xtc_chash_t;
82
83/* Key comparator: qsort(3) convention, but chash only ever tests the
84 * sign of the result for == 0 (equal) vs. != 0 (not equal) -- it does
85 * not order buckets, so a cmp that only distinguishes equal/unequal
86 * (e.g. always returns 0 or 1) is fine too. */
87typedef int (*xtc_chash_cmp_fn)(const void *a, const void *b);
88
89/* Key hash: any distribution is correct; a poor one just chains more.
90 * Same key must always hash the same for the table's lifetime. */
91typedef uint64_t (*xtc_chash_hash_fn)(const void *key);
92
93/*
94 * PUBLIC: int xtc_chash_create __P((xtc_chash_cmp_fn, xtc_chash_hash_fn, size_t, xtc_chash_t **));
95 * PUBLIC: void xtc_chash_destroy __P((xtc_chash_t *));
96 * PUBLIC: int xtc_chash_get __P((xtc_chash_t *, const void *, void **));
97 * PUBLIC: int xtc_chash_insert __P((xtc_chash_t *, void *, void *, void **));
98 * PUBLIC: int xtc_chash_remove __P((xtc_chash_t *, const void *, void **));
99 * PUBLIC: size_t xtc_chash_size __P((const xtc_chash_t *));
100 * PUBLIC: void xtc_chash_set_auto_shrink __P((xtc_chash_t *, int));
101 * PUBLIC: int xtc_chash_get_auto_shrink __P((const xtc_chash_t *));
102 */
103
104/*
105 * Create a table with `initial_capacity` buckets (rounded up to the
106 * next power of two; 0 defaults to 16). cmp and hash are required
107 * (non-NULL). Returns XTC_OK and writes *out, or XTC_E_INVAL /
108 * XTC_E_NOMEM.
109 */
110XTC_API int xtc_chash_create(xtc_chash_cmp_fn cmp, xtc_chash_hash_fn hash,
111 size_t initial_capacity, xtc_chash_t **out);
112
113/*
114 * Destroy the table. NOT concurrency-safe with any other call on
115 * this table -- the caller must ensure no reader or writer is active
116 * (same contract as xtc_rcu_fini: quiesce first). Frees every
117 * remaining node's bookkeeping but NOT the caller-owned key/value
118 * pointers (caller-owned lifetime, same as xtc_chan/xtc_pdict).
119 */
120XTC_API void xtc_chash_destroy(xtc_chash_t *h);
121
122/*
123 * Look up `key`. On a hit, returns XTC_OK and writes the value
124 * pointer to *out_value. On a miss, returns XTC_E_NOTFOUND and does
125 * not touch *out_value. MUST be called inside the caller's own
126 * xtc_rcu_read_lock/_read_unlock bracket (nesting is fine); the value
127 * pointer written to *out_value is valid only until the caller's
128 * read-side ends -- do not stash it past xtc_rcu_read_unlock unless
129 * you know the value's lifetime is otherwise pinned.
130 */
131XTC_API int xtc_chash_get(xtc_chash_t *h, const void *key, void **out_value);
132
133/*
134 * Insert (key, value), replacing any existing entry for an equal key.
135 * Takes the read-side internally for its traversal; safe to call
136 * without an outer read-side. On replace, the OLD value pointer is
137 * written to *out_old_value (out_old_value may be NULL if the caller
138 * does not care) and the old key/value pointers are the caller's
139 * responsibility to free (this table never frees caller payloads).
140 * On a fresh insert, *out_old_value (if non-NULL) is set to NULL.
141 * May trigger a resize (a grow). Returns XTC_OK or XTC_E_NOMEM.
142 */
143XTC_API int xtc_chash_insert(xtc_chash_t *h, void *key, void *value,
144 void **out_old_value);
145
146/*
147 * Remove the entry for `key`. On a hit, returns XTC_OK and writes
148 * the removed value pointer to *out_value (may be NULL). The node's
149 * memory is retired via xtc_rcu_retire, never freed synchronously --
150 * a concurrent reader that is already inside the chain sees a fully
151 * valid node until it leaves its read-side. On a miss, returns
152 * XTC_E_NOTFOUND.
153 */
154XTC_API int xtc_chash_remove(xtc_chash_t *h, const void *key, void **out_value);
155
156/* Approximate live entry count (an _Atomic counter; exact at any
157 * instant with no concurrent writers, a fresh-enough estimate
158 * otherwise). */
159XTC_API size_t xtc_chash_size(const xtc_chash_t *h);
160
161/*
162 * Enable (on != 0) or disable (on == 0) automatic shrink; default OFF.
163 * When enabled, xtc_chash_remove may halve the bucket array once the
164 * load factor drops below 18.75%, never below the initial capacity
165 * passed to xtc_chash_create (minimum 16). Shrink uses the same
166 * RCU-swap machinery as grow: it takes every stripe lock, rehashes
167 * every live node into the smaller array, publishes it with a release
168 * store, and retires the old array via xtc_rcu_retire, so a concurrent
169 * reader mid-lookup on the old array stays correct until its read-side
170 * ends. The 0.75-grow / 0.1875-shrink gap is deliberate hysteresis --
171 * a table near a boundary cannot thrash grow<->shrink. Enable it if
172 * your workload has large delete phases and you want the memory
173 * reclaimed; leave it OFF (the default) if you want stable memory and
174 * a lighter remove path. Takes effect immediately and is safe to call
175 * from any thread (a single atomic flag).
176 */
177XTC_API void xtc_chash_set_auto_shrink(xtc_chash_t *h, int on);
178XTC_API int xtc_chash_get_auto_shrink(const xtc_chash_t *h);
179
180#endif /* XTC_CHASH_H */