/* KInterbasDB Python Package - Header File for Connection Timeout
 *
 * Version 3.3
 *
 * The following contributors hold Copyright (C) over their respective
 * portions of code (see license.txt for details):
 *
 * [Original Author (maintained through version 2.0-0.3.1):]
 *   1998-2001 [alex]  Alexander Kuznetsov   <alexan@users.sourceforge.net>
 * [Maintainers (after version 2.0-0.3.1):]
 *   2001-2002 [maz]   Marek Isalski         <kinterbasdb@maz.nu>
 *   2002-2007 [dsr]   David Rushby          <woodsplitter@rocketmail.com>
 * [Contributors:]
 *   2001      [eac]   Evgeny A. Cherkashin  <eugeneai@icc.ru>
 *   2001-2002 [janez] Janez Jere            <janez.jere@void.si>
 */

#ifndef _KICORE_CONNECTION_TIMEOUT_H
#define _KICORE_CONNECTION_TIMEOUT_H

#ifdef ENABLE_CONNECTION_TIMEOUT

#include "_kinterbasdb.h"
#include "_kisupport.h"

/* Type definitions: */
typedef enum {
  CONOP_IDLE,
    /* There is no operation active on the Connection at present, and it has
     * not timed out. */
  CONOP_ACTIVE,
    /* There is an operation active, and the Connection has not timed out. */
  CONOP_TIMED_OUT_TRANSPARENTLY,
    /* The Connection was closed by the ConnectionTimeoutThread, and can be
     * transparently resurrected because no transaction was in progress when
     * the timeout occurred. */
  CONOP_TIMED_OUT_NONTRANSPARENTLY,
    /* The Connection was closed by the ConnectionTimeoutThread, and cannot be
     * transparently resurrected because a transaction was in progress when the
     * timeout occurred.  The next attempt to use the Connection (directly, by
     * calling a method of the Connection, or indirectly, by calling a method
     * of a dependent object such as a Cursor) will raise a
     * kinterbasdb.ConnectionTimedOut exception. */
  CONOP_PERMANENTLY_CLOSED
} ConnectionOpState;

/* The user-supplied before_callback returns one of these codes to indicate how
 * the ConnectionTimeoutThread should proceed: */
typedef enum {
  CT_VETO,
    /* Don't time the connection out, and don't check it again until at least a
     * full ->timeout_period has elapsed. */
  CT_ROLLBACK,
    /* Roll back the connection's unresolved transaction (if any) before timing
     * the connection out. */
  CT_COMMIT,
    /* Commit the connection's unresolved transaction (if any) before timing
     * the connection out. */
  CT_NONTRANSPARENT
    /* Roll back the connection's unresolved transaction (if any) and time the
     * connection out nontransparently (so that future attempts to use it will
     * raise a kinterbasdb.ConnectionTimedOut exception). */
} CTCallbackVerdict;
const CTCallbackVerdict CT_DEFAULT = CT_NONTRANSPARENT;

typedef struct _ConnectionTimeoutParams {
  /* Each Connection for which timeout is enabled has a ConnectionTimeoutParams
   * instance associated with it. */

  PyThread_type_lock lock;
  PlatformThreadIdType owner;

  volatile ConnectionOpState state;
  /* state and timeout_period are placed together so that on a 32-bit platform,
   * they won't need padding (reduces size of struct from 48 to 40 bytes on
   * Win32). */
  volatile long timeout_period;

  volatile LONG_LONG connected_at;
  volatile LONG_LONG last_active;
  /* soonest_might_time_out = last_active + timeout_period: */
  volatile LONG_LONG soonest_might_time_out;

  /* User-supplied callbacks: */
  volatile PyObject *py_callback_before;
  volatile PyObject *py_callback_after;
} ConnectionTimeoutParams;

/* The global ConnectionTimeoutManager uses a linked list of these to keep
 * track of all connections on which timeout is enabled: */
typedef struct _ConnectionTracker {
  volatile CConnection *contained;
  volatile struct _ConnectionTracker *next;
} ConnectionTracker;

/* Cast away volatile qualifier: */
#define DV_CT(ct_ptr) ((ConnectionTracker *) (ct))

struct ConnectionTimeoutManager {
  /* There's only one instance of ConnectionTimeoutManager (named global_ctm)
   * in the entire process. */

  PlatformMutexType lock;

  /* There are too many differences between Windows Events and pthread
   * condition variables for us to handle them with a uniform abstraction. */
  #ifdef PLATFORM_WINDOWS
    HANDLE
  #else
    pthread_cond_t
  #endif
  reconsider_wait_interval;

  volatile Py_ssize_t n_cons; /* Number of nodes in linked list cons. */
  volatile ConnectionTracker *cons;
  /* The soonest point in time (expressed in milliseconds since the epoch) that
   * one of the connections tracked in cons might need to be timed out: */
  volatile LONG_LONG soonest_next_connection_might_timeout;

  /* timeout_thread_py is a reference to a Python object, but a typical client
   * of global_ctm does not hold the GIL.  The only operation performed on
   * timeout_thread_py without the GIL held is to nullify it, so no problem. */
  PyObject *timeout_thread_py;
  PlatformThreadRefType timeout_thread;
  PlatformThreadIdType timeout_thread_id;
  volatile boolean ctt_should_stop;
} global_ctm;

/* Misc. constraints: */
#define RUNNING_IN_CONNECTION_TIMEOUT_THREAD \
  (Thread_ids_equal(Thread_current_id(), global_ctm.timeout_thread_id))

#define NOT_RUNNING_IN_CONNECTION_TIMEOUT_THREAD \
  (!RUNNING_IN_CONNECTION_TIMEOUT_THREAD)

#define S_IN_14_DAYS     1209600
#define MS_IN_14_DAYS 1209600000
#define TIMEOUT_PERIOD_IS_IN_RANGE(period) \
  ((period) > 0 && (period) <= MS_IN_14_DAYS)

/* ConnectionTimeoutManager method prototypes: */
static int CTM_initialize(void);
static int CTM_add(volatile CConnection *con, ConnectionTimeoutParams *tp);
static int CTM_remove(volatile CConnection *con);

#define CTM_LOCK \
  debug_print3("CTM-> ?ACQUIRE: %ld file %s line %d\n", \
      PyThread_get_thread_ident(), __FILE__, __LINE__ \
    ); \
  Mutex_lock(&global_ctm.lock); \
  debug_print3("CTM-> !!ACQUIRED: %ld file %s line %d\n", \
      PyThread_get_thread_ident(), __FILE__, __LINE__ \
    );

#define CTM_UNLOCK \
  debug_print3("CTM-> ?RELEASE: %ld file %s line %d\n", \
      PyThread_get_thread_ident(), __FILE__, __LINE__ \
    ); \
  Mutex_unlock(&global_ctm.lock); \
  debug_print3("CTM-> !!RELEASED: %ld file %s line %d\n", \
      PyThread_get_thread_ident(), __FILE__, __LINE__ \
    );

/* ConnectionTimeoutParams method prototypes: */
static ConnectionTimeoutParams *ConnectionTimeoutParams_create(
    long period,
    PyObject *py_callback_before, PyObject *py_callback_after
  );
static int _ConnectionTimeoutParams_destroy_(
    ConnectionTimeoutParams **tp_, boolean should_destroy_lock
  );
static int ConnectionTimeoutParams_destroy(ConnectionTimeoutParams **tp_);
static ConnectionOpState ConnectionTimeoutParams_trans_while_already_locked(
    ConnectionTimeoutParams *tp,
    ConnectionOpState expected_old_state, ConnectionOpState requested_new_state
  );
static ConnectionOpState ConnectionTimeoutParams_trans(
    ConnectionTimeoutParams *tp,
    ConnectionOpState expected_old_state, ConnectionOpState requested_new_state
  );
static void _ConnectionTimeoutParams_touch(ConnectionTimeoutParams *tp);

/* Connection activation and deactivation: */
#define TP_RECORD_OWNERSHIP(tp) \
  (tp)->owner = Thread_current_id()
#define TP_RELEASE_OWNERSHIP(tp) \
  (tp)->owner = THREAD_ID_NONE

static boolean CURRENT_THREAD_OWNS_TP(ConnectionTimeoutParams *tp) {
  assert (tp != NULL);
  return Thread_ids_equal(Thread_current_id(), tp->owner);
}

static boolean CURRENT_THREAD_OWNS_CON_TP(CConnection *con) {
  assert (con != NULL);
  if (!Connection_timeout_enabled(con)) {
    return TRUE;
  } else {
    ConnectionTimeoutParams *tp = con->timeout;
    assert (tp != NULL);
    return CURRENT_THREAD_OWNS_TP(tp);
  }
}

/* TP_LOCK must not be called when the GIL is held; use
 * ACQUIRE_TP_WITH_GIL_HELD for that.  Violating this rule could result in a
 * deadlock between the violating thread and the ConnectionTimeoutThread. */
#define TP_LOCK(tp) \
  debug_print4("TP(%p)-> ?ACQUIRE: %ld file %s line %d\n", \
      tp, PyThread_get_thread_ident(), __FILE__, __LINE__ \
    ); \
  PyThread_acquire_lock((tp)->lock, WAIT_LOCK); \
  TP_RECORD_OWNERSHIP(tp); \
  debug_print4("TP(%p)-> !!ACQUIRED: %ld file %s line %d\n", \
      tp, PyThread_get_thread_ident(), __FILE__, __LINE__ \
    );

static boolean TP_TRYLOCK(ConnectionTimeoutParams *tp);

#define TP_UNLOCK(tp) \
  debug_print4("TP(%p)-> ?RELEASE: %ld file %s line %d\n", \
      tp, PyThread_get_thread_ident(), __FILE__, __LINE__ \
    ); \
  TP_RELEASE_OWNERSHIP(tp); \
  PyThread_release_lock((tp)->lock); \
  debug_print4("TP(%p)-> !!RELEASED: %ld file %s line %d\n", \
      tp, PyThread_get_thread_ident(), __FILE__, __LINE__ \
    );

/* ACQUIRE_TP_WITH_GIL_HELD:
 * (This macro is meant to be used only by the thread that performs routine
 * operations on a connection (ROT), not by the ConnectionTimeoutThread (CTT).)
 *
 * There can be at most two threads vying for control of a connection at once:
 *   - the ROT
 *   - the CTT
 *
 * ACQUIRE_TP_WITH_GIL_HELD, invoked by the ROT, first tries to acquire the
 * lock over the ConnectionTimeoutParams object tp.  If that attempt fails, the
 * ROT knows that the CTT was holding the lock.  The CTT sometimes tries to
 * acquire the GIL while it is holding a ConnectionTimeoutParams lock, so in
 * order to avoid deadlock when the ROT detects that the CTT is holding
 * tp->lock, the ROT must release the GIL and wait to acquire tp->lock before
 * reacquiring the GIL. */
#define ACQUIRE_TP_WITH_GIL_HELD(tp) \
  debug_print4("TP(%p)-> ?TRY-ACQUIRE: %ld file %s line %d\n", \
      tp, PyThread_get_thread_ident(), __FILE__, __LINE__ \
    ); \
  if (TP_TRYLOCK(tp)) { \
    debug_print4("TP(%p)-> ?TRY-ACQUIRED: %ld file %s line %d\n", \
        tp, PyThread_get_thread_ident(), __FILE__, __LINE__ \
      ); \
  } else { \
    debug_print4("TP(%p)-> ?TRY-ACQUIRE-FAILED: %ld file %s line %d\n", \
        tp, PyThread_get_thread_ident(), __FILE__, __LINE__ \
      ); \
    { \
      PyThreadState *tstate = PyThreadState_Get(); \
      LEAVE_GIL_USING_THREADSTATE(tstate); \
      TP_LOCK(tp); \
      ENTER_GIL_USING_THREADSTATE(tstate); \
    } \
  }

#define ACQUIRE_CON_TP_WITH_GIL_HELD(con) \
  if (Connection_timeout_enabled(con)) { \
    ACQUIRE_TP_WITH_GIL_HELD((con)->timeout); \
  }

/* RELEASE_CON_TP is the inverse of ACQUIRE_CON_TP_WITH_GIL_HELD. */
#define RELEASE_CON_TP(con) \
  if (Connection_timeout_enabled(con)) { \
    TP_UNLOCK((con)->timeout); \
  }

static int Connection_activate(CConnection *con,
    const boolean con_tp_already_locked,
    const boolean allow_transparent_resumption
  );

/* Connection Activation and passivation macros: */
#define _CON_ACTIVATE( \
  con, failure_action, con_tp_already_locked, allow_transparent_resumption \
 ) \
  /* The GIL must be held when this macro is used. */ \
  assert (con != NULL); \
  if (Connection_activate(con, con_tp_already_locked, \
        allow_transparent_resumption \
      ) != 0 \
     ) \
  { \
    assert (PyErr_Occurred()); \
    failure_action; \
  }

#define CON_ACTIVATE(con, failure_action) \
  _CON_ACTIVATE(con, failure_action, FALSE, TRUE)

#define CON_ACTIVATE__FORBID_TRANSPARENT_RESUMPTION(con, failure_action) \
  _CON_ACTIVATE(con, failure_action, FALSE, FALSE)

#define CON_ACTIVATE__ALREADY_LOCKED(con, failure_action) \
  _CON_ACTIVATE(con, failure_action, TRUE, TRUE)

#define CON_ACTIVATE__FORBID_TRANSPARENT_RESUMPTION__ALREADY_LOCKED( \
  con, failure_action \
 ) \
  _CON_ACTIVATE(con, failure_action, TRUE, FALSE)


#define _CON_PASSIVATE(con, state_trans_func) \
  if (Connection_timeout_enabled(con)) { \
    LONG_LONG orig_last_active; \
    ConnectionOpState achieved_state; \
    \
    assert ((con)->timeout->state == CONOP_ACTIVE); \
    orig_last_active = (con)->timeout->last_active; \
    /* This should never fail: */ \
    achieved_state = state_trans_func( \
        (con)->timeout, CONOP_ACTIVE, CONOP_IDLE \
      ); \
    assert (achieved_state == CONOP_IDLE); \
    /* The last_active timestamp should now be later than it was (but it \
     * might be mathematically the same if the timer resolution is
     * course). */ \
    assert ((con)->timeout->last_active - orig_last_active >= 0); \
  }

#define CON_PASSIVATE(con) \
  _CON_PASSIVATE(con, ConnectionTimeoutParams_trans)

#define CON_PASSIVATE__ALREADY_LOCKED(con) \
  _CON_PASSIVATE(con, ConnectionTimeoutParams_trans_while_already_locked)

#define CON_MUST_ALREADY_BE_ACTIVE(con) \
  /* This macro performs no locking because it's only intended to be used in \
   * places where the connection is expected to *retain a state of \
   * CONOP_ACTIVE set earlier while a lock was held* (the \
   * ConnectionTimeoutThread never times out a connection that has state \
   * CONOP_ACTIVE, so the fact that the lock has been released since is no \
   * problem). */ \
  assert ((con) != NULL); \
  assert ( \
         !Connection_timeout_enabled(con) \
      || (con)->timeout->state == CONOP_ACTIVE \
    );

#define CON_MUST_NOT_BE_ACTIVE(con) \
  assert (con != NULL); \
  assert ( \
         !Connection_timeout_enabled(con) \
      || (con)->timeout->state != CONOP_ACTIVE \
    );

#else /* not defined(ENABLE_CONNECTION_TIMEOUT) */

  #define RUNNING_IN_CONNECTION_TIMEOUT_THREAD       FALSE
  #define NOT_RUNNING_IN_CONNECTION_TIMEOUT_THREAD   TRUE

  /* Connection Activation and Passivation macros: */
  #define _CON_ACTIVATE( \
      con, failure_action, \
      con_tp_already_locked, allow_transparent_resumption \
    ) \
      assert (con != NULL); \
      if (con == null_connection || (con)->state != CON_STATE_OPEN) { \
        raise_exception(ProgrammingError, "Invalid connection state.  The" \
            " connection must be open to perform this operation." \
          ); \
        failure_action; \
      }

  #define CON_ACTIVATE(con, failure_action) \
    _CON_ACTIVATE(con, failure_action, FALSE, FALSE)

  #define CON_ACTIVATE__FORBID_TRANSPARENT_RESUMPTION(con, failure_action) \
    CON_ACTIVATE(con, failure_action)

  #define CON_PASSIVATE(con)

  #define CON_MUST_ALREADY_BE_ACTIVE(con)
  #define CON_MUST_NOT_BE_ACTIVE(con)

#endif /* defined(ENABLE_CONNECTION_TIMEOUT) */

/* Cursor activation and deactivation: */
#define CUR_REQUIRE_OPEN_WITH_FAILURE(cursor, failure_action) \
  if (_Cursor_require_open(cursor, NULL) != 0) { failure_action; }

#define CUR_REQUIRE_OPEN(cursor) \
  CUR_REQUIRE_OPEN_WITH_FAILURE(cursor, return NULL)

#define CUR_REQUIRE_OPEN2(cursor, failure_message) \
  if (_Cursor_require_open(cursor, failure_message) != 0) { return NULL; }

#define _CUR_ACTIVATE(cursor, failure_action, allow_transparent_resumption) \
  assert (cursor != NULL); \
  if ((cursor)->trans != NULL) { \
    /* If cursor has a connection at all, _CON_ACTIVATE should be executed \
     * before CUR_REQUIRE_OPEN_WITH_FAILURE, so that a ConnectionTimedOut \
     * exception will be thrown (instead of ProgrammingError) where \
     * appropriate: */ \
    CConnection *con = Transaction_get_con((cursor)->trans); \
    if (con != NULL) { \
      _CON_ACTIVATE(con, failure_action, FALSE, allow_transparent_resumption); \
    } \
  } \
  CUR_REQUIRE_OPEN_WITH_FAILURE(cursor, failure_action); \

#define CUR_ACTIVATE(cursor, failure_action) \
  _CUR_ACTIVATE(cursor, failure_action, TRUE)

#define CUR_ACTIVATE__FORBID_TRANSPARENT_RESUMPTION(cursor, failure_action) \
  _CUR_ACTIVATE(cursor, failure_action, FALSE)


#define CUR_PASSIVATE(cursor) \
  assert (cursor != NULL); \
  assert ((cursor)->trans != NULL); \
  assert (Transaction_get_con((cursor)->trans) != NULL); \
  CON_PASSIVATE(Transaction_get_con((cursor)->trans))

#endif /* not def _KICORE_CONNECTION_TIMEOUT_H */
