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