/*-------------------------------------------------------------------------
 *
 * sharedfileset.c
 *	  Shared temporary file management.
 *
 * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * IDENTIFICATION
 *	  src/backend/storage/file/sharedfileset.c
 *
 * SharedFileSets provide a temporary namespace (think directory) so that
 * files can be discovered by name, and a shared ownership semantics so that
 * shared files survive until the last user detaches.
 *
 * SharedFileSets can be used by backends when the temporary files need to be
 * opened/closed multiple times and the underlying files need to survive across
 * transactions.
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include <limits.h>

#include "catalog/pg_tablespace.h"
#include "commands/tablespace.h"
#include "common/hashfn.h"
#include "miscadmin.h"
#include "storage/dsm.h"
#include "storage/ipc.h"
#include "storage/sharedfileset.h"
#include "utils/builtins.h"

static List *filesetlist = NIL;

static void SharedFileSetOnDetach(dsm_segment *segment, Datum datum);
static void SharedFileSetDeleteOnProcExit(int status, Datum arg);
static void SharedFileSetPath(char *path, SharedFileSet *fileset, Oid tablespace);
static void SharedFilePath(char *path, SharedFileSet *fileset, const char *name);
static Oid	ChooseTablespace(const SharedFileSet *fileset, const char *name);

/*
 * Initialize a space for temporary files that can be opened by other backends.
 * Other backends must attach to it before accessing it.  Associate this
 * SharedFileSet with 'seg'.  Any contained files will be deleted when the
 * last backend detaches.
 *
 * We can also use this interface if the temporary files are used only by
 * single backend but the files need to be opened and closed multiple times
 * and also the underlying files need to survive across transactions.  For
 * such cases, dsm segment 'seg' should be passed as NULL.  Callers are
 * expected to explicitly remove such files by using SharedFileSetDelete/
 * SharedFileSetDeleteAll or we remove such files on proc exit.
 *
 * Files will be distributed over the tablespaces configured in
 * temp_tablespaces.
 *
 * Under the covers the set is one or more directories which will eventually
 * be deleted.
 */
void
SharedFileSetInit(SharedFileSet *fileset, dsm_segment *seg)
{
	static uint32 counter = 0;

	SpinLockInit(&fileset->mutex);
	fileset->refcnt = 1;
	fileset->creator_pid = MyProcPid;
	fileset->number = counter;
	counter = (counter + 1) % INT_MAX;

	/* Capture the tablespace OIDs so that all backends agree on them. */
	PrepareTempTablespaces();
	fileset->ntablespaces =
		GetTempTablespaces(&fileset->tablespaces[0],
						   lengthof(fileset->tablespaces));
	if (fileset->ntablespaces == 0)
	{
		/* If the GUC is empty, use current database's default tablespace */
		fileset->tablespaces[0] = MyDatabaseTableSpace;
		fileset->ntablespaces = 1;
	}
	else
	{
		int			i;

		/*
		 * An entry of InvalidOid means use the default tablespace for the
		 * current database.  Replace that now, to be sure that all users of
		 * the SharedFileSet agree on what to do.
		 */
		for (i = 0; i < fileset->ntablespaces; i++)
		{
			if (fileset->tablespaces[i] == InvalidOid)
				fileset->tablespaces[i] = MyDatabaseTableSpace;
		}
	}

	/* Register our cleanup callback. */
	if (seg)
		on_dsm_detach(seg, SharedFileSetOnDetach, PointerGetDatum(fileset));
	else
	{
		static bool registered_cleanup = false;

		if (!registered_cleanup)
		{
			/*
			 * We must not have registered any fileset before registering the
			 * fileset clean up.
			 */
			Assert(filesetlist == NIL);
			on_proc_exit(SharedFileSetDeleteOnProcExit, 0);
			registered_cleanup = true;
		}

		filesetlist = lcons((void *) fileset, filesetlist);
	}
}

/*
 * Attach to a set of directories that was created with SharedFileSetInit.
 */
void
SharedFileSetAttach(SharedFileSet *fileset, dsm_segment *seg)
{
	bool		success;

	SpinLockAcquire(&fileset->mutex);
	if (fileset->refcnt == 0)
		success = false;
	else
	{
		++fileset->refcnt;
		success = true;
	}
	SpinLockRelease(&fileset->mutex);

	if (!success)
		ereport(ERROR,
				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
				 errmsg("could not attach to a SharedFileSet that is already destroyed")));

	/* Register our cleanup callback. */
	on_dsm_detach(seg, SharedFileSetOnDetach, PointerGetDatum(fileset));
}

/*
 * Create a new file in the given set.
 */
File
SharedFileSetCreate(SharedFileSet *fileset, const char *name)
{
	char		path[MAXPGPATH];
	File		file;

	SharedFilePath(path, fileset, name);
	file = PathNameCreateTemporaryFile(path, false);

	/* If we failed, see if we need to create the directory on demand. */
	if (file <= 0)
	{
		char		tempdirpath[MAXPGPATH];
		char		filesetpath[MAXPGPATH];
		Oid			tablespace = ChooseTablespace(fileset, name);

		TempTablespacePath(tempdirpath, tablespace);
		SharedFileSetPath(filesetpath, fileset, tablespace);
		PathNameCreateTemporaryDir(tempdirpath, filesetpath);
		file = PathNameCreateTemporaryFile(path, true);
	}

	return file;
}

/*
 * Open a file that was created with SharedFileSetCreate(), possibly in
 * another backend.
 */
File
SharedFileSetOpen(SharedFileSet *fileset, const char *name, int mode)
{
	char		path[MAXPGPATH];
	File		file;

	SharedFilePath(path, fileset, name);
	file = PathNameOpenTemporaryFile(path, mode);

	return file;
}

/*
 * Delete a file that was created with SharedFileSetCreate().
 * Return true if the file existed, false if didn't.
 */
bool
SharedFileSetDelete(SharedFileSet *fileset, const char *name,
					bool error_on_failure)
{
	char		path[MAXPGPATH];

	SharedFilePath(path, fileset, name);

	return PathNameDeleteTemporaryFile(path, error_on_failure);
}

/*
 * Delete all files in the set.
 */
void
SharedFileSetDeleteAll(SharedFileSet *fileset)
{
	char		dirpath[MAXPGPATH];
	int			i;

	/*
	 * Delete the directory we created in each tablespace.  Doesn't fail
	 * because we use this in error cleanup paths, but can generate LOG
	 * message on IO error.
	 */
	for (i = 0; i < fileset->ntablespaces; ++i)
	{
		SharedFileSetPath(dirpath, fileset, fileset->tablespaces[i]);
		PathNameDeleteTemporaryDir(dirpath);
	}

	/* Unregister the shared fileset */
	SharedFileSetUnregister(fileset);
}

/*
 * Callback function that will be invoked when this backend detaches from a
 * DSM segment holding a SharedFileSet that it has created or attached to.  If
 * we are the last to detach, then try to remove the directories and
 * everything in them.  We can't raise an error on failures, because this runs
 * in error cleanup paths.
 */
static void
SharedFileSetOnDetach(dsm_segment *segment, Datum datum)
{
	bool		unlink_all = false;
	SharedFileSet *fileset = (SharedFileSet *) DatumGetPointer(datum);

	SpinLockAcquire(&fileset->mutex);
	Assert(fileset->refcnt > 0);
	if (--fileset->refcnt == 0)
		unlink_all = true;
	SpinLockRelease(&fileset->mutex);

	/*
	 * If we are the last to detach, we delete the directory in all
	 * tablespaces.  Note that we are still actually attached for the rest of
	 * this function so we can safely access its data.
	 */
	if (unlink_all)
		SharedFileSetDeleteAll(fileset);
}

/*
 * Callback function that will be invoked on the process exit.  This will
 * process the list of all the registered sharedfilesets and delete the
 * underlying files.
 */
static void
SharedFileSetDeleteOnProcExit(int status, Datum arg)
{
	/*
	 * Remove all the pending shared fileset entries. We don't use foreach() here
	 * because SharedFileSetDeleteAll will remove the current element in
	 * filesetlist. Though we have used foreach_delete_current() to remove the
	 * element from filesetlist it could only fix up the state of one of the
	 * loops, see SharedFileSetUnregister.
	 */
	while (list_length(filesetlist) > 0)
	{
		SharedFileSet *fileset = (SharedFileSet *) linitial(filesetlist);

		SharedFileSetDeleteAll(fileset);
	}

	filesetlist = NIL;
}

/*
 * Unregister the shared fileset entry registered for cleanup on proc exit.
 */
void
SharedFileSetUnregister(SharedFileSet *input_fileset)
{
	ListCell   *l;

	/*
	 * If the caller is following the dsm based cleanup then we don't maintain
	 * the filesetlist so return.
	 */
	if (filesetlist == NIL)
		return;

	foreach(l, filesetlist)
	{
		SharedFileSet *fileset = (SharedFileSet *) lfirst(l);

		/* Remove the entry from the list */
		if (input_fileset == fileset)
		{
			filesetlist = foreach_delete_current(filesetlist, l);
			return;
		}
	}

	/* Should have found a match */
	Assert(false);
}

/*
 * Build the path for the directory holding the files backing a SharedFileSet
 * in a given tablespace.
 */
static void
SharedFileSetPath(char *path, SharedFileSet *fileset, Oid tablespace)
{
	char		tempdirpath[MAXPGPATH];

	TempTablespacePath(tempdirpath, tablespace);
	snprintf(path, MAXPGPATH, "%s/%s%lu.%u.sharedfileset",
			 tempdirpath, PG_TEMP_FILE_PREFIX,
			 (unsigned long) fileset->creator_pid, fileset->number);
}

/*
 * Sorting hat to determine which tablespace a given shared temporary file
 * belongs in.
 */
static Oid
ChooseTablespace(const SharedFileSet *fileset, const char *name)
{
	uint32		hash = hash_any((const unsigned char *) name, strlen(name));

	return fileset->tablespaces[hash % fileset->ntablespaces];
}

/*
 * Compute the full path of a file in a SharedFileSet.
 */
static void
SharedFilePath(char *path, SharedFileSet *fileset, const char *name)
{
	char		dirpath[MAXPGPATH];

	SharedFileSetPath(dirpath, fileset, ChooseTablespace(fileset, name));
	snprintf(path, MAXPGPATH, "%s/%s", dirpath, name);
}
