/****************************************************************
 *								*
 * Copyright (c) 2001-2020 Fidelity National Information	*
 * Services, Inc. and/or its subsidiaries. All rights reserved.	*
 *								*
 *	This source code contains the intellectual property	*
 *	of its copyright holder(s), and is made available	*
 *	under a license.  If you do not know the terms of	*
 *	the license, please stop and do not read further.	*
 *								*
 ****************************************************************/

/* ------------------------------------------------------------------
 * 	Routines & data for managing TP timeouts
 *	----------------------------------------
 *
 * These functions implement TP timeout state transitions. The states
 * below are defined by three flag variables:
 *
 *              		| Flag:
 *    State     		|  timed    expired	set-xfer
 *    --------  		|  -------  -------	--------
 *     Clear    		|  FALSE    FALSE	FALSE
 *     Set-timer      		|  TRUE     FALSE	FALSE
 *     Expired		 	|  TRUE     TRUE	FALSE
 *     Clearing-no-set-xfer 	|  FALSE    TRUE	FALSE
 *     Expired-set-xfer		|  TRUE     TRUE	TRUE
 *     Clearing1-set-xfer	|  FALSE    TRUE	TRUE
 *     Clearing2-set-xfer	|  FALSE    FALSE	TRUE
 *
 * Only the following transitions are allowed:
 *
 *   Transition
 *   ----------
 *   Clear -> Set
 *   Set -> Clear
 *   Set -> Expired
 *   Expired -> Clearing-no-set-xfer
 *   Expired -> Expired-set-xfer
 *   Clearing-no-set-xfer -> Clear
 *   Clearing1-set-xfer -> Clearing2-set-xfer
 *   Clearing2-set-xfer -> Clear
 *
 * NOTE:
 *   - Each "state" represents multiple program states.
 *   - Transitions are designed to be monotonic, so that
 *     variable values always correctly represent the current state
 *     (even when interrupted between individual operations).
 *   - Memory pipelining effects will require barriers in a fully
 *     reentrant environment (e.g. VMS 7.x + kernel threads).
 * ------------------------------------------------------------------
 */

#include "mdef.h"

#include "gtm_stdio.h"
#if defined (VMS)
#  include "efn.h"
#  include <ssdef.h>
#endif

/* tp_timeout.h needs to be included to potentially define DEBUG_TPTIMEOUT_DEFERRAL */
#include "tp_timeout.h"
#ifdef DEBUG_TPTIMEOUT_DEFERRAL
#  include "gtm_time.h"
#endif

#include "outofband.h"
#include "gt_timer.h"
#include "xfer_enum.h"
#include "deferred_events.h"
#include "deferred_events_queue.h"
#include "op.h"
#include "fix_xfer_entry.h"
#include "error_trap.h"

#define TP_TIMER_ID (TID)&tp_start_timer
#define TP_QUEUE_ID &tptimeout_set

/* If debugging timeout deferral, it is helpful to timestamp the messages. Encapsulate our debugging macro with
 * enough processing to be able to do that.
 */
#ifdef DEBUG_TPTIMEOUT_DEFERRAL
#  define TIME_EXT_FMT "%T"
#  define DBGWTIME(x) 									\
{											\
	time_t		now;								\
	struct tm	*tm_struct;							\
	char		asccurtime[10];							\
	size_t		len;								\
	now = time(NULL);								\
	GTM_LOCALTIME(tm_struct, &now);							\
	STRFTIME(asccurtime, SIZEOF(asccurtime), TIME_EXT_FMT, tm_struct, len);		\
	DBGTPTDFRL(x);									\
}
#else
#  define DBGWTIME(x)
#endif

/* External variables */
GBLREF dollar_ecode_type	dollar_ecode;
GBLREF volatile int4		outofband;
GBLREF xfer_entry_t     	xfer_table[];
GBLREF boolean_t		in_timed_tn;
GBLREF boolean_t		tp_timeout_set_xfer;
GBLREF boolean_t		tp_timeout_deferred;
GBLREF boolean_t		dollar_zininterrupt;
GBLREF boolean_t		ztrap_explicit_null;

error_def(ERR_TPTIMEOUT);

void tptimeout_set(int4 dummy_param);
STATICFNDCL void tp_expire_now(void);

/* =============================================================================
 * FILE-SCOPE FUNCTIONS
 * =============================================================================
 */

/* ------------------------------------------------------------------
 * Timer handler (Set -> Expired)
 *
 * - Sets flag to indicate timeout has occurred.
 * - Should only happen if a timeout has been started (and not cancelled),
 *   and has not yet expired.
 * - Static because it's for internal use only.
 * ------------------------------------------------------------------
 */
STATICFNDEF void tp_expire_now(void)
{
	DBGWTIME((stderr, "%s tp_expire_now: Driving xfer_set_handlers\n" VMS_ONLY("\n"),
		  asccurtime));
	assert(in_timed_tn);
	tp_timeout_set_xfer = xfer_set_handlers(outofband_event, &tptimeout_set, 0, FALSE);
}

/* ------------------------------------------------------------------
 * Set transfer table for synchronous handling of TP timeout.
 * Should be called only from set_xfer_handlers.
 *
 * Notes:
 *  - Dummy parameter is for calling compatibility.
 *  - Prototype goes in deferred events header file, not in
 *    tp_timeout header file, because it's not for general use.
 * ------------------------------------------------------------------
 */
void tptimeout_set(int4 dummy_param)
{
	DCL_THREADGBL_ACCESS;

	SETUP_THREADGBL_ACCESS;
	/* TP timeout deferral is UNIX-only. This is because the mechanism becomes much more complicated on VMS
	 * due to the mixing of timers and TP timeout, both of which use the same event flag so don't play well
	 * together. It could be fixed for VMS with some perhaps non-trivial work but with VMS approaching EOL,
	 * the effort was deemed unnecessary.
	 */
	if (((0 < dollar_ecode.index) && (ETRAP_IN_EFFECT)) UNIX_ONLY( || dollar_zininterrupt))
	{	/* Error handling or job interrupt is in effect - defer tp timeout
		 * until $ECODE is cleared and/or we have unrolled the job interrupt
		 * frame
		 */
		assert(!tp_timeout_deferred);	/* Note: even though we come back thru tptimeout_set() from op_svput and
						 * other places when tp_timeout_deferred was known to be true, we shouldn't
						 * come back through with the above conditions letting us in THIS block. So
						 * We should only be coming through here via the initial timeout where we
						 * should be garranteed this flag is OFF.
						 */
		tp_timeout_deferred = TRUE;
		SAVE_XFER_ENTRY(outofband_event, &tptimeout_set, 0);
		DBGWTIME((stderr, "%s tptimeout_set: TP timeout deferred function address: %d\n"
				, asccurtime,&tptimeout_set));
		return;
	} else
	{
		DBGWTIME((stderr, "%s tptimeout_set: TP timeout *NOT* deferred - ecode index: %d  etrap: %d\n"
					VMS_ONLY("\n"), asccurtime, dollar_ecode.index, ETRAP_IN_EFFECT));
	}
	if (tptimeout != outofband)
	{
		outofband = tptimeout;
		FIX_XFER_ENTRY(xf_linefetch, op_fetchintrrpt);
		FIX_XFER_ENTRY(xf_linestart, op_startintrrpt);
		FIX_XFER_ENTRY(xf_zbfetch, op_fetchintrrpt);
		FIX_XFER_ENTRY(xf_zbstart, op_startintrrpt);
		FIX_XFER_ENTRY(xf_forchk1, op_startintrrpt);
		FIX_XFER_ENTRY(xf_forloop, op_forintrrpt);
		tp_timeout_set_xfer = TRUE;
#		ifdef VMS
		/* Set event flag now that intercept is in place */
		status = sys$setef(efn_outofband);
		assert(SS$_WASCLR == status);
		assertpro((SS$_WASCLR == status) || (SS$_WASSET == status));
		sys$wake(0,0);
#		endif
	} else
	{
		DBGWTIME((stderr, "%s tptimeout_set: tptimeout outofband already set\n" VMS_ONLY("\n"), asccurtime));
	}
	UNIX_ONLY(tp_timeout_deferred = FALSE);	/* Clear flag now that intercept setup or already installed */
}

/* ------------------------------------------------------------------
 * Start timer (Clear -> Set-timer)
 *
 * Change state before starting timer so
 * pops will always happen in the Set state.
 *
 * Note: timer handler data length and pointer are specified as
 * 0 and null, respectively. Handler parameter list
 * should therefore probably be (int4, char*), due to
 * how it's called from timer_handler(), but no one else does that.
 * ------------------------------------------------------------------
 */
void tp_start_timer(int4 timeout_seconds)
{
	assert(!in_timed_tn);
	DBGWTIME((stderr, "%s tp_start_timer: Starting timer for tptimeout\n" VMS_ONLY("\n"), asccurtime));
	in_timed_tn = TRUE;
	start_timer(TP_TIMER_ID, (1000 * timeout_seconds), &tp_expire_now, 0, NULL);
}

/* ------------------------------------------------------------------
 * Transaction done, clear pending timeout:
 *     (Set-timer -> Clear)
 *     (Expired -> Clearing-no-set-xfer
 *              -> Clear)
 *     (Expired-set-xfer -> Clearing1-set-xfer
 *                       -> Clearing2-set-xfer
 *                       -> Clear)
 *
 * - Ok to call even if no timeout was set.
 * - Reset transfer table if expired and timeout was the reason.
 * - Resets expired flag AFTER cancelling the timer,
 *   in case the alarm expires just before it's cancelled.
 *
 * Notes:
 * - Test for tp_timeout_set_xfer may obsolete use of conditional
 *   xfer_table reset function. If so, should change this routine
 *   to simply return value of tp_timeout_set_xfer when entered.
 * ------------------------------------------------------------------
 */
void tp_clear_timeout(void)
{
	boolean_t tp_timeout_check = FALSE;

	DBGWTIME((stderr, "%s tp_clear_timeout: Transaction complete - clearing tptimeout\n" VMS_ONLY("\n"), asccurtime));
	tp_timeout_deferred = FALSE;
	if (in_timed_tn)
	{
		/* ------------------------------------------------
		 * Works whether or not timer already expired.
		 *
		 * Would be faster to only cancel if expired, but
		 * could miss a last-minute timer pop that way.
		 *
		 * Can save time by making timers more efficient, or
		 * by setting a "cancelling" flag to ensure no
		 * missed pops, and then only cancel if expired.
		 * ------------------------------------------------
		 */
		cancel_timer(TP_TIMER_ID);
		REMOVE_QUEUE_ENTRY(TP_QUEUE_ID);
		/* --------------------------------------------
		 * For unambiguous states, clear this flag
		 * after cancelling timer and before clearing
		 * expired flag.
		 * --------------------------------------------
		 */
		in_timed_tn = FALSE;
		/* -----------------------------------------------------
		 * Should clear xfer settings only if set them.
		 * -----------------------------------------------------
		 */
		if (tp_timeout_set_xfer)
		{
			/* ------------------------------------------------
			 * Get here only if set xfer_table due to timer pop.
			 * - If timeout is aborting transaction, or
			 * - If committing and timer popped too late to
			 *   stop it => want to undo xfer_table change
			 *   before it invokes ZTRAP via async_action.
			 * - Assert should never trigger due to test of
			 *   tp_timeout_set_xfer.
			 * --------------------------------------------
			 */

		        tp_timeout_check = xfer_reset_if_setter(outofband_event);
			DBGWTIME((stderr, "%s tp_clear_timeout: tptimeout timer had already popped - clearing driver "
				  "indicicator\n" VMS_ONLY("\n"), asccurtime));
			assert(tp_timeout_check);
			tp_timeout_set_xfer = FALSE;
		} else
		{
			/* ----------------------------------------------------
			 * Get here only if clearing TP timer when a
			 * TP timeout did not set xfer_table.
			 * Examples:
			 *   -- Before timer popped, via op_tcommit, if
			 *      successfully committing a timed transaction.
			 *      NOTE: other events could be pending, if they
			 *      occurred late in transaction.
			 *   -- After timer popped, also via op_tcommit,
			 *      if successfully committing
			 *      a timed transaction and another event
			 *      occurred first (but too late in transaction to
			 *      abort it).
			 *   Before or after timer popped:
			 *   -- If another event (e.g. ^C) happened first,
			 *      and now that event is clearing TP timer to
			 *      prevent timer pop in M handler (before it can
			 *      do TROLLBACK).
			 *   -- Via op_trollback(), whether in user code
			 *      or otherwise (e.g. at exit).
			 * ----------------------------------------------------
			 */
			; /* (There's nothing to do) */
		}
	}
}

/*
 * Routine is driven at tp timeout recognition point by outofband_action().
 */
void tp_timeout_action(void)
{
	/* Since tp timeout error is about to be driven, reset the interrupt mechanism before
	 * any error handler gets driven so we don't trip another call inside the error handler.
	 */
	DBGWTIME((stderr, "%s tp_timeout_action: Driving TP timeout error\n" VMS_ONLY("\n"), asccurtime));
	tp_clear_timeout();
	rts_error_csa(CSA_ARG(NULL) VARLSTCNT(1) ERR_TPTIMEOUT);
}
