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