/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
#include "lib.h"
#include "hash.h"
#include "llist.h"
#include "settings-parser.h"
#include "master-service-private.h"
#include "master-service-settings.h"
#include "master-service-settings-cache.h"
/* we start with just a guess. it's updated later. */
#define CACHE_INITIAL_ENTRY_POOL_SIZE (1024*16)
#define CACHE_ADD_ENTRY_POOL_SIZE 1024
struct settings_entry {
struct settings_entry *prev, *next;
pool_t pool;
const char *local_name;
struct ip_addr local_ip;
struct setting_parser_context *parser;
};
struct master_service_settings_cache {
pool_t pool;
struct master_service *service;
const char *module;
const char *service_name;
size_t max_cache_size;
/* global settings for this service (after they've been read) */
struct setting_parser_context *global_parser;
/* cache for other settings (local_ip/local_name set) */
struct settings_entry *oldest, *newest;
/* separate list for entries whose parser=global_parser */
struct settings_entry *oldest_global, *newest_global;
/* local_name, local_ip => struct settings_entry */
HASH_TABLE(char *, struct settings_entry *) local_name_hash;
HASH_TABLE(struct ip_addr *, struct settings_entry *) local_ip_hash;
/* Initial size for new settings entry pools */
size_t approx_entry_pool_size;
/* number of bytes malloced by cached settings entries
(doesn't count memory used by hash table or global sets) */
size_t cache_malloc_size;
bool done_initial_lookup:1;
bool service_uses_local:1;
bool service_uses_remote:1;
};
struct master_service_settings_cache *
master_service_settings_cache_init(struct master_service *service,
const char *module, const char *service_name)
{
struct master_service_settings_cache *cache;
pool_t pool;
pool = pool_alloconly_create(MEMPOOL_GROWING"master service settings cache",
1024*12);
cache = p_new(pool, struct master_service_settings_cache, 1);
cache->pool = pool;
cache->service = service;
cache->module = p_strdup(pool, module);
cache->service_name = p_strdup(pool, service_name);
cache->max_cache_size = (size_t)-1;
return cache;
}
void master_service_settings_cache_deinit(struct master_service_settings_cache **_cache)
{
struct master_service_settings_cache *cache = *_cache;
struct settings_entry *entry, *next;
/* parsers need to be deinitialized, because they reference the pool */
for (entry = cache->oldest_global; entry != NULL; entry = next) {
next = entry->next;
i_assert(entry->parser == cache->global_parser);
pool_unref(&entry->pool);
}
for (entry = cache->oldest; entry != NULL; entry = next) {
next = entry->next;
i_assert(entry->parser != cache->global_parser);
settings_parser_deinit(&entry->parser);
pool_unref(&entry->pool);
}
if (hash_table_is_created(cache->local_name_hash))
hash_table_destroy(&cache->local_name_hash);
if (hash_table_is_created(cache->local_ip_hash))
hash_table_destroy(&cache->local_ip_hash);
if (cache->global_parser != NULL)
settings_parser_deinit(&cache->global_parser);
pool_unref(&cache->pool);
}
static bool
cache_can_return_global(struct master_service_settings_cache *cache,
const struct master_service_settings_input *input)
{
if (cache->service_uses_local) {
if (input->local_name != NULL || input->local_ip.family != 0)
return FALSE;
}
if (cache->service_uses_remote) {
if (input->remote_ip.family != 0)
return FALSE;
}
return TRUE;
}
static bool
cache_find(struct master_service_settings_cache *cache,
const struct master_service_settings_input *input,
const struct setting_parser_context **parser_r)
{
struct settings_entry *entry = NULL;
if (!cache->done_initial_lookup)
return FALSE;
if (cache_can_return_global(cache, input)) {
if (cache->global_parser != NULL) {
*parser_r = cache->global_parser;
return TRUE;
}
return FALSE;
}
if (cache->service_uses_remote)
return FALSE;
/* see if we have it already in cache. if local_name is specified,
don't even try to use local_ip (even though we have it), because
there may be different settings specifically for local_name */
if (input->local_name != NULL) {
if (hash_table_is_created(cache->local_name_hash)) {
entry = hash_table_lookup(cache->local_name_hash,
input->local_name);
}
} else if (hash_table_is_created(cache->local_ip_hash) &&
input->local_ip.family != 0) {
entry = hash_table_lookup(cache->local_ip_hash,
&input->local_ip);
}
if (entry != NULL) {
if (entry->parser != cache->global_parser) {
DLLIST2_REMOVE(&cache->oldest, &cache->newest, entry);
DLLIST2_APPEND(&cache->oldest, &cache->newest, entry);
}
*parser_r = entry->parser;
return TRUE;
}
return FALSE;
}
static void
setting_entry_detach(struct master_service_settings_cache *cache,
struct settings_entry *entry)
{
DLLIST2_REMOVE(&cache->oldest, &cache->newest, entry);
cache->cache_malloc_size -=
pool_alloconly_get_total_alloc_size(entry->pool);
if (entry->local_name != NULL)
hash_table_remove(cache->local_name_hash, entry->local_name);
else if (entry->local_ip.family != 0)
hash_table_remove(cache->local_ip_hash, &entry->local_ip);
settings_parser_deinit(&entry->parser);
}
static struct setting_parser_context *
cache_add(struct master_service_settings_cache *cache,
const struct master_service_settings_input *input,
const struct master_service_settings_output *output,
struct setting_parser_context *parser)
{
struct settings_entry *entry;
pool_t pool;
size_t pool_size;
char *entry_local_name;
if (!output->used_local && !output->used_remote) {
/* these are same as global settings */
if (cache->global_parser == NULL) {
cache->global_parser =
settings_parser_dup(parser, cache->pool);
}
}
if (cache->service_uses_remote) {
/* for now we don't try to handle caching remote IPs */
return parser;
}
if (input->local_name == NULL && input->local_ip.family == 0)
return parser;
if (!output->used_local) {
/* use global settings, but add local_ip/host to hash tables
so we'll find them */
pool = pool_alloconly_create("settings global entry", 256);
} else if (cache->cache_malloc_size >= cache->max_cache_size) {
/* free the oldest and reuse its pool */
pool = cache->oldest->pool;
setting_entry_detach(cache, cache->oldest);
p_clear(pool); /* note: frees also entry */
} else {
pool_size = cache->approx_entry_pool_size != 0 ?
cache->approx_entry_pool_size :
CACHE_INITIAL_ENTRY_POOL_SIZE;
pool = pool_alloconly_create("settings entry", pool_size);
}
entry = p_new(pool, struct settings_entry, 1);
entry->pool = pool;
entry_local_name = p_strdup(pool, input->local_name);
entry->local_name = entry_local_name;
entry->local_ip = input->local_ip;
if (!output->used_local) {
entry->parser = cache->global_parser;
DLLIST2_APPEND(&cache->oldest_global, &cache->newest_global,
entry);
} else {
entry->parser = settings_parser_dup(parser, entry->pool);
DLLIST2_APPEND(&cache->oldest, &cache->newest, entry);
pool_size = pool_alloconly_get_total_used_size(pool);
if (pool_size > cache->approx_entry_pool_size) {
cache->approx_entry_pool_size = pool_size +
CACHE_ADD_ENTRY_POOL_SIZE;
}
}
cache->cache_malloc_size += pool_alloconly_get_total_alloc_size(pool);
if (input->local_name != NULL) {
if (!hash_table_is_created(cache->local_name_hash)) {
hash_table_create(&cache->local_name_hash,
cache->pool, 0, str_hash, strcmp);
}
i_assert(hash_table_lookup(cache->local_name_hash,
entry_local_name) == NULL);
hash_table_insert(cache->local_name_hash,
entry_local_name, entry);
} else if (input->local_ip.family != 0) {
if (!hash_table_is_created(cache->local_ip_hash)) {
hash_table_create(&cache->local_ip_hash, cache->pool, 0,
net_ip_hash, net_ip_cmp);
}
i_assert(hash_table_lookup(cache->local_ip_hash,
&entry->local_ip) == NULL);
hash_table_insert(cache->local_ip_hash,
&entry->local_ip, entry);
}
return entry->parser;
}
int master_service_settings_cache_read(struct master_service_settings_cache *cache,
const struct master_service_settings_input *input,
const struct dynamic_settings_parser *dyn_parsers,
const struct setting_parser_context **parser_r,
const char **error_r)
{
struct master_service_settings_output output;
struct master_service_settings_input new_input;
const struct master_service_settings *set;
i_assert(null_strcmp(input->module, cache->module) == 0);
i_assert(null_strcmp(input->service, cache->service_name) == 0);
if (cache_find(cache, input, parser_r))
return 0;
new_input = *input;
if (dyn_parsers != NULL) {
settings_parser_dyn_update(cache->pool, &new_input.roots,
dyn_parsers);
}
if (master_service_settings_read(cache->service, &new_input,
&output, error_r) < 0)
return -1;
if (!cache->done_initial_lookup) {
cache->done_initial_lookup = TRUE;
cache->service_uses_local = output.service_uses_local;
cache->service_uses_remote = output.service_uses_remote;
set = master_service_settings_get(cache->service);
cache->max_cache_size = set->config_cache_size;
}
if (output.used_local && !cache->service_uses_local) {
*error_r = "BUG: config unexpectedly returned local settings";
return -1;
}
if (output.used_remote && !cache->service_uses_remote) {
*error_r = "BUG: config unexpectedly returned remote settings";
return -1;
}
*parser_r = cache_add(cache, &new_input, &output,
cache->service->set_parser);
return 0;
}