321 lines
8.9 KiB
C
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;
|
|
}
|