mirror of
https://github.com/shadowsocks/shadowsocks-libev.git
synced 2026-10-23 08:03:54 +00:00
Rewrite the cache on a fixed slab without per-entry allocation
cache.cpp keeps its C API but replaces uthash and the two mallocs per entry with one slab of nodes holding their keys inline, linked in last-use order, and an open-addressing index of node ids that hashes through the slab with a per-cache random seed. Both blocks are reserved by cache_create() and touched only as entries are used, so a large, mostly empty cache costs no resident memory. Semantics are unchanged: an insert evicts the least recently used entry once the cache holds capacity entries (the AEAD-2022 session table's 4095/4096 boundary), lookups refresh recency and age, cache_clear() drops stale entries, and free_cb may re-enter the cache. Re-inserting a key now replaces its data instead of shadowing it. cache_create_keyed() sizes keys per cache; the AEAD-2022 salt pool and session table use it. test_cache_cpp covers the boundary, ordering, replacement, ageing, re-entrant callbacks and limits; alloc_steady_cache requires zero allocations across 100,000 inserts on a full cache. AEAD-2022 ss-server drops from 84 to 80 allocations per relayed connection.
This commit is contained in:
@@ -5,7 +5,6 @@
|
||||
src/aead.cpp 4
|
||||
src/aead2022.cpp 3
|
||||
src/aead2022_udp.cpp 5
|
||||
src/cache.cpp 3
|
||||
src/crypto.cpp 4
|
||||
src/ipset.cpp 3
|
||||
src/jconf.cpp 16
|
||||
|
||||
@@ -123,6 +123,9 @@ if(ENABLE_NFTABLES)
|
||||
list(APPEND SS_SERVER_DEPS ${MNL_LIB} ${NFTNL_LIB})
|
||||
endif()
|
||||
|
||||
# Modules rewritten in C++ compile with the stricter warning set.
|
||||
ss_strict_warnings(cache.cpp)
|
||||
|
||||
function(ss_program name module sources)
|
||||
add_executable(${name} ${sources})
|
||||
target_compile_definitions(${name} PRIVATE ${module})
|
||||
|
||||
+1
-1
@@ -113,7 +113,7 @@ static int
|
||||
ss2022_salt_check_and_add(const uint8_t *salt, size_t salt_len)
|
||||
{
|
||||
if (ss2022_salt_pool == NULL) {
|
||||
if (cache_create(&ss2022_salt_pool, SS2022_SALT_POOL_CAP, NULL) != 0)
|
||||
if (cache_create_keyed(&ss2022_salt_pool, SS2022_SALT_POOL_CAP, MAX_KEY_LENGTH, NULL) != 0)
|
||||
return CRYPTO_ERROR;
|
||||
}
|
||||
|
||||
|
||||
@@ -202,8 +202,8 @@ static ss2022_udp_session_t *
|
||||
session_by_id(const uint8_t *session_id, cipher_t *cipher)
|
||||
{
|
||||
if (ss2022_sessions == NULL
|
||||
&& cache_create(&ss2022_sessions, SS2022_SESSION_TABLE_CAP,
|
||||
session_free_cb) != 0)
|
||||
&& cache_create_keyed(&ss2022_sessions, SS2022_SESSION_TABLE_CAP,
|
||||
SS2022_SESSION_ID_LEN, session_free_cb) != 0)
|
||||
return NULL;
|
||||
|
||||
ss2022_udp_session_t *s = NULL;
|
||||
|
||||
+306
-220
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* cache.c - Manage the connection cache for UDPRELAY
|
||||
* cache.cpp - Fixed-capacity LRU cache for the UDP relay and AEAD-2022
|
||||
*
|
||||
* Copyright (C) 2013 - 2019, Max Lv <max.c.lv@gmail.com>
|
||||
*
|
||||
@@ -20,293 +20,379 @@
|
||||
* <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Original Author: Oliver Lorenz (ol), olli@olorenz.org, https://olorenz.org
|
||||
* License: This is licensed under the same terms as uthash itself
|
||||
*/
|
||||
// Layout: one slab of `capacity` nodes, each holding its key inline, linked
|
||||
// in least-recently-used order, plus an open-addressing index of node ids
|
||||
// (linear probing, backward-shift deletion, at most half full). Keys are
|
||||
// stored once, in the slab; the index hashes through it. Both blocks are
|
||||
// reserved by cache_create() and only touched as entries are used: nodes
|
||||
// are handed out in order before recycled ones, and the index stores
|
||||
// node id + 1 so that zero-filled memory reads as empty.
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <cerrno>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
|
||||
#include <sodium.h>
|
||||
|
||||
#include "cache.h"
|
||||
#include "core/fixed_map.hpp"
|
||||
#include "core/region.hpp"
|
||||
#include "platform.h"
|
||||
#include "utils.h"
|
||||
|
||||
/** Creates a new cache object
|
||||
*
|
||||
* @param dst
|
||||
* Where the newly allocated cache object will be stored in
|
||||
*
|
||||
* @param capacity
|
||||
* The maximum number of elements this cache object can hold
|
||||
*
|
||||
* @return EINVAL if dst is NULL, ENOMEM if malloc fails, 0 otherwise
|
||||
*/
|
||||
namespace {
|
||||
|
||||
constexpr std::uint32_t kNone = UINT32_MAX;
|
||||
|
||||
struct Node {
|
||||
std::uint32_t prev = kNone;
|
||||
std::uint32_t next = kNone;
|
||||
double ts = 0;
|
||||
void *data = nullptr;
|
||||
std::uint16_t key_len = 0;
|
||||
bool used = false;
|
||||
// key bytes (max_key_len + 1, NUL-terminated for free_cb) follow
|
||||
};
|
||||
|
||||
std::size_t align_up(std::size_t n, std::size_t a)
|
||||
{
|
||||
return (n + a - 1) & ~(a - 1);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct cache {
|
||||
std::size_t capacity = 0; // as requested; eviction threshold
|
||||
std::size_t max_key_len = 0;
|
||||
std::size_t stride = 0; // bytes per node, key included
|
||||
void (*free_cb)(void *key, void *element) = nullptr;
|
||||
ss::Region nodes;
|
||||
ss::Region index; // node id + 1 per slot, 0 when empty
|
||||
std::uint32_t node_count = 0; // slab size (capacity, at least 1)
|
||||
std::uint32_t fresh = 0; // nodes [fresh, node_count) were never used
|
||||
std::uint32_t mask = 0; // index slots - 1
|
||||
std::uint32_t size = 0;
|
||||
std::uint32_t free_head = kNone;
|
||||
std::uint32_t oldest = kNone;
|
||||
std::uint32_t newest = kNone;
|
||||
std::uint64_t seed = 0;
|
||||
|
||||
Node *node(std::uint32_t id) const
|
||||
{
|
||||
return reinterpret_cast<Node *>(nodes.data() + static_cast<std::size_t>(id) * stride);
|
||||
}
|
||||
char *key(std::uint32_t id) const { return reinterpret_cast<char *>(node(id)) + sizeof(Node); }
|
||||
std::uint32_t *slots() const { return reinterpret_cast<std::uint32_t *>(index.data()); }
|
||||
bool empty_slot(std::uint32_t i) const { return slots()[i] == 0; }
|
||||
std::uint32_t at(std::uint32_t i) const { return slots()[i] - 1; }
|
||||
void put(std::uint32_t i, std::uint32_t id) const { slots()[i] = id + 1; }
|
||||
|
||||
std::uint32_t home(const char *k, std::size_t len) const
|
||||
{
|
||||
return static_cast<std::uint32_t>(ss::hash_bytes(k, len, seed)) & mask;
|
||||
}
|
||||
|
||||
bool matches(std::uint32_t id, const char *k, std::size_t len) const
|
||||
{
|
||||
const Node *n = node(id);
|
||||
return n->key_len == len && std::memcmp(key(id), k, len) == 0;
|
||||
}
|
||||
|
||||
// Index slot holding the key, or the empty slot that ends its probe run.
|
||||
std::uint32_t probe(const char *k, std::size_t len, bool &found) const
|
||||
{
|
||||
std::uint32_t i = home(k, len);
|
||||
while (!empty_slot(i)) {
|
||||
if (matches(at(i), k, len)) {
|
||||
found = true;
|
||||
return i;
|
||||
}
|
||||
i = (i + 1) & mask;
|
||||
}
|
||||
found = false;
|
||||
return i;
|
||||
}
|
||||
|
||||
void unindex(std::uint32_t hole)
|
||||
{
|
||||
slots()[hole] = 0;
|
||||
for (std::uint32_t j = (hole + 1) & mask; !empty_slot(j); j = (j + 1) & mask) {
|
||||
std::uint32_t id = at(j);
|
||||
std::uint32_t k = home(key(id), node(id)->key_len);
|
||||
bool movable = (hole <= j) ? (k <= hole || k > j) : (k <= hole && k > j);
|
||||
if (movable) {
|
||||
put(hole, id);
|
||||
slots()[j] = 0;
|
||||
hole = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void unlink(std::uint32_t id)
|
||||
{
|
||||
Node *n = node(id);
|
||||
if (n->prev != kNone) {
|
||||
node(n->prev)->next = n->next;
|
||||
} else {
|
||||
oldest = n->next;
|
||||
}
|
||||
if (n->next != kNone) {
|
||||
node(n->next)->prev = n->prev;
|
||||
} else {
|
||||
newest = n->prev;
|
||||
}
|
||||
n->prev = n->next = kNone;
|
||||
}
|
||||
|
||||
void link_newest(std::uint32_t id)
|
||||
{
|
||||
Node *n = node(id);
|
||||
n->prev = newest;
|
||||
n->next = kNone;
|
||||
if (newest != kNone) {
|
||||
node(newest)->next = id;
|
||||
} else {
|
||||
oldest = id;
|
||||
}
|
||||
newest = id;
|
||||
}
|
||||
|
||||
void touch(std::uint32_t id)
|
||||
{
|
||||
node(id)->ts = ss_monotonic_time();
|
||||
if (id != newest) {
|
||||
unlink(id);
|
||||
link_newest(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Unlink the entry in index slot `slot` and release its data. The node
|
||||
// is back on the free list before free_cb runs, so free_cb may use the
|
||||
// cache.
|
||||
void drop(std::uint32_t slot, bool release_data)
|
||||
{
|
||||
std::uint32_t id = at(slot);
|
||||
Node *n = node(id);
|
||||
char key_copy[256];
|
||||
std::size_t len = n->key_len;
|
||||
std::memcpy(key_copy, key(id), len);
|
||||
key_copy[len] = '\0';
|
||||
void *data = n->data;
|
||||
|
||||
unindex(slot);
|
||||
unlink(id);
|
||||
n->used = false;
|
||||
n->data = nullptr;
|
||||
n->next = free_head;
|
||||
free_head = id;
|
||||
--size;
|
||||
|
||||
if (release_data && data != nullptr) {
|
||||
if (free_cb != nullptr) {
|
||||
free_cb(key_copy, data);
|
||||
} else {
|
||||
std::free(data); // heap-ok: caller-owned data, per the C API contract
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void drop_id(std::uint32_t id, bool release_data)
|
||||
{
|
||||
bool found = false;
|
||||
std::uint32_t slot = probe(key(id), node(id)->key_len, found);
|
||||
if (!found) {
|
||||
std::abort(); // index and slab disagree
|
||||
}
|
||||
drop(slot, release_data);
|
||||
}
|
||||
};
|
||||
|
||||
extern "C" {
|
||||
|
||||
int
|
||||
cache_create_keyed(struct cache **dst, size_t capacity, size_t max_key_len,
|
||||
void (*free_cb)(void *key, void *element))
|
||||
{
|
||||
if (dst == nullptr || max_key_len == 0 || max_key_len > 255 || capacity > (UINT32_MAX >> 2)) {
|
||||
return EINVAL;
|
||||
}
|
||||
auto *c = static_cast<struct cache *>(std::malloc(sizeof(struct cache))); // heap-ok: created at startup
|
||||
if (c == nullptr) {
|
||||
return ENOMEM;
|
||||
}
|
||||
new (c) struct cache();
|
||||
c->capacity = capacity;
|
||||
c->max_key_len = max_key_len;
|
||||
c->free_cb = free_cb;
|
||||
c->node_count = capacity == 0 ? 1 : static_cast<std::uint32_t>(capacity);
|
||||
c->stride = align_up(sizeof(Node) + max_key_len + 1, alignof(Node));
|
||||
std::uint32_t slots = 2;
|
||||
while (slots < c->node_count * 2U) {
|
||||
slots <<= 1;
|
||||
}
|
||||
c->mask = slots - 1;
|
||||
c->seed = (static_cast<std::uint64_t>(randombytes_random()) << 32) | randombytes_random();
|
||||
|
||||
auto nodes = ss::Region::reserve(static_cast<std::size_t>(c->node_count) * c->stride, alignof(Node));
|
||||
auto index = ss::Region::reserve(static_cast<std::size_t>(slots) * sizeof(std::uint32_t), alignof(std::uint32_t));
|
||||
if (!nodes || !index) {
|
||||
c->~cache();
|
||||
std::free(c); // heap-ok: failed startup reservation
|
||||
return ENOMEM;
|
||||
}
|
||||
c->nodes = std::move(*nodes);
|
||||
c->index = std::move(*index);
|
||||
*dst = c;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
cache_create(struct cache **dst, const size_t capacity,
|
||||
void (*free_cb)(void *key, void *element))
|
||||
{
|
||||
struct cache *created = NULL;
|
||||
|
||||
if (!dst) {
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
if ((created = (struct cache *)malloc(sizeof(*created))) == NULL) {
|
||||
return ENOMEM;
|
||||
}
|
||||
|
||||
created->max_entries = capacity;
|
||||
created->entries = NULL;
|
||||
created->free_cb = free_cb;
|
||||
*dst = created;
|
||||
return 0;
|
||||
return cache_create_keyed(dst, capacity, CACHE_DEFAULT_MAX_KEY, free_cb);
|
||||
}
|
||||
|
||||
/** Frees an allocated cache object
|
||||
*
|
||||
* @param cache
|
||||
* The cache object to free
|
||||
*
|
||||
* @param keep_data
|
||||
* Whether to free contained data or just delete references to it
|
||||
*
|
||||
* @return EINVAL if cache is NULL, 0 otherwise
|
||||
*/
|
||||
int
|
||||
cache_delete(struct cache *cache, int keep_data)
|
||||
{
|
||||
struct cache_entry *entry, *tmp;
|
||||
|
||||
if (!cache) {
|
||||
if (cache == nullptr) {
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
if (keep_data) {
|
||||
HASH_CLEAR(hh, cache->entries);
|
||||
} else {
|
||||
HASH_ITER(hh, cache->entries, entry, tmp){
|
||||
// NOLINTNEXTLINE(clang-analyzer-unix.Malloc): uthash unlinks here; entry is freed afterwards
|
||||
HASH_DEL(cache->entries, entry);
|
||||
if (entry->data != NULL) {
|
||||
if (cache->free_cb) {
|
||||
cache->free_cb(entry->key, entry->data);
|
||||
} else {
|
||||
ss_free(entry->data);
|
||||
}
|
||||
}
|
||||
ss_free(entry->key);
|
||||
ss_free(entry);
|
||||
}
|
||||
while (cache->oldest != kNone) {
|
||||
cache->drop_id(cache->oldest, !keep_data);
|
||||
}
|
||||
|
||||
ss_free(cache);
|
||||
cache->~cache();
|
||||
std::free(cache); // heap-ok: created at startup
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Clear old cache object
|
||||
*
|
||||
* @param cache
|
||||
* The cache object to clear
|
||||
*
|
||||
* @param age
|
||||
* Clear only objects older than the age (sec)
|
||||
*
|
||||
* @return EINVAL if cache is NULL, 0 otherwise
|
||||
*/
|
||||
int
|
||||
cache_clear(struct cache *cache, double age)
|
||||
{
|
||||
struct cache_entry *entry, *tmp;
|
||||
|
||||
if (!cache) {
|
||||
if (cache == nullptr) {
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
double now = ss_monotonic_time();
|
||||
|
||||
HASH_ITER(hh, cache->entries, entry, tmp){
|
||||
if (now - entry->ts > age) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-unix.Malloc): uthash unlinks here; entry is freed afterwards
|
||||
HASH_DEL(cache->entries, entry);
|
||||
if (entry->data != NULL) {
|
||||
if (cache->free_cb) {
|
||||
cache->free_cb(entry->key, entry->data);
|
||||
} else {
|
||||
ss_free(entry->data);
|
||||
}
|
||||
}
|
||||
ss_free(entry->key);
|
||||
ss_free(entry);
|
||||
}
|
||||
// Entries are in last-use order, so the stale ones are at the front.
|
||||
while (cache->oldest != kNone && now - cache->node(cache->oldest)->ts > age) {
|
||||
cache->drop_id(cache->oldest, true);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Removes a cache entry
|
||||
*
|
||||
* @param cache
|
||||
* The cache object
|
||||
*
|
||||
* @param key
|
||||
* The key of the entry to remove
|
||||
*
|
||||
* @param key_len
|
||||
* The length of key
|
||||
*
|
||||
* @return EINVAL if cache is NULL, 0 otherwise
|
||||
*/
|
||||
int
|
||||
cache_remove(struct cache *cache, char *key, size_t key_len)
|
||||
{
|
||||
struct cache_entry *tmp;
|
||||
|
||||
if (!cache || !key) {
|
||||
if (cache == nullptr || key == nullptr) {
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
HASH_FIND(hh, cache->entries, key, key_len, tmp);
|
||||
|
||||
if (tmp) {
|
||||
HASH_DEL(cache->entries, tmp);
|
||||
if (tmp->data != NULL) {
|
||||
if (cache->free_cb) {
|
||||
cache->free_cb(tmp->key, tmp->data);
|
||||
} else {
|
||||
ss_free(tmp->data);
|
||||
}
|
||||
}
|
||||
ss_free(tmp->key);
|
||||
ss_free(tmp);
|
||||
bool found = false;
|
||||
std::uint32_t slot = cache->probe(key, key_len, found);
|
||||
if (found) {
|
||||
cache->drop(slot, true);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Checks if a given key is in the cache
|
||||
*
|
||||
* @param cache
|
||||
* The cache object
|
||||
*
|
||||
* @param key
|
||||
* The key to look-up
|
||||
*
|
||||
* @param key_len
|
||||
* The length of key
|
||||
*
|
||||
* @param result
|
||||
* Where to store the result if key is found.
|
||||
*
|
||||
* A warning: Even though result is just a pointer,
|
||||
* you have to call this function with a **ptr,
|
||||
* otherwise this will blow up in your face.
|
||||
*
|
||||
* @return EINVAL if cache is NULL, 0 otherwise
|
||||
*/
|
||||
int
|
||||
cache_lookup(struct cache *cache, char *key, size_t key_len, void *result)
|
||||
{
|
||||
struct cache_entry *tmp = NULL;
|
||||
void **out = (void **)result;
|
||||
|
||||
if (!cache || !key || !result) {
|
||||
if (cache == nullptr || key == nullptr || result == nullptr) {
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
HASH_FIND(hh, cache->entries, key, key_len, tmp);
|
||||
if (tmp) {
|
||||
HASH_DELETE(hh, cache->entries, tmp);
|
||||
tmp->ts = ss_monotonic_time();
|
||||
// NOLINTNEXTLINE(clang-analyzer-core.DivideZero): uthash bucket count is never zero
|
||||
HASH_ADD_KEYPTR(hh, cache->entries, tmp->key, key_len, tmp);
|
||||
*out = tmp->data;
|
||||
} else {
|
||||
*out = NULL;
|
||||
void **out = static_cast<void **>(result);
|
||||
bool found = false;
|
||||
std::uint32_t slot = cache->probe(key, key_len, found);
|
||||
if (!found) {
|
||||
*out = nullptr;
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::uint32_t id = cache->at(slot);
|
||||
cache->touch(id);
|
||||
*out = cache->node(id)->data;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
cache_key_exist(struct cache *cache, char *key, size_t key_len)
|
||||
{
|
||||
struct cache_entry *tmp = NULL;
|
||||
|
||||
if (!cache || !key) {
|
||||
if (cache == nullptr || key == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
HASH_FIND(hh, cache->entries, key, key_len, tmp);
|
||||
if (tmp) {
|
||||
HASH_DELETE(hh, cache->entries, tmp);
|
||||
tmp->ts = ss_monotonic_time();
|
||||
// NOLINTNEXTLINE(clang-analyzer-core.DivideZero): uthash bucket count is never zero
|
||||
HASH_ADD_KEYPTR(hh, cache->entries, tmp->key, key_len, tmp);
|
||||
return 1;
|
||||
bool found = false;
|
||||
std::uint32_t slot = cache->probe(key, key_len, found);
|
||||
if (!found) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
cache->touch(cache->at(slot));
|
||||
return 1;
|
||||
}
|
||||
|
||||
/** Inserts a given <key, value> pair into the cache
|
||||
*
|
||||
* @param cache
|
||||
* The cache object
|
||||
*
|
||||
* @param key
|
||||
* The key that identifies <value>
|
||||
*
|
||||
* @param key_len
|
||||
* The length of key
|
||||
*
|
||||
* @param data
|
||||
* Data associated with <key>
|
||||
*
|
||||
* @return EINVAL if cache is NULL, ENOMEM if malloc fails, 0 otherwise
|
||||
*/
|
||||
int
|
||||
cache_insert(struct cache *cache, char *key, size_t key_len, void *data)
|
||||
{
|
||||
struct cache_entry *entry = NULL;
|
||||
struct cache_entry *tmp_entry = NULL;
|
||||
|
||||
if (!cache) {
|
||||
if (cache == nullptr || key == nullptr || key_len > cache->max_key_len) {
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
if ((entry = (struct cache_entry *)malloc(sizeof(*entry))) == NULL) {
|
||||
return ENOMEM;
|
||||
}
|
||||
|
||||
entry->key = (char *)ss_malloc(key_len + 1);
|
||||
memcpy(entry->key, key, key_len);
|
||||
entry->key[key_len] = 0;
|
||||
|
||||
entry->data = data;
|
||||
entry->ts = ss_monotonic_time();
|
||||
// NOLINTNEXTLINE(clang-analyzer-core.DivideZero): uthash bucket count is never zero
|
||||
HASH_ADD_KEYPTR(hh, cache->entries, entry->key, key_len, entry);
|
||||
|
||||
if (HASH_COUNT(cache->entries) >= cache->max_entries) {
|
||||
HASH_ITER(hh, cache->entries, entry, tmp_entry){
|
||||
HASH_DELETE(hh, cache->entries, entry);
|
||||
if (entry->data != NULL) {
|
||||
if (cache->free_cb) {
|
||||
cache->free_cb(entry->key, entry->data);
|
||||
} else {
|
||||
ss_free(entry->data);
|
||||
}
|
||||
bool found = false;
|
||||
std::uint32_t slot = cache->probe(key, key_len, found);
|
||||
if (found) {
|
||||
// Re-inserting a key replaces its data; the old data is released
|
||||
// as if the entry had been evicted.
|
||||
std::uint32_t id = cache->at(slot);
|
||||
Node *n = cache->node(id);
|
||||
void *old = n->data;
|
||||
n->data = data;
|
||||
cache->touch(id);
|
||||
if (old != nullptr && old != data) {
|
||||
char key_copy[256];
|
||||
std::memcpy(key_copy, key, key_len);
|
||||
key_copy[key_len] = '\0';
|
||||
if (cache->free_cb != nullptr) {
|
||||
cache->free_cb(key_copy, old);
|
||||
} else {
|
||||
std::free(old); // heap-ok: caller-owned data, per the C API contract
|
||||
}
|
||||
ss_free(entry->key);
|
||||
ss_free(entry);
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
std::uint32_t id;
|
||||
if (cache->free_head != kNone) {
|
||||
id = cache->free_head;
|
||||
cache->free_head = cache->node(id)->next;
|
||||
} else if (cache->fresh < cache->node_count) {
|
||||
id = cache->fresh++;
|
||||
new (cache->node(id)) Node();
|
||||
} else {
|
||||
std::abort(); // eviction below keeps at least one node free
|
||||
}
|
||||
Node *n = cache->node(id);
|
||||
n->used = true;
|
||||
n->data = data;
|
||||
n->ts = ss_monotonic_time();
|
||||
n->key_len = static_cast<std::uint16_t>(key_len);
|
||||
std::memcpy(cache->key(id), key, key_len);
|
||||
cache->key(id)[key_len] = '\0';
|
||||
cache->put(slot, id);
|
||||
cache->link_newest(id);
|
||||
++cache->size;
|
||||
|
||||
if (cache->size >= cache->capacity) {
|
||||
cache->drop_id(cache->oldest, true);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t
|
||||
cache_size(const struct cache *cache)
|
||||
{
|
||||
return cache == nullptr ? 0 : cache->size;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
+21
-19
@@ -21,46 +21,48 @@
|
||||
*/
|
||||
|
||||
/*
|
||||
* Original Author: Oliver Lorenz (ol), olli@olorenz.org, https://olorenz.org
|
||||
* License: This is licensed under the same terms as uthash itself
|
||||
* The first implementation was by Oliver Lorenz (ol), olli@olorenz.org,
|
||||
* https://olorenz.org, on top of uthash.
|
||||
*/
|
||||
|
||||
#ifndef _CACHE_
|
||||
#define _CACHE_
|
||||
|
||||
#include "uthash.h"
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* A cache entry
|
||||
/*
|
||||
* A fixed-capacity cache of (key, data) pairs ordered by last use. All of
|
||||
* its memory is reserved by cache_create(), so inserting, looking up and
|
||||
* evicting never allocate.
|
||||
*
|
||||
* Inserting evicts the least recently used entry once the cache holds
|
||||
* `capacity` entries, so at most capacity - 1 entries remain afterwards.
|
||||
* Evicted, removed and cleared entries are handed to free_cb(key, data);
|
||||
* without a free_cb their data is released with free(). Entries whose data
|
||||
* is NULL are dropped silently. free_cb may use the cache.
|
||||
*/
|
||||
struct cache_entry {
|
||||
char *key; /**<The key */
|
||||
void *data; /**<Payload */
|
||||
double ts; /**<Timestamp */
|
||||
UT_hash_handle hh; /**<Hash Handle for uthash */
|
||||
};
|
||||
struct cache;
|
||||
|
||||
/**
|
||||
* A cache object
|
||||
*/
|
||||
struct cache {
|
||||
size_t max_entries; /**<Amount of entries this cache object can hold */
|
||||
struct cache_entry *entries; /**<Head pointer for uthash */
|
||||
void (*free_cb)(void *key, void *element); /**<Callback function to free cache entries */
|
||||
};
|
||||
/* Default longest key: a UDP relay key (struct sockaddr_storage plus an
|
||||
* address family). */
|
||||
#define CACHE_DEFAULT_MAX_KEY 160
|
||||
|
||||
int cache_create(struct cache **dst, const size_t capacity,
|
||||
void (*free_cb)(void *key, void *element));
|
||||
/* Like cache_create() for keys of at most max_key_len bytes (at most 255). */
|
||||
int cache_create_keyed(struct cache **dst, size_t capacity, size_t max_key_len,
|
||||
void (*free_cb)(void *key, void *element));
|
||||
int cache_delete(struct cache *cache, int keep_data);
|
||||
int cache_clear(struct cache *cache, double age);
|
||||
int cache_lookup(struct cache *cache, char *key, size_t key_len, void *result);
|
||||
int cache_insert(struct cache *cache, char *key, size_t key_len, void *data);
|
||||
int cache_remove(struct cache *cache, char *key, size_t key_len);
|
||||
int cache_key_exist(struct cache *cache, char *key, size_t key_len);
|
||||
size_t cache_size(const struct cache *cache);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -130,6 +130,12 @@ ss_add_test(test_cache
|
||||
"test_cache.c;${PROJECT_SOURCE_DIR}/src/cache.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp"
|
||||
"ss_sodium")
|
||||
|
||||
# test_cache_cpp - fixed-capacity cache edge cases
|
||||
ss_add_test(test_cache_cpp
|
||||
"test_cache_cpp.cpp;${PROJECT_SOURCE_DIR}/src/cache.cpp"
|
||||
"ss_sodium")
|
||||
ss_strict_warnings(test_cache_cpp.cpp ${PROJECT_SOURCE_DIR}/src/cache.cpp)
|
||||
|
||||
# test_ppbloom - needs bloom
|
||||
set(TEST_PPBLOOM_LIBS "")
|
||||
list(APPEND TEST_PPBLOOM_LIBS bloom)
|
||||
@@ -294,6 +300,10 @@ if(SS_BUILD_EXECUTABLES AND NOT CMAKE_CROSSCOMPILING)
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/alloc_audit/check_steady.py
|
||||
--shim $<TARGET_FILE:ss_alloc_audit> -- $<TARGET_FILE:test_crypto_aead_cpp>)
|
||||
set_tests_properties(alloc_steady_crypto_aead PROPERTIES LABELS "unit" TIMEOUT 60)
|
||||
add_test(NAME alloc_steady_cache
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/alloc_audit/check_steady.py
|
||||
--shim $<TARGET_FILE:ss_alloc_audit> -- $<TARGET_FILE:test_cache_cpp>)
|
||||
set_tests_properties(alloc_steady_cache PROPERTIES LABELS "unit" TIMEOUT 60)
|
||||
add_test(NAME alloc_audit_report
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/alloc_audit/measure_alloc.py
|
||||
--bin ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
// Edge cases of the fixed-capacity cache (src/cache.cpp) beyond
|
||||
// test_cache.c, and proof that a full cache churns without allocating.
|
||||
#include <cerrno>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include "alloc_audit/alloc_probe.hpp"
|
||||
#include "cache.h"
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
extern "C" {
|
||||
int verbose = 0;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
int g_freed = 0;
|
||||
struct cache *g_victim = nullptr;
|
||||
|
||||
void count_free(void *, void *) { ++g_freed; }
|
||||
|
||||
// Re-enters the cache it belongs to, as udprelay's callbacks may.
|
||||
void reentrant_free(void *key, void *)
|
||||
{
|
||||
++g_freed;
|
||||
char probe[] = "other";
|
||||
(void)cache_key_exist(g_victim, probe, 5);
|
||||
(void)cache_remove(g_victim, static_cast<char *>(key), std::strlen(static_cast<char *>(key)));
|
||||
}
|
||||
|
||||
void key(char (&buf)[16], int i)
|
||||
{
|
||||
std::snprintf(buf, sizeof(buf), "k%d", i);
|
||||
}
|
||||
|
||||
void test_capacity_boundary()
|
||||
{
|
||||
// The AEAD-2022 session table relies on this: a cache of N keeps N - 1.
|
||||
struct cache *c = nullptr;
|
||||
SS_CHECK(cache_create(&c, 4096, count_free) == 0);
|
||||
g_freed = 0;
|
||||
static int token;
|
||||
char k[16];
|
||||
for (int i = 0; i < 4095; ++i) {
|
||||
key(k, i);
|
||||
SS_CHECK(cache_insert(c, k, std::strlen(k), &token) == 0);
|
||||
}
|
||||
SS_CHECK(cache_size(c) == 4095 && g_freed == 0);
|
||||
key(k, 4095);
|
||||
SS_CHECK(cache_insert(c, k, std::strlen(k), &token) == 0);
|
||||
SS_CHECK(cache_size(c) == 4095 && g_freed == 1);
|
||||
key(k, 0);
|
||||
SS_CHECK(cache_key_exist(c, k, std::strlen(k)) == 0); // the oldest went
|
||||
SS_CHECK(cache_delete(c, 0) == 0);
|
||||
SS_CHECK(g_freed == 4096);
|
||||
}
|
||||
|
||||
void test_lru_order_and_replace()
|
||||
{
|
||||
struct cache *c = nullptr;
|
||||
SS_CHECK(cache_create(&c, 4, count_free) == 0);
|
||||
g_freed = 0;
|
||||
static int a, b, d, e, f;
|
||||
SS_CHECK(cache_insert(c, (char *)"a", 1, &a) == 0);
|
||||
SS_CHECK(cache_insert(c, (char *)"b", 1, &b) == 0);
|
||||
SS_CHECK(cache_insert(c, (char *)"d", 1, &d) == 0);
|
||||
// Touching "a" makes "b" the eviction candidate.
|
||||
void *found = nullptr;
|
||||
SS_CHECK(cache_lookup(c, (char *)"a", 1, &found) == 0 && found == &a);
|
||||
SS_CHECK(cache_insert(c, (char *)"e", 1, &e) == 0);
|
||||
SS_CHECK(cache_key_exist(c, (char *)"b", 1) == 0);
|
||||
SS_CHECK(cache_key_exist(c, (char *)"a", 1) == 1);
|
||||
// Re-inserting replaces the data and releases the old one.
|
||||
SS_CHECK(cache_insert(c, (char *)"a", 1, &f) == 0);
|
||||
SS_CHECK(cache_lookup(c, (char *)"a", 1, &found) == 0 && found == &f);
|
||||
SS_CHECK(g_freed == 2);
|
||||
SS_CHECK(cache_lookup(c, (char *)"zz", 2, &found) == 0 && found == nullptr);
|
||||
// keep_data skips the callbacks.
|
||||
SS_CHECK(cache_delete(c, 1) == 0);
|
||||
SS_CHECK(g_freed == 2);
|
||||
}
|
||||
|
||||
void test_clear_by_age()
|
||||
{
|
||||
struct cache *c = nullptr;
|
||||
SS_CHECK(cache_create(&c, 8, count_free) == 0);
|
||||
g_freed = 0;
|
||||
static int x;
|
||||
SS_CHECK(cache_insert(c, (char *)"old", 3, &x) == 0);
|
||||
SS_CHECK(cache_clear(c, 3600) == 0);
|
||||
SS_CHECK(cache_size(c) == 1 && g_freed == 0);
|
||||
SS_CHECK(cache_clear(c, -1) == 0); // everything is older than -1 s
|
||||
SS_CHECK(cache_size(c) == 0 && g_freed == 1);
|
||||
SS_CHECK(cache_delete(c, 0) == 0);
|
||||
}
|
||||
|
||||
void test_reentrant_callback_and_limits()
|
||||
{
|
||||
struct cache *c = nullptr;
|
||||
SS_CHECK(cache_create_keyed(&c, 2, 8, reentrant_free) == 0);
|
||||
g_victim = c;
|
||||
g_freed = 0;
|
||||
static int x;
|
||||
SS_CHECK(cache_insert(c, (char *)"one", 3, &x) == 0);
|
||||
SS_CHECK(cache_insert(c, (char *)"two", 3, &x) == 0); // evicts "one" -> callback re-enters
|
||||
SS_CHECK(g_freed == 1 && cache_size(c) == 1);
|
||||
SS_CHECK(cache_insert(c, (char *)"too-long-key", 12, &x) == EINVAL);
|
||||
SS_CHECK(cache_create_keyed(&c, 2, 0, nullptr) == EINVAL);
|
||||
SS_CHECK(cache_create_keyed(&c, 2, 256, nullptr) == EINVAL);
|
||||
SS_CHECK(cache_delete(g_victim, 0) == 0);
|
||||
SS_CHECK(cache_insert(nullptr, (char *)"x", 1, &x) == EINVAL);
|
||||
}
|
||||
|
||||
void test_zero_capacity()
|
||||
{
|
||||
// Matches the uthash version: every insert evicts immediately.
|
||||
struct cache *c = nullptr;
|
||||
SS_CHECK(cache_create(&c, 0, count_free) == 0);
|
||||
g_freed = 0;
|
||||
static int x;
|
||||
SS_CHECK(cache_insert(c, (char *)"a", 1, &x) == 0);
|
||||
SS_CHECK(cache_insert(c, (char *)"b", 1, &x) == 0);
|
||||
SS_CHECK(cache_size(c) == 0 && g_freed == 2);
|
||||
SS_CHECK(cache_delete(c, 0) == 0);
|
||||
}
|
||||
|
||||
void test_steady_churn_without_allocation()
|
||||
{
|
||||
struct cache *c = nullptr;
|
||||
SS_CHECK(cache_create(&c, 512, count_free) == 0);
|
||||
static int x;
|
||||
char k[16];
|
||||
for (int i = 0; i < 2048; ++i) { // warm-up touches every node
|
||||
key(k, i);
|
||||
SS_CHECK(cache_insert(c, k, std::strlen(k), &x) == 0);
|
||||
}
|
||||
ss::test::alloc_mark();
|
||||
void *found = nullptr;
|
||||
for (int i = 0; i < 100000; ++i) {
|
||||
key(k, i);
|
||||
SS_CHECK(cache_insert(c, k, std::strlen(k), &x) == 0);
|
||||
SS_CHECK(cache_lookup(c, k, std::strlen(k), &found) == 0 && found == &x);
|
||||
if (i % 7 == 0) {
|
||||
SS_CHECK(cache_remove(c, k, std::strlen(k)) == 0);
|
||||
}
|
||||
}
|
||||
(void)cache_clear(c, -1);
|
||||
ss::test::alloc_mark();
|
||||
SS_CHECK(cache_size(c) == 0);
|
||||
SS_CHECK(cache_delete(c, 0) == 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
SS_RUN(test_capacity_boundary);
|
||||
SS_RUN(test_lru_order_and_replace);
|
||||
SS_RUN(test_clear_by_age);
|
||||
SS_RUN(test_reentrant_callback_and_limits);
|
||||
SS_RUN(test_zero_capacity);
|
||||
SS_RUN(test_steady_churn_without_allocation);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user