/*
 * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * See the COPYRIGHT file distributed with this work for additional
 * information regarding copyright ownership.
 */

/* $Id: acache.c,v 1.22 2008/02/07 23:46:54 tbox Exp $ */

#include <config.h>

#include <isc/atomic.h>
#include <isc/event.h>
#include <isc/hash.h>
#include <isc/magic.h>
#include <isc/mem.h>
#include <isc/mutex.h>
#include <isc/platform.h>
#include <isc/random.h>
#include <isc/refcount.h>
#include <isc/rwlock.h>
#include <isc/serial.h>
#include <isc/task.h>
#include <isc/time.h>
#include <isc/timer.h>
#include <isc/util.h>

#include <dns/acache.h>
#include <dns/db.h>
#include <dns/events.h>
#include <dns/log.h>
#include <dns/message.h>
#include <dns/name.h>
#include <dns/rdataset.h>
#include <dns/result.h>
#include <dns/zone.h>

#if defined(ISC_PLATFORM_HAVESTDATOMIC)
#if defined(__cplusplus)
#include <isc/stdatomic.h>
#else
#include <stdatomic.h>
#endif
#endif

#define ACACHE_MAGIC			ISC_MAGIC('A', 'C', 'H', 'E')
#define DNS_ACACHE_VALID(acache)	ISC_MAGIC_VALID(acache, ACACHE_MAGIC)

#define ACACHEENTRY_MAGIC		ISC_MAGIC('A', 'C', 'E', 'T')
#define DNS_ACACHEENTRY_VALID(entry)	ISC_MAGIC_VALID(entry, ACACHEENTRY_MAGIC)

#define DBBUCKETS	67

#if 0
#define ATRACE(m)       isc_log_write(dns_lctx, \
				      DNS_LOGCATEGORY_DATABASE, \
				      DNS_LOGMODULE_ACACHE, \
				      ISC_LOG_DEBUG(3), \
				      "acache %p: %s", acache, (m))
#define AATRACE(a,m)    isc_log_write(dns_lctx, \
				      DNS_LOGCATEGORY_DATABASE, \
				      DNS_LOGMODULE_ACACHE, \
				      ISC_LOG_DEBUG(3), \
				      "acache %p: %s", (a), (m))
#else
#define ATRACE(m)
#define AATRACE(a, m)
#endif

/*
 * The following variables control incremental cleaning.
 * MINSIZE is how many bytes is the floor for dns_acache_setcachesize().
 * CLEANERINCREMENT is how many entries are examined in one pass.
 * (XXX simply derived from definitions in cache.c  There may be better
 *  constants here.)
 */
#define DNS_ACACHE_MINSIZE 		2097152U /* Bytes.  2097152 = 2 MB */
#define DNS_ACACHE_CLEANERINCREMENT	1000	 /* Number of entries. */

#define DEFAULT_ACACHE_ENTRY_LOCK_COUNT	1009	 /*%< Should be prime. */

#if defined(ISC_RWLOCK_USEATOMIC) &&					\
	((defined(ISC_PLATFORM_HAVESTDATOMIC) && defined(ATOMIC_LONG_LOCK_FREE)) || \
	 defined(ISC_PLATFORM_HAVEATOMICSTORE))
#define ACACHE_USE_RWLOCK 1
#if (defined(ISC_PLATFORM_HAVESTDATOMIC) && defined(ATOMIC_LONG_LOCK_FREE))
#define ACACHE_HAVESTDATOMIC 1
#endif
#endif

#ifdef ACACHE_USE_RWLOCK
#define ACACHE_INITLOCK(l)	isc_rwlock_init((l), 0, 0)
#define ACACHE_DESTROYLOCK(l)	isc_rwlock_destroy(l)
#define ACACHE_LOCK(l, t)	RWLOCK((l), (t))
#define ACACHE_UNLOCK(l, t)	RWUNLOCK((l), (t))

#ifdef ACACHE_HAVESTDATOMIC
#define acache_storetime(entry, t) \
	atomic_store_explicit(&(entry)->lastused, (t), \
			      memory_order_relaxed);
#else
#define acache_storetime(entry, t) \
	(isc_atomic_store((int32_t *)&(entry)->lastused, (t)))
#endif

#else
#define ACACHE_INITLOCK(l)	isc_mutex_init(l)
#define ACACHE_DESTROYLOCK(l)	DESTROYLOCK(l)
#define ACACHE_LOCK(l, t)	LOCK(l)
#define ACACHE_UNLOCK(l, t)	UNLOCK(l)

#define acache_storetime(entry, t) ((entry)->lastused = (t))
#endif

/* Locked by acache lock */
typedef struct dbentry {
	ISC_LINK(struct dbentry)	link;

	dns_db_t			*db;
	ISC_LIST(dns_acacheentry_t)	originlist;
	ISC_LIST(dns_acacheentry_t)	referlist;
} dbentry_t;

typedef ISC_LIST(dbentry_t) dbentrylist_t;

typedef struct acache_cleaner acache_cleaner_t;

typedef enum {
	cleaner_s_idle,	/* Waiting for cleaning-interval to expire. */
	cleaner_s_busy,	/* Currently cleaning. */
	cleaner_s_done  /* Freed enough memory after being overmem. */
} cleaner_state_t;

/*
 * Convenience macros for comprehensive assertion checking.
 */
#define CLEANER_IDLE(c) ((c)->state == cleaner_s_idle && \
			 (c)->resched_event != NULL)
#define CLEANER_BUSY(c) ((c)->state == cleaner_s_busy && \
			 (c)->resched_event == NULL)

struct acache_cleaner {
	isc_mutex_t		lock;
	/*
	 * Locks overmem_event, overmem.  (See cache.c)
	 */

	dns_acache_t		*acache;
	unsigned int		cleaning_interval; /* The cleaning-interval
						      from named.conf,
						      in seconds. */

	isc_stdtime_t		last_cleanup_time; /* The time when the last
						      cleanup task completed */

	isc_timer_t 		*cleaning_timer;
	isc_event_t		*resched_event;	/* Sent by cleaner task to
						   itself to reschedule */
	isc_event_t		*overmem_event;

	dns_acacheentry_t	*current_entry;	/* The bookmark entry to
						   restart the cleaning.
						   Locked by acache lock. */
	int 		 	increment;	/* Number of entries to
						   clean in one increment */

	unsigned long		ncleaned;	/* Number of entries cleaned
						   up (for logging purposes) */
	cleaner_state_t  	state;		/* Idle/Busy/Done. */
	bool	 	overmem;	/* The acache is in an overmem
						   state. */
};

struct dns_acachestats {
	unsigned int			hits;
	unsigned int			queries;
	unsigned int			misses;
	unsigned int			adds;
	unsigned int			deleted;
	unsigned int			cleaned;
	unsigned int			cleaner_runs;
	unsigned int			overmem;
	unsigned int			overmem_nocreates;
	unsigned int			nomem;
};

/*
 * The actual acache object.
 */

struct dns_acache {
	unsigned int			magic;

	isc_mem_t			*mctx;
	isc_refcount_t			refs;

#ifdef ACACHE_USE_RWLOCK
	isc_rwlock_t 			*entrylocks;
#else
	isc_mutex_t 			*entrylocks;
#endif

	isc_mutex_t			lock;

	int				live_cleaners;
	acache_cleaner_t		cleaner;
	ISC_LIST(dns_acacheentry_t)	entries;
	unsigned int			dbentries;
	dbentrylist_t			dbbucket[DBBUCKETS];

	bool			shutting_down;

	isc_task_t 			*task;
	isc_event_t			cevent;
	bool			cevent_sent;

	dns_acachestats_t		stats;
};

struct dns_acacheentry {
	unsigned int 		magic;

	unsigned int		locknum;
	isc_refcount_t 		references;

	dns_acache_t 		*acache;

	/* Data for Management of cache entries */
	ISC_LINK(dns_acacheentry_t) link;
	ISC_LINK(dns_acacheentry_t) olink;
	ISC_LINK(dns_acacheentry_t) rlink;

	dns_db_t 		*origdb; /* reference to the DB
					    holding this entry */

	/* Cache data */
	dns_zone_t 		*zone;		/* zone this entry
						   belongs to */
	dns_db_t		*db;   		/* DB this entry belongs to */
	dns_dbversion_t		*version;	/* the version of the DB */
	dns_dbnode_t 		*node;		/* node this entry
						   belongs to */
	dns_name_t 		*foundname;	/* corresponding DNS name
						   and rdataset */

	/* Callback function and its argument */
	void 			(*callback)(dns_acacheentry_t *, void **);
	void 			*cbarg;

	/* Timestamp of the last time this entry is referred to */
#ifdef ACACHE_HAVESTDATOMIC
	atomic_uint_fast32_t	lastused;
#else
	isc_stdtime_t		lastused;
#endif
};

/*
 *	Internal functions (and prototypes).
 */
static inline bool check_noentry(dns_acache_t *acache);
static void destroy(dns_acache_t *acache);
static void shutdown_entries(dns_acache_t *acache);
static void shutdown_buckets(dns_acache_t *acache);
static void destroy_entry(dns_acacheentry_t *ent);
static inline void unlink_dbentries(dns_acache_t *acache,
				    dns_acacheentry_t *ent);
static inline isc_result_t finddbent(dns_acache_t *acache,
				     dns_db_t *db, dbentry_t **dbentryp);
static inline void clear_entry(dns_acache_t *acache, dns_acacheentry_t *entry);
static isc_result_t acache_cleaner_init(dns_acache_t *acache,
					isc_timermgr_t *timermgr,
					acache_cleaner_t *cleaner);
static void acache_cleaning_timer_action(isc_task_t *task, isc_event_t *event);
static void acache_incremental_cleaning_action(isc_task_t *task,
					       isc_event_t *event);
static void acache_overmem_cleaning_action(isc_task_t *task,
					   isc_event_t *event);
static void acache_cleaner_shutdown_action(isc_task_t *task,
					   isc_event_t *event);

/*
 * acache should be locked.  If it is not, the stats can get out of whack,
 * which is not a big deal for us since this is for debugging / stats
 */
static void
reset_stats(dns_acache_t *acache) {
	acache->stats.hits = 0;
	acache->stats.queries = 0;
	acache->stats.misses = 0;
	acache->stats.adds = 0;
	acache->stats.deleted = 0;
	acache->stats.cleaned = 0;
	acache->stats.overmem = 0;
	acache->stats.overmem_nocreates = 0;
	acache->stats.nomem = 0;
}

/*
 * The acache must be locked before calling.
 */
static inline bool
check_noentry(dns_acache_t *acache) {
	if (ISC_LIST_EMPTY(acache->entries) && acache->dbentries == 0) {
		return (true);
	}

	return (false);
}

/*
 * The acache must be locked before calling.
 */
static void
shutdown_entries(dns_acache_t *acache) {
	dns_acacheentry_t *entry, *entry_next;

	REQUIRE(DNS_ACACHE_VALID(acache));
	INSIST(acache->shutting_down);

	/*
	 * Release the dependency of all entries, and detach them.
	 */
	for (entry = ISC_LIST_HEAD(acache->entries);
	     entry != NULL;
	     entry = entry_next) {
		entry_next = ISC_LIST_NEXT(entry, link);

		ACACHE_LOCK(&acache->entrylocks[entry->locknum],
			    isc_rwlocktype_write);

		/*
		 * If the cleaner holds this entry, it will be unlinked and
		 * freed in the cleaner later.
		 */
		if (acache->cleaner.current_entry != entry)
			ISC_LIST_UNLINK(acache->entries, entry, link);
		unlink_dbentries(acache, entry);
		if (entry->callback != NULL) {
			(entry->callback)(entry, &entry->cbarg);
			entry->callback = NULL;
		}

		ACACHE_UNLOCK(&acache->entrylocks[entry->locknum],
			      isc_rwlocktype_write);

		if (acache->cleaner.current_entry != entry)
			dns_acache_detachentry(&entry);
	}
}

/*
 * The acache must be locked before calling.
 */
static void
shutdown_buckets(dns_acache_t *acache) {
	int i;
	dbentry_t *dbent;

	REQUIRE(DNS_ACACHE_VALID(acache));
	INSIST(acache->shutting_down);

	for (i = 0; i < DBBUCKETS; i++) {
		while ((dbent = ISC_LIST_HEAD(acache->dbbucket[i])) != NULL) {
			INSIST(ISC_LIST_EMPTY(dbent->originlist) &&
			       ISC_LIST_EMPTY(dbent->referlist));
			ISC_LIST_UNLINK(acache->dbbucket[i], dbent, link);

			dns_db_detach(&dbent->db);

			isc_mem_put(acache->mctx, dbent, sizeof(*dbent));

			acache->dbentries--;
		}
	}

	INSIST(acache->dbentries == 0);
}

static void
shutdown_task(isc_task_t *task, isc_event_t *ev) {
	dns_acache_t *acache;

	UNUSED(task);

	acache = ev->ev_arg;
	INSIST(DNS_ACACHE_VALID(acache));

	isc_event_free(&ev);

	LOCK(&acache->lock);

	shutdown_entries(acache);
	shutdown_buckets(acache);

	UNLOCK(&acache->lock);

	dns_acache_detach(&acache);
}

/* The acache and the entry must be locked before calling. */
static inline void
unlink_dbentries(dns_acache_t *acache, dns_acacheentry_t *ent) {
	isc_result_t result;
	dbentry_t *dbent;

	if (ISC_LINK_LINKED(ent, olink)) {
		INSIST(ent->origdb != NULL);
		dbent = NULL;
		result = finddbent(acache, ent->origdb, &dbent);
		INSIST(result == ISC_R_SUCCESS);

		ISC_LIST_UNLINK(dbent->originlist, ent, olink);
	}
	if (ISC_LINK_LINKED(ent, rlink)) {
		INSIST(ent->db != NULL);
		dbent = NULL;
		result = finddbent(acache, ent->db, &dbent);
		INSIST(result == ISC_R_SUCCESS);

		ISC_LIST_UNLINK(dbent->referlist, ent, rlink);
	}
}

/* There must not be a reference to this entry. */
static void
destroy_entry(dns_acacheentry_t *entry) {
	dns_acache_t *acache;

	REQUIRE(DNS_ACACHEENTRY_VALID(entry));

	acache = entry->acache;
	REQUIRE(DNS_ACACHE_VALID(acache));

	/*
	 * Since there is no reference to this entry, it is safe to call
	 * clear_entry() here.
	 */
	clear_entry(acache, entry);

	isc_mem_put(acache->mctx, entry, sizeof(*entry));

	dns_acache_detach(&acache);
}

static void
destroy(dns_acache_t *acache) {
	int i;

	REQUIRE(DNS_ACACHE_VALID(acache));

	ATRACE("destroy");

	isc_mem_setwater(acache->mctx, NULL, NULL, 0, 0);

	if (acache->cleaner.overmem_event != NULL)
		isc_event_free(&acache->cleaner.overmem_event);

	if (acache->cleaner.resched_event != NULL)
		isc_event_free(&acache->cleaner.resched_event);

	if (acache->task != NULL)
		isc_task_detach(&acache->task);

	for (i = 0; i < DEFAULT_ACACHE_ENTRY_LOCK_COUNT; i++)
		ACACHE_DESTROYLOCK(&acache->entrylocks[i]);
	isc_mem_put(acache->mctx, acache->entrylocks,
		    sizeof(*acache->entrylocks) *
		    DEFAULT_ACACHE_ENTRY_LOCK_COUNT);

	DESTROYLOCK(&acache->cleaner.lock);

	DESTROYLOCK(&acache->lock);
	acache->magic = 0;

	isc_mem_putanddetach(&acache->mctx, acache, sizeof(*acache));
}

static inline isc_result_t
finddbent(dns_acache_t *acache, dns_db_t *db, dbentry_t **dbentryp) {
	int bucket;
	dbentry_t *dbentry;

	REQUIRE(DNS_ACACHE_VALID(acache));
	REQUIRE(db != NULL);
	REQUIRE(dbentryp != NULL && *dbentryp == NULL);

	/*
	 * The caller must be holding the acache lock.
	 */

	bucket = isc_hash_function(&db, sizeof(db), true, NULL) % DBBUCKETS;

	for (dbentry = ISC_LIST_HEAD(acache->dbbucket[bucket]);
	     dbentry != NULL;
	     dbentry = ISC_LIST_NEXT(dbentry, link)) {
		if (dbentry->db == db)
			break;
	}

	*dbentryp = dbentry;

	if (dbentry == NULL)
		return (ISC_R_NOTFOUND);
	else
		return (ISC_R_SUCCESS);
}

static inline void
clear_entry(dns_acache_t *acache, dns_acacheentry_t *entry) {
	REQUIRE(DNS_ACACHE_VALID(acache));
	REQUIRE(DNS_ACACHEENTRY_VALID(entry));

	/*
	 * The caller must be holing the entry lock.
	 */

	if (entry->foundname) {
		dns_rdataset_t *rdataset, *rdataset_next;

		for (rdataset = ISC_LIST_HEAD(entry->foundname->list);
		     rdataset != NULL;
		     rdataset = rdataset_next) {
			rdataset_next = ISC_LIST_NEXT(rdataset, link);
			ISC_LIST_UNLINK(entry->foundname->list,
					rdataset, link);
			dns_rdataset_disassociate(rdataset);
			isc_mem_put(acache->mctx, rdataset, sizeof(*rdataset));
		}
		if (dns_name_dynamic(entry->foundname))
			dns_name_free(entry->foundname, acache->mctx);
		isc_mem_put(acache->mctx, entry->foundname,
			    sizeof(*entry->foundname));
		entry->foundname = NULL;
	}

	if (entry->node != NULL) {
		INSIST(entry->db != NULL);
		dns_db_detachnode(entry->db, &entry->node);
	}
	if (entry->version != NULL) {
		INSIST(entry->db != NULL);
		dns_db_closeversion(entry->db, &entry->version, false);
	}
	if (entry->db != NULL)
		dns_db_detach(&entry->db);
	if (entry->zone != NULL)
		dns_zone_detach(&entry->zone);

	if (entry->origdb != NULL)
		dns_db_detach(&entry->origdb);
}

static isc_result_t
acache_cleaner_init(dns_acache_t *acache, isc_timermgr_t *timermgr,
		    acache_cleaner_t *cleaner)
{
	int result;

	ATRACE("acache cleaner init");

	result = isc_mutex_init(&cleaner->lock);
	if (result != ISC_R_SUCCESS)
		goto fail;

	cleaner->increment = DNS_ACACHE_CLEANERINCREMENT;
	cleaner->state = cleaner_s_idle;
	cleaner->acache = acache;
	cleaner->overmem = false;

	cleaner->cleaning_timer = NULL;
	cleaner->resched_event = NULL;
	cleaner->overmem_event = NULL;
	cleaner->current_entry = NULL;

	if (timermgr != NULL) {
		cleaner->acache->live_cleaners++;

		result = isc_task_onshutdown(acache->task,
					     acache_cleaner_shutdown_action,
					     acache);
		if (result != ISC_R_SUCCESS) {
			UNEXPECTED_ERROR(__FILE__, __LINE__,
					 "acache cleaner: "
					 "isc_task_onshutdown() failed: %s",
					 dns_result_totext(result));
			goto cleanup;
		}

		cleaner->cleaning_interval = 0; /* Initially turned off. */
		isc_stdtime_get(&cleaner->last_cleanup_time);
		result = isc_timer_create(timermgr, isc_timertype_inactive,
					  NULL, NULL,
					  acache->task,
					  acache_cleaning_timer_action,
					  cleaner, &cleaner->cleaning_timer);
		if (result != ISC_R_SUCCESS) {
			UNEXPECTED_ERROR(__FILE__, __LINE__,
					 "isc_timer_create() failed: %s",
					 dns_result_totext(result));
			result = ISC_R_UNEXPECTED;
			goto cleanup;
		}

		cleaner->resched_event =
			isc_event_allocate(acache->mctx, cleaner,
					   DNS_EVENT_ACACHECLEAN,
					   acache_incremental_cleaning_action,
					   cleaner, sizeof(isc_event_t));
		if (cleaner->resched_event == NULL) {
			result = ISC_R_NOMEMORY;
			goto cleanup;
		}

		cleaner->overmem_event =
			isc_event_allocate(acache->mctx, cleaner,
					   DNS_EVENT_ACACHEOVERMEM,
					   acache_overmem_cleaning_action,
					   cleaner, sizeof(isc_event_t));
		if (cleaner->overmem_event == NULL) {
			result = ISC_R_NOMEMORY;
			goto cleanup;
		}
	}

	return (ISC_R_SUCCESS);

 cleanup:
	if (cleaner->overmem_event != NULL)
		isc_event_free(&cleaner->overmem_event);
	if (cleaner->resched_event != NULL)
		isc_event_free(&cleaner->resched_event);
	if (cleaner->cleaning_timer != NULL)
		isc_timer_detach(&cleaner->cleaning_timer);
	cleaner->acache->live_cleaners--;
	DESTROYLOCK(&cleaner->lock);
 fail:
	return (result);
}

static void
begin_cleaning(acache_cleaner_t *cleaner) {
	dns_acacheentry_t *head;
	dns_acache_t *acache = cleaner->acache;

	/*
	 * This function does not have to lock the cleaner, since critical
	 * parameters (except current_entry, which is locked by acache lock,)
	 * are only used in a single task context.
	 */

	REQUIRE(CLEANER_IDLE(cleaner));
	INSIST(DNS_ACACHE_VALID(acache));
	INSIST(cleaner->current_entry == NULL);

	isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
		      DNS_LOGMODULE_ACACHE, ISC_LOG_DEBUG(1),
		      "begin acache cleaning, mem inuse %lu",
		      (unsigned long)isc_mem_inuse(cleaner->acache->mctx));

	LOCK(&acache->lock);

	head = ISC_LIST_HEAD(acache->entries);
	if (head != NULL)
		dns_acache_attachentry(head, &cleaner->current_entry);

	UNLOCK(&acache->lock);

	if (cleaner->current_entry != NULL) {
		cleaner->ncleaned = 0;
		cleaner->state = cleaner_s_busy;
		isc_task_send(acache->task, &cleaner->resched_event);
	}

	return;
}

static void
end_cleaning(acache_cleaner_t *cleaner, isc_event_t *event) {
	dns_acache_t *acache = cleaner->acache;

	REQUIRE(CLEANER_BUSY(cleaner));
	REQUIRE(event != NULL);
	REQUIRE(DNS_ACACHEENTRY_VALID(cleaner->current_entry));

	/* No need to lock the cleaner (see begin_cleaning()). */

	LOCK(&acache->lock);

	/*
	 * Even if the cleaner has the last reference to the entry, which means
	 * the entry has been unused, it may still be linked if unlinking the
	 * entry has been delayed due to the reference.
	 */
	if (isc_refcount_current(&cleaner->current_entry->references) == 1) {
		INSIST(cleaner->current_entry->callback == NULL);

		if (ISC_LINK_LINKED(cleaner->current_entry, link)) {
			ISC_LIST_UNLINK(acache->entries,
					cleaner->current_entry, link);
		}
	}
	dns_acache_detachentry(&cleaner->current_entry);

	if (cleaner->overmem)
		acache->stats.overmem++;
	acache->stats.cleaned += cleaner->ncleaned;
	acache->stats.cleaner_runs++;

	isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE, DNS_LOGMODULE_ACACHE,
		      ISC_LOG_NOTICE,
		      "acache %p stats: hits=%d misses=%d queries=%d "
		      "adds=%d deleted=%d "
		      "cleaned=%d cleaner_runs=%d overmem=%d "
		      "overmem_nocreates=%d nomem=%d",
		      acache,
		      acache->stats.hits, acache->stats.misses,
		      acache->stats.queries,
		      acache->stats.adds, acache->stats.deleted,
		      acache->stats.cleaned, acache->stats.cleaner_runs,
		      acache->stats.overmem, acache->stats.overmem_nocreates,
		      acache->stats.nomem);
	reset_stats(acache);

	isc_stdtime_get(&cleaner->last_cleanup_time);

	UNLOCK(&acache->lock);

	dns_acache_setcleaninginterval(cleaner->acache,
				       cleaner->cleaning_interval);

	isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE, DNS_LOGMODULE_ACACHE,
		      ISC_LOG_DEBUG(1), "end acache cleaning, "
		      "%lu entries cleaned, mem inuse %lu",
		      cleaner->ncleaned,
		      (unsigned long)isc_mem_inuse(cleaner->acache->mctx));

	if (cleaner->overmem) {
		isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
			      DNS_LOGMODULE_ACACHE, ISC_LOG_NOTICE,
			      "acache is still in overmem state "
			      "after cleaning");
	}

	cleaner->ncleaned = 0;
	cleaner->state = cleaner_s_idle;
	cleaner->resched_event = event;
}

/*
 * This is run once for every acache-cleaning-interval as defined
 * in named.conf.
 */
static void
acache_cleaning_timer_action(isc_task_t *task, isc_event_t *event) {
	acache_cleaner_t *cleaner = event->ev_arg;

	UNUSED(task);

	INSIST(event->ev_type == ISC_TIMEREVENT_TICK);

	isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE, DNS_LOGMODULE_ACACHE,
		      ISC_LOG_DEBUG(1), "acache cleaning timer fired, "
		      "cleaner state = %d", cleaner->state);

	if (cleaner->state == cleaner_s_idle)
		begin_cleaning(cleaner);

	isc_event_free(&event);
}

/* The caller must hold entry lock. */
static inline bool
entry_stale(acache_cleaner_t *cleaner, dns_acacheentry_t *entry,
	    isc_stdtime_t now32, unsigned int interval)
{
	/*
	 * If the callback has been canceled, we definitely do not need the
	 * entry.
	 */
	if (entry->callback == NULL)
		return (true);

	if (interval > cleaner->cleaning_interval)
		interval = cleaner->cleaning_interval;

	if (entry->lastused + interval < now32)
		return (true);

	/*
	 * If the acache is in the overmem state, probabilistically decide if
	 * the entry should be purged, based on the time passed from its last
	 * use and the cleaning interval.
	 */
	if (cleaner->overmem) {
		unsigned int passed;
		uint32_t val;

		if (isc_serial_ge(now32, entry->lastused))
			passed = now32 - entry->lastused; /* <= interval */
		else
			passed = 0;

		if (passed > interval / 2)
			return (true);
		isc_random_get(&val);
		if (passed > interval / 4)
			return (val % 4 == 0);
		return (val % 8 == 0);
	}

	return (false);
}

/*
 * Do incremental cleaning.
 */
static void
acache_incremental_cleaning_action(isc_task_t *task, isc_event_t *event) {
	acache_cleaner_t *cleaner = event->ev_arg;
	dns_acache_t *acache = cleaner->acache;
	dns_acacheentry_t *entry, *next = NULL;
	int n_entries;
	isc_stdtime_t now32, last32;
	isc_stdtime_t now;
	unsigned int interval;

	INSIST(DNS_ACACHE_VALID(acache));
	INSIST(task == acache->task);
	INSIST(event->ev_type == DNS_EVENT_ACACHECLEAN);

	if (cleaner->state == cleaner_s_done) {
		cleaner->state = cleaner_s_busy;
		end_cleaning(cleaner, event);
		return;
	}

	INSIST(CLEANER_BUSY(cleaner));

	n_entries = cleaner->increment;

	isc_stdtime_get(&now);
	isc_stdtime_convert32(now, &now32);

	LOCK(&acache->lock);

	entry = cleaner->current_entry;
	isc_stdtime_convert32(cleaner->last_cleanup_time, &last32);
	if (isc_serial_ge(now32, last32))
		interval = now32 - last32;
	else
		interval = 0;

	while (n_entries-- > 0) {
		bool is_stale = false;

		INSIST(entry != NULL);

		next = ISC_LIST_NEXT(entry, link);

		ACACHE_LOCK(&acache->entrylocks[entry->locknum],
			    isc_rwlocktype_write);

		is_stale = entry_stale(cleaner, entry, now32, interval);
		if (is_stale) {
			ISC_LIST_UNLINK(acache->entries, entry, link);
			unlink_dbentries(acache, entry);
			if (entry->callback != NULL)
				(entry->callback)(entry, &entry->cbarg);
			entry->callback = NULL;

			cleaner->ncleaned++;
		}

		ACACHE_UNLOCK(&acache->entrylocks[entry->locknum],
			      isc_rwlocktype_write);

		if (is_stale)
			dns_acache_detachentry(&entry);

		if (next == NULL) {
			if (cleaner->overmem) {
				entry = ISC_LIST_HEAD(acache->entries);
				if (entry != NULL) {
					/*
					 * If we are still in the overmem
					 * state, keep cleaning.  In case we
					 * exit from the loop immediately after
					 * this, reset next to the head entry
					 * as we'll expect it will be never
					 * NULL.
					 */
					isc_log_write(dns_lctx,
						      DNS_LOGCATEGORY_DATABASE,
						      DNS_LOGMODULE_ACACHE,
						      ISC_LOG_DEBUG(1),
						      "acache cleaner: "
						      "still overmem, "
						      "reset and try again");
					next = entry;
					continue;
				}
			}

			UNLOCK(&acache->lock);
			end_cleaning(cleaner, event);
			return;
		}

		entry = next;
	}

	/*
	 * We have successfully performed a cleaning increment but have
	 * not gone through the entire cache.  Remember the entry that will
	 * be the starting point in the next clean-up, and reschedule another
	 * batch.  If it fails, just try to continue anyway.
	 */
	INSIST(next != NULL);
	dns_acache_detachentry(&cleaner->current_entry);
	dns_acache_attachentry(next, &cleaner->current_entry);

	UNLOCK(&acache->lock);

	isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE, DNS_LOGMODULE_ACACHE,
		      ISC_LOG_DEBUG(1), "acache cleaner: checked %d entries, "
		      "mem inuse %lu, sleeping", cleaner->increment,
		      (unsigned long)isc_mem_inuse(cleaner->acache->mctx));

	isc_task_send(task, &event);
	INSIST(CLEANER_BUSY(cleaner));

	return;
}

/*
 * This is called when the acache either surpasses its upper limit
 * or shrinks beyond its lower limit.
 */
static void
acache_overmem_cleaning_action(isc_task_t *task, isc_event_t *event) {
	acache_cleaner_t *cleaner = event->ev_arg;
	bool want_cleaning = false;

	UNUSED(task);

	INSIST(event->ev_type == DNS_EVENT_ACACHEOVERMEM);
	INSIST(cleaner->overmem_event == NULL);

	isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE, DNS_LOGMODULE_ACACHE,
		      ISC_LOG_DEBUG(1), "overmem_cleaning_action called, "
		      "overmem = %d, state = %d", cleaner->overmem,
		      cleaner->state);

	LOCK(&cleaner->lock);

	if (cleaner->overmem) {
		if (cleaner->state == cleaner_s_idle)
			want_cleaning = true;
	} else {
		if (cleaner->state == cleaner_s_busy)
			/*
			 * end_cleaning() can't be called here because
			 * then both cleaner->overmem_event and
			 * cleaner->resched_event will point to this
			 * event.  Set the state to done, and then
			 * when the acache_incremental_cleaning_action() event
			 * is posted, it will handle the end_cleaning.
			 */
			cleaner->state = cleaner_s_done;
	}

	cleaner->overmem_event = event;

	UNLOCK(&cleaner->lock);

	if (want_cleaning)
		begin_cleaning(cleaner);
}

static void
water(void *arg, int mark) {
	dns_acache_t *acache = arg;
	bool overmem = (mark == ISC_MEM_HIWATER);

	REQUIRE(DNS_ACACHE_VALID(acache));

	isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
		      DNS_LOGMODULE_ACACHE, ISC_LOG_DEBUG(1),
		      "acache memory reaches %s watermark, mem inuse %lu",
		      overmem ? "high" : "low",
		      (unsigned long)isc_mem_inuse(acache->mctx));

	LOCK(&acache->cleaner.lock);

	if (acache->cleaner.overmem != overmem) {
		acache->cleaner.overmem = overmem;

		if (acache->cleaner.overmem_event != NULL)
			isc_task_send(acache->task,
				      &acache->cleaner.overmem_event);
		isc_mem_waterack(acache->mctx, mark);
	}

	UNLOCK(&acache->cleaner.lock);
}

/*
 * The cleaner task is shutting down; do the necessary cleanup.
 */
static void
acache_cleaner_shutdown_action(isc_task_t *task, isc_event_t *event) {
	dns_acache_t *acache = event->ev_arg;
	bool should_free = false;

	INSIST(task == acache->task);
	INSIST(event->ev_type == ISC_TASKEVENT_SHUTDOWN);
	INSIST(DNS_ACACHE_VALID(acache));

	ATRACE("acache cleaner shutdown");

	if (CLEANER_BUSY(&acache->cleaner))
		end_cleaning(&acache->cleaner, event);
	else
		isc_event_free(&event);

	LOCK(&acache->lock);

	acache->live_cleaners--;
	INSIST(acache->live_cleaners == 0);

	if (isc_refcount_current(&acache->refs) == 0) {
		INSIST(check_noentry(acache) == true);
		should_free = true;
	}

	/*
	 * By detaching the timer in the context of its task,
	 * we are guaranteed that there will be no further timer
	 * events.
	 */
	if (acache->cleaner.cleaning_timer != NULL)
		isc_timer_detach(&acache->cleaner.cleaning_timer);

	/* Make sure we don't reschedule anymore. */
	(void)isc_task_purge(task, NULL, DNS_EVENT_ACACHECLEAN, NULL);

	UNLOCK(&acache->lock);

	if (should_free)
		destroy(acache);
}

/*
 *	Public functions.
 */

isc_result_t
dns_acache_create(dns_acache_t **acachep, isc_mem_t *mctx,
		  isc_taskmgr_t *taskmgr, isc_timermgr_t *timermgr)
{
	int i;
	isc_result_t result;
	dns_acache_t *acache;

	REQUIRE(acachep != NULL && *acachep == NULL);
	REQUIRE(mctx != NULL);
	REQUIRE(taskmgr != NULL);

	acache = isc_mem_get(mctx, sizeof(*acache));
	if (acache == NULL)
		return (ISC_R_NOMEMORY);

	ATRACE("create");

	result = isc_refcount_init(&acache->refs, 1);
	if (result != ISC_R_SUCCESS) {
		isc_mem_put(mctx, acache, sizeof(*acache));
		return (result);
	}

	result = isc_mutex_init(&acache->lock);
	if (result != ISC_R_SUCCESS) {
		isc_refcount_decrement(&acache->refs, NULL);
		isc_refcount_destroy(&acache->refs);
		isc_mem_put(mctx, acache, sizeof(*acache));
		return (result);
	}

	acache->mctx = NULL;
	isc_mem_attach(mctx, &acache->mctx);
	ISC_LIST_INIT(acache->entries);

	acache->shutting_down = false;

	acache->task = NULL;
	acache->entrylocks = NULL;

	result = isc_task_create(taskmgr, 1, &acache->task);
	if (result != ISC_R_SUCCESS) {
		UNEXPECTED_ERROR(__FILE__, __LINE__,
				 "isc_task_create() failed(): %s",
				 dns_result_totext(result));
		result = ISC_R_UNEXPECTED;
		goto cleanup;
	}
	isc_task_setname(acache->task, "acachetask", acache);
	ISC_EVENT_INIT(&acache->cevent, sizeof(acache->cevent), 0, NULL,
		       DNS_EVENT_ACACHECONTROL, shutdown_task, NULL,
		       NULL, NULL, NULL);
	acache->cevent_sent = false;

	acache->dbentries = 0;
	for (i = 0; i < DBBUCKETS; i++)
		ISC_LIST_INIT(acache->dbbucket[i]);

	acache->entrylocks = isc_mem_get(mctx, sizeof(*acache->entrylocks) *
					 DEFAULT_ACACHE_ENTRY_LOCK_COUNT);
	if (acache->entrylocks == NULL) {
		result = ISC_R_NOMEMORY;
		goto cleanup;
	}
	for (i = 0; i < DEFAULT_ACACHE_ENTRY_LOCK_COUNT; i++) {
		result = ACACHE_INITLOCK(&acache->entrylocks[i]);
		if (result != ISC_R_SUCCESS) {
			while (i-- > 0)
				ACACHE_DESTROYLOCK(&acache->entrylocks[i]);
			isc_mem_put(mctx, acache->entrylocks,
				    sizeof(*acache->entrylocks) *
				    DEFAULT_ACACHE_ENTRY_LOCK_COUNT);
			acache->entrylocks = NULL;
			goto cleanup;
		}
	}

	acache->live_cleaners = 0;
	result = acache_cleaner_init(acache, timermgr, &acache->cleaner);
	if (result != ISC_R_SUCCESS)
		goto cleanup;

	acache->stats.cleaner_runs = 0;
	reset_stats(acache);

	acache->magic = ACACHE_MAGIC;

	*acachep = acache;
	return (ISC_R_SUCCESS);

 cleanup:
	if (acache->task != NULL)
		isc_task_detach(&acache->task);
	DESTROYLOCK(&acache->lock);
	isc_refcount_decrement(&acache->refs, NULL);
	isc_refcount_destroy(&acache->refs);
	if (acache->entrylocks != NULL) {
		for (i = 0; i < DEFAULT_ACACHE_ENTRY_LOCK_COUNT; i++)
			ACACHE_DESTROYLOCK(&acache->entrylocks[i]);
		isc_mem_put(mctx, acache->entrylocks,
			    sizeof(*acache->entrylocks) *
			    DEFAULT_ACACHE_ENTRY_LOCK_COUNT);
	}
	isc_mem_put(mctx, acache, sizeof(*acache));
	isc_mem_detach(&mctx);

	return (result);
}

void
dns_acache_attach(dns_acache_t *source, dns_acache_t **targetp) {
	REQUIRE(DNS_ACACHE_VALID(source));
	REQUIRE(targetp != NULL && *targetp == NULL);

	AATRACE(source, "attach");

	isc_refcount_increment(&source->refs, NULL);

	*targetp = source;
}

void
dns_acache_countquerymiss(dns_acache_t *acache) {
	acache->stats.misses++; 	/* XXXSK danger: unlocked! */
	acache->stats.queries++;	/* XXXSK danger: unlocked! */
}

void
dns_acache_detach(dns_acache_t **acachep) {
	dns_acache_t *acache;
	unsigned int refs;
	bool should_free = false;

	REQUIRE(acachep != NULL && DNS_ACACHE_VALID(*acachep));
	acache = *acachep;

	ATRACE("detach");

	isc_refcount_decrement(&acache->refs, &refs);
	if (refs == 0) {
		INSIST(check_noentry(acache) == true);
		should_free = true;
	}

	*acachep = NULL;

	/*
	 * If we're exiting and the cleaner task exists, let it free the cache.
	 */
	if (should_free && acache->live_cleaners > 0) {
		isc_task_shutdown(acache->task);
		should_free = false;
	}

	if (should_free)
		destroy(acache);
}

void
dns_acache_shutdown(dns_acache_t *acache) {
	REQUIRE(DNS_ACACHE_VALID(acache));

	LOCK(&acache->lock);

	ATRACE("shutdown");

	if (!acache->shutting_down) {
		isc_event_t *event;
		dns_acache_t *acache_evarg = NULL;

		INSIST(!acache->cevent_sent);

		acache->shutting_down = true;

		isc_mem_setwater(acache->mctx, NULL, NULL, 0, 0);

		/*
		 * Self attach the object in order to prevent it from being
		 * destroyed while waiting for the event.
		 */
		dns_acache_attach(acache, &acache_evarg);
		event = &acache->cevent;
		event->ev_arg = acache_evarg;
		isc_task_send(acache->task, &event);
		acache->cevent_sent = true;
	}

	UNLOCK(&acache->lock);
}

isc_result_t
dns_acache_setdb(dns_acache_t *acache, dns_db_t *db) {
	int bucket;
	dbentry_t *dbentry;
	isc_result_t result = ISC_R_SUCCESS;

	REQUIRE(DNS_ACACHE_VALID(acache));
	REQUIRE(db != NULL);

	ATRACE("setdb");

	LOCK(&acache->lock);

	dbentry = NULL;
	result = finddbent(acache, db, &dbentry);
	if (result == ISC_R_SUCCESS) {
		result = ISC_R_EXISTS;
		goto end;
	}
	result = ISC_R_SUCCESS;

	dbentry = isc_mem_get(acache->mctx, sizeof(*dbentry));
	if (dbentry == NULL) {
		result = ISC_R_NOMEMORY;
		goto end;
	}

	ISC_LINK_INIT(dbentry, link);
	ISC_LIST_INIT(dbentry->originlist);
	ISC_LIST_INIT(dbentry->referlist);

	dbentry->db = NULL;
	dns_db_attach(db, &dbentry->db);

	bucket = isc_hash_function(&db, sizeof(db), true, NULL) % DBBUCKETS;

	ISC_LIST_APPEND(acache->dbbucket[bucket], dbentry, link);

	acache->dbentries++;

 end:
	UNLOCK(&acache->lock);

	return (result);
}

isc_result_t
dns_acache_putdb(dns_acache_t *acache, dns_db_t *db) {
	int bucket;
	isc_result_t result;
	dbentry_t *dbentry;
	dns_acacheentry_t *entry;

	REQUIRE(DNS_ACACHE_VALID(acache));
	REQUIRE(db != NULL);

	ATRACE("putdb");

	LOCK(&acache->lock);

	dbentry = NULL;
	result = finddbent(acache, db, &dbentry);
	if (result != ISC_R_SUCCESS) {
		/*
		 * The entry may have not been created due to memory shortage.
		 */
		UNLOCK(&acache->lock);
		return (ISC_R_NOTFOUND);
	}

	/*
	 * Release corresponding cache entries: for each entry, release all
	 * links the entry has, and then callback to the entry holder (if any).
	 * If no other external references exist (this can happen if the
	 * original holder has canceled callback,) destroy it here.
	 */
	while ((entry = ISC_LIST_HEAD(dbentry->originlist)) != NULL) {
		ACACHE_LOCK(&acache->entrylocks[entry->locknum],
			    isc_rwlocktype_write);

		/*
		 * Releasing olink first would avoid finddbent() in
		 * unlink_dbentries().
		 */
		ISC_LIST_UNLINK(dbentry->originlist, entry, olink);
		if (acache->cleaner.current_entry != entry)
			ISC_LIST_UNLINK(acache->entries, entry, link);
		unlink_dbentries(acache, entry);

		if (entry->callback != NULL)
			(entry->callback)(entry, &entry->cbarg);
		entry->callback = NULL;

		ACACHE_UNLOCK(&acache->entrylocks[entry->locknum],
			      isc_rwlocktype_write);

		if (acache->cleaner.current_entry != entry)
			dns_acache_detachentry(&entry);
	}
	while ((entry = ISC_LIST_HEAD(dbentry->referlist)) != NULL) {
		ACACHE_LOCK(&acache->entrylocks[entry->locknum],
			    isc_rwlocktype_write);

		ISC_LIST_UNLINK(dbentry->referlist, entry, rlink);
		if (acache->cleaner.current_entry != entry)
			ISC_LIST_UNLINK(acache->entries, entry, link);
		unlink_dbentries(acache, entry);

		if (entry->callback != NULL)
			(entry->callback)(entry, &entry->cbarg);
		entry->callback = NULL;

		ACACHE_UNLOCK(&acache->entrylocks[entry->locknum],
			      isc_rwlocktype_write);

		if (acache->cleaner.current_entry != entry)
			dns_acache_detachentry(&entry);
	}

	INSIST(ISC_LIST_EMPTY(dbentry->originlist) &&
	       ISC_LIST_EMPTY(dbentry->referlist));

	bucket = isc_hash_function(&db, sizeof(db), true, NULL) % DBBUCKETS;

	ISC_LIST_UNLINK(acache->dbbucket[bucket], dbentry, link);
	dns_db_detach(&dbentry->db);

	isc_mem_put(acache->mctx, dbentry, sizeof(*dbentry));

	acache->dbentries--;

	acache->stats.deleted++;

	UNLOCK(&acache->lock);

	return (ISC_R_SUCCESS);
}

isc_result_t
dns_acache_createentry(dns_acache_t *acache, dns_db_t *origdb,
		       void (*callback)(dns_acacheentry_t *, void **),
		       void *cbarg, dns_acacheentry_t **entryp)
{
	dns_acacheentry_t *newentry;
	isc_result_t result;
	uint32_t r;
	isc_stdtime_t tmptime;

	REQUIRE(DNS_ACACHE_VALID(acache));
	REQUIRE(entryp != NULL && *entryp == NULL);
	REQUIRE(origdb != NULL);

	/*
	 * Should we exceed our memory limit for some reason (for
	 * example, if the cleaner does not run aggressively enough),
	 * then we will not create additional entries.
	 *
	 * XXXSK: It might be better to lock the acache->cleaner->lock,
	 * but locking may be an expensive bottleneck. If we misread
	 * the value, we will occasionally refuse to create a few
	 * cache entries, or create a few that we should not. I do not
	 * expect this to happen often, and it will not have very bad
	 * effects when it does. So no lock for now.
	 */
	if (acache->cleaner.overmem) {
		acache->stats.overmem_nocreates++; /* XXXSK danger: unlocked! */
		return (ISC_R_NORESOURCES);
	}

	newentry = isc_mem_get(acache->mctx, sizeof(*newentry));
	if (newentry == NULL) {
		acache->stats.nomem++;  /* XXXMLG danger: unlocked! */
		return (ISC_R_NOMEMORY);
	}

	isc_random_get(&r);
	newentry->locknum = r % DEFAULT_ACACHE_ENTRY_LOCK_COUNT;

	result = isc_refcount_init(&newentry->references, 1);
	if (result != ISC_R_SUCCESS) {
		isc_mem_put(acache->mctx, newentry, sizeof(*newentry));
		return (result);
	};

	ISC_LINK_INIT(newentry, link);
	ISC_LINK_INIT(newentry, olink);
	ISC_LINK_INIT(newentry, rlink);

	newentry->acache = NULL;
	dns_acache_attach(acache, &newentry->acache);

	newentry->zone = NULL;
	newentry->db = NULL;
	newentry->version = NULL;
	newentry->node = NULL;
	newentry->foundname = NULL;

	newentry->callback = callback;
	newentry->cbarg = cbarg;
	newentry->origdb = NULL;
	dns_db_attach(origdb, &newentry->origdb);

	isc_stdtime_get(&tmptime);
	acache_storetime(newentry, tmptime);

	newentry->magic = ACACHEENTRY_MAGIC;

	*entryp = newentry;

	return (ISC_R_SUCCESS);
}

isc_result_t
dns_acache_getentry(dns_acacheentry_t *entry, dns_zone_t **zonep,
		    dns_db_t **dbp, dns_dbversion_t **versionp,
		    dns_dbnode_t **nodep, dns_name_t *fname,
		    dns_message_t *msg, isc_stdtime_t now)
{
	isc_result_t result = ISC_R_SUCCESS;
	dns_rdataset_t *erdataset;
	isc_stdtime_t	now32;
	dns_acache_t *acache;
	int locknum;

	REQUIRE(DNS_ACACHEENTRY_VALID(entry));
	REQUIRE(zonep == NULL || *zonep == NULL);
	REQUIRE(dbp != NULL && *dbp == NULL);
	REQUIRE(versionp != NULL && *versionp == NULL);
	REQUIRE(nodep != NULL && *nodep == NULL);
	REQUIRE(fname != NULL);
	REQUIRE(msg != NULL);
	acache = entry->acache;
	REQUIRE(DNS_ACACHE_VALID(acache));

	locknum = entry->locknum;
	ACACHE_LOCK(&acache->entrylocks[locknum], isc_rwlocktype_read);

	isc_stdtime_convert32(now, &now32);
	acache_storetime(entry, now32);

	if (entry->zone != NULL && zonep != NULL)
		dns_zone_attach(entry->zone, zonep);

	if (entry->db == NULL) {
		*dbp = NULL;
		*versionp = NULL;
	} else {
		dns_db_attach(entry->db, dbp);
		dns_db_attachversion(entry->db, entry->version, versionp);
	}
	if (entry->node == NULL)
		*nodep = NULL;
	else {
		dns_db_attachnode(entry->db, entry->node, nodep);

		INSIST(entry->foundname != NULL);
		dns_name_copy(entry->foundname, fname, NULL);
		for (erdataset = ISC_LIST_HEAD(entry->foundname->list);
		     erdataset != NULL;
		     erdataset = ISC_LIST_NEXT(erdataset, link)) {
			dns_rdataset_t *ardataset;

			ardataset = NULL;
			result = dns_message_gettemprdataset(msg, &ardataset);
			if (result != ISC_R_SUCCESS) {
				ACACHE_UNLOCK(&acache->entrylocks[locknum],
					      isc_rwlocktype_read);
				goto fail;
			}

			/*
			 * XXXJT: if we simply clone the rdataset, we'll get
			 * lost wrt cyclic ordering.  We'll need an additional
			 * trick to get the latest counter from the original
			 * header.
			 */
			dns_rdataset_clone(erdataset, ardataset);
			ISC_LIST_APPEND(fname->list, ardataset, link);
		}
	}

	entry->acache->stats.hits++; /* XXXMLG danger: unlocked! */
	entry->acache->stats.queries++;

	ACACHE_UNLOCK(&acache->entrylocks[locknum], isc_rwlocktype_read);

	return (result);

  fail:
	while ((erdataset = ISC_LIST_HEAD(fname->list)) != NULL) {
		ISC_LIST_UNLINK(fname->list, erdataset, link);
		dns_rdataset_disassociate(erdataset);
		dns_message_puttemprdataset(msg, &erdataset);
	}
	if (*nodep != NULL)
		dns_db_detachnode(*dbp, nodep);
	if (*versionp != NULL)
		dns_db_closeversion(*dbp, versionp, false);
	if (*dbp != NULL)
		dns_db_detach(dbp);
	if (zonep != NULL && *zonep != NULL)
		dns_zone_detach(zonep);

	return (result);
}

isc_result_t
dns_acache_setentry(dns_acache_t *acache, dns_acacheentry_t *entry,
		    dns_zone_t *zone, dns_db_t *db, dns_dbversion_t *version,
		    dns_dbnode_t *node, dns_name_t *fname)
{
	isc_result_t result;
	dbentry_t *odbent;
	dbentry_t *rdbent = NULL;
	bool close_version = false;
	dns_acacheentry_t *dummy_entry = NULL;

	REQUIRE(DNS_ACACHE_VALID(acache));
	REQUIRE(DNS_ACACHEENTRY_VALID(entry));

	LOCK(&acache->lock);	/* XXX: need to lock it here for ordering */
	ACACHE_LOCK(&acache->entrylocks[entry->locknum], isc_rwlocktype_write);

	/* Set zone */
	if (zone != NULL)
		dns_zone_attach(zone, &entry->zone);
	/* Set DB */
	if (db != NULL)
		dns_db_attach(db, &entry->db);
	/*
	 * Set DB version.  If the version is not given by the caller,
	 * which is the case for glue or cache DBs, use the current version.
	 */
	if (version == NULL) {
		if (db != NULL) {
			dns_db_currentversion(db, &version);
			close_version = true;
		}
	}
	if (version != NULL) {
		INSIST(db != NULL);
		dns_db_attachversion(db, version, &entry->version);
	}
	if (close_version)
		dns_db_closeversion(db, &version, false);
	/* Set DB node. */
	if (node != NULL) {
		INSIST(db != NULL);
		dns_db_attachnode(db, node, &entry->node);
	}

	/*
	 * Set list of the corresponding rdatasets, if given.
	 * To minimize the overhead and memory consumption, we'll do this for
	 * positive cache only, in which case the DB node is non NULL.
	 * We do not want to cache incomplete information, so give up the
	 * entire entry when a memory shortage happen during the process.
	 */
	if (node != NULL) {
		dns_rdataset_t *ardataset, *crdataset;

		entry->foundname = isc_mem_get(acache->mctx,
					       sizeof(*entry->foundname));

		if (entry->foundname == NULL) {
			result = ISC_R_NOMEMORY;
			goto fail;
		}
		dns_name_init(entry->foundname, NULL);
		result = dns_name_dup(fname, acache->mctx,
				      entry->foundname);
		if (result != ISC_R_SUCCESS)
			goto fail;

		for (ardataset = ISC_LIST_HEAD(fname->list);
		     ardataset != NULL;
		     ardataset = ISC_LIST_NEXT(ardataset, link)) {
			crdataset = isc_mem_get(acache->mctx,
						sizeof(*crdataset));
			if (crdataset == NULL) {
				result = ISC_R_NOMEMORY;
				goto fail;
			}

			dns_rdataset_init(crdataset);
			dns_rdataset_clone(ardataset, crdataset);
			ISC_LIST_APPEND(entry->foundname->list, crdataset,
					link);
		}
	}

	odbent = NULL;
	result = finddbent(acache, entry->origdb, &odbent);
	if (result != ISC_R_SUCCESS)
		goto fail;
	if (db != NULL) {
		rdbent = NULL;
		result = finddbent(acache, db, &rdbent);
		if (result != ISC_R_SUCCESS)
			goto fail;
	}

	ISC_LIST_APPEND(acache->entries, entry, link);
	ISC_LIST_APPEND(odbent->originlist, entry, olink);
	if (rdbent != NULL)
		ISC_LIST_APPEND(rdbent->referlist, entry, rlink);

	/*
	 * The additional cache needs an implicit reference to entries in its
	 * link.
	 */
	dns_acache_attachentry(entry, &dummy_entry);

	ACACHE_UNLOCK(&acache->entrylocks[entry->locknum],
		      isc_rwlocktype_write);

	acache->stats.adds++;
	UNLOCK(&acache->lock);

	return (ISC_R_SUCCESS);

 fail:
	clear_entry(acache, entry);

	ACACHE_UNLOCK(&acache->entrylocks[entry->locknum],
		      isc_rwlocktype_write);
	UNLOCK(&acache->lock);

	return (result);
}

bool
dns_acache_cancelentry(dns_acacheentry_t *entry) {
	dns_acache_t *acache;
	bool callback_active;

	REQUIRE(DNS_ACACHEENTRY_VALID(entry));

	acache = entry->acache;

	INSIST(DNS_ACACHE_VALID(entry->acache));

	LOCK(&acache->lock);
	ACACHE_LOCK(&acache->entrylocks[entry->locknum], isc_rwlocktype_write);

	callback_active = (entry->cbarg != NULL);

	/*
	 * Release dependencies stored in this entry as much as possible.
	 * The main link cannot be released, since the acache object has
	 * a reference to this entry; the empty entry will be released in
	 * the next cleaning action.
	 */
	unlink_dbentries(acache, entry);
	clear_entry(entry->acache, entry);

	entry->callback = NULL;
	entry->cbarg = NULL;

	ACACHE_UNLOCK(&acache->entrylocks[entry->locknum],
		      isc_rwlocktype_write);
	UNLOCK(&acache->lock);

	return (callback_active);
}

void
dns_acache_attachentry(dns_acacheentry_t *source,
		       dns_acacheentry_t **targetp)
{
	REQUIRE(DNS_ACACHEENTRY_VALID(source));
	REQUIRE(targetp != NULL && *targetp == NULL);

	isc_refcount_increment(&source->references, NULL);

	*targetp = source;
}

void
dns_acache_detachentry(dns_acacheentry_t **entryp) {
	dns_acacheentry_t *entry;
	unsigned int refs;

	REQUIRE(entryp != NULL && DNS_ACACHEENTRY_VALID(*entryp));
	entry = *entryp;

	isc_refcount_decrement(&entry->references, &refs);

	/*
	 * If there are no references to the entry, the entry must have been
	 * unlinked and can be destroyed safely.
	 */
	if (refs == 0) {
		INSIST(!ISC_LINK_LINKED(entry, link));
		(*entryp)->acache->stats.deleted++;
		destroy_entry(entry);
	}

	*entryp = NULL;
}

void
dns_acache_setcleaninginterval(dns_acache_t *acache, unsigned int t) {
	isc_interval_t interval;
	isc_result_t result;

	REQUIRE(DNS_ACACHE_VALID(acache));

	ATRACE("dns_acache_setcleaninginterval");

	LOCK(&acache->lock);

	/*
	 * It may be the case that the acache has already shut down.
	 * If so, it has no timer.  (Not sure if this can really happen.)
	 */
	if (acache->cleaner.cleaning_timer == NULL)
		goto unlock;

	acache->cleaner.cleaning_interval = t;

	if (t == 0) {
		result = isc_timer_reset(acache->cleaner.cleaning_timer,
					 isc_timertype_inactive,
					 NULL, NULL, true);
	} else {
		isc_interval_set(&interval, acache->cleaner.cleaning_interval,
				 0);
		result = isc_timer_reset(acache->cleaner.cleaning_timer,
					 isc_timertype_ticker,
					 NULL, &interval, false);
	}
	if (result != ISC_R_SUCCESS)
		isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
			      DNS_LOGMODULE_ACACHE, ISC_LOG_WARNING,
			      "could not set acache cleaning interval: %s",
			      isc_result_totext(result));
	else
		isc_log_write(dns_lctx, DNS_LOGCATEGORY_DATABASE,
			      DNS_LOGMODULE_ACACHE, ISC_LOG_NOTICE,
			      "acache %p cleaning interval set to %d.",
			      acache, t);

 unlock:
	UNLOCK(&acache->lock);
}

/*
 * This function was derived from cache.c:dns_cache_setcachesize().  See the
 * function for more details about the logic.
 */
void
dns_acache_setcachesize(dns_acache_t *acache, size_t size) {
	size_t hiwater, lowater;

	REQUIRE(DNS_ACACHE_VALID(acache));

	if (size != 0U && size < DNS_ACACHE_MINSIZE)
		size = DNS_ACACHE_MINSIZE;

	hiwater = size - (size >> 3);
	lowater = size - (size >> 2);

	if (size == 0U || hiwater == 0U || lowater == 0U)
		isc_mem_setwater(acache->mctx, water, acache, 0, 0);
	else
		isc_mem_setwater(acache->mctx, water, acache,
				 hiwater, lowater);
}
