Home | History | Annotate | Line # | Download | only in util
      1 /*	$NetBSD: dict_cache.c,v 1.5 2026/05/09 18:49:22 christos Exp $	*/
      2 
      3 /*++
      4 /* NAME
      5 /*	dict_cache 3
      6 /* SUMMARY
      7 /*	External cache manager
      8 /* SYNOPSIS
      9 /*	#include <dict_cache.h>
     10 /*
     11 /*	DICT_CACHE *dict_cache_open(dbname, open_flags, dict_flags)
     12 /*	const char *dbname;
     13 /*	int	open_flags;
     14 /*	int	dict_flags;
     15 /*
     16 /*	void	dict_cache_close(cache)
     17 /*	DICT_CACHE *cache;
     18 /*
     19 /*	const char *dict_cache_lookup(cache, cache_key)
     20 /*	DICT_CACHE *cache;
     21 /*	const char *cache_key;
     22 /*
     23 /*	int	dict_cache_update(cache, cache_key, cache_val)
     24 /*	DICT_CACHE *cache;
     25 /*	const char *cache_key;
     26 /*	const char *cache_val;
     27 /*
     28 /*	int	dict_cache_delete(cache, cache_key)
     29 /*	DICT_CACHE *cache;
     30 /*	const char *cache_key;
     31 /*
     32 /*	int	dict_cache_sequence(cache, first_next, cache_key, cache_val)
     33 /*	DICT_CACHE *cache;
     34 /*	int	first_next;
     35 /*	const char **cache_key;
     36 /*	const char **cache_val;
     37 /*
     38 /*	int	dict_cache_error(cache)
     39 /*	DICT_CACHE *cache;
     40 /* AUXILIARY FUNCTIONS
     41 /*	void	dict_cache_control(cache, name, value, ...)
     42 /*	DICT_CACHE *cache;
     43 /*	int	name;
     44 /*
     45 /*	typedef int (*DICT_CACHE_VALIDATOR_FN) (const char *cache_key,
     46 /*		const char *cache_val, void *context);
     47 /*
     48 /*	const char *dict_cache_name(cache)
     49 /*	DICT_CACHE	*cache;
     50 /* DESCRIPTION
     51 /*	This module maintains external cache files with support
     52 /*	for expiration. The underlying table must implement the
     53 /*	"lookup", "update", "delete" and "sequence" operations.
     54 /*
     55 /*	Although this API is similar to the one documented in
     56 /*	dict_open(3), there are subtle differences in the interaction
     57 /*	between the iterators that access all cache elements, and
     58 /*	other operations that access individual cache elements.
     59 /*
     60 /*	In particular, when a "sequence" or "cleanup" operation is
     61 /*	in progress the cache intercepts requests to delete the
     62 /*	"current" entry, as this would cause some databases to
     63 /*	mis-behave. Instead, the cache implements a "delete behind"
     64 /*	strategy, and deletes such an entry after the "sequence"
     65 /*	or "cleanup" operation moves on to the next cache element.
     66 /*	The "delete behind" strategy also affects the cache lookup
     67 /*	and update operations as detailed below.
     68 /*
     69 /*	dict_cache_open() is a wrapper around the dict_open()
     70 /*	function.  It opens the specified cache and returns a handle
     71 /*	that must be used for subsequent access. This function does
     72 /*	not return in case of error.
     73 /*
     74 /*	dict_cache_close() closes the specified cache and releases
     75 /*	memory that was allocated by dict_cache_open(), and terminates
     76 /*	any thread that was started with dict_cache_control().
     77 /*
     78 /*	dict_cache_lookup() looks up the specified cache entry.
     79 /*	The result value is a null pointer when the cache entry was
     80 /*	not found, or when the entry is scheduled for "delete
     81 /*	behind".
     82 /*
     83 /*	dict_cache_update() updates the specified cache entry. If
     84 /*	the entry is scheduled for "delete behind", the delete
     85 /*	operation is canceled (because of this, the cache must be
     86 /*	opened with DICT_FLAG_DUP_REPLACE). This function does not
     87 /*	return in case of error.
     88 /*
     89 /*	dict_cache_delete() removes the specified cache entry.  If
     90 /*	this is the "current" entry of a "sequence" operation, the
     91 /*	entry is scheduled for "delete behind". The result value
     92 /*	is zero when the entry was found.
     93 /*
     94 /*	dict_cache_sequence() iterates over the specified cache and
     95 /*	returns each entry in an implementation-defined order.  The
     96 /*	result value is zero when a cache entry was found.
     97 /*
     98 /*	Important: programs must not use both dict_cache_sequence()
     99 /*	and the built-in cache cleanup feature.
    100 /*
    101 /*	dict_cache_control() provides control over the built-in
    102 /*	cache cleanup feature and logging. The arguments are a list
    103 /*	of macros with zero or more arguments, terminated with
    104 /*	CA_DICT_CACHE_CTL_END which has none.  The following lists
    105 /*	the macros and corresponding argument types.
    106 /* .IP "CA_DICT_CACHE_CTL_FLAGS(int flags)"
    107 /*	The arguments to this command are the bit-wise OR of zero
    108 /*	or more of the following:
    109 /* .RS
    110 /* .IP CA_DICT_CACHE_CTL_FLAG_VERBOSE
    111 /*	Enable verbose logging of cache activity.
    112 /* .IP CA_DICT_CACHE_CTL_FLAG_EXP_SUMMARY
    113 /*	Log cache statistics after each cache cleanup run.
    114 /* .RE
    115 /* .IP "CA_DICT_CACHE_CTL_INTERVAL(int interval)"
    116 /*	The interval between cache cleanup runs. Specify a null
    117 /*	validator or interval to stop cache cleanup and log cache
    118 /*	statistics if a cleanup run was in progress.
    119 /* .IP "CA_DICT_CACHE_CTL_VALIDATOR(DICT_CACHE_VALIDATOR_FN validator)"
    120 /*	An application call-back routine that returns non-zero when
    121 /*	a cache entry should be kept. The call-back function should
    122 /*	not make changes to the cache. Specify a null validator or
    123 /*	interval to stop cache cleanup.
    124 /* .IP "CA_DICT_CACHE_CTL_CONTEXT(void *context)"
    125 /*	Application context that is passed to the validator function.
    126 /* .RE
    127 /* .PP
    128 /*	dict_cache_name() returns the name of the specified cache.
    129 /*
    130 /*	dict_cache_error() returns the error status for the underlying
    131 /*	dictionary.
    132 /*
    133 /*	Arguments:
    134 /* .IP "dbname, open_flags, dict_flags"
    135 /*	These are passed unchanged to dict_open(). The cache must
    136 /*	be opened with DICT_FLAG_DUP_REPLACE.
    137 /* .IP cache
    138 /*	Cache handle created with dict_cache_open().
    139 /* .IP cache_key
    140 /*	Cache lookup key.
    141 /* .IP cache_val
    142 /*	Information that is stored under a cache lookup key.
    143 /* .IP first_next
    144 /*	One of DICT_SEQ_FUN_FIRST (first cache element) or
    145 /*	DICT_SEQ_FUN_NEXT (next cache element).
    146 /* .sp
    147 /*	Note: there is no "stop" request. To ensure that the "delete
    148 /*	behind" strategy does not interfere with database access,
    149 /*	allow dict_cache_sequence() to run to completion.
    150 /* .IP table
    151 /*	A bare dictionary handle.
    152 /* DIAGNOSTICS
    153 /*	When a request is satisfied, the lookup routine returns
    154 /*	non-null, and the update, delete and sequence routines
    155 /*	return zero.  The cache->error value is zero when a request
    156 /*	could not be satisfied because an item did not exist (delete,
    157 /*	sequence) or if it could not be updated. The cache->error
    158 /*	value is non-zero only when a request could not be satisfied,
    159 /*	and the cause was a database error.
    160 /*
    161 /*	Cache access errors are logged with a warning message. To
    162 /*	avoid spamming the log, each type of operation logs no more
    163 /*	than one cache access error per second, per cache. Specify
    164 /*	the DICT_CACHE_FLAG_VERBOSE flag (see above) to log all
    165 /*	warnings.
    166 /* BUGS
    167 /*	There should be a way to suspend automatic program suicide
    168 /*	until a cache cleanup run is completed. Some entries may
    169 /*	never be removed when the process max_idle time is less
    170 /*	than the time needed to make a full pass over the cache.
    171 /*
    172 /*	The delete-behind strategy assumes that all updates are
    173 /*	made by a single process. Otherwise, delete-behind may
    174 /*	remove an entry that was updated after it was scheduled for
    175 /*	deletion.
    176 /* LICENSE
    177 /* .ad
    178 /* .fi
    179 /*	The Secure Mailer license must be distributed with this software.
    180 /* HISTORY
    181 /* .ad
    182 /* .fi
    183 /*	A predecessor of this code was written first for the Postfix
    184 /*	tlsmgr(8) daemon.
    185 /* AUTHOR(S)
    186 /*	Wietse Venema
    187 /*	IBM T.J. Watson Research
    188 /*	P.O. Box 704
    189 /*	Yorktown Heights, NY 10598, USA
    190 /*--*/
    191 
    192 /* System library. */
    193 
    194 #include <sys_defs.h>
    195 #include <string.h>
    196 #include <stdlib.h>
    197 
    198 /* Utility library. */
    199 
    200 #include <msg.h>
    201 #include <dict.h>
    202 #include <mymalloc.h>
    203 #include <events.h>
    204 #include <dict_cache.h>
    205 
    206 /* Application-specific. */
    207 
    208  /*
    209   * XXX Deleting entries while enumerating a map can he tricky. Some map
    210   * types have a concept of cursor and support a "delete the current element"
    211   * operation. Some map types without cursors don't behave well when the
    212   * current first/next entry is deleted (example: with Berkeley DB < 2, the
    213   * "next" operation produces garbage). To avoid trouble, we delete an entry
    214   * after advancing the current first/next position beyond it; we use the
    215   * same strategy with application requests to delete the current entry.
    216   */
    217 
    218  /*
    219   * Opaque data structure. Use dict_cache_name() to access the name of the
    220   * underlying database.
    221   */
    222 struct DICT_CACHE {
    223     char   *name;			/* full name including proxy: */
    224     int     cache_flags;		/* see below */
    225     int     user_flags;			/* logging */
    226     DICT   *db;				/* database handle */
    227     int     error;			/* last operation only */
    228 
    229     /* Delete-behind support. */
    230     char   *saved_curr_key;		/* "current" cache lookup key */
    231     char   *saved_curr_val;		/* "current" cache lookup result */
    232 
    233     /* Cleanup support. */
    234     int     exp_interval;		/* time between cleanup runs */
    235     DICT_CACHE_VALIDATOR_FN exp_validator;	/* expiration call-back */
    236     void   *exp_context;		/* call-back context */
    237     int     retained;			/* entries retained in cleanup run */
    238     int     dropped;			/* entries removed in cleanup run */
    239 
    240     /* Rate-limited logging support. */
    241     int     log_delay;
    242     time_t  upd_log_stamp;		/* last update warning */
    243     time_t  get_log_stamp;		/* last lookup warning */
    244     time_t  del_log_stamp;		/* last delete warning */
    245     time_t  seq_log_stamp;		/* last sequence warning */
    246 };
    247 
    248 #define DC_FLAG_DEL_SAVED_CURRENT_KEY	(1<<0)	/* delete-behind is scheduled */
    249 
    250  /*
    251   * Don't log cache access errors more than once per second.
    252   */
    253 #define DC_DEF_LOG_DELAY	1
    254 
    255  /*
    256   * Macros to make obscure code more readable.
    257   */
    258 #define DC_SCHEDULE_FOR_DELETE_BEHIND(cp) \
    259     ((cp)->cache_flags |= DC_FLAG_DEL_SAVED_CURRENT_KEY)
    260 
    261 #define DC_MATCH_SAVED_CURRENT_KEY(cp, cache_key) \
    262     ((cp)->saved_curr_key && strcmp((cp)->saved_curr_key, (cache_key)) == 0)
    263 
    264 #define DC_IS_SCHEDULED_FOR_DELETE_BEHIND(cp) \
    265     (/* NOT: (cp)->saved_curr_key && */ \
    266 	((cp)->cache_flags & DC_FLAG_DEL_SAVED_CURRENT_KEY) != 0)
    267 
    268 #define DC_CANCEL_DELETE_BEHIND(cp) \
    269     ((cp)->cache_flags &= ~DC_FLAG_DEL_SAVED_CURRENT_KEY)
    270 
    271  /*
    272   * Special key to store the time of the last cache cleanup run completion.
    273   */
    274 #define DC_LAST_CACHE_CLEANUP_COMPLETED "_LAST_CACHE_CLEANUP_COMPLETED_"
    275 
    276 /* dict_cache_lookup - load entry from cache */
    277 
    278 const char *dict_cache_lookup(DICT_CACHE *cp, const char *cache_key)
    279 {
    280     const char *myname = "dict_cache_lookup";
    281     const char *cache_val;
    282     DICT   *db = cp->db;
    283 
    284     /*
    285      * Search for the cache entry. Don't return an entry that is scheduled
    286      * for delete-behind.
    287      */
    288     if (DC_IS_SCHEDULED_FOR_DELETE_BEHIND(cp)
    289 	&& DC_MATCH_SAVED_CURRENT_KEY(cp, cache_key)) {
    290 	if (cp->user_flags & DICT_CACHE_FLAG_VERBOSE)
    291 	    msg_info("%s: key=%s (pretend not found  - scheduled for deletion)",
    292 		     myname, cache_key);
    293 	DICT_ERR_VAL_RETURN(cp, DICT_ERR_NONE, (char *) 0);
    294     } else {
    295 	cache_val = dict_get(db, cache_key);
    296 	if (cache_val == 0 && db->error != 0)
    297 	    msg_rate_delay(&cp->get_log_stamp, cp->log_delay, msg_warn,
    298 			   "%s: cache lookup for '%s' failed due to error",
    299 			   cp->name, cache_key);
    300 	if (cp->user_flags & DICT_CACHE_FLAG_VERBOSE)
    301 	    msg_info("%s: key=%s value=%s", myname, cache_key,
    302 		     cache_val ? cache_val : db->error ?
    303 		     "error" : "(not found)");
    304 	DICT_ERR_VAL_RETURN(cp, db->error, cache_val);
    305     }
    306 }
    307 
    308 /* dict_cache_update - save entry to cache */
    309 
    310 int     dict_cache_update(DICT_CACHE *cp, const char *cache_key,
    311 			          const char *cache_val)
    312 {
    313     const char *myname = "dict_cache_update";
    314     DICT   *db = cp->db;
    315     int     put_res;
    316 
    317     /*
    318      * Store the cache entry and cancel the delete-behind operation.
    319      */
    320     if (DC_IS_SCHEDULED_FOR_DELETE_BEHIND(cp)
    321 	&& DC_MATCH_SAVED_CURRENT_KEY(cp, cache_key)) {
    322 	if (cp->user_flags & DICT_CACHE_FLAG_VERBOSE)
    323 	    msg_info("%s: cancel delete-behind for key=%s", myname, cache_key);
    324 	DC_CANCEL_DELETE_BEHIND(cp);
    325     }
    326     if (cp->user_flags & DICT_CACHE_FLAG_VERBOSE)
    327 	msg_info("%s: key=%s value=%s", myname, cache_key, cache_val);
    328     put_res = dict_put(db, cache_key, cache_val);
    329     if (put_res != 0)
    330 	msg_rate_delay(&cp->upd_log_stamp, cp->log_delay, msg_warn,
    331 		  "%s: could not update entry for %s", cp->name, cache_key);
    332     DICT_ERR_VAL_RETURN(cp, db->error, put_res);
    333 }
    334 
    335 /* dict_cache_delete - delete entry from cache */
    336 
    337 int     dict_cache_delete(DICT_CACHE *cp, const char *cache_key)
    338 {
    339     const char *myname = "dict_cache_delete";
    340     int     del_res;
    341     DICT   *db = cp->db;
    342 
    343     /*
    344      * Delete the entry, unless we would delete the current first/next entry.
    345      * In that case, schedule the "current" entry for delete-behind to avoid
    346      * mis-behavior by some databases.
    347      */
    348     if (DC_MATCH_SAVED_CURRENT_KEY(cp, cache_key)) {
    349 	DC_SCHEDULE_FOR_DELETE_BEHIND(cp);
    350 	if (cp->user_flags & DICT_CACHE_FLAG_VERBOSE)
    351 	    msg_info("%s: key=%s (current entry - schedule for delete-behind)",
    352 		     myname, cache_key);
    353 	DICT_ERR_VAL_RETURN(cp, DICT_ERR_NONE, DICT_STAT_SUCCESS);
    354     } else {
    355 	del_res = dict_del(db, cache_key);
    356 	if (del_res != 0)
    357 	    msg_rate_delay(&cp->del_log_stamp, cp->log_delay, msg_warn,
    358 		  "%s: could not delete entry for %s", cp->name, cache_key);
    359 	if (cp->user_flags & DICT_CACHE_FLAG_VERBOSE)
    360 	    msg_info("%s: key=%s (%s)", myname, cache_key,
    361 		     del_res == 0 ? "found" :
    362 		     db->error ? "error" : "not found");
    363 	DICT_ERR_VAL_RETURN(cp, db->error, del_res);
    364     }
    365 }
    366 
    367 /* dict_cache_sequence - look up the first/next cache entry */
    368 
    369 int     dict_cache_sequence(DICT_CACHE *cp, int first_next,
    370 			            const char **cache_key,
    371 			            const char **cache_val)
    372 {
    373     const char *myname = "dict_cache_sequence";
    374     int     seq_res;
    375     const char *raw_cache_key;
    376     const char *raw_cache_val;
    377     char   *previous_curr_key;
    378     char   *previous_curr_val;
    379     DICT   *db = cp->db;
    380 
    381     /*
    382      * Find the first or next database entry. Hide the record with the cache
    383      * cleanup completion time stamp.
    384      */
    385     seq_res = dict_seq(db, first_next, &raw_cache_key, &raw_cache_val);
    386     if (seq_res == 0
    387 	&& strcmp(raw_cache_key, DC_LAST_CACHE_CLEANUP_COMPLETED) == 0)
    388 	seq_res =
    389 	    dict_seq(db, DICT_SEQ_FUN_NEXT, &raw_cache_key, &raw_cache_val);
    390     if (cp->user_flags & DICT_CACHE_FLAG_VERBOSE)
    391 	msg_info("%s: key=%s value=%s", myname,
    392 		 seq_res == 0 ? raw_cache_key : db->error ?
    393 		 "(error)" : "(not found)",
    394 		 seq_res == 0 ? raw_cache_val : db->error ?
    395 		 "(error)" : "(not found)");
    396     if (db->error)
    397 	msg_rate_delay(&cp->seq_log_stamp, cp->log_delay, msg_warn,
    398 		       "%s: sequence error", cp->name);
    399 
    400     /*
    401      * Save the current cache_key and cache_val before they are clobbered by
    402      * our own delete operation below. This also prevents surprises when the
    403      * application accesses the database after this function returns.
    404      *
    405      * We also use the saved cache_key to protect the current entry against
    406      * application delete requests.
    407      */
    408     previous_curr_key = cp->saved_curr_key;
    409     previous_curr_val = cp->saved_curr_val;
    410     if (seq_res == 0) {
    411 	cp->saved_curr_key = mystrdup(raw_cache_key);
    412 	cp->saved_curr_val = mystrdup(raw_cache_val);
    413     } else {
    414 	cp->saved_curr_key = 0;
    415 	cp->saved_curr_val = 0;
    416     }
    417 
    418     /*
    419      * Delete behind.
    420      */
    421     if (db->error == 0 && DC_IS_SCHEDULED_FOR_DELETE_BEHIND(cp)) {
    422 	DC_CANCEL_DELETE_BEHIND(cp);
    423 	if (cp->user_flags & DICT_CACHE_FLAG_VERBOSE)
    424 	    msg_info("%s: delete-behind key=%s value=%s",
    425 		     myname, previous_curr_key, previous_curr_val);
    426 	if (dict_del(db, previous_curr_key) != 0)
    427 	    msg_rate_delay(&cp->del_log_stamp, cp->log_delay, msg_warn,
    428 			   "%s: could not delete entry for %s",
    429 			   cp->name, previous_curr_key);
    430     }
    431 
    432     /*
    433      * Clean up previous iteration key and value.
    434      */
    435     if (previous_curr_key)
    436 	myfree(previous_curr_key);
    437     if (previous_curr_val)
    438 	myfree(previous_curr_val);
    439 
    440     /*
    441      * Return the result.
    442      */
    443     *cache_key = (cp)->saved_curr_key;
    444     *cache_val = (cp)->saved_curr_val;
    445     DICT_ERR_VAL_RETURN(cp, db->error, seq_res);
    446 }
    447 
    448 /* dict_cache_delete_behind_reset - reset "delete behind" state */
    449 
    450 static void dict_cache_delete_behind_reset(DICT_CACHE *cp)
    451 {
    452 #define FREE_AND_WIPE(s) do { if (s) { myfree(s); (s) = 0; } } while (0)
    453 
    454     DC_CANCEL_DELETE_BEHIND(cp);
    455     FREE_AND_WIPE(cp->saved_curr_key);
    456     FREE_AND_WIPE(cp->saved_curr_val);
    457 }
    458 
    459 /* dict_cache_clean_stat_log_reset - log and reset cache cleanup statistics */
    460 
    461 static void dict_cache_clean_stat_log_reset(DICT_CACHE *cp,
    462 					            const char *full_partial)
    463 {
    464     if (cp->user_flags & DICT_CACHE_FLAG_STATISTICS)
    465 	msg_info("cache %s %s cleanup: retained=%d dropped=%d entries",
    466 		 cp->name, full_partial, cp->retained, cp->dropped);
    467     cp->retained = cp->dropped = 0;
    468 }
    469 
    470 /* dict_cache_clean_event - examine one cache entry */
    471 
    472 static void dict_cache_clean_event(int unused_event, void *cache_context)
    473 {
    474     const char *myname = "dict_cache_clean_event";
    475     DICT_CACHE *cp = (DICT_CACHE *) cache_context;
    476     const char *cache_key;
    477     const char *cache_val;
    478     int     next_interval;
    479     VSTRING *stamp_buf;
    480     int     first_next;
    481 
    482     /*
    483      * We interleave cache cleanup with other processing, so that the
    484      * application's service remains available, with perhaps increased
    485      * latency.
    486      */
    487 
    488     /*
    489      * Start a new cache cleanup run.
    490      */
    491     if (cp->saved_curr_key == 0) {
    492 	cp->retained = cp->dropped = 0;
    493 	first_next = DICT_SEQ_FUN_FIRST;
    494 	if (cp->user_flags & DICT_CACHE_FLAG_VERBOSE)
    495 	    msg_info("%s: start %s cache cleanup", myname, cp->name);
    496     }
    497 
    498     /*
    499      * Continue a cache cleanup run in progress.
    500      */
    501     else {
    502 	first_next = DICT_SEQ_FUN_NEXT;
    503     }
    504 
    505     /*
    506      * Examine one cache entry.
    507      */
    508     if (dict_cache_sequence(cp, first_next, &cache_key, &cache_val) == 0) {
    509 	if (cp->exp_validator(cache_key, cache_val, cp->exp_context) == 0) {
    510 	    DC_SCHEDULE_FOR_DELETE_BEHIND(cp);
    511 	    cp->dropped++;
    512 	    if (cp->user_flags & DICT_CACHE_FLAG_VERBOSE)
    513 		msg_info("%s: drop %s cache entry for %s",
    514 			 myname, cp->name, cache_key);
    515 	} else {
    516 	    cp->retained++;
    517 	    if (cp->user_flags & DICT_CACHE_FLAG_VERBOSE)
    518 		msg_info("%s: keep %s cache entry for %s",
    519 			 myname, cp->name, cache_key);
    520 	}
    521 	next_interval = 0;
    522     }
    523 
    524     /*
    525      * Cache cleanup completed. Report vital statistics.
    526      */
    527     else if (cp->error != 0) {
    528 	msg_warn("%s: cache cleanup scan terminated due to error", cp->name);
    529 	dict_cache_clean_stat_log_reset(cp, "partial");
    530 	next_interval = cp->exp_interval;
    531     } else {
    532 	if (cp->user_flags & DICT_CACHE_FLAG_VERBOSE)
    533 	    msg_info("%s: done %s cache cleanup scan", myname, cp->name);
    534 	dict_cache_clean_stat_log_reset(cp, "full");
    535 	stamp_buf = vstring_alloc(100);
    536 	vstring_sprintf(stamp_buf, "%ld", (long) event_time());
    537 	dict_put(cp->db, DC_LAST_CACHE_CLEANUP_COMPLETED,
    538 		 vstring_str(stamp_buf));
    539 	vstring_free(stamp_buf);
    540 	next_interval = cp->exp_interval;
    541     }
    542     event_request_timer(dict_cache_clean_event, cache_context, next_interval);
    543 }
    544 
    545 /* dict_cache_control - schedule or stop the cache cleanup thread */
    546 
    547 void    dict_cache_control(DICT_CACHE *cp,...)
    548 {
    549     const char *myname = "dict_cache_control";
    550     const char *last_done;
    551     time_t  next_interval;
    552     int     cache_cleanup_is_active = (cp->exp_validator && cp->exp_interval);
    553     va_list ap;
    554     int     name;
    555 
    556     /*
    557      * Update the control settings.
    558      */
    559     va_start(ap, cp);
    560     while ((name = va_arg(ap, int)) > 0) {
    561 	switch (name) {
    562 	case DICT_CACHE_CTL_END:
    563 	    break;
    564 	case DICT_CACHE_CTL_FLAGS:
    565 	    cp->user_flags = va_arg(ap, int);
    566 	    cp->log_delay = (cp->user_flags & DICT_CACHE_FLAG_VERBOSE) ?
    567 		0 : DC_DEF_LOG_DELAY;
    568 	    break;
    569 	case DICT_CACHE_CTL_INTERVAL:
    570 	    cp->exp_interval = va_arg(ap, int);
    571 	    if (cp->exp_interval < 0)
    572 		msg_panic("%s: bad %s cache cleanup interval %d",
    573 			  myname, cp->name, cp->exp_interval);
    574 	    break;
    575 	case DICT_CACHE_CTL_VALIDATOR:
    576 	    cp->exp_validator = va_arg(ap, DICT_CACHE_VALIDATOR_FN);
    577 	    break;
    578 	case DICT_CACHE_CTL_CONTEXT:
    579 	    cp->exp_context = va_arg(ap, void *);
    580 	    break;
    581 	default:
    582 	    msg_panic("%s: bad command: %d", myname, name);
    583 	}
    584     }
    585     va_end(ap);
    586 
    587     /*
    588      * Schedule the cache cleanup thread.
    589      */
    590     if (cp->exp_interval && cp->exp_validator) {
    591 
    592 	/*
    593 	 * Sanity checks.
    594 	 */
    595 	if (cache_cleanup_is_active)
    596 	    msg_panic("%s: %s cache cleanup is already scheduled",
    597 		      myname, cp->name);
    598 
    599 	/*
    600 	 * The next start time depends on the last completion time.
    601 	 */
    602 #define NEXT_START(last, delta) ((delta) + (unsigned long) atol(last))
    603 #define NOW	(time((time_t *) 0))		/* NOT: event_time() */
    604 
    605 	if ((last_done = dict_get(cp->db, DC_LAST_CACHE_CLEANUP_COMPLETED)) == 0
    606 	    || (next_interval = (NEXT_START(last_done, cp->exp_interval) - NOW)) < 0)
    607 	    next_interval = 0;
    608 	if (next_interval > cp->exp_interval)
    609 	    next_interval = cp->exp_interval;
    610 	if ((cp->user_flags & DICT_CACHE_FLAG_VERBOSE) && next_interval > 0)
    611 	    msg_info("%s cache cleanup will start after %ds",
    612 		     cp->name, (int) next_interval);
    613 	event_request_timer(dict_cache_clean_event, (void *) cp,
    614 			    (int) next_interval);
    615     }
    616 
    617     /*
    618      * Cancel the cache cleanup thread.
    619      */
    620     else if (cache_cleanup_is_active) {
    621 	if (cp->retained || cp->dropped)
    622 	    dict_cache_clean_stat_log_reset(cp, "partial");
    623 	dict_cache_delete_behind_reset(cp);
    624 	event_cancel_timer(dict_cache_clean_event, (void *) cp);
    625     }
    626 }
    627 
    628 /* dict_cache_open - open cache file */
    629 
    630 DICT_CACHE *dict_cache_open(const char *dbname, int open_flags, int dict_flags)
    631 {
    632     DICT_CACHE *cp;
    633     DICT   *dict;
    634 
    635     /*
    636      * Open the database as requested. Don't attempt to second-guess the
    637      * application.
    638      */
    639     dict = dict_open(dbname, open_flags, dict_flags);
    640 
    641     /*
    642      * Create the DICT_CACHE object.
    643      */
    644     cp = (DICT_CACHE *) mymalloc(sizeof(*cp));
    645     cp->name = mystrdup(dbname);
    646     cp->cache_flags = 0;
    647     cp->user_flags = 0;
    648     cp->db = dict;
    649     cp->saved_curr_key = 0;
    650     cp->saved_curr_val = 0;
    651     cp->exp_interval = 0;
    652     cp->exp_validator = 0;
    653     cp->exp_context = 0;
    654     cp->retained = 0;
    655     cp->dropped = 0;
    656     cp->log_delay = DC_DEF_LOG_DELAY;
    657     cp->upd_log_stamp = cp->get_log_stamp =
    658 	cp->del_log_stamp = cp->seq_log_stamp = 0;
    659 
    660     return (cp);
    661 }
    662 
    663 /* dict_cache_close - close cache file */
    664 
    665 void    dict_cache_close(DICT_CACHE *cp)
    666 {
    667 
    668     /*
    669      * Cancel the cache cleanup thread. This also logs (and resets)
    670      * statistics for a scan that is in progress.
    671      */
    672     dict_cache_control(cp, DICT_CACHE_CTL_INTERVAL, 0, DICT_CACHE_CTL_END);
    673 
    674     /*
    675      * Destroy the DICT_CACHE object.
    676      */
    677     myfree(cp->name);
    678     dict_close(cp->db);
    679     if (cp->saved_curr_key)
    680 	myfree(cp->saved_curr_key);
    681     if (cp->saved_curr_val)
    682 	myfree(cp->saved_curr_val);
    683     myfree((void *) cp);
    684 }
    685 
    686 /* dict_cache_name - get the cache name */
    687 
    688 const char *dict_cache_name(DICT_CACHE *cp)
    689 {
    690 
    691     /*
    692      * This is used for verbose logging or warning messages, so the cost of a
    693      * call is only made where needed (well sort of - code that does not
    694      * execute still presents overhead for the processor pipeline, processor
    695      * cache, etc).
    696      */
    697     return (cp->name);
    698 }
    699 
    700 /* dict_cache_error - get the dictionary error status */
    701 
    702 int     dict_cache_error(DICT_CACHE *cp)
    703 {
    704     return (cp->error);
    705 }
    706 
    707  /*
    708   * Test driver with support for interleaved access. First, enter a number of
    709   * requests to look up, update or delete a sequence of cache entries, then
    710   * interleave those sequences with the "run" command.
    711   */
    712 #ifdef TEST
    713 #include <msg_vstream.h>
    714 #include <vstring_vstream.h>
    715 #include <argv.h>
    716 #include <stringops.h>
    717 
    718 #define DELIMS	" "
    719 #define USAGE	"\n\tTo manage settings:" \
    720 		"\n\tverbose <level> (verbosity level)" \
    721 		"\n\telapsed <level> (0=don't show elapsed time)" \
    722 		"\n\tlmdb_map_size <limit> (initial LMDB size limit)" \
    723 		"\n\tcache <type>:<name> (switch to named database)" \
    724 		"\n\tstatus (show map size, cache, pending requests)" \
    725 		"\n\n\tTo manage pending requests:" \
    726 		"\n\treset (discard pending requests)" \
    727 		"\n\trun (execute pending requests in interleaved order)" \
    728 		"\n\n\tTo add a pending request:" \
    729 		"\n\tquery <key-suffix> <count> (negative to reverse order)" \
    730 		"\n\tupdate <key-suffix> <count> (negative to reverse order)" \
    731 		"\n\tdelete <key-suffix> <count> (negative to reverse order)" \
    732 		"\n\tpurge <key-suffix>" \
    733 		"\n\tcount <key-suffix>"
    734 
    735  /*
    736   * For realism, open the cache with the same flags as postscreen(8) and
    737   * verify(8).
    738   */
    739 #define DICT_CACHE_OPEN_FLAGS (DICT_FLAG_DUP_REPLACE | DICT_FLAG_SYNC_UPDATE | \
    740 	DICT_FLAG_OPEN_LOCK)
    741 
    742  /*
    743   * Storage for one request to access a sequence of cache entries.
    744   */
    745 typedef struct DICT_CACHE_SREQ {
    746     int     flags;			/* per-request: reverse, purge */
    747     char   *cmd;			/* command for status report */
    748     void    (*action) (struct DICT_CACHE_SREQ *, DICT_CACHE *, VSTRING *);
    749     char   *suffix;			/* key suffix */
    750     int     done;			/* progress indicator */
    751     int     todo;			/* number of entries to process */
    752     int     first_next;			/* first/next */
    753 } DICT_CACHE_SREQ;
    754 
    755 #define DICT_CACHE_SREQ_FLAG_PURGE	(1<<1)	/* purge instead of count */
    756 #define DICT_CACHE_SREQ_FLAG_REVERSE	(1<<2)	/* reverse instead of forward */
    757 
    758 #define DICT_CACHE_SREQ_LIMIT		10
    759 
    760  /*
    761   * All test requests combined.
    762   */
    763 typedef struct DICT_CACHE_TEST {
    764     int     flags;			/* exclusion flags */
    765     int     size;			/* allocated slots */
    766     int     used;			/* used slots */
    767     DICT_CACHE_SREQ job_list[1];	/* actually, a bunch */
    768 } DICT_CACHE_TEST;
    769 
    770 #define DICT_CACHE_TEST_FLAG_ITER	(1<<0)	/* count or purge */
    771 
    772 #define STR(x)	vstring_str(x)
    773 
    774 int     show_elapsed = 1;		/* show elapsed time */
    775 
    776 #ifdef HAS_LMDB
    777 extern size_t dict_lmdb_map_size;	/* LMDB-specific */
    778 
    779 #endif
    780 
    781 /* usage - command-line usage message */
    782 
    783 static NORETURN usage(const char *progname)
    784 {
    785     msg_fatal("usage: %s (no argument)", progname);
    786 }
    787 
    788 /* make_tagged_key - make tagged search key */
    789 
    790 static void make_tagged_key(VSTRING *bp, DICT_CACHE_SREQ *cp)
    791 {
    792     if (cp->done < 0)
    793 	msg_panic("make_tagged_key: bad done count: %d", cp->done);
    794     if (cp->todo < 1)
    795 	msg_panic("make_tagged_key: bad todo count: %d", cp->todo);
    796     vstring_sprintf(bp, "%d-%s",
    797 		    (cp->flags & DICT_CACHE_SREQ_FLAG_REVERSE) ?
    798 		    cp->todo - cp->done - 1 : cp->done, cp->suffix);
    799 }
    800 
    801 /* create_requests - create request list */
    802 
    803 static DICT_CACHE_TEST *create_requests(int count)
    804 {
    805     DICT_CACHE_TEST *tp;
    806     DICT_CACHE_SREQ *cp;
    807 
    808     tp = (DICT_CACHE_TEST *) mymalloc(sizeof(DICT_CACHE_TEST) +
    809 				      (count - 1) *sizeof(DICT_CACHE_SREQ));
    810     tp->flags = 0;
    811     tp->size = count;
    812     tp->used = 0;
    813     for (cp = tp->job_list; cp < tp->job_list + count; cp++) {
    814 	cp->flags = 0;
    815 	cp->cmd = 0;
    816 	cp->action = 0;
    817 	cp->suffix = 0;
    818 	cp->todo = 0;
    819 	cp->first_next = DICT_SEQ_FUN_FIRST;
    820     }
    821     return (tp);
    822 }
    823 
    824 /* reset_requests - reset request list */
    825 
    826 static void reset_requests(DICT_CACHE_TEST *tp)
    827 {
    828     DICT_CACHE_SREQ *cp;
    829 
    830     tp->flags = 0;
    831     tp->used = 0;
    832     for (cp = tp->job_list; cp < tp->job_list + tp->size; cp++) {
    833 	cp->flags = 0;
    834 	if (cp->cmd) {
    835 	    myfree(cp->cmd);
    836 	    cp->cmd = 0;
    837 	}
    838 	cp->action = 0;
    839 	if (cp->suffix) {
    840 	    myfree(cp->suffix);
    841 	    cp->suffix = 0;
    842 	}
    843 	cp->todo = 0;
    844 	cp->first_next = DICT_SEQ_FUN_FIRST;
    845     }
    846 }
    847 
    848 /* free_requests - destroy request list */
    849 
    850 static void free_requests(DICT_CACHE_TEST *tp)
    851 {
    852     reset_requests(tp);
    853     myfree((void *) tp);
    854 }
    855 
    856 /* run_requests - execute pending requests in interleaved order */
    857 
    858 static void run_requests(DICT_CACHE_TEST *tp, DICT_CACHE *dp, VSTRING *bp)
    859 {
    860     DICT_CACHE_SREQ *cp;
    861     int     todo;
    862     struct timeval start;
    863     struct timeval finish;
    864     struct timeval elapsed;
    865 
    866     if (dp == 0) {
    867 	msg_warn("no cache");
    868 	return;
    869     }
    870     GETTIMEOFDAY(&start);
    871     do {
    872 	todo = 0;
    873 	for (cp = tp->job_list; cp < tp->job_list + tp->used; cp++) {
    874 	    if (cp->done < cp->todo) {
    875 		todo = 1;
    876 		cp->action(cp, dp, bp);
    877 	    }
    878 	}
    879     } while (todo);
    880     GETTIMEOFDAY(&finish);
    881     timersub(&finish, &start, &elapsed);
    882     if (show_elapsed)
    883 	vstream_printf("Elapsed: %g\n",
    884 		       elapsed.tv_sec + elapsed.tv_usec / 1000000.0);
    885 
    886     reset_requests(tp);
    887 }
    888 
    889 /* show_status - show settings and pending requests */
    890 
    891 static void show_status(DICT_CACHE_TEST *tp, DICT_CACHE *dp)
    892 {
    893     DICT_CACHE_SREQ *cp;
    894 
    895 #ifdef HAS_LMDB
    896     vstream_printf("lmdb_map_size\t%ld\n", (long) dict_lmdb_map_size);
    897 #endif
    898     vstream_printf("cache\t%s\n", dp ? dp->name : "(none)");
    899 
    900     if (tp->used == 0)
    901 	vstream_printf("No pending requests\n");
    902     else
    903 	vstream_printf("%s\t%s\t%s\t%s\t%s\t%s\n",
    904 		     "cmd", "dir", "suffix", "count", "done", "first/next");
    905 
    906     for (cp = tp->job_list; cp < tp->job_list + tp->used; cp++)
    907 	if (cp->todo > 0)
    908 	    vstream_printf("%s\t%s\t%s\t%d\t%d\t%d\n",
    909 			   cp->cmd,
    910 			   (cp->flags & DICT_CACHE_SREQ_FLAG_REVERSE) ?
    911 			   "reverse" : "forward",
    912 			   cp->suffix ? cp->suffix : "(null)", cp->todo,
    913 			   cp->done, cp->first_next);
    914 }
    915 
    916 /* query_action - lookup cache entry */
    917 
    918 static void query_action(DICT_CACHE_SREQ *cp, DICT_CACHE *dp, VSTRING *bp)
    919 {
    920     const char *lookup;
    921 
    922     make_tagged_key(bp, cp);
    923     if ((lookup = dict_cache_lookup(dp, STR(bp))) == 0) {
    924 	if (dp->error)
    925 	    msg_warn("query_action: query failed: %s: %m", STR(bp));
    926 	else
    927 	    msg_warn("query_action: query failed: %s", STR(bp));
    928     } else if (strcmp(STR(bp), lookup) != 0) {
    929 	msg_warn("lookup result \"%s\" differs from key \"%s\"",
    930 		 lookup, STR(bp));
    931     }
    932     cp->done += 1;
    933 }
    934 
    935 /* update_action - update cache entry */
    936 
    937 static void update_action(DICT_CACHE_SREQ *cp, DICT_CACHE *dp, VSTRING *bp)
    938 {
    939     make_tagged_key(bp, cp);
    940     if (dict_cache_update(dp, STR(bp), STR(bp)) != 0) {
    941 	if (dp->error)
    942 	    msg_warn("update_action: update failed: %s: %m", STR(bp));
    943 	else
    944 	    msg_warn("update_action: update failed: %s", STR(bp));
    945     }
    946     cp->done += 1;
    947 }
    948 
    949 /* delete_action - delete cache entry */
    950 
    951 static void delete_action(DICT_CACHE_SREQ *cp, DICT_CACHE *dp, VSTRING *bp)
    952 {
    953     make_tagged_key(bp, cp);
    954     if (dict_cache_delete(dp, STR(bp)) != 0) {
    955 	if (dp->error)
    956 	    msg_warn("delete_action: delete failed: %s: %m", STR(bp));
    957 	else
    958 	    msg_warn("delete_action: delete failed: %s", STR(bp));
    959     }
    960     cp->done += 1;
    961 }
    962 
    963 /* iter_action - iterate over cache and act on entries with given suffix */
    964 
    965 static void iter_action(DICT_CACHE_SREQ *cp, DICT_CACHE *dp, VSTRING *bp)
    966 {
    967     const char *cache_key;
    968     const char *cache_val;
    969     const char *what;
    970     const char *suffix;
    971 
    972     if (dict_cache_sequence(dp, cp->first_next, &cache_key, &cache_val) == 0) {
    973 	if (strcmp(cache_key, cache_val) != 0)
    974 	    msg_warn("value \"%s\" differs from key \"%s\"",
    975 		     cache_val, cache_key);
    976 	suffix = cache_key + strspn(cache_key, "0123456789");
    977 	if (suffix[0] == '-' && strcmp(suffix + 1, cp->suffix) == 0) {
    978 	    cp->done += 1;
    979 	    cp->todo = cp->done + 1;		/* XXX */
    980 	    if ((cp->flags & DICT_CACHE_SREQ_FLAG_PURGE)
    981 		&& dict_cache_delete(dp, cache_key) != 0) {
    982 		if (dp->error)
    983 		    msg_warn("purge_action: delete failed: %s: %m", STR(bp));
    984 		else
    985 		    msg_warn("purge_action: delete failed: %s", STR(bp));
    986 	    }
    987 	}
    988 	cp->first_next = DICT_SEQ_FUN_NEXT;
    989     } else {
    990 	what = (cp->flags & DICT_CACHE_SREQ_FLAG_PURGE) ? "purge" : "count";
    991 	if (dp->error)
    992 	    msg_warn("%s error after %d: %m", what, cp->done);
    993 	else
    994 	    vstream_printf("suffix=%s %s=%d\n", cp->suffix, what, cp->done);
    995 	cp->todo = 0;
    996     }
    997 }
    998 
    999  /*
   1000   * Table-driven support.
   1001   */
   1002 typedef struct DICT_CACHE_SREQ_INFO {
   1003     const char *name;
   1004     int     argc;
   1005     void    (*action) (DICT_CACHE_SREQ *, DICT_CACHE *, VSTRING *);
   1006     int     test_flags;
   1007     int     req_flags;
   1008 } DICT_CACHE_SREQ_INFO;
   1009 
   1010 static DICT_CACHE_SREQ_INFO req_info[] = {
   1011     {"query", 3, query_action},
   1012     {"update", 3, update_action},
   1013     {"delete", 3, delete_action},
   1014     {"count", 2, iter_action, DICT_CACHE_TEST_FLAG_ITER},
   1015     {"purge", 2, iter_action, DICT_CACHE_TEST_FLAG_ITER, DICT_CACHE_SREQ_FLAG_PURGE},
   1016     0,
   1017 };
   1018 
   1019 /* add_request - add a request to the list */
   1020 
   1021 static void add_request(DICT_CACHE_TEST *tp, ARGV *argv)
   1022 {
   1023     DICT_CACHE_SREQ_INFO *rp;
   1024     DICT_CACHE_SREQ *cp;
   1025     int     req_flags;
   1026     int     count;
   1027     char   *cmd = argv->argv[0];
   1028     char   *suffix = (argv->argc > 1 ? argv->argv[1] : 0);
   1029     char   *todo = (argv->argc > 2 ? argv->argv[2] : "1");	/* XXX */
   1030 
   1031     if (tp->used >= tp->size) {
   1032 	msg_warn("%s: request list is full", cmd);
   1033 	return;
   1034     }
   1035     for (rp = req_info; /* See below */ ; rp++) {
   1036 	if (rp->name == 0) {
   1037 	    vstream_printf("usage: %s\n", USAGE);
   1038 	    return;
   1039 	}
   1040 	if (strcmp(rp->name, argv->argv[0]) == 0
   1041 	    && rp->argc == argv->argc)
   1042 	    break;
   1043     }
   1044     req_flags = rp->req_flags;
   1045     if (todo[0] == '-') {
   1046 	req_flags |= DICT_CACHE_SREQ_FLAG_REVERSE;
   1047 	todo += 1;
   1048     }
   1049     if (!alldig(todo) || (count = atoi(todo)) == 0) {
   1050 	msg_warn("%s: bad count: %s", cmd, todo);
   1051 	return;
   1052     }
   1053     if (tp->flags & rp->test_flags) {
   1054 	msg_warn("%s: command conflicts with other command", cmd);
   1055 	return;
   1056     }
   1057     tp->flags |= rp->test_flags;
   1058     cp = tp->job_list + tp->used;
   1059     cp->cmd = mystrdup(cmd);
   1060     cp->action = rp->action;
   1061     if (suffix)
   1062 	cp->suffix = mystrdup(suffix);
   1063     cp->done = 0;
   1064     cp->flags = req_flags;
   1065     cp->todo = count;
   1066     tp->used += 1;
   1067 }
   1068 
   1069 /* main - main program */
   1070 
   1071 int     main(int argc, char **argv)
   1072 {
   1073     DICT_CACHE_TEST *test_job;
   1074     VSTRING *inbuf = vstring_alloc(100);
   1075     char   *bufp;
   1076     ARGV   *args;
   1077     DICT_CACHE *cache = 0;
   1078     int     stdin_is_tty;
   1079 
   1080     msg_vstream_init(argv[0], VSTREAM_ERR);
   1081     if (argc != 1)
   1082 	usage(argv[0]);
   1083 
   1084 
   1085     test_job = create_requests(DICT_CACHE_SREQ_LIMIT);
   1086 
   1087     stdin_is_tty = isatty(0);
   1088 
   1089     for (;;) {
   1090 	if (stdin_is_tty) {
   1091 	    vstream_printf("> ");
   1092 	    vstream_fflush(VSTREAM_OUT);
   1093 	}
   1094 	if (vstring_fgets_nonl(inbuf, VSTREAM_IN) == 0)
   1095 	    break;
   1096 	bufp = vstring_str(inbuf);
   1097 	if (!stdin_is_tty) {
   1098 	    vstream_printf("> %s\n", bufp);
   1099 	    vstream_fflush(VSTREAM_OUT);
   1100 	}
   1101 	if (*bufp == '#')
   1102 	    continue;
   1103 	args = argv_split(bufp, DELIMS);
   1104 	if (argc == 0) {
   1105 	    vstream_printf("usage: %s\n", USAGE);
   1106 	    vstream_fflush(VSTREAM_OUT);
   1107 	    continue;
   1108 	}
   1109 	if (strcmp(args->argv[0], "verbose") == 0 && args->argc == 2) {
   1110 	    msg_verbose = atoi(args->argv[1]);
   1111 	} else if (strcmp(args->argv[0], "elapsed") == 0 && args->argc == 2) {
   1112 	    show_elapsed = atoi(args->argv[1]);
   1113 #ifdef HAS_LMDB
   1114 	} else if (strcmp(args->argv[0], "lmdb_map_size") == 0 && args->argc == 2) {
   1115 	    dict_lmdb_map_size = atol(args->argv[1]);
   1116 #endif
   1117 	} else if (strcmp(args->argv[0], "cache") == 0 && args->argc == 2) {
   1118 	    if (cache)
   1119 		dict_cache_close(cache);
   1120 	    cache = dict_cache_open(args->argv[1], O_CREAT | O_RDWR,
   1121 				    DICT_CACHE_OPEN_FLAGS);
   1122 	} else if (strcmp(args->argv[0], "reset") == 0 && args->argc == 1) {
   1123 	    reset_requests(test_job);
   1124 	} else if (strcmp(args->argv[0], "run") == 0 && args->argc == 1) {
   1125 	    run_requests(test_job, cache, inbuf);
   1126 	} else if (strcmp(args->argv[0], "status") == 0 && args->argc == 1) {
   1127 	    show_status(test_job, cache);
   1128 	} else {
   1129 	    add_request(test_job, args);
   1130 	}
   1131 	vstream_fflush(VSTREAM_OUT);
   1132 	argv_free(args);
   1133     }
   1134 
   1135     vstring_free(inbuf);
   1136     free_requests(test_job);
   1137     if (cache)
   1138 	dict_cache_close(cache);
   1139     return (0);
   1140 }
   1141 
   1142 #endif
   1143