278 lines
8.0 KiB
C
278 lines
8.0 KiB
C
/* Copyright (c) 2017 - 2022 LiteSpeed Technologies Inc. See LICENSE. */
|
|
#include <errno.h>
|
|
#include <inttypes.h>
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/queue.h>
|
|
|
|
#include "lsquic.h"
|
|
#include "lsquic_types.h"
|
|
#include "lsquic_int_types.h"
|
|
#include "lsquic_xxhash.h"
|
|
#include "lsquic_purga.h"
|
|
|
|
#define LSQUIC_LOGGER_MODULE LSQLM_PURGA
|
|
#include "lsquic_logger.h"
|
|
|
|
|
|
/* To avoid scannig the whole list of CIDs, we use a Bloom filter.
|
|
*
|
|
* The Bloom filter is constructed using a 8192-bit bit field and 6 hash
|
|
* functions. With 273 elements per page, this gives us 0.004% possibility
|
|
* of a false positive. In other words, when we do have to search a page
|
|
* for a particular CID, the chance of finding the CID is 99.99%.
|
|
*
|
|
* Quick calc:
|
|
* perl -E '$k=6;$m=1<<13;$n=273;printf("%f\n", (1-exp(1)**-($k*$n/$m))**$k)'
|
|
*
|
|
* To extract 6 13-bit values from a 64-bit integer, they are overlapped:
|
|
* 0 10 20 30 40 50 60
|
|
* +----------------------------------------------------------------+
|
|
* | |
|
|
* +----------------------------------------------------------------+
|
|
* 1111111111111
|
|
* 2222222222222
|
|
* 3333333333333
|
|
* 4444444444444
|
|
* 5555555555555
|
|
* 6666666666666
|
|
*
|
|
* This is not 100% kosher, but having 6 functions gives a better guarantee
|
|
* and it happens to work in practice.
|
|
*/
|
|
|
|
#define BLOOM_N_FUNCS 6
|
|
#define BLOOM_SHIFT 10
|
|
#define BLOOM_N_BITS (1 << 13)
|
|
typedef uint64_t bloom_mask_el_t;
|
|
#define BLOOM_SET_SHIFT 6 /* log2(sizeof(bloom_mask_el_t)) */
|
|
#define BLOOM_BITS_PER_EL (1 << BLOOM_SET_SHIFT)
|
|
#define BLOOM_N_MASK_ELS (BLOOM_N_BITS / BLOOM_BITS_PER_EL)
|
|
#define BLOOM_ONE 1ull
|
|
|
|
#define PURGA_ELS_PER_PAGE 273
|
|
|
|
struct purga_page
|
|
{
|
|
TAILQ_ENTRY(purga_page) pupa_next;
|
|
lsquic_time_t pupa_last;
|
|
unsigned pupa_count;
|
|
bloom_mask_el_t pupa_mask[BLOOM_N_MASK_ELS];
|
|
lsquic_cid_t pupa_cids[PURGA_ELS_PER_PAGE];
|
|
void * pupa_peer_ctx[PURGA_ELS_PER_PAGE];
|
|
struct purga_el pupa_els[PURGA_ELS_PER_PAGE];
|
|
};
|
|
|
|
#define PAGE_IS_FULL(page) ((page)->pupa_count >= PURGA_ELS_PER_PAGE)
|
|
|
|
TAILQ_HEAD(purga_pages, purga_page);
|
|
|
|
struct lsquic_purga
|
|
{
|
|
lsquic_time_t pur_min_life;
|
|
lsquic_cids_update_f pur_remove_cids;
|
|
void *pur_remove_ctx;
|
|
struct purga_pages pur_pages;
|
|
#ifndef NDEBUG
|
|
struct purga_bloom_stats pur_stats;
|
|
#endif
|
|
};
|
|
|
|
|
|
struct lsquic_purga *
|
|
lsquic_purga_new (lsquic_time_t min_life, lsquic_cids_update_f remove_cids,
|
|
void *remove_ctx)
|
|
{
|
|
struct lsquic_purga *purga;
|
|
|
|
purga = calloc(1, sizeof(*purga));
|
|
if (!purga)
|
|
{
|
|
LSQ_WARN("cannot create purgatory: malloc failed: %s", strerror(errno));
|
|
return NULL;
|
|
}
|
|
|
|
purga->pur_min_life = min_life;
|
|
purga->pur_remove_cids = remove_cids;
|
|
purga->pur_remove_ctx = remove_ctx;
|
|
TAILQ_INIT(&purga->pur_pages);
|
|
LSQ_INFO("create purgatory, min life %"PRIu64" usec", min_life);
|
|
|
|
return purga;
|
|
}
|
|
|
|
|
|
static struct purga_page *
|
|
purga_get_page (struct lsquic_purga *purga)
|
|
{
|
|
struct purga_page *page;
|
|
|
|
page = TAILQ_LAST(&purga->pur_pages, purga_pages);
|
|
if (page && !PAGE_IS_FULL(page))
|
|
return page;
|
|
|
|
page = malloc(sizeof(*page));
|
|
if (!page)
|
|
{
|
|
LSQ_INFO("failed to allocate page: %s", strerror(errno));
|
|
return NULL;
|
|
}
|
|
|
|
page->pupa_count = 0;
|
|
page->pupa_last = 0;
|
|
memset(page->pupa_mask, 0, sizeof(page->pupa_mask));
|
|
TAILQ_INSERT_TAIL(&purga->pur_pages, page, pupa_next);
|
|
LSQ_DEBUG("allocated new page");
|
|
return page;
|
|
}
|
|
|
|
|
|
static void
|
|
purga_remove_cids (struct lsquic_purga *purga, struct purga_page *page)
|
|
{
|
|
LSQ_DEBUG("calling remove_cids with %u CID%.*s", page->pupa_count,
|
|
page->pupa_count != 1, "s");
|
|
/* XXX It is interesting that pur_remove_ctx is called with peer_ctx
|
|
* as an argument, which should could vary (at least theoretically or
|
|
* in the future) by path.
|
|
*/
|
|
purga->pur_remove_cids(purga->pur_remove_ctx, page->pupa_peer_ctx,
|
|
page->pupa_cids, page->pupa_count);
|
|
}
|
|
|
|
|
|
struct purga_el *
|
|
lsquic_purga_add (struct lsquic_purga *purga, const lsquic_cid_t *cid,
|
|
void *peer_ctx, enum purga_type putype, lsquic_time_t now)
|
|
{
|
|
struct purga_page *last_page, *page;
|
|
uint64_t hash;
|
|
unsigned i, idx, set, bit;
|
|
|
|
last_page = purga_get_page(purga);
|
|
if (!last_page)
|
|
return NULL; /* We do best effort, nothing to do if malloc fails */
|
|
|
|
idx = last_page->pupa_count++;
|
|
last_page->pupa_cids [idx] = *cid;
|
|
last_page->pupa_peer_ctx[idx] = peer_ctx;
|
|
last_page->pupa_els [idx] = (struct purga_el) {
|
|
.puel_type = putype,
|
|
};
|
|
|
|
hash = XXH64(cid->idbuf, cid->len, 0);
|
|
for (i = 0; i < BLOOM_N_FUNCS; ++i)
|
|
{
|
|
bit = (unsigned) hash & (BLOOM_BITS_PER_EL - 1);
|
|
set = ((unsigned) hash >> BLOOM_SET_SHIFT) & (BLOOM_N_MASK_ELS - 1);
|
|
last_page->pupa_mask[set] |= BLOOM_ONE << bit;
|
|
hash >>= BLOOM_SHIFT;
|
|
}
|
|
|
|
LSQ_DEBUGC("added %"CID_FMT" to the set", CID_BITS(cid));
|
|
if (PAGE_IS_FULL(last_page))
|
|
{
|
|
LSQ_DEBUG("last page is full, set timestamp to %"PRIu64, now);
|
|
last_page->pupa_last = now;
|
|
}
|
|
|
|
while ((page = TAILQ_FIRST(&purga->pur_pages))
|
|
&& PAGE_IS_FULL(page)
|
|
&& page != last_page /* This is theoretical, but still... */
|
|
&& page->pupa_last + purga->pur_min_life < now)
|
|
{
|
|
LSQ_DEBUG("page at timestamp %"PRIu64" expired; now is %"PRIu64,
|
|
page->pupa_last, now);
|
|
TAILQ_REMOVE(&purga->pur_pages, page, pupa_next);
|
|
if (purga->pur_remove_cids && page->pupa_count)
|
|
purga_remove_cids(purga, page);
|
|
free(page);
|
|
}
|
|
|
|
return &last_page->pupa_els[idx];
|
|
}
|
|
|
|
|
|
struct purga_el *
|
|
lsquic_purga_contains (struct lsquic_purga *purga, const lsquic_cid_t *cid)
|
|
{
|
|
struct purga_page *page;
|
|
unsigned i, bit, set, hits;
|
|
uint64_t cid_hash, hash;
|
|
|
|
page = TAILQ_FIRST(&purga->pur_pages);
|
|
if (!page)
|
|
goto end;
|
|
|
|
cid_hash = XXH64(cid->idbuf, cid->len, 0);
|
|
do
|
|
{
|
|
#ifndef NDEBUG
|
|
++purga->pur_stats.searches;
|
|
#endif
|
|
hash = cid_hash;
|
|
hits = 0;
|
|
for (i = 0; i < BLOOM_N_FUNCS; ++i)
|
|
{
|
|
bit = (unsigned) hash & (BLOOM_BITS_PER_EL - 1);
|
|
set = ((unsigned) hash >> BLOOM_SET_SHIFT) & (BLOOM_N_MASK_ELS - 1);
|
|
hits += (page->pupa_mask[set] & (BLOOM_ONE << bit)) != 0;
|
|
hash >>= BLOOM_SHIFT;
|
|
}
|
|
if (hits < BLOOM_N_FUNCS)
|
|
goto next_page;
|
|
for (i = 0; i < page->pupa_count; ++i)
|
|
if (LSQUIC_CIDS_EQ(&page->pupa_cids[i], cid))
|
|
{
|
|
LSQ_DEBUGC("found %"CID_FMT, CID_BITS(cid));
|
|
return &page->pupa_els[i];
|
|
}
|
|
#ifndef NDEBUG
|
|
++purga->pur_stats.false_hits;
|
|
#endif
|
|
next_page:
|
|
page = TAILQ_NEXT(page, pupa_next);
|
|
}
|
|
while (page);
|
|
|
|
end:
|
|
LSQ_DEBUGC("%"CID_FMT" not found", CID_BITS(cid));
|
|
return NULL;
|
|
}
|
|
|
|
|
|
void
|
|
lsquic_purga_destroy (struct lsquic_purga *purga)
|
|
{
|
|
struct purga_page *page;
|
|
|
|
while ((page = TAILQ_FIRST(&purga->pur_pages)))
|
|
{
|
|
TAILQ_REMOVE(&purga->pur_pages, page, pupa_next);
|
|
if (purga->pur_remove_cids && page->pupa_count)
|
|
purga_remove_cids(purga, page);
|
|
free(page);
|
|
}
|
|
free(purga);
|
|
LSQ_INFO("destroyed");
|
|
}
|
|
|
|
|
|
unsigned
|
|
lsquic_purga_cids_per_page (void)
|
|
{
|
|
LSQ_DEBUG("CIDs per page: %u", PURGA_ELS_PER_PAGE);
|
|
return PURGA_ELS_PER_PAGE;
|
|
}
|
|
|
|
|
|
#ifndef NDEBUG
|
|
struct purga_bloom_stats *
|
|
lsquic_purga_get_bloom_stats (struct lsquic_purga *purga)
|
|
{
|
|
return &purga->pur_stats;
|
|
}
|
|
#endif
|