1
0
Fork 0
knot-resolver/utils/cache_gc/kr_cache_gc.c
Daniel Baumann fbc604e215
Adding upstream version 5.7.5.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-21 13:56:17 +02:00

321 lines
8.9 KiB
C

/* SPDX-License-Identifier: GPL-3.0-or-later */
// standard includes
#include <inttypes.h>
#include <limits.h>
#include <stdio.h>
#include <time.h>
// libknot includes
#include <libknot/libknot.h>
// resolver includes
#include <lib/cache/api.h>
#include <lib/cache/impl.h>
#include <lib/defines.h>
#include "lib/cache/cdb_lmdb.h"
#include "lib/generic/array.h"
#include "lib/utils.h"
#include "kr_cache_gc.h"
#include "categories.h"
#include "db.h"
// section: dbval_copy
static knot_db_val_t *dbval_copy(const knot_db_val_t * from)
{
knot_db_val_t *to = malloc(sizeof(knot_db_val_t) + from->len);
if (to != NULL) {
memcpy(to, from, sizeof(knot_db_val_t));
to->data = to + 1; // == ((uit8_t *)to) + sizeof(knot_db_val_t)
memcpy(to->data, from->data, from->len);
}
return to;
}
// section: rrtype list
typedef array_t(uint16_t) rrtype_array_t;
static void rrtypelist_add(rrtype_array_t *arr, uint16_t add_type)
{
bool already_present = false;
for (size_t i = 0; i < arr->len; i++) {
if (arr->at[i] == add_type) {
already_present = true;
break;
}
}
if (!already_present) {
kr_require(array_push(*arr, add_type) >= 0);
}
}
static void rrtypelist_print(rrtype_array_t *arr)
{
char type_s[32] = { 0 };
for (size_t i = 0; i < arr->len; i++) {
knot_rrtype_to_string(arr->at[i], type_s, sizeof(type_s));
printf(" %s", type_s);
}
printf("\n");
}
typedef array_t(knot_db_val_t *) entry_array_t;
static void entry_array_deep_free(entry_array_t *d)
{
for (size_t i = 0; i < d->len; i++) {
free(d->at[i]);
}
array_clear(*d);
}
typedef struct {
size_t categories_sizes[CATEGORIES];
size_t records;
} ctx_compute_categories_t;
int cb_compute_categories(const knot_db_val_t * key, gc_record_info_t * info,
void *vctx)
{
ctx_compute_categories_t *ctx = vctx;
category_t cat = kr_gc_categorize(info);
(void)key;
ctx->categories_sizes[cat] += info->entry_size;
ctx->records++;
return KNOT_EOK;
}
typedef struct {
category_t limit_category;
entry_array_t to_delete;
size_t cfg_temp_keys_space;
size_t used_space;
size_t oversize_records;
} ctx_delete_categories_t;
int cb_delete_categories(const knot_db_val_t * key, gc_record_info_t * info,
void *vctx)
{
ctx_delete_categories_t *ctx = vctx;
category_t cat = kr_gc_categorize(info);
if (cat >= ctx->limit_category) {
knot_db_val_t *todelete = dbval_copy(key);
size_t used = ctx->used_space + key->len + sizeof(*key);
if ((ctx->cfg_temp_keys_space > 0 &&
used > ctx->cfg_temp_keys_space) || todelete == NULL) {
ctx->oversize_records++;
free(todelete);
} else {
kr_require(array_push(ctx->to_delete, todelete) >= 0);
ctx->used_space = used;
}
}
return KNOT_EOK;
}
struct kr_cache_gc_state {
struct kr_cache kres_db;
knot_db_t *db;
};
void kr_cache_gc_free_state(kr_cache_gc_state_t **state)
{
if (kr_fails_assert(state))
return;
if (!*state) { // not open
return;
}
kr_gc_cache_close(&(*state)->kres_db, (*state)->db);
free(*state);
*state = NULL;
}
int kr_cache_gc(kr_cache_gc_cfg_t *cfg, kr_cache_gc_state_t **state)
{
// The whole function works in four "big phases":
//// 1. find out whether we should even do analysis and deletion.
if (kr_fails_assert(cfg && state))
return KNOT_EINVAL;
int ret;
// Ensure that we have open and "healthy" cache.
if (!*state) {
*state = calloc(1, sizeof(**state));
if (!*state) {
return KNOT_ENOMEM;
}
ret = kr_gc_cache_open(cfg->cache_path, &(*state)->kres_db,
&(*state)->db);
} else { // To be sure, we guard against the file getting replaced.
ret = kr_gc_cache_check_health(&(*state)->kres_db, &(*state)->db);
// In particular, missing data.mdb gives us kr_error(ENOENT) == KNOT_ENOENT
}
if (ret) {
free(*state);
*state = NULL;
return ret;
}
knot_db_t *const db = (*state)->db; // frequently used shortcut
const double db_usage = kr_cdb_lmdb()->usage_percent((*state)->kres_db.db);
const bool large_usage = db_usage >= cfg->cache_max_usage;
if (cfg->dry_run || large_usage || VERBOSE_STATUS) { // don't print this on every size check
printf("Usage: %.2lf%%\n", db_usage);
}
if (cfg->dry_run || !large_usage) {
return KNOT_EOK;
}
//// 2. classify all cache items into categories
// and compute which categories to delete.
kr_timer_t timer_analyze = { 0 }, timer_choose = { 0 }, timer_delete =
{ 0 }, timer_rw_txn = { 0 };
kr_timer_start(&timer_analyze);
ctx_compute_categories_t cats = { { 0 }
};
ret = kr_gc_cache_iter(db, cfg, cb_compute_categories, &cats);
if (ret != KNOT_EOK) {
kr_cache_gc_free_state(state);
return ret;
}
//ssize_t amount_tofree = knot_db_lmdb_get_mapsize(db) * cfg->cache_to_be_freed / 100;
// Mixing ^^ page usage and entry sizes (key+value lengths) didn't work
// too well, probably due to internal fragmentation after some GC cycles.
// Therefore let's scale this by the ratio of these two sums.
size_t cats_sumsize = 0;
for (int i = 0; i < CATEGORIES; ++i) {
cats_sumsize += cats.categories_sizes[i];
}
/* use less precise variant to avoid 32-bit overflow */
size_t amount_tofree = cats_sumsize / 100 * cfg->cache_to_be_freed;
kr_log_debug(CACHE, "tofree: %zd / %zd\n", amount_tofree, cats_sumsize);
if (VERBOSE_STATUS) {
for (int i = 0; i < CATEGORIES; i++) {
if (cats.categories_sizes[i] > 0) {
printf("category %.2d size %zu\n", i,
cats.categories_sizes[i]);
}
}
}
category_t limit_category = CATEGORIES;
while (limit_category > 0) {
size_t cat_size = cats.categories_sizes[--limit_category];
if (cat_size > amount_tofree)
break;
amount_tofree -= cat_size;
}
printf("Cache analyzed in %.0lf msecs, %zu records, limit category is %d.\n",
kr_timer_elapsed(&timer_analyze) * 1000, cats.records, limit_category);
//// 3. pass whole cache again to collect a list of keys that should be deleted.
kr_timer_start(&timer_choose);
ctx_delete_categories_t to_del = { 0 };
to_del.cfg_temp_keys_space = cfg->temp_keys_space;
to_del.limit_category = limit_category;
ret = kr_gc_cache_iter(db, cfg, cb_delete_categories, &to_del);
if (ret != KNOT_EOK) {
entry_array_deep_free(&to_del.to_delete);
kr_cache_gc_free_state(state);
return ret;
}
printf
("%zu records to be deleted using %.2lf MBytes of temporary memory, %zu records skipped due to memory limit.\n",
to_del.to_delete.len, ((double)to_del.used_space / 1048576.0),
to_del.oversize_records);
//// 4. execute the planned deletions.
const knot_db_api_t *api = knot_db_lmdb_api();
knot_db_txn_t txn = { 0 };
size_t deleted_records = 0, already_gone = 0, rw_txn_count = 0;
kr_timer_start(&timer_delete);
kr_timer_start(&timer_rw_txn);
rrtype_array_t deleted_rrtypes = { 0 };
ret = api->txn_begin(db, &txn, 0);
if (ret != KNOT_EOK) {
printf("Error starting R/W DB transaction (%s).\n",
knot_strerror(ret));
entry_array_deep_free(&to_del.to_delete);
kr_cache_gc_free_state(state);
return ret;
}
for (size_t i = 0; i < to_del.to_delete.len; i++) {
knot_db_val_t *val = to_del.to_delete.at[i];
ret = api->del(&txn, val);
switch (ret) {
case KNOT_EOK:
deleted_records++;
const int entry_type = kr_gc_key_consistent(*val);
if (entry_type >= 0) // some "inconsistent" entries are OK
rrtypelist_add(&deleted_rrtypes, entry_type);
break;
case KNOT_ENOENT:
already_gone++;
if (VERBOSE_STATUS) {
// kresd normally only inserts (or overwrites),
// so it's generally suspicious when a key goes missing.
printf("Record already gone (key len %zu): ", val->len);
debug_printbin(val->data, val->len);
printf("\n");
}
break;
case KNOT_ESPACE:
printf("Warning: out of space, bailing out to retry later.\n");
api->txn_abort(&txn);
goto finish;
default:
printf("Warning: skipping deletion because of error (%s)\n",
knot_strerror(ret));
api->txn_abort(&txn);
ret = api->txn_begin(db, &txn, 0);
if (ret != KNOT_EOK) {
printf
("Error: can't begin txn because of error (%s)\n",
knot_strerror(ret));
goto finish;
}
continue;
}
if ((cfg->rw_txn_items > 0 &&
(deleted_records + already_gone) % cfg->rw_txn_items == 0) ||
(cfg->rw_txn_duration > 0 &&
kr_timer_elapsed_us(&timer_rw_txn) > cfg->rw_txn_duration)) {
ret = api->txn_commit(&txn);
if (ret == KNOT_EOK) {
rw_txn_count++;
usleep(cfg->rw_txn_delay);
kr_timer_start(&timer_rw_txn);
ret = api->txn_begin(db, &txn, 0);
}
if (ret != KNOT_EOK) {
printf("Error: transaction failed (%s)\n",
knot_strerror(ret));
goto finish;
}
}
}
ret = api->txn_commit(&txn);
finish:
printf("Deleted %zu records (%zu already gone) types", deleted_records,
already_gone);
rrtypelist_print(&deleted_rrtypes);
printf("It took %.0lf msecs, %zu transactions (%s)\n\n",
kr_timer_elapsed(&timer_delete) * 1000, rw_txn_count, knot_strerror(ret));
array_clear(deleted_rrtypes);
entry_array_deep_free(&to_del.to_delete);
// OK, let's close it in this case.
kr_cache_gc_free_state(state);
return ret;
}