mirror of
git://git.acid.vegas/unrealircd.git
synced 2025-04-17 06:28:27 +00:00
447 lines
11 KiB
C
447 lines
11 KiB
C
/* src/modules/history_backend_mem.c - History Backend: memory
|
|
* (C) Copyright 2019 Bram Matthys (Syzop) and the UnrealIRCd team
|
|
* License: GPLv2
|
|
*/
|
|
#include "unrealircd.h"
|
|
|
|
/* This is the memory type backend. It is optimized for speed.
|
|
* For example, per-channel, it caches the field "number of lines"
|
|
* and "oldest record", so frequent cleaning operations such as
|
|
* "delete any record older than time T" or "keep only N lines"
|
|
* are executed as fast as possible.
|
|
*/
|
|
|
|
ModuleHeader MOD_HEADER
|
|
= {
|
|
"history_backend_mem",
|
|
"2.0",
|
|
"History backend: memory",
|
|
"UnrealIRCd Team",
|
|
"unrealircd-5",
|
|
};
|
|
|
|
/* Defines */
|
|
#define OBJECTLEN ((NICKLEN > CHANNELLEN) ? NICKLEN : CHANNELLEN)
|
|
#define HISTORY_BACKEND_MEM_HASH_TABLE_SIZE 1019
|
|
|
|
/* The regular history cleaning (by timer) is spread out
|
|
* a bit, rather than doing ALL channels every T time.
|
|
* HISTORY_SPREAD: how much to spread the "cleaning", eg 1 would be
|
|
* to clean everything in 1 go, 2 would mean the first event would
|
|
* clean half of the channels, and the 2nd event would clean the rest.
|
|
* Obviously more = better to spread the load, but doing a reasonable
|
|
* amount of work is also benefitial for performance (think: CPU cache).
|
|
* HISTORY_MAX_OFF_SECS: how many seconds may the history be 'off',
|
|
* that is: how much may we store the history longer than required.
|
|
* The other 2 macros are calculated based on that target.
|
|
*/
|
|
#define HISTORY_SPREAD 16
|
|
#define HISTORY_MAX_OFF_SECS 128
|
|
#define HISTORY_CLEAN_PER_LOOP (HISTORY_BACKEND_MEM_HASH_TABLE_SIZE/HISTORY_SPREAD)
|
|
#define HISTORY_TIMER_EVERY (HISTORY_MAX_OFF_SECS/HISTORY_SPREAD)
|
|
|
|
/* Definitions (structs, etc.) */
|
|
typedef struct HistoryLogLine HistoryLogLine;
|
|
struct HistoryLogLine {
|
|
HistoryLogLine *prev, *next;
|
|
time_t t;
|
|
MessageTag *mtags;
|
|
char line[1];
|
|
};
|
|
|
|
typedef struct HistoryLogObject HistoryLogObject;
|
|
struct HistoryLogObject {
|
|
HistoryLogObject *prev, *next;
|
|
HistoryLogLine *head; /**< Start of the log (the earliest entry) */
|
|
HistoryLogLine *tail; /**< End of the log (the latest entry) */
|
|
int num_lines; /**< Number of lines of log */
|
|
time_t oldest_t; /**< Oldest time in log */
|
|
int max_lines; /**< Maximum number of lines permitted */
|
|
long max_time; /**< Maximum number of seconds to retain history */
|
|
char name[OBJECTLEN+1];
|
|
};
|
|
|
|
/* Global variables */
|
|
static char siphashkey_history_backend_mem[SIPHASH_KEY_LENGTH];
|
|
HistoryLogObject *history_hash_table[HISTORY_BACKEND_MEM_HASH_TABLE_SIZE];
|
|
|
|
/* Forward declarations */
|
|
int hbm_history_add(char *object, MessageTag *mtags, char *line);
|
|
int hbm_history_cleanup(HistoryLogObject *h);
|
|
int hbm_history_request(Client *client, char *object, HistoryFilter *filter);
|
|
int hbm_history_destroy(char *object);
|
|
int hbm_history_set_limit(char *object, int max_lines, long max_time);
|
|
EVENT(history_mem_clean);
|
|
|
|
MOD_INIT()
|
|
{
|
|
HistoryBackendInfo hbi;
|
|
|
|
MARK_AS_OFFICIAL_MODULE(modinfo);
|
|
ModuleSetOptions(modinfo->handle, MOD_OPT_PERM, 1);
|
|
|
|
memset(&history_hash_table, 0, sizeof(history_hash_table));
|
|
siphash_generate_key(siphashkey_history_backend_mem);
|
|
|
|
memset(&hbi, 0, sizeof(hbi));
|
|
hbi.name = "mem";
|
|
hbi.history_add = hbm_history_add;
|
|
hbi.history_request = hbm_history_request;
|
|
hbi.history_destroy = hbm_history_destroy;
|
|
hbi.history_set_limit = hbm_history_set_limit;
|
|
if (!HistoryBackendAdd(modinfo->handle, &hbi))
|
|
return MOD_FAILED;
|
|
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
MOD_LOAD()
|
|
{
|
|
EventAdd(modinfo->handle, "history_mem_clean", history_mem_clean, NULL, HISTORY_TIMER_EVERY*1000, 0);
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
MOD_UNLOAD()
|
|
{
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
uint64_t hbm_hash(char *object)
|
|
{
|
|
return siphash_nocase(object, siphashkey_history_backend_mem) % HISTORY_BACKEND_MEM_HASH_TABLE_SIZE;
|
|
}
|
|
|
|
HistoryLogObject *hbm_find_object(char *object)
|
|
{
|
|
int hashv = hbm_hash(object);
|
|
HistoryLogObject *h;
|
|
|
|
for (h = history_hash_table[hashv]; h; h = h->next)
|
|
{
|
|
if (!strcasecmp(object, h->name))
|
|
return h;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
HistoryLogObject *hbm_find_or_add_object(char *object)
|
|
{
|
|
int hashv = hbm_hash(object);
|
|
HistoryLogObject *h;
|
|
|
|
for (h = history_hash_table[hashv]; h; h = h->next)
|
|
{
|
|
if (!strcasecmp(object, h->name))
|
|
return h;
|
|
}
|
|
/* Create new one */
|
|
h = safe_alloc(sizeof(HistoryLogObject));
|
|
strlcpy(h->name, object, sizeof(h->name));
|
|
AddListItem(h, history_hash_table[hashv]);
|
|
return h;
|
|
}
|
|
|
|
void hbm_delete_object_hlo(HistoryLogObject *h)
|
|
{
|
|
int hashv = hbm_hash(h->name);
|
|
|
|
DelListItem(h, history_hash_table[hashv]);
|
|
safe_free(h);
|
|
}
|
|
|
|
void hbm_duplicate_mtags(HistoryLogLine *l, MessageTag *m)
|
|
{
|
|
MessageTag *n;
|
|
|
|
/* Duplicate all message tags */
|
|
for (; m; m = m->next)
|
|
{
|
|
n = duplicate_mtag(m);
|
|
AppendListItem(n, l->mtags);
|
|
}
|
|
n = find_mtag(l->mtags, "time");
|
|
if (!n)
|
|
{
|
|
/* This is duplicate code from src/modules/server-time.c
|
|
* which seems silly.
|
|
*/
|
|
struct timeval t;
|
|
struct tm *tm;
|
|
time_t sec;
|
|
char buf[64];
|
|
|
|
gettimeofday(&t, NULL);
|
|
sec = t.tv_sec;
|
|
tm = gmtime(&sec);
|
|
snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ",
|
|
tm->tm_year + 1900,
|
|
tm->tm_mon + 1,
|
|
tm->tm_mday,
|
|
tm->tm_hour,
|
|
tm->tm_min,
|
|
tm->tm_sec,
|
|
(int)(t.tv_usec / 1000));
|
|
|
|
n = safe_alloc(sizeof(MessageTag));
|
|
safe_strdup(n->name, "time");
|
|
safe_strdup(n->value, buf);
|
|
AddListItem(n, l->mtags);
|
|
}
|
|
/* Now convert the "time" message tag to something we can use in l->t */
|
|
l->t = server_time_to_unix_time(n->value);
|
|
}
|
|
|
|
/** Add a line to a history object */
|
|
void hbm_history_add_line(HistoryLogObject *h, MessageTag *mtags, char *line)
|
|
{
|
|
HistoryLogLine *l = safe_alloc(sizeof(HistoryLogLine) + strlen(line));
|
|
strcpy(l->line, line); /* safe, see memory allocation above ^ */
|
|
hbm_duplicate_mtags(l, mtags);
|
|
if (h->tail)
|
|
{
|
|
/* append to tail */
|
|
h->tail->next = l;
|
|
l->prev = h->tail;
|
|
h->tail = l;
|
|
} else {
|
|
/* no tail, no head */
|
|
h->head = h->tail = l;
|
|
}
|
|
h->num_lines++;
|
|
if ((l->t < h->oldest_t) || (h->oldest_t == 0))
|
|
h->oldest_t = l->t;
|
|
}
|
|
|
|
/** Delete a line from a history object */
|
|
void hbm_history_del_line(HistoryLogObject *h, HistoryLogLine *l)
|
|
{
|
|
if (l->prev)
|
|
l->prev->next = l->next;
|
|
if (l->next)
|
|
l->next->prev = l->prev;
|
|
if (h->head == l)
|
|
{
|
|
/* New head */
|
|
h->head = l->next;
|
|
}
|
|
if (h->tail == l)
|
|
{
|
|
/* New tail */
|
|
h->tail = l->prev; /* could be NULL now */
|
|
}
|
|
|
|
free_message_tags(l->mtags);
|
|
safe_free(l);
|
|
|
|
h->num_lines--;
|
|
|
|
/* IMPORTANT: updating h->oldest_t takes place at the caller
|
|
* because it is in a better position to optimize the process
|
|
*/
|
|
}
|
|
|
|
/** Add history entry */
|
|
int hbm_history_add(char *object, MessageTag *mtags, char *line)
|
|
{
|
|
HistoryLogObject *h = hbm_find_or_add_object(object);
|
|
if (!h->max_lines)
|
|
{
|
|
sendto_realops("hbm_history_add() for '%s', which has no limit", h->name);
|
|
#ifdef DEBUGMODE
|
|
abort();
|
|
#else
|
|
h->max_lines = 50;
|
|
h->max_time = 86400;
|
|
#endif
|
|
}
|
|
if (h->num_lines >= h->max_lines)
|
|
{
|
|
/* Delete previous line */
|
|
hbm_history_del_line(h, h->head);
|
|
}
|
|
hbm_history_add_line(h, mtags, line);
|
|
return 0;
|
|
}
|
|
|
|
int can_receive_history(Client *client)
|
|
{
|
|
if (HasCapability(client, "server-time"))
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
void hbm_send_line(Client *client, HistoryLogLine *l, char *batchid)
|
|
{
|
|
if (can_receive_history(client))
|
|
{
|
|
if (BadPtr(batchid))
|
|
{
|
|
sendto_one(client, l->mtags, "%s", l->line);
|
|
} else {
|
|
MessageTag *m = safe_alloc(sizeof(MessageTag));
|
|
m->name = "batch";
|
|
m->value = batchid;
|
|
AddListItem(m, l->mtags);
|
|
sendto_one(client, l->mtags, "%s", l->line);
|
|
DelListItem(m, l->mtags);
|
|
safe_free(m);
|
|
}
|
|
} else {
|
|
/* without server-time, log playback is a bit annoying, so skip it? */
|
|
}
|
|
}
|
|
|
|
int hbm_history_request(Client *client, char *object, HistoryFilter *filter)
|
|
{
|
|
HistoryLogObject *h = hbm_find_object(object);
|
|
HistoryLogLine *l;
|
|
char batch[BATCHLEN+1];
|
|
long redline; /* Imaginary timestamp. Before the red line, history is too old. */
|
|
int lines_sendable = 0, lines_to_skip = 0, cnt = 0;
|
|
|
|
if (!h || !can_receive_history(client))
|
|
return 0;
|
|
|
|
batch[0] = '\0';
|
|
|
|
if (HasCapability(client, "batch"))
|
|
{
|
|
/* Start a new batch */
|
|
generate_batch_id(batch);
|
|
sendto_one(client, NULL, ":%s BATCH +%s chathistory %s", me.name, batch, object);
|
|
}
|
|
|
|
redline = TStime() - h->max_time;
|
|
|
|
/* Once the filter API expands, the following will change too.
|
|
* For now, this is sufficient, since requests are only about lines:
|
|
*/
|
|
lines_sendable = 0;
|
|
for (l = h->head; l; l = l->next)
|
|
if (l->t >= redline)
|
|
lines_sendable++;
|
|
if (filter && (lines_sendable > filter->last_lines))
|
|
lines_to_skip = lines_sendable - filter->last_lines;
|
|
|
|
for (l = h->head; l; l = l->next)
|
|
{
|
|
/* Make sure we don't send too old entries:
|
|
* We only have to check for time here, as line count is already
|
|
* taken into account in hbm_history_add.
|
|
*/
|
|
if (l->t >= redline && (++cnt > lines_to_skip))
|
|
hbm_send_line(client, l, batch);
|
|
}
|
|
|
|
/* End of batch */
|
|
if (*batch)
|
|
sendto_one(client, NULL, ":%s BATCH -%s", me.name, batch);
|
|
return 1;
|
|
}
|
|
|
|
/** Clean up expired entries */
|
|
int hbm_history_cleanup(HistoryLogObject *h)
|
|
{
|
|
HistoryLogLine *l, *l_next = NULL;
|
|
long redline = TStime() - h->max_time;
|
|
|
|
/* First enforce 'h->max_time', after that enforce 'h->max_lines' */
|
|
|
|
/* Checking for time */
|
|
if (h->oldest_t < redline)
|
|
{
|
|
h->oldest_t = 0; /* recalculate in next loop */
|
|
|
|
for (l = h->head; l; l = l_next)
|
|
{
|
|
l_next = l->next;
|
|
if (l->t < redline)
|
|
{
|
|
hbm_history_del_line(h, l); /* too old, delete it */
|
|
continue;
|
|
}
|
|
if ((h->oldest_t == 0) || (l->t < h->oldest_t))
|
|
h->oldest_t = l->t;
|
|
}
|
|
}
|
|
|
|
if (h->num_lines > h->max_lines)
|
|
{
|
|
h->oldest_t = 0; /* recalculate in next loop */
|
|
|
|
for (l = h->head; l; l = l_next)
|
|
{
|
|
l_next = l->next;
|
|
if (h->num_lines > h->max_lines)
|
|
{
|
|
hbm_history_del_line(h, l);
|
|
continue;
|
|
}
|
|
if ((h->oldest_t == 0) || (l->t < h->oldest_t))
|
|
h->oldest_t = l->t;
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
int hbm_history_destroy(char *object)
|
|
{
|
|
HistoryLogObject *h = hbm_find_object(object);
|
|
HistoryLogLine *l, *l_next;
|
|
|
|
if (!h)
|
|
return 0;
|
|
|
|
for (l = h->head; l; l = l_next)
|
|
{
|
|
l_next = l->next;
|
|
/* We could use hbm_history_del_line() here but
|
|
* it does unnecessary work, this is quicker.
|
|
* The only danger is that we may forget to free some
|
|
* fields that are added later there but not here.
|
|
*/
|
|
free_message_tags(l->mtags);
|
|
safe_free(l);
|
|
}
|
|
|
|
hbm_delete_object_hlo(h);
|
|
return 1;
|
|
}
|
|
|
|
/** Set new limit on history object */
|
|
int hbm_history_set_limit(char *object, int max_lines, long max_time)
|
|
{
|
|
HistoryLogObject *h = hbm_find_or_add_object(object);
|
|
h->max_lines = max_lines;
|
|
h->max_time = max_time;
|
|
hbm_history_cleanup(h); /* impose new restrictions */
|
|
return 1;
|
|
}
|
|
|
|
/** Periodically clean the history.
|
|
* Instead of doing all channels in 1 go, we do a limited number
|
|
* of channels each call, hence the 'static int' and the do { } while
|
|
* rather than a regular for loop.
|
|
* Note that we already impose the line limit in hbm_history_add,
|
|
* so this history_mem_clean is for removals due to max_time limits.
|
|
*/
|
|
EVENT(history_mem_clean)
|
|
{
|
|
static int hashnum = 0;
|
|
int loopcnt = 0;
|
|
Channel *channel;
|
|
HistoryLogObject *h;
|
|
|
|
do
|
|
{
|
|
for (h = history_hash_table[hashnum++]; h; h = h->next)
|
|
hbm_history_cleanup(h);
|
|
|
|
hashnum++;
|
|
|
|
if (hashnum >= HISTORY_BACKEND_MEM_HASH_TABLE_SIZE)
|
|
hashnum = 0;
|
|
} while(loopcnt++ < HISTORY_CLEAN_PER_LOOP);
|
|
}
|