/*-------------------------------------------------------------------------
 *
 * pg_stat_logmsg.c
 *		Track log messages across a whole database cluster.
 *
 * Starting code cribbed from pg_stat_statements and modified to suit
 *
 * Copyright (c) 2023, PostgreSQL Global Development Group
 *
 * IDENTIFICATION
 *	  contrib/pg_stat_logmsg/pg_stat_logmsg.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include <math.h>
#include <sys/stat.h>
#include <unistd.h>

#include "access/parallel.h"
#include "access/xact.h"
#include "catalog/pg_authid.h"
#include "common/hashfn.h"
#include "funcapi.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "storage/fd.h"
#include "storage/ipc.h"
#include "storage/lwlock.h"
#include "storage/shmem.h"
#include "storage/spin.h"
#include "tcop/utility.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/memutils.h"
#include "utils/timestamp.h"

PG_MODULE_MAGIC;

/* Location of permanent stats file (valid when database is shut down) */
#define PGLM_DUMP_FILE	PGSTAT_STAT_PERMANENT_DIRECTORY "/pg_stat_logmsg.stat"

/*
 * Location of external logmsg text file.
 */
#define PGLM_TEXT_FILE	PG_STAT_TMP_DIR "/pglm_logmsg_texts.stat"

/* Magic number identifying the stats file format */
static const uint32 PGLM_FILE_HEADER = 0x20230625;

/* PostgreSQL major version number, changes in which invalidate all entries */
static const uint32 PGLM_PG_MAJOR_VERSION = PG_VERSION_NUM / 100;

#define ASSUMED_MEDIAN_INIT		(10.0)	/* initial assumed median usage */
#define ASSUMED_LENGTH_INIT		1024	/* initial assumed mean query length */
#define USAGE_DECREASE_FACTOR	(0.99)	/* decreased every entry_dealloc */
#define USAGE_DEALLOC_PERCENT	5		/* free this % of entries at once */
#define USAGE_INIT				(1.0)	/* including initial planning */
#define MAXFILENAMELEN			64
#define MAXFUNCNAMELEN			64

/*
 * Hashtable key that defines the identity of a hashtable entry.
 *
 * If you add a new key to this struct, make sure to teach pglm_store() to
 * zero the padding bytes.  Otherwise, things will break, because pglm_hash is
 * created using HASH_BLOBS, and thus tag_hash is used to hash this.
 */
typedef struct pglmHashKey
{
	char	filename[MAXFILENAMELEN];	/* filename */
	int		lineno;						/* line number */
	int		elevel;						/* log level */
} pglmHashKey;

/*
 * The actual stats counters kept within pglmEntry.
 */
typedef struct Counters
{
	int64	msgcnt;		/* # of times planned/executed */
	double	usage;		/* usage factor */
} Counters;

/*
 * Global statistics for pg_stat_logmsg
 */
typedef struct pglmGlobalStats
{
	int64		dealloc;		/* # of times entries were deallocated */
	TimestampTz stats_reset;	/* timestamp with all stats reset */
} pglmGlobalStats;

/*
 * Statistics per statement
 *
 * Note: in event of a failure in garbage collection of the logmsg text file,
 * we reset logmsg_offset to zero and logmsg_len to -1.  This will be seen as
 * an invalid state by logmsg_fetch().
 */
typedef struct pglmEntry
{
	pglmHashKey key;						/* hash key of entry - MUST BE FIRST */
	Counters	counters;					/* the statistics for this logmsg */
	Size		logmsg_offset;				/* logmsg text offset in external file */
	int			logmsg_len;					/* # of valid bytes in logmsg string, or -1 */
	int			encoding;					/* logmsg text encoding */
	int			sqlerrcode;					/* encoded ERRSTATE */
	char		funcname[MAXFUNCNAMELEN];	/* edata function name */
	slock_t		mutex;						/* protects the counters only */
} pglmEntry;

/*
 * Global shared state
 */
typedef struct pglmSharedState
{
	LWLock	   *lock;			/* protects hashtable search/modification */
	double		cur_median_usage;	/* current median usage in hashtable */
	Size		mean_logmsg_len; /* current mean entry text length */
	slock_t		mutex;			/* protects following fields only: */
	Size		extent;			/* current extent of query file */
	int			n_writers;		/* number of active writers to query file */
	int			gc_count;		/* query file garbage collection cycle count */
	pglmGlobalStats stats;		/* global statistics for pglm */
} pglmSharedState;

/*---- Local variables ----*/

/* anti recursion */
static bool is_in_log_hook = false;

/* Saved hook values in case of unload */
static shmem_request_hook_type prev_shmem_request_hook = NULL;
static shmem_startup_hook_type prev_shmem_startup_hook = NULL;
static emit_log_hook_type prev_emit_log_hook = NULL;

/* Links to shared memory state */
static pglmSharedState *pglm = NULL;
static HTAB *pglm_hash = NULL;

/*---- GUC variables ----*/
static int	pglm_max = 5000;	/* max # statements to track */

#define record_gc_msgfmts() \
	do { \
		volatile pglmSharedState *s = (volatile pglmSharedState *) pglm; \
		SpinLockAcquire(&s->mutex); \
		s->gc_count++; \
		SpinLockRelease(&s->mutex); \
	} while(0)

/*---- Function declarations ----*/
PG_FUNCTION_INFO_V1(pg_stat_logmsg_reset);
PG_FUNCTION_INFO_V1(pg_stat_logmsg);
PG_FUNCTION_INFO_V1(pg_stat_logmsg_info);

static void pglm_shmem_request(void);
static void pglm_shmem_startup(void);
static void pglm_shmem_shutdown(int code, Datum arg);
static void pglm_log_hook(ErrorData *edata);
static void pglm_store(ErrorData *edata);
static void pg_stat_logmsg_internal(FunctionCallInfo fcinfo);
static Size pglm_memsize(void);
static pglmEntry *entry_alloc(pglmHashKey *key, Size logmsg_offset,
							  int logmsg_len, int encoding,
							  int sqlerrcode, char *funcname);
static void entry_dealloc(void);
static bool logmsg_store(ErrorData *edata, Size *logmsg_offset,
						 int *logmsg_len, int *gc_count);
static char *logmsg_load_file(Size *buffer_size);
static char *logmsg_fetch(Size logmsg_offset, int logmsg_len,
						 char *buffer, Size buffer_size);
static bool need_gc_logmsgs(void);
static void gc_logmsgs(void);
static void entry_reset(char *filename, int lineno, int elevel);
static void logmsg_xact_callback(XactEvent event, void *arg);

/*
 * Module load callback
 */
void
_PG_init(void)
{
	/*
	 * In order to create our shared memory area, we have to be loaded via
	 * shared_preload_libraries.  If not, fall out without hooking into any of
	 * the main system.  (We don't throw error here because it seems useful to
	 * allow the pg_stat_logmsg functions to be created even when the
	 * module isn't active.  The functions must protect themselves against
	 * being called then, however.)
	 */
	if (!process_shared_preload_libraries_in_progress)
		return;

	/*
	 * Define (or redefine) custom GUC variables.
	 */
	DefineCustomIntVariable("pg_stat_logmsg.max",
							"Sets the maximum number of messages tracked by pg_stat_logmsg.",
							NULL,
							&pglm_max,
							5000,
							100,
							INT_MAX / 2,
							PGC_POSTMASTER,
							0,
							NULL,
							NULL,
							NULL);

	MarkGUCPrefixReserved("pg_stat_logmsg");

	/*
	 * Install hooks.
	 */
	prev_shmem_request_hook = shmem_request_hook;
	shmem_request_hook = pglm_shmem_request;

	prev_shmem_startup_hook = shmem_startup_hook;
	shmem_startup_hook = pglm_shmem_startup;

	prev_emit_log_hook = emit_log_hook;
	emit_log_hook = pglm_log_hook;

	/* Arrange to restore state in case of ERROR */
	RegisterXactCallback(logmsg_xact_callback, NULL);
}

/*
 * shmem_request hook: request additional shared resources.  We'll allocate or
 * attach to the shared resources in pglm_shmem_startup().
 */
static void
pglm_shmem_request(void)
{
	if (prev_shmem_request_hook)
		prev_shmem_request_hook();

	RequestAddinShmemSpace(pglm_memsize());
	RequestNamedLWLockTranche("pg_stat_logmsg", 1);
}

/*
 * shmem_startup hook: allocate or attach to shared memory,
 * then load any pre-existing statistics from file.
 * Also create and load the query-texts file, which is expected to exist
 * (even if empty) while the module is enabled.
 */
static void
pglm_shmem_startup(void)
{
	bool		found;
	HASHCTL		info;
	FILE	   *file = NULL;
	FILE	   *lmfile = NULL;
	uint32		header;
	int32		num;
	int32		pgver;
	int32		i;
	int			buffer_size;
	char	   *buffer = NULL;

	if (prev_shmem_startup_hook)
		prev_shmem_startup_hook();

	/* reset in case this is a restart within the postmaster */
	pglm = NULL;
	pglm_hash = NULL;

	/*
	 * Create or attach to the shared memory state, including hash table
	 */
	LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);

	pglm = ShmemInitStruct("pg_stat_logmsg",
						   sizeof(pglmSharedState),
						   &found);

	if (!found)
	{
		/* First time through ... */
		pglm->lock = &(GetNamedLWLockTranche("pg_stat_logmsg"))->lock;
		pglm->cur_median_usage = ASSUMED_MEDIAN_INIT;
		pglm->mean_logmsg_len = ASSUMED_LENGTH_INIT;
		SpinLockInit(&pglm->mutex);
		pglm->extent = 0;
		pglm->n_writers = 0;
		pglm->gc_count = 0;
		pglm->stats.dealloc = 0;
		pglm->stats.stats_reset = GetCurrentTimestamp();
	}

	info.keysize = sizeof(pglmHashKey);
	info.entrysize = sizeof(pglmEntry);
	pglm_hash = ShmemInitHash("pg_stat_logmsg hash",
							  pglm_max, pglm_max,
							  &info,
							  HASH_ELEM | HASH_BLOBS);

	LWLockRelease(AddinShmemInitLock);

	/*
	 * If we're in the postmaster (or a standalone backend...), set up a shmem
	 * exit hook to dump the statistics to disk.
	 */
	if (!IsUnderPostmaster)
		on_shmem_exit(pglm_shmem_shutdown, (Datum) 0);

	/*
	 * Done if some other process already completed our initialization.
	 */
	if (found)
		return;

	/*
	 * Note: we don't bother with locks here, because there should be no other
	 * processes running when this code is reached.
	 */

	/* Unlink logmsg text file possibly left over from crash */
	unlink(PGLM_TEXT_FILE);

	/* Allocate new logmsg text temp file */
	lmfile = AllocateFile(PGLM_TEXT_FILE, PG_BINARY_W);
	if (lmfile == NULL)
		goto write_error;

	/*
	 * Attempt to load old statistics from the dump file.
	 */
	file = AllocateFile(PGLM_DUMP_FILE, PG_BINARY_R);
	if (file == NULL)
	{
		if (errno != ENOENT)
			goto read_error;
		/* No existing persisted stats file, so we're done */
		FreeFile(lmfile);
		return;
	}

	buffer_size = 2048;
	buffer = (char *) palloc(buffer_size);

	if (fread(&header, sizeof(uint32), 1, file) != 1 ||
		fread(&pgver, sizeof(uint32), 1, file) != 1 ||
		fread(&num, sizeof(int32), 1, file) != 1)
		goto read_error;

	if (header != PGLM_FILE_HEADER ||
		pgver != PGLM_PG_MAJOR_VERSION)
		goto data_error;

	for (i = 0; i < num; i++)
	{
		pglmEntry	temp;
		pglmEntry  *entry;
		Size		logmsg_offset;

		if (fread(&temp, sizeof(pglmEntry), 1, file) != 1)
			goto read_error;

		/* Encoding is the only field we can easily sanity-check */
		if (!PG_VALID_BE_ENCODING(temp.encoding))
			goto data_error;

		/* Resize buffer as needed */
		if (temp.logmsg_len >= buffer_size)
		{
			buffer_size = Max(buffer_size * 2, temp.logmsg_len + 1);
			buffer = repalloc(buffer, buffer_size);
		}

		if (fread(buffer, 1, temp.logmsg_len + 1, file) != temp.logmsg_len + 1)
			goto read_error;

		/* Should have a trailing null, but let's make sure */
		buffer[temp.logmsg_len] = '\0';

		/* Store the logmsg text */
		logmsg_offset = pglm->extent;
		if (fwrite(buffer, 1, temp.logmsg_len + 1, lmfile) != temp.logmsg_len + 1)
			goto write_error;
		pglm->extent += temp.logmsg_len + 1;

		/* make the hashtable entry (discards old entries if too many) */
		entry = entry_alloc(&temp.key, logmsg_offset, temp.logmsg_len,
							temp.encoding, temp.sqlerrcode, temp.funcname);

		/* copy in the actual stats */
		entry->counters = temp.counters;
	}

	/* Read global statistics for pg_stat_logmsg */
	if (fread(&pglm->stats, sizeof(pglmGlobalStats), 1, file) != 1)
		goto read_error;

	pfree(buffer);
	FreeFile(file);
	FreeFile(lmfile);

	/*
	 * Remove the persisted stats file so it's not included in
	 * backups/replication standbys, etc.  A new file will be written on next
	 * shutdown.
	 *
	 * Note: it's okay if the PGLM_TEXT_FILE is included in a basebackup,
	 * because we remove that file on startup; it acts inversely to
	 * PGLM_DUMP_FILE, in that it is only supposed to be around when the
	 * server is running, whereas PGLM_DUMP_FILE is only supposed to be around
	 * when the server is not running.  Leaving the file creates no danger of
	 * a newly restored database having a spurious record of execution costs,
	 * which is what we're really concerned about here.
	 */
	unlink(PGLM_DUMP_FILE);

	return;

read_error:
	ereport(LOG,
			(errcode_for_file_access(),
			 errmsg("could not read file \"%s\": %m",
					PGLM_DUMP_FILE)));
	goto fail;
data_error:
	ereport(LOG,
			(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
			 errmsg("ignoring invalid data in file \"%s\"",
					PGLM_DUMP_FILE)));
	goto fail;
write_error:
	ereport(LOG,
			(errcode_for_file_access(),
			 errmsg("could not write file \"%s\": %m",
					PGLM_TEXT_FILE)));
fail:
	if (buffer)
		pfree(buffer);
	if (file)
		FreeFile(file);
	if (lmfile)
		FreeFile(lmfile);
	/* If possible, throw away the bogus file; ignore any error */
	unlink(PGLM_DUMP_FILE);

	/*
	 * Don't unlink PGLM_TEXT_FILE here; it should always be around while the
	 * server is running with pg_stat_logmsg enabled
	 */
}

/*
 * shmem_shutdown hook: Dump statistics into file.
 *
 * Note: we don't bother with acquiring lock, because there should be no
 * other processes running when this is called.
 */
static void
pglm_shmem_shutdown(int code, Datum arg)
{
	FILE	   *file;
	char	   *mlbuffer = NULL;
	Size		mlbuffer_size = 0;
	HASH_SEQ_STATUS hash_seq;
	int32		num_entries;
	pglmEntry  *entry;

	/* Don't try to dump during a crash. */
	if (code)
		return;

	/* Safety check ... shouldn't get here unless shmem is set up. */
	if (!pglm || !pglm_hash)
		return;

	file = AllocateFile(PGLM_DUMP_FILE ".tmp", PG_BINARY_W);
	if (file == NULL)
		goto error;

	if (fwrite(&PGLM_FILE_HEADER, sizeof(uint32), 1, file) != 1)
		goto error;
	if (fwrite(&PGLM_PG_MAJOR_VERSION, sizeof(uint32), 1, file) != 1)
		goto error;
	num_entries = hash_get_num_entries(pglm_hash);
	if (fwrite(&num_entries, sizeof(int32), 1, file) != 1)
		goto error;

	mlbuffer = logmsg_load_file(&mlbuffer_size);
	if (mlbuffer == NULL)
		goto error;

	/*
	 * When serializing to disk, we store logmsg texts immediately after their
	 * entry data.  Any orphaned logmsg texts are thereby excluded.
	 */
	hash_seq_init(&hash_seq, pglm_hash);
	while ((entry = hash_seq_search(&hash_seq)) != NULL)
	{
		int			len = entry->logmsg_len;
		char	   *qstr = logmsg_fetch(entry->logmsg_offset, len,
									   mlbuffer, mlbuffer_size);

		if (qstr == NULL)
			continue;			/* Ignore any entries with bogus texts */

		if (fwrite(entry, sizeof(pglmEntry), 1, file) != 1 ||
			fwrite(qstr, 1, len + 1, file) != len + 1)
		{
			/* note: we assume hash_seq_term won't change errno */
			hash_seq_term(&hash_seq);
			goto error;
		}
	}

	/* Dump global statistics for pg_stat_logmsg */
	if (fwrite(&pglm->stats, sizeof(pglmGlobalStats), 1, file) != 1)
		goto error;

	free(mlbuffer);
	mlbuffer = NULL;

	if (FreeFile(file))
	{
		file = NULL;
		goto error;
	}

	/*
	 * Rename file into place, so we atomically replace any old one.
	 */
	(void) durable_rename(PGLM_DUMP_FILE ".tmp", PGLM_DUMP_FILE, LOG);

	/* Unlink logmsg-texts file; it's not needed while shutdown */
	unlink(PGLM_TEXT_FILE);

	return;

error:
	ereport(LOG,
			(errcode_for_file_access(),
			 errmsg("could not write file \"%s\": %m",
					PGLM_DUMP_FILE ".tmp")));
	free(mlbuffer);
	if (file)
		FreeFile(file);
	unlink(PGLM_DUMP_FILE ".tmp");
	unlink(PGLM_TEXT_FILE);
}

/*
 * log hook: store results if needed
 */
static void
pglm_log_hook(ErrorData *edata)
{
	if (!is_in_log_hook)
	{
		is_in_log_hook = true;
		pglm_store(edata);
		is_in_log_hook = false;
	}

	if (prev_emit_log_hook)
		prev_emit_log_hook(edata);
}

/*
 * Store statistics for a log message.
 */
static void
pglm_store(ErrorData *edata)
{
	pglmHashKey key;
	pglmEntry  *entry;
	int			encoding = GetDatabaseEncoding();
	size_t		filename_len;

	Assert(edata != NULL);

	/* Safety check... */
	if (!pglm || !pglm_hash)
		return;

	/* Set up key for hashtable search */

	/* memset() is required when pglmHashKey is without padding only */
	memset(&key, 0, sizeof(pglmHashKey));

	/* make persistent copies of strings */
	if (edata->filename)
	{
		filename_len = strlen(edata->filename);
		memcpy(key.filename, edata->filename,
			   filename_len < MAXFILENAMELEN ?
							  filename_len : MAXFILENAMELEN );
	}

	key.lineno = edata->lineno;
	key.elevel = edata->elevel;

	/* Lookup the hash table entry with shared lock. */
	LWLockAcquire(pglm->lock, LW_SHARED);

	entry = (pglmEntry *) hash_search(pglm_hash, &key, HASH_FIND, NULL);

	/* Create new entry, if not present */
	if (!entry)
	{
		Size		logmsg_offset;
		int			logmsg_len; 
		int			gc_count;
		bool		stored;
		bool		do_gc;

		/* Append new logmsg text to file with only shared lock held */
		stored = logmsg_store(edata, &logmsg_offset, &logmsg_len, &gc_count);

		/*
		 * Determine whether we need to garbage collect external logmsg texts
		 * while the shared lock is still held.  This micro-optimization
		 * avoids taking the time to decide this while holding exclusive lock.
		 */
		do_gc = need_gc_logmsgs();

		/* Need exclusive lock to make a new hashtable entry - promote */
		LWLockRelease(pglm->lock);
		LWLockAcquire(pglm->lock, LW_EXCLUSIVE);

		/*
		 * A garbage collection may have occurred while we weren't holding the
		 * lock.  In the unlikely event that this happens, the logmsg text we
		 * stored above will have been garbage collected, so write it again.
		 * This should be infrequent enough that doing it while holding
		 * exclusive lock isn't a performance problem.
		 */
		if (!stored || pglm->gc_count != gc_count)
			stored = logmsg_store(edata, &logmsg_offset, &logmsg_len, NULL);

		/* If we failed to write to the text file, give up */
		if (!stored)
			goto done;

		/* OK to create a new hashtable entry */
		entry = entry_alloc(&key, logmsg_offset, logmsg_len,
							encoding, edata->sqlerrcode,
							(char *) edata->funcname);

		/* If needed, perform garbage collection while exclusive lock held */
		if (do_gc)
			gc_logmsgs();
	}

	/*
	 * Grab the spinlock while updating the counters (see comment about
	 * locking rules at the head of the file)
	 */
	{
		volatile pglmEntry *e = (volatile pglmEntry *) entry;

		SpinLockAcquire(&e->mutex);
		e->counters.msgcnt += 1;
		SpinLockRelease(&e->mutex);
	}

done:
	LWLockRelease(pglm->lock);
}

/*
 * Reset statement statistics.
 */
Datum
pg_stat_logmsg_reset(PG_FUNCTION_ARGS)
{
	entry_reset(NULL, 0, 0);

	PG_RETURN_VOID();
}

/* Number of output arguments (columns) */
#define PG_STAT_LOGMSG_COLS			7

Datum
pg_stat_logmsg(PG_FUNCTION_ARGS)
{
	pg_stat_logmsg_internal(fcinfo);

	return (Datum) 0;
}

/* Common code for all versions of pg_stat_statements() */
static void
pg_stat_logmsg_internal(FunctionCallInfo fcinfo)
{
	ReturnSetInfo  *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
	Oid				userid = GetUserId();
	char		   *mlbuffer = NULL;
	Size			mlbuffer_size = 0;
	Size			extent = 0;
	int				gc_count = 0;
	HASH_SEQ_STATUS hash_seq;
	pglmEntry	   *entry;
	int				n_writers;

	/*
	 * Superusers or roles with the privileges of pg_read_all_stats members
	 * are allowed
	 */
	if (!has_privs_of_role(userid, ROLE_PG_READ_ALL_STATS))
		ereport(ERROR,
				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
				 errmsg("must be member of %s role",
				 GetUserNameFromId(ROLE_PG_READ_ALL_STATS, false))));

	/* hash table must exist already */
	if (!pglm || !pglm_hash)
		ereport(ERROR,
				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
				 errmsg("pg_stat_logmsg must be loaded via shared_preload_libraries")));

	InitMaterializedSRF(fcinfo, 0);

	/* Check we have the expected number of output arguments. */
	if (rsinfo->setDesc->natts != PG_STAT_LOGMSG_COLS)
		elog(ERROR, "incorrect number of output arguments");

	/*
	 * We'd like to load the logmsg text file (if needed) while not holding any
	 * lock on pglm->lock.  In the worst case we'll have to do this again
	 * after we have the lock, but it's unlikely enough to make this a win
	 * despite occasional duplicated work.  We need to reload if anybody
	 * writes to the file (either a retail logmsg_store(), or a garbage
	 * collection) between this point and where we've gotten shared lock.  If
	 * a logmsg_store is actually in progress when we look, we might as well
	 * skip the speculative load entirely.
	 */

	/* Take the mutex so we can examine variables */
	{
		volatile pglmSharedState *s = (volatile pglmSharedState *) pglm;

		SpinLockAcquire(&s->mutex);
		extent = s->extent;
		n_writers = s->n_writers;
		gc_count = s->gc_count;
		SpinLockRelease(&s->mutex);
	}

	/* No point in loading file now if there are active writers */
	if (n_writers == 0)
		mlbuffer = logmsg_load_file(&mlbuffer_size);

	/*
	 * Get shared lock, load or reload the logmsg text file if we must, and
	 * iterate over the hashtable entries.
	 *
	 * With a large hash table, we might be holding the lock rather longer
	 * than one could wish.  However, this only blocks creation of new hash
	 * table entries, and the larger the hash table the less likely that is to
	 * be needed.  So we can hope this is okay.  Perhaps someday we'll decide
	 * we need to partition the hash table to limit the time spent holding any
	 * one lock.
	 */
	LWLockAcquire(pglm->lock, LW_SHARED);

	/*
	 * Here it is safe to examine extent and gc_count without taking the
	 * mutex.  Note that although other processes might change
	 * pglm->extent just after we look at it, the strings they then write
	 * into the file cannot yet be referenced in the hashtable, so we
	 * don't care whether we see them or not.
	 *
	 * If logmsg_load_file fails, we just press on; we'll return NULL for
	 * every logmsg text.
	 */
	if (mlbuffer == NULL ||
		pglm->extent != extent ||
		pglm->gc_count != gc_count)
	{
		free(mlbuffer);
		mlbuffer = logmsg_load_file(&mlbuffer_size);
	}

	hash_seq_init(&hash_seq, pglm_hash);
	while ((entry = hash_seq_search(&hash_seq)) != NULL)
	{
		Datum		values[PG_STAT_LOGMSG_COLS];
		bool		nulls[PG_STAT_LOGMSG_COLS];
		int			i = 0;
		Counters	tmp;
		char	   *mstr;

		memset(values, 0, sizeof(values));
		memset(nulls, 0, sizeof(nulls));

		values[i++] = CStringGetTextDatum(entry->key.filename);
		values[i++] = Int32GetDatum(entry->key.lineno);
		values[i++] = CStringGetTextDatum(_(error_severity(entry->key.elevel)));

		if (entry->funcname[0])
			values[i++] = CStringGetTextDatum(entry->funcname);
		else
			nulls[i++] = true;

		values[i++] = CStringGetTextDatum(unpack_sql_state(entry->sqlerrcode));

		mstr = logmsg_fetch(entry->logmsg_offset,
							entry->logmsg_len,
							mlbuffer,
							mlbuffer_size);

		if (mstr)
		{
			char	   *enc;

			enc = pg_any_to_server(mstr,
								   entry->logmsg_len,
								   entry->encoding);
			values[i++] = CStringGetTextDatum(_(enc));

			if (enc != mstr)
				pfree(enc);
		}
		else
		{
			/* Just return a null if we fail to find the text */
			nulls[i++] = true;
		}

		/* copy counters to a local variable to keep locking time short */
		{
			volatile pglmEntry *e = (volatile pglmEntry *) entry;

			SpinLockAcquire(&e->mutex);
			tmp = e->counters;
			SpinLockRelease(&e->mutex);
		}

		values[i++] = Int64GetDatumFast(tmp.msgcnt);

		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
	}

	LWLockRelease(pglm->lock);

	free(mlbuffer);
}

/* Number of output arguments (columns) for pg_stat_logmsg_info */
#define PG_STAT_LOGMSG_INFO_COLS	2

/*
 * Return statistics of pg_stat_logmsg.
 */
Datum
pg_stat_logmsg_info(PG_FUNCTION_ARGS)
{
	pglmGlobalStats stats;
	TupleDesc	tupdesc;
	Datum		values[PG_STAT_LOGMSG_INFO_COLS] = {0};
	bool		nulls[PG_STAT_LOGMSG_INFO_COLS] = {0};

	if (!pglm || !pglm_hash)
		ereport(ERROR,
				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
				 errmsg("pg_stat_logmsg must be loaded via shared_preload_libraries")));

	/* Build a tuple descriptor for our result type */
	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
		elog(ERROR, "return type must be a row type");

	/* Read global statistics for pg_stat_logmsg */
	{
		volatile pglmSharedState *s = (volatile pglmSharedState *) pglm;

		SpinLockAcquire(&s->mutex);
		stats = s->stats;
		SpinLockRelease(&s->mutex);
	}

	values[0] = Int64GetDatum(stats.dealloc);
	values[1] = TimestampTzGetDatum(stats.stats_reset);

	PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
}

/*
 * Estimate shared memory space needed.
 */
static Size
pglm_memsize(void)
{
	Size		size;

	size = MAXALIGN(sizeof(pglmSharedState));
	size = add_size(size, hash_estimate_size(pglm_max, sizeof(pglmEntry)));

	return size;
}

/*
 * Allocate a new hashtable entry.
 * caller must hold an exclusive lock on pglm->lock
 *
 * Note: despite needing exclusive lock, it's not an error for the target
 * entry to already exist.  This is because pglm_store releases and
 * reacquires lock after failing to find a match; so someone else could
 * have made the entry while we waited to get exclusive lock.
 */
static pglmEntry *
entry_alloc(pglmHashKey *key,
			Size logmsg_offset,
			int logmsg_len,
			int encoding,
			int sqlerrcode,
			char *efuncname)
{
	pglmEntry  *entry;
	bool		found;

	/* Make space if needed */
	while (hash_get_num_entries(pglm_hash) >= pglm_max)
		entry_dealloc();

	/* Find or create an entry with desired hash code */
	entry = (pglmEntry *) hash_search(pglm_hash, key, HASH_ENTER, &found);

	if (!found)
	{
		/* New entry, initialize it */

		/* reset the statistics */
		memset(&entry->counters, 0, sizeof(Counters));
		/* set the appropriate initial usage count */
		entry->counters.usage = USAGE_INIT;
		/* re-initialize the mutex each time ... we assume no one using it */
		SpinLockInit(&entry->mutex);
		/* ... and don't forget the logmsg text metadata */
		Assert(logmsg_len >= 0);
		entry->logmsg_offset = logmsg_offset;
		entry->logmsg_len = logmsg_len;
		entry->encoding = encoding;
		entry->sqlerrcode = sqlerrcode;

		if (efuncname)
		{
			size_t		funcname_len = strlen(efuncname);

			memcpy(entry->funcname, efuncname,
				   funcname_len < MAXFUNCNAMELEN ?
								  funcname_len : MAXFUNCNAMELEN );
		}
	}

	return entry;
}

/*
 * qsort comparator for sorting into increasing usage order
 */
static int
entry_cmp(const void *lhs, const void *rhs)
{
	double		l_usage = (*(pglmEntry *const *) lhs)->counters.usage;
	double		r_usage = (*(pglmEntry *const *) rhs)->counters.usage;

	if (l_usage < r_usage)
		return -1;
	else if (l_usage > r_usage)
		return +1;
	else
		return 0;
}

/*
 * Deallocate least-used entries.
 *
 * Caller must hold an exclusive lock on pglm->lock.
 */
static void
entry_dealloc(void)
{
	HASH_SEQ_STATUS hash_seq;
	pglmEntry **entries;
	pglmEntry  *entry;
	int			nvictims;
	int			i;
	Size		tottextlen;
	int			nvalidtexts;

	/*
	 * Sort entries by usage and deallocate USAGE_DEALLOC_PERCENT of them.
	 * While we're scanning the table, apply the decay factor to the usage
	 * values, and update the mean query length.
	 *
	 * Note that the mean query length is almost immediately obsolete, since
	 * we compute it before not after discarding the least-used entries.
	 * Hopefully, that doesn't affect the mean too much; it doesn't seem worth
	 * making two passes to get a more current result.  Likewise, the new
	 * cur_median_usage includes the entries we're about to zap.
	 */

	entries = palloc(hash_get_num_entries(pglm_hash) * sizeof(pglmEntry *));

	i = 0;
	tottextlen = 0;
	nvalidtexts = 0;

	hash_seq_init(&hash_seq, pglm_hash);
	while ((entry = hash_seq_search(&hash_seq)) != NULL)
	{
		entries[i++] = entry;
		entry->counters.usage *= USAGE_DECREASE_FACTOR;
		/* In the mean length computation, ignore dropped texts. */
		if (entry->logmsg_len >= 0)
		{
			tottextlen += entry->logmsg_len + 1;
			nvalidtexts++;
		}
	}

	/* Sort into increasing order by usage */
	qsort(entries, i, sizeof(pglmEntry *), entry_cmp);

	/* Record the (approximate) median usage */
	if (i > 0)
		pglm->cur_median_usage = entries[i / 2]->counters.usage;
	/* Record the mean query length */
	if (nvalidtexts > 0)
		pglm->mean_logmsg_len = tottextlen / nvalidtexts;
	else
		pglm->mean_logmsg_len = ASSUMED_LENGTH_INIT;

	/* Now zap an appropriate fraction of lowest-usage entries */
	nvictims = Max(10, i * USAGE_DEALLOC_PERCENT / 100);
	nvictims = Min(nvictims, i);

	for (i = 0; i < nvictims; i++)
		hash_search(pglm_hash, &entries[i]->key, HASH_REMOVE, NULL);

	pfree(entries);

	/* Increment the number of times entries are deallocated */
	{
		volatile pglmSharedState *s = (volatile pglmSharedState *) pglm;

		SpinLockAcquire(&s->mutex);
		s->stats.dealloc += 1;
		SpinLockRelease(&s->mutex);
	}
}

/*
 * Given a logmsg string (not necessarily null-terminated), allocate a new
 * entry in the external logmsg text file and store the string there.
 *
 * If successful, returns true, and stores the new entry's offset in the file
 * into *logmsg_offset.  Also, if gc_count isn't NULL, *gc_count is set to the
 * number of garbage collections that have occurred so far.
 *
 * On failure, returns false.
 *
 * At least a shared lock on pglm->lock must be held by the caller, so as
 * to prevent a concurrent garbage collection.  Share-lock-holding callers
 * should pass a gc_count pointer to obtain the number of garbage collections,
 * so that they can recheck the count after obtaining exclusive lock to
 * detect whether a garbage collection occurred (and removed this entry).
 */
static bool
logmsg_store(ErrorData *edata, Size *logmsg_offset,
			 int *logmsg_len, int *gc_count)
{
	Size		off;
	int			fd;

	if (edata->message_id)
		*logmsg_len = strlen(edata->message_id);

	/*
	 * We use a spinlock to protect extent/n_writers/gc_count, so that
	 * multiple processes may execute this function concurrently.
	 */
	{
		volatile pglmSharedState *s = (volatile pglmSharedState *) pglm;

		SpinLockAcquire(&s->mutex);
		off = s->extent;
		s->extent += *logmsg_len + 1;
		s->n_writers++;
		if (gc_count)
			*gc_count = s->gc_count;
		SpinLockRelease(&s->mutex);
	}

	*logmsg_offset = off;

	/*
	 * Don't allow the file to grow larger than what logmsg_load_file can
	 * (theoretically) handle.  This has been seen to be reachable on 32-bit
	 * platforms.
	 */
	if (unlikely(*logmsg_len >= MaxAllocHugeSize - off))
	{
		errno = EFBIG;			/* not quite right, but it'll do */
		fd = -1;
		goto error;
	}

	/* Now write the data into the successfully-reserved part of the file */
	fd = OpenTransientFile(PGLM_TEXT_FILE, O_RDWR | O_CREAT | PG_BINARY);
	if (fd < 0)
		goto error;

	if (pg_pwrite(fd, edata->message_id, *logmsg_len, off) != *logmsg_len)
		goto error;
	if (pg_pwrite(fd, "\0", 1, off + *logmsg_len) != 1)
		goto error;

	CloseTransientFile(fd);

	/* Mark our write complete */
	{
		volatile pglmSharedState *s = (volatile pglmSharedState *) pglm;

		SpinLockAcquire(&s->mutex);
		s->n_writers--;
		SpinLockRelease(&s->mutex);
	}

	return true;

error:
	ereport(LOG,
			(errcode_for_file_access(),
			 errmsg("could not write file \"%s\": %m",
					PGLM_TEXT_FILE)));

	if (fd >= 0)
		CloseTransientFile(fd);

	/* Mark our write complete */
	{
		volatile pglmSharedState *s = (volatile pglmSharedState *) pglm;

		SpinLockAcquire(&s->mutex);
		s->n_writers--;
		SpinLockRelease(&s->mutex);
	}

	return false;
}

/*
 * Read the external logmsg text file into a malloc'd buffer.
 *
 * Returns NULL (without throwing an error) if unable to read, eg
 * file not there or insufficient memory.
 *
 * On success, the buffer size is also returned into *buffer_size.
 *
 * This can be called without any lock on pglm->lock, but in that case
 * the caller is responsible for verifying that the result is sane.
 */
static char *
logmsg_load_file(Size *buffer_size)
{
	char	   *buf;
	int			fd;
	struct stat stat;
	Size		nread;

	fd = OpenTransientFile(PGLM_TEXT_FILE, O_RDONLY | PG_BINARY);
	if (fd < 0)
	{
		if (errno != ENOENT)
			ereport(LOG,
					(errcode_for_file_access(),
					 errmsg("could not read file \"%s\": %m",
							PGLM_TEXT_FILE)));
		return NULL;
	}

	/* Get file length */
	if (fstat(fd, &stat))
	{
		ereport(LOG,
				(errcode_for_file_access(),
				 errmsg("could not stat file \"%s\": %m",
						PGLM_TEXT_FILE)));
		CloseTransientFile(fd);
		return NULL;
	}

	/* Allocate buffer; beware that off_t might be wider than size_t */
	if (stat.st_size <= MaxAllocHugeSize)
		buf = (char *) malloc(stat.st_size);
	else
		buf = NULL;

	if (buf == NULL)
	{
		ereport(LOG,
				(errcode(ERRCODE_OUT_OF_MEMORY),
				 errmsg("out of memory"),
				 errdetail("Could not allocate enough memory to read file \"%s\".",
						   PGLM_TEXT_FILE)));
		CloseTransientFile(fd);
		return NULL;
	}

	/*
	 * OK, slurp in the file.  Windows fails if we try to read more than
	 * INT_MAX bytes at once, and other platforms might not like that either,
	 * so read a very large file in 1GB segments.
	 */
	nread = 0;
	while (nread < stat.st_size)
	{
		int			toread = Min(1024 * 1024 * 1024, stat.st_size - nread);

		/*
		 * If we get a short read and errno doesn't get set, the reason is
		 * probably that garbage collection truncated the file since we did
		 * the fstat(), so we don't log a complaint --- but we don't return
		 * the data, either, since it's most likely corrupt due to concurrent
		 * writes from garbage collection.
		 */
		errno = 0;
		if (read(fd, buf + nread, toread) != toread)
		{
			if (errno)
				ereport(LOG,
						(errcode_for_file_access(),
						 errmsg("could not read file \"%s\": %m",
								PGLM_TEXT_FILE)));
			free(buf);
			CloseTransientFile(fd);
			return NULL;
		}
		nread += toread;
	}

	if (CloseTransientFile(fd) != 0)
		ereport(LOG,
				(errcode_for_file_access(),
				 errmsg("could not close file \"%s\": %m", PGLM_TEXT_FILE)));

	*buffer_size = nread;
	return buf;
}

/*
 * Locate a logmsg text in the file image previously read by logmsg_load_file().
 *
 * We validate the given offset/length, and return NULL if bogus.  Otherwise,
 * the result points to a null-terminated string within the buffer.
 */
static char *
logmsg_fetch(Size logmsg_offset, int logmsg_len,
			char *buffer, Size buffer_size)
{
	/* File read failed? */
	if (buffer == NULL)
		return NULL;
	/* Bogus offset/length? */
	if (logmsg_len < 0 ||
		logmsg_offset + logmsg_len >= buffer_size)
		return NULL;
	/* As a further sanity check, make sure there's a trailing null */
	if (buffer[logmsg_offset + logmsg_len] != '\0')
		return NULL;
	/* Looks OK */
	return buffer + logmsg_offset;
}

/*
 * Do we need to garbage-collect the external logmsg text file?
 *
 * Caller should hold at least a shared lock on pglm->lock.
 */
static bool
need_gc_logmsgs(void)
{
	Size		extent;

	/* Read shared extent pointer */
	{
		volatile pglmSharedState *s = (volatile pglmSharedState *) pglm;

		SpinLockAcquire(&s->mutex);
		extent = s->extent;
		SpinLockRelease(&s->mutex);
	}

	/*
	 * Don't proceed if file does not exceed 512 bytes per possible entry.
	 *
	 * Here and in the next test, 32-bit machines have overflow hazards if
	 * pglm_max and/or mean_logmsg_len are large.  Force the multiplications
	 * and comparisons to be done in uint64 arithmetic to forestall trouble.
	 */
	if ((uint64) extent < (uint64) 512 * pglm_max)
		return false;

	/*
	 * Don't proceed if file is less than about 50% bloat.  Nothing can or
	 * should be done in the event of unusually large logmsg texts accounting
	 * for file's large size.  We go to the trouble of maintaining the mean
	 * query length in order to prevent garbage collection from thrashing
	 * uselessly.
	 */
	if ((uint64) extent < (uint64) pglm->mean_logmsg_len * pglm_max * 2)
		return false;

	return true;
}

/*
 * Garbage-collect orphaned logmsg texts in external file.
 *
 * This won't be called often in the typical case, since it's likely that
 * there won't be too much churn, and besides, a similar compaction process
 * occurs when serializing to disk at shutdown or as part of resetting.
 * Despite this, it seems prudent to plan for the edge case where the file
 * becomes unreasonably large, with no other method of compaction likely to
 * occur in the foreseeable future.
 *
 * The caller must hold an exclusive lock on pglm->lock.
 *
 * At the first sign of trouble we unlink the logmsg text file to get a clean
 * slate (although existing statistics are retained), rather than risk
 * thrashing by allowing the same problem case to recur indefinitely.
 */
static void
gc_logmsgs(void)
{
	char	   *mlbuffer;
	Size		mlbuffer_size;
	FILE	   *lmfile = NULL;
	HASH_SEQ_STATUS hash_seq;
	pglmEntry  *entry;
	Size		extent;
	int			nentries;

	/*
	 * When called from pglm_store, some other session might have proceeded
	 * with garbage collection in the no-lock-held interim of lock strength
	 * escalation.  Check once more that this is actually necessary.
	 */
	if (!need_gc_logmsgs())
		return;

	/*
	 * Load the old texts file.  If we fail (out of memory, for instance),
	 * invalidate logmsg texts.  Hopefully this is rare.  It might seem better
	 * to leave things alone on an OOM failure, but the problem is that the
	 * file is only going to get bigger; hoping for a future non-OOM result is
	 * risky and can easily lead to complete denial of service.
	 */
	mlbuffer = logmsg_load_file(&mlbuffer_size);
	if (mlbuffer == NULL)
		goto gc_fail;

	/*
	 * We overwrite the logmsg texts file in place, so as to reduce the risk of
	 * an out-of-disk-space failure.  Since the file is guaranteed not to get
	 * larger, this should always work on traditional filesystems; though we
	 * could still lose on copy-on-write filesystems.
	 */
	lmfile = AllocateFile(PGLM_TEXT_FILE, PG_BINARY_W);
	if (lmfile == NULL)
	{
		ereport(LOG,
				(errcode_for_file_access(),
				 errmsg("could not write file \"%s\": %m",
						PGLM_TEXT_FILE)));
		goto gc_fail;
	}

	extent = 0;
	nentries = 0;

	hash_seq_init(&hash_seq, pglm_hash);
	while ((entry = hash_seq_search(&hash_seq)) != NULL)
	{
		int			logmsg_len = entry->logmsg_len;
		char	   *msg = logmsg_fetch(entry->logmsg_offset,
									   logmsg_len,
									   mlbuffer,
									   mlbuffer_size);

		if (msg == NULL)
		{
			/* Trouble ... drop the text */
			entry->logmsg_offset = 0;
			entry->logmsg_len = -1;
			/* entry will not be counted in mean logmsg length computation */
			continue;
		}

		if (fwrite(msg, 1, logmsg_len + 1, lmfile) != logmsg_len + 1)
		{
			ereport(LOG,
					(errcode_for_file_access(),
					 errmsg("could not write file \"%s\": %m",
							PGLM_TEXT_FILE)));
			hash_seq_term(&hash_seq);
			goto gc_fail;
		}

		entry->logmsg_offset = extent;
		extent += logmsg_len + 1;
		nentries++;
	}

	/*
	 * Truncate away any now-unused space.  If this fails for some odd reason,
	 * we log it, but there's no need to fail.
	 */
	if (ftruncate(fileno(lmfile), extent) != 0)
		ereport(LOG,
				(errcode_for_file_access(),
				 errmsg("could not truncate file \"%s\": %m",
						PGLM_TEXT_FILE)));

	if (FreeFile(lmfile))
	{
		ereport(LOG,
				(errcode_for_file_access(),
				 errmsg("could not write file \"%s\": %m",
						PGLM_TEXT_FILE)));
		lmfile = NULL;
		goto gc_fail;
	}

	elog(DEBUG1, "pglm gc of queries file shrunk size from %zu to %zu",
		 pglm->extent, extent);

	/* Reset the shared extent pointer */
	pglm->extent = extent;

	/*
	 * Also update the mean logmsg length, to be sure that need_gc_logmsgs()
	 * won't still think we have a problem.
	 */
	if (nentries > 0)
		pglm->mean_logmsg_len = extent / nentries;
	else
		pglm->mean_logmsg_len = ASSUMED_LENGTH_INIT;

	free(mlbuffer);

	/*
	 * OK, count a garbage collection cycle.  (Note: even though we have
	 * exclusive lock on pglm->lock, we must take pglm->mutex for this, since
	 * other processes may examine gc_count while holding only the mutex.
	 * Also, we have to advance the count *after* we've rewritten the file,
	 * else other processes might not realize they read a stale file.)
	 */
	record_gc_msgfmts();

	return;

gc_fail:
	/* clean up resources */
	if (lmfile)
		FreeFile(lmfile);
	free(mlbuffer);

	/*
	 * Since the contents of the external file are now uncertain, mark all
	 * hashtable entries as having invalid texts.
	 */
	hash_seq_init(&hash_seq, pglm_hash);
	while ((entry = hash_seq_search(&hash_seq)) != NULL)
	{
		entry->logmsg_offset = 0;
		entry->logmsg_len = -1;
	}

	/*
	 * Destroy the logmsg text file and create a new, empty one
	 */
	(void) unlink(PGLM_TEXT_FILE);
	lmfile = AllocateFile(PGLM_TEXT_FILE, PG_BINARY_W);
	if (lmfile == NULL)
		ereport(LOG,
				(errcode_for_file_access(),
				 errmsg("could not recreate file \"%s\": %m",
						PGLM_TEXT_FILE)));
	else
		FreeFile(lmfile);

	/* Reset the shared extent pointer */
	pglm->extent = 0;

	/* Reset mean_logmsg_len to match the new state */
	pglm->mean_logmsg_len = ASSUMED_LENGTH_INIT;

	/*
	 * Bump the GC count even though we failed.
	 *
	 * This is needed to make concurrent readers of file without any lock on
	 * pglm->lock notice existence of new version of file.  Once readers
	 * subsequently observe a change in GC count with pglm->lock held, that
	 * forces a safe reopen of file.  Writers also require that we bump here,
	 * of course.  (As required by locking protocol, readers and writers don't
	 * trust earlier file contents until gc_count is found unchanged after
	 * pglm->lock acquired in shared or exclusive mode respectively.)
	 */
	record_gc_msgfmts();
}

/*
 * Release entries corresponding to parameters passed.
 */
static void
entry_reset(char *filename, int lineno, int elevel)
{
	HASH_SEQ_STATUS hash_seq;
	pglmEntry  *entry;
	FILE	   *lmfile;
	long		num_entries;
	long		num_remove = 0;
	pglmHashKey key;

	if (!pglm || !pglm_hash)
		ereport(ERROR,
				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
				 errmsg("pg_stat_logmsg must be loaded via shared_preload_libraries")));

	LWLockAcquire(pglm->lock, LW_EXCLUSIVE);
	num_entries = hash_get_num_entries(pglm_hash);

	if (filename != NULL && lineno != 0 && elevel != 0)
	{
		/* If all the parameters are available, use the fast path. */
		memset(&key, 0, sizeof(pglmHashKey));

		if (filename)
		{
			size_t		filename_len = strlen(filename);

			memcpy(key.filename, filename,
				   filename_len < MAXFILENAMELEN ?
								  filename_len : MAXFILENAMELEN );
		}
		else
			memset(key.filename, '\0', MAXFILENAMELEN);

		key.lineno = lineno;
		key.elevel = elevel;

		/* Remove the key if exists */
		entry = (pglmEntry *) hash_search(pglm_hash, &key, HASH_REMOVE, NULL);
		if (entry)				/* found */
			num_remove++;
	}
	else if (filename != NULL || lineno != 0 || elevel != 0)
	{
		/* Remove entries corresponding to valid parameters. */
		hash_seq_init(&hash_seq, pglm_hash);
		while ((entry = hash_seq_search(&hash_seq)) != NULL)
		{
			if ((!filename || entry->key.filename == filename) &&
				(!lineno || entry->key.lineno == lineno) &&
				(!elevel || entry->key.elevel == elevel))
			{
				hash_search(pglm_hash, &entry->key, HASH_REMOVE, NULL);
				num_remove++;
			}
		}
	}
	else
	{
		/* Remove all entries. */
		hash_seq_init(&hash_seq, pglm_hash);
		while ((entry = hash_seq_search(&hash_seq)) != NULL)
		{
			hash_search(pglm_hash, &entry->key, HASH_REMOVE, NULL);
			num_remove++;
		}
	}

	/* All entries are removed? */
	if (num_entries != num_remove)
		goto release_lock;

	/*
	 * Reset global statistics for pg_stat_logmsg since all entries are
	 * removed.
	 */
	{
		volatile pglmSharedState *s = (volatile pglmSharedState *) pglm;
		TimestampTz stats_reset = GetCurrentTimestamp();

		SpinLockAcquire(&s->mutex);
		s->stats.dealloc = 0;
		s->stats.stats_reset = stats_reset;
		SpinLockRelease(&s->mutex);
	}

	/*
	 * Write new empty logmsg file, perhaps even creating a new one to recover
	 * if the file was missing.
	 */
	lmfile = AllocateFile(PGLM_TEXT_FILE, PG_BINARY_W);
	if (lmfile == NULL)
	{
		ereport(LOG,
				(errcode_for_file_access(),
				 errmsg("could not create file \"%s\": %m",
						PGLM_TEXT_FILE)));
		goto done;
	}

	/* If ftruncate fails, log it, but it's not a fatal problem */
	if (ftruncate(fileno(lmfile), 0) != 0)
		ereport(LOG,
				(errcode_for_file_access(),
				 errmsg("could not truncate file \"%s\": %m",
						PGLM_TEXT_FILE)));

	FreeFile(lmfile);

done:
	pglm->extent = 0;
	/* This counts as a logmsg text garbage collection for our purposes */
	record_gc_msgfmts();

release_lock:
	LWLockRelease(pglm->lock);
}

/*
 * logmsg_xact_callback --- cleanup at main-transaction end.
 */
static void
logmsg_xact_callback(XactEvent event, void *arg)
{
	is_in_log_hook = false;
}
