/* KInterbasDB Python Package - Implementation of 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>
 */

#include "_kicore_connection_timeout.h"

/*************************** PRELIMINARIES: BEGIN ****************************/
static double ConnectionTimeoutParams_active_secs(ConnectionTimeoutParams *tp);
static double ConnectionTimeoutParams_idle_secs(ConnectionTimeoutParams *tp);

/* Inform the ConnectionTimeoutThread that a new connection has been
 * added to the tracker: */
#ifdef PLATFORM_WINDOWS
  #define WAKE_TIMEOUT_THREAD \
    SetEvent(global_ctm.reconsider_wait_interval)
#else
  #define WAKE_TIMEOUT_THREAD \
    pthread_cond_signal(&global_ctm.reconsider_wait_interval)
#endif

#define CLEAR_CTT_REFS \
  global_ctm.timeout_thread_py = NULL; \
  global_ctm.timeout_thread = THREAD_REF_INVALID; \
  global_ctm.timeout_thread_id = THREAD_ID_NONE

#define ASSERT_CTT_REFS_ARE_CLEAR \
  assert (global_ctm.timeout_thread_py == NULL); \
  assert (global_ctm.timeout_thread == THREAD_REF_INVALID); \
  assert (global_ctm.timeout_thread_id == THREAD_ID_NONE)

static boolean TP_TRYLOCK(ConnectionTimeoutParams *tp) {
  const boolean acquired = (boolean)
    PyThread_acquire_lock(tp->lock, NOWAIT_LOCK);
  if (acquired) {
    TP_RECORD_OWNERSHIP(tp);
    debug_print4("TP(%p)-> ?ACQUIRE: %ld file %s line %d\n",
        tp, PyThread_get_thread_ident(), __FILE__, __LINE__
      );
    debug_print4("TP(%p)-> !!ACQUIRED: %ld file %s line %d\n",
        tp, PyThread_get_thread_ident(), __FILE__, __LINE__
      );
  }
  return acquired;
} /* TP_TRYLOCK */
/**************************** PRELIMINARIES: END *****************************/

/*********************** CACHED PYTHON OBJECTS: BEGIN ************************/
static PyObject *con_timeout__s_period;
static PyObject *con_timeout__s_callback_before;
static PyObject *con_timeout__s_callback_after;

static PyObject *con_timeout__s_dsn;
static PyObject *con_timeout__s_has_transaction;
static PyObject *con_timeout__s_active_secs;
static PyObject *con_timeout__s_idle_secs;
/************************ CACHED PYTHON OBJECTS: END *************************/

/************************* MISC SUPPORT CODE: BEGIN **************************/
static int init_kidb_connection_timeout(PyObject *k_mod) {
  #define CT_CACHE_STRING(s) \
    con_timeout__s_ ## s = PyString_FromString(#s); \
    if (con_timeout__s_ ## s == NULL) { goto fail; }

  CT_CACHE_STRING(period);
  CT_CACHE_STRING(callback_before);
  CT_CACHE_STRING(callback_after);

  CT_CACHE_STRING(dsn);
  CT_CACHE_STRING(has_transaction);
  CT_CACHE_STRING(active_secs);
  CT_CACHE_STRING(idle_secs);

  if (CTM_initialize() != 0) {
    PyErr_SetString(PyExc_ImportError, "Unable to initialize CTM.");
    goto fail;
  }

  return 0;

  fail:
    assert (PyErr_Occurred());
    return -1;
} /* init_kidb_connection_timeout */

static ConnectionTimeoutParams *c_timeout_from_py(PyObject *py_timeout) {
  /* The GIL *IS HELD* when this function is called. */
  ConnectionTimeoutParams *tp = NULL;

  PyObject *py_period = NULL;
  PyObject *py_callback_before = NULL;
  PyObject *py_callback_after = NULL;
  long timeout_period_ms;

  if (!PyDict_Check(py_timeout)) {
    raise_exception(ProgrammingError, "The 'timeout' keyword argument to"
        " kinterbasdb.connect must be either None (the default--no timeout)"
        " or a dict."
      );
    goto fail;
  }

  py_period = PyDict_GetItem(py_timeout, con_timeout__s_period);
  {
    LONG_LONG timeout_ms_LL = py_seconds_to_milliseconds(py_period,
        ProgrammingError, "The timeout dict, if supplied, must contain a"
        " 'period' entry, the value of which must be a number of seconds"
        " between 0.001 (one millisecond) and 1209600 (the number of"
        " seconds in 14 days).  The Python object %s is not acceptable.",
        1, MS_IN_14_DAYS
      );
    if (PyErr_Occurred()) { goto fail; }
    /* py_seconds_to_milliseconds constrained the user-supplied timeout to
     * between 1 and MS_IN_14_DAYS (inclusive), so the following cast is
     * safe: */
    assert (timeout_ms_LL >= 1 && timeout_ms_LL <= MS_IN_14_DAYS);
    timeout_period_ms = (long) timeout_ms_LL;
  }
  py_period = NULL; /* Ref was borrowed. */

  #define VALIDATE_CALLBACK(before_or_after) \
    py_callback_ ## before_or_after = PyDict_GetItem(py_timeout, \
        con_timeout__s_callback_ ## before_or_after \
      ); /* BorRef */ \
    if (py_callback_ ## before_or_after != NULL) { \
      if (py_callback_ ## before_or_after == Py_None) { \
        py_callback_ ## before_or_after = NULL; \
      } else { \
        if (!PyCallable_Check(py_callback_ ## before_or_after)) { \
          raise_exception(ProgrammingError, "The optional '" \
              # before_or_after " callback', if specified, must be" \
              " either a callable object or None." \
            ); \
          goto fail; \
        } \
      } \
    }

  VALIDATE_CALLBACK(before);
  VALIDATE_CALLBACK(after);

  /* If py_timeout contains any keys other than
   *   'period', 'callback_before', 'callback_after'
   * then we complain.  This is to prevent the user from accidentally using
   * the wrong key for (e.g.) a callback, and having it never called. */
  {
    PyObject *key;
    Py_ssize_t pos = 0;

    while (PyDict_Next(py_timeout, &pos, &key, NULL)) {
      if (
          (   PyObject_Compare(key, con_timeout__s_period) != 0
           && PyObject_Compare(key, con_timeout__s_callback_before) != 0
           && PyObject_Compare(key, con_timeout__s_callback_after) != 0
          )
          || PyErr_Occurred()
         )
      {
        PyObject *key_repr = PyObject_Repr(key);
        if (key_repr != NULL) {
          PyObject *err_msg = PyString_FromFormat(
              "Unrecognized key %s in connection timeout dict."
              "  The following keys are allowed:"
              "  'period', 'callback_before', 'callback_after'.",
              PyString_AS_STRING(key_repr)
            );
          if (err_msg != NULL) {
            raise_exception(ProgrammingError, PyString_AS_STRING(err_msg));
            Py_DECREF(err_msg);
          }
          Py_DECREF(key_repr);
        }
        goto fail;
      }
    }
  }

  /* On the basis of timeout_period_ms and the Python callbacks, create a
   * ConnectionTimeoutParams structure: */
  tp = ConnectionTimeoutParams_create(timeout_period_ms,
      py_callback_before, py_callback_after
    );
  if (tp == NULL) {
    assert (PyErr_Occurred());
    goto fail;
  }

  return tp;

  fail:
    assert (PyErr_Occurred());
    if (tp != NULL) {
      if (ConnectionTimeoutParams_destroy(&tp) == 0) {
        assert (tp == NULL);
      }
    }
    return NULL;
} /* c_timeout_from_py */

static const char *ConnectionOpState_describe(ConnectionOpState state) {
  char *desc = NULL;

  switch (state) {
    case CONOP_IDLE:                       desc = "IDLE";
                                           break;
    case CONOP_ACTIVE:                     desc = "ACTIVE";
                                           break;
    case CONOP_TIMED_OUT_TRANSPARENTLY:    desc = "TIMED_OUT_TRANSPARENTLY";
                                           break;
    case CONOP_TIMED_OUT_NONTRANSPARENTLY: desc = "TIMED_OUT_NONTRANSPARENTLY";
                                           break;
    case CONOP_PERMANENTLY_CLOSED:         desc = "PERMANENTLY_CLOSED";
                                           break;
  }

  return desc;
} /* ConnectionOpState_describe */
/************************** MISC SUPPORT CODE: END ***************************/

/****** ConnectionTracker MEMBER FUNC DEFS AND SUPPORTING FUNCS: BEGIN *******/
/* A "function" with the signature of CConnection_untrack must be present to
 * satisfy the _kisupport_lifo_linked_list.h infrastructure, but in this case,
 * it doesn't need to do anything: */
#define CConnection_untrack(con, allowed_to_raise) 0

#include "_kisupport_lifo_linked_list.h"

/* Note that the ConnectionTracker is defined with ..._SYSALLOC, so its methods
 * can safely be called when the GIL is not held. */
LIFO_LINKED_LIST_DEFINE_BASIC_METHODS_SYSALLOC(
    ConnectionTracker, volatile ConnectionTracker,
    CConnection, volatile CConnection
  )
/******* ConnectionTracker MEMBER FUNC DEFS AND SUPPORTING FUNCS: END ********/

/*** ConnectionTimeoutManager MEMBER FUNC DEFS AND SUPPORTING FUNCS: BEGIN ***/
static int CTM_initialize(void) {
  /* The GIL *IS HELD* when this function is called. */
  if (Mutex_init(&global_ctm.lock) != 0) { goto fail; }

  #ifdef PLATFORM_WINDOWS
    /* Auto-reset event, initially non-signalled: */
    global_ctm.reconsider_wait_interval = CreateEvent(NULL, FALSE, FALSE,
        NULL
      );
    if (global_ctm.reconsider_wait_interval == NULL) { goto fail; }
  #else
    if (pthread_cond_init(&global_ctm.reconsider_wait_interval, NULL) != 0) {
      goto fail;
    }
  #endif

  global_ctm.n_cons = 0;
  global_ctm.cons = NULL;
  global_ctm.soonest_next_connection_might_timeout = 0;

  /* The ConnectionTimeoutThread is not actually started until a connection
   * with timeout enabled is created. */
  CLEAR_CTT_REFS;
  global_ctm.ctt_should_stop = FALSE;

  return 0;

  fail:
    return -1;
} /* CTM_initialize */

static int CTM_add(volatile CConnection *con, ConnectionTimeoutParams *tp) {
  /* The GIL *IS NOT HELD* when this function is called. */
  /* This thread also holds tp, and acquires the CTM lock, but there's no risk
   * of a deadlock because the CTT can't possibly be holding the CTM lock and
   * trying to acquire tp (because con is not even in the connection tracker
   * yet). */
  int status = 0;

  assert (NOT_RUNNING_IN_CONNECTION_TIMEOUT_THREAD);
  assert (tp != NULL);
  assert (CURRENT_THREAD_OWNS_TP(tp));

  /* It is the responsibility of this method to actually finalize the
   * association between a connection and its timeout parameter structure; that
   * should not have been done already: */
  assert (con->timeout == NULL);

  CTM_LOCK;
  /* Critical section within these brackets: */
  {
    #ifndef NDEBUG
    ConnectionOpState state =
    #endif
      ConnectionTimeoutParams_trans_while_already_locked(tp,
        CONOP_ACTIVE, CONOP_IDLE
      );
    assert (state == CONOP_IDLE);

    assert (tp->connected_at > 0);
    assert (tp->connected_at <= time_millis());
    assert (tp->last_active > 0);
    assert (tp->last_active <= time_millis());
    assert (tp->soonest_might_time_out > tp->last_active);

    status = ConnectionTracker_add(&global_ctm.cons, con);

    assert (!Connection_timeout_enabled(con));
    if (status == 0) {
      ++global_ctm.n_cons;
      assert (global_ctm.n_cons > 0);

      /* In essence:
       * global_ctm.soonest_next_connection_might_timeout = SOONER_OF(
       *     tp->soonest_might_time_out,
       *     global_ctm.soonest_next_connection_might_timeout
       *   ); */
      if (global_ctm.soonest_next_connection_might_timeout == 0
          || (  global_ctm.soonest_next_connection_might_timeout
              - tp->soonest_might_time_out
             ) > 0
         )
      {
        global_ctm.soonest_next_connection_might_timeout =
          tp->soonest_might_time_out;
      }

      /* Associate the ConnectionTimeoutParams object with the connection in
       * order to indicate that the connection is officially "being tracked
       * for timeout." */
      con->timeout = tp;
      assert (Connection_timeout_enabled(con));

      debug_print1("CTM_add will now wake up CTT (global_ctm.n_cons: %d)\n",
          (int) global_ctm.n_cons
        );
      WAKE_TIMEOUT_THREAD;
    }
  }
  CTM_UNLOCK;

  return status;
} /* CTM_add */

static int CTM_remove(volatile CConnection *con) {
  /* The GIL *IS NOT HELD* when this function is called. */
  int status;
  assert (NOT_RUNNING_IN_CONNECTION_TIMEOUT_THREAD);

  CTM_LOCK;
  /* Critical section within these brackets: */
  {
    status = ConnectionTracker_remove(&global_ctm.cons, con, TRUE);
    if (status == 0) {
      assert (global_ctm.n_cons > 0);
      --global_ctm.n_cons;
    }
  }
  CTM_UNLOCK;

  return status;
} /* CTM_remove */

static int CTM_apply_timeout(PyThreadState *tstate) {
  /* The GIL *IS NOT HELD* when this function is called, although this function
   * sometimes acquires it. */

  /* This function should only be called by the ConnectionTimeoutThread, and
   * that thread should already hold global_ctm's lock before calling this
   * function. */

  int status = 0;

  Py_ssize_t n_cons_tried_to_time_out = 0;
  Py_ssize_t n_cons_timed_out = 0;

  LONG_LONG soonest_timeout_in_next_pass = 0;
  #define UPDATE_STINP_IF_SP_SMTO_IS_SOONER(tp) \
    /* We're not timing tp's connection out during this pass, but as we \
     * sweep, we need to determine the soonest that another sweep might be \
     * needed.  If tp->soonest_might_time_out is sooner than anything we've \
     * previously seen, record it. */ \
    if (   soonest_timeout_in_next_pass == 0 \
        || (tp)->soonest_might_time_out - soonest_timeout_in_next_pass < 0 \
       ) \
    { soonest_timeout_in_next_pass = (tp)->soonest_might_time_out; }


  const LONG_LONG official_sweep_time = time_millis();

  volatile ConnectionTracker *ct_prev = NULL;
  volatile ConnectionTracker *ct = global_ctm.cons;

  assert (RUNNING_IN_CONNECTION_TIMEOUT_THREAD);

  while (ct != NULL) {
    volatile CConnection *con = ct->contained;
    ConnectionTimeoutParams *tp;
    assert (con != NULL);
    assert (con->ob_refcnt > 0);
    assert (con->state == CON_STATE_OPEN);

    tp = con->timeout;
    assert (tp != NULL);

    TP_LOCK(tp);
    /* Critical section (over tp) within these brackets: */
    {
      if (   tp->state == CONOP_IDLE
          && tp->soonest_might_time_out - official_sweep_time <= 0
         )
      { /* Time the connection out unless the user-supplied callback vetoes. */
        boolean should_time_out = TRUE;
        boolean timeout_was_transparent = FALSE;
        boolean had_transaction;
        const double active_secs = ConnectionTimeoutParams_active_secs(tp);
        const double idle_secs = ConnectionTimeoutParams_idle_secs(tp);

        PyObject *py_dsn = NULL;
        PyObject *py_active_secs = NULL;
        PyObject *py_idle_secs = NULL;

        assert (active_secs >= 0.0);
        assert (idle_secs >= 0.0);

        /* It might seem inefficient to acquire and release the GIL around each
         * connection timeout, but in fact it is not.  In a real program, a
         * given connection is likely to be checked to see whether it should be
         * timed out *MANY* more times than it is actually timed out.
         * Therefore, it is desirable for the checking process to avoid holding
         * the GIL insofar as possible; the acquisition and release of the GIL
         * which are required to actually time a connection out are executed
         * relatively rarely. */
        ENTER_GIL_USING_THREADSTATE(tstate);

        /* GIL must be held before Connection_has_any_open_transaction is
         * called: */
        had_transaction = Connection_has_any_open_transaction(DV_CCON(con));

        assert (con->dsn != NULL && con->dsn_len > 0);
        if (tp->py_callback_before != NULL || tp->py_callback_after != NULL) {
          py_dsn = PyString_FromStringAndSize(con->dsn, con->dsn_len);
          if (py_dsn == NULL) { SUPPRESS_EXCEPTION; }

          /* py_active_secs and py_idle_secs will be checked for successful
           * construction later. */
          py_active_secs = PyFloat_FromDouble(active_secs);
          py_idle_secs = PyFloat_FromDouble(idle_secs);
        }

        /* Call the user-supplied "before" callback, if any.  The single
         * argument to the callback is a dict of the form:
         *   {'dsn': dsn, 'has_transaction': boolean,
         *    'active_secs': float, 'idle_secs': float}
         * The connection itself is deliberately not exposed, because
         * kinterbasdb Connections aren't designed to be manipulated by
         * multiple threads except for those few operations necessary for the
         * ConnectionTimeoutThread to time the connection out (and to avoid
         * doing so unless the connection is truly idle). */
        if (tp->py_callback_before == NULL) {
          /* If the user didn't supply a callback, and there's an active
           * transaction, then the timeout is non-transparent: */
          timeout_was_transparent = !had_transaction;
        } else if (py_dsn != NULL) {
          /* Notice that we don't enter this block if we couldn't create
           * py_dsn, even if the user has supplied a callback. */

          boolean continue_callback_attempt = (
              py_active_secs != NULL && py_idle_secs != NULL
            );

          PyObject *py_callback_dict = NULL;
          PyObject *py_has_transaction = NULL;

          /* The user-supplied callback should've been validated much
           * earlier. */
          assert (PyCallable_Check(DV_PYO(tp->py_callback_before)));

          py_has_transaction = PyBool_FromLong(had_transaction);
          /* PyBool_FromLong is never supposed to fail: */
          assert (py_has_transaction != NULL);

          if (continue_callback_attempt) {
            py_callback_dict = PyDict_New();
            if (py_callback_dict == NULL) {
              continue_callback_attempt = FALSE;
              assert (should_time_out);
            } else {
              if (
                     PyDict_SetItem(py_callback_dict,
                       con_timeout__s_dsn, py_dsn                        ) == 0
                  && PyDict_SetItem(py_callback_dict,
                       con_timeout__s_has_transaction, py_has_transaction) == 0
                  && PyDict_SetItem(py_callback_dict,
                       con_timeout__s_active_secs, py_active_secs        ) == 0
                  && PyDict_SetItem(py_callback_dict,
                       con_timeout__s_idle_secs, py_idle_secs            ) == 0
                 )
              {
                PyObject *py_callback_res = PyObject_CallFunctionObjArgs(
                    DV_PYO(tp->py_callback_before), py_callback_dict, NULL
                  );
                if (py_callback_res == NULL) {
                  /* If an exception arose, time the connection out anyway. */
                  SUPPRESS_EXCEPTION;
                  assert (should_time_out);
                  assert (!timeout_was_transparent);
                } else {
                  /* We stick with the default action if the user-supplied
                   * callback returned an object of the wrong type, or of the
                   * right type but wrong value. */
                  CTCallbackVerdict verdict = CT_DEFAULT;
                  if (PyInt_CheckExact(py_callback_res)) {
                    long verdict_L = PyInt_AS_LONG(py_callback_res);
                    if (   verdict_L == CT_VETO
                        || verdict_L == CT_ROLLBACK
                        || verdict_L == CT_COMMIT
                        || verdict_L == CT_NONTRANSPARENT
                       )
                    {
                      verdict = (CTCallbackVerdict) verdict_L;
                    }
                  }

                  switch (verdict) {
                    case CT_VETO:
                      should_time_out = FALSE;
                      /* A veto is considered "activity"; we won't initiate
                       * another timeout attempt for this connection until at
                       * least tp->timeout_period seconds have passed. */
                      _ConnectionTimeoutParams_touch(tp);
                      break;

                    case CT_ROLLBACK:
                    case CT_COMMIT:
                    case CT_NONTRANSPARENT:
                      if (!had_transaction) {
                        /* If the connection didn't have a transaction
                         * originally, it shouldn't now. */
                        assert (!Connection_has_any_open_transaction(
                            DV_CCON(con)
                          ));
                        timeout_was_transparent =
                          (verdict != CT_NONTRANSPARENT);
                      } else {
                        const WhichTransactionOperation op = (
                            verdict == CT_COMMIT ? OP_COMMIT : OP_ROLLBACK
                          );
                        const TransactionalOperationResult trans_res_status =
                          Connection_resolve_all_transactions_from_CTT(con, op);
                        /* Connection_resolve_all_transactions_from_CTT should
                         * have already suppressed any Python exception: */
                        assert (!PyErr_Occurred());

                        if (trans_res_status == OP_RESULT_OK) {
                          assert (!Connection_has_any_open_transaction(
                              DV_CCON(con)
                            ));
                          timeout_was_transparent =
                            (verdict != CT_NONTRANSPARENT);
                        } else {
                          assert (!timeout_was_transparent);
                        }
                      }
                      assert (should_time_out);
                      break;

                    default:
                      /* This should never be reached, because verdict should
                       * not have received the value of verdict_L unless that
                       * value was a recognized member of CTCallbackVerdict: */
                      assert (FALSE);
                  }
                  Py_DECREF(py_callback_res);
                } /* end of is-py_callback_res-null block */
              } else {
                SUPPRESS_EXCEPTION;
                continue_callback_attempt = FALSE;
                assert (should_time_out);
              }
              /* If an exception arose, it should've been cleared so the sweep
               * can continue. */
              assert (!PyErr_Occurred());
            } /* end of is-py_callback_dict-null block */
          } /* end of initial should-continue_callback_attempt block */

          Py_XDECREF(py_callback_dict);
          Py_XDECREF(py_has_transaction);
        } /* end of did-user-supply-callback block */

        /* If the callback did not veto the timeout, close the connection.  Use
         * Connection_close_from_CTT so that this thread doesn't need to
         * release global_ctm.lock during the connection's closure.  This
         * thread removes the connection from the tracker manually (see block
         * below, after GIL release). */
        if (should_time_out) {
          ++n_cons_tried_to_time_out;
          if (Connection_close_from_CTT(con) == 0) {
            const ConnectionOpState desired_state =
                timeout_was_transparent ?
                    CONOP_TIMED_OUT_TRANSPARENTLY
                  : CONOP_TIMED_OUT_NONTRANSPARENTLY
              ;
            #ifndef NDEBUG
            const ConnectionOpState achieved_state =
            #endif
              ConnectionTimeoutParams_trans_while_already_locked(tp,
                CONOP_IDLE, desired_state
              );
            assert (achieved_state == desired_state);
            ++n_cons_timed_out;
          } else {
            /* Note that we'll remove the connection from the tracker even if
             * our attempt to close it failed.  The only reason for failure
             * would be a network problem, and if that has occurred, the
             * connection is already [effectively] closed, so it shouldn't
             * remain in the tracker.
             * We set the state to CONOP_TIMED_OUT_NONTRANSPARENTLY because
             * even though the connection didn't actually time out, it can be
             * recovered with no greater disruption than recovering from a
             * non-transparent timeout. */
            if (PyErr_Occurred()) {
              SUPPRESS_EXCEPTION;
            } else {
              #ifndef NDEBUG
              ConnectionOpState achieved_state =
              #endif
                ConnectionTimeoutParams_trans_while_already_locked(tp,
                    CONOP_IDLE, CONOP_TIMED_OUT_NONTRANSPARENTLY
                  );
              assert (achieved_state == CONOP_TIMED_OUT_NONTRANSPARENTLY);
            }
          }
        }
        assert (!PyErr_Occurred());

        /* Call the user-supplied "after" callback, if any.  The single
         * argument to the callback is a dict of the form:
         *   {'dsn': dsn
         *    'active_secs': float, 'idle_secs': float}
         * The connection itself is deliberately not exposed, for reasons
         * explained previously. */
        if (!should_time_out) {
          /* The timeout attempt was vetoed by the user-supplied
           * callback_before, so we need to consider its soonest_might_time_out
           * timestamp when computing the CTT's after-sweep sleep duration. */
          UPDATE_STINP_IF_SP_SMTO_IS_SOONER(tp);
        } else {
          if (   tp->py_callback_after != NULL && py_dsn != NULL
              && py_active_secs != NULL && py_idle_secs != NULL
             )
          {
            PyObject *py_callback_dict = PyDict_New();
            if (py_callback_dict == NULL) {
              SUPPRESS_EXCEPTION;
            } else {
              if (
                     PyDict_SetItem(py_callback_dict,
                       con_timeout__s_dsn, py_dsn                        ) == 0
                  && PyDict_SetItem(py_callback_dict,
                       con_timeout__s_active_secs, py_active_secs        ) == 0
                  && PyDict_SetItem(py_callback_dict,
                       con_timeout__s_idle_secs, py_idle_secs            ) == 0
                 )
              {
                assert (!Connection_has_any_open_transaction(DV_CCON(con)));
                {
                  PyObject *py_callback_res = PyObject_CallFunctionObjArgs(
                        DV_PYO(tp->py_callback_after), py_callback_dict, NULL
                      );
                  if (py_callback_res == NULL) {
                    SUPPRESS_EXCEPTION;
                  } else {
                    Py_DECREF(py_callback_res);
                  }
                }
              } else {
                SUPPRESS_EXCEPTION;
              }
              Py_DECREF(py_callback_dict);
            }
          }
        }
        assert (!PyErr_Occurred());

        if (py_dsn != NULL) {
          Py_DECREF(py_dsn);
          py_dsn = NULL;
        }
        if (py_active_secs != NULL) {
          Py_DECREF(py_active_secs);
          py_active_secs = NULL;
        }
        if (py_idle_secs != NULL) {
          Py_DECREF(py_idle_secs);
          py_idle_secs = NULL;
        }

        LEAVE_GIL_USING_THREADSTATE(tstate);

        /* Remove the timed-out connection from the tracker, unless the
         * callback prevented us from actually timing it out. */
        if (should_time_out) {
          if (ct_prev == NULL) {
            /* ct is the first node. */
            assert (ct == global_ctm.cons);
            global_ctm.cons = ct->next;
            kimem_plain_free(DV_CT(ct));
            ct = NULL;
          } else {
            /* ct is not the first node. */
            ct_prev->next = ct->next;
            kimem_plain_free(DV_CT(ct));
            ct = ct_prev;
          }
          assert (global_ctm.n_cons > 0);
          --global_ctm.n_cons;
        }
      } else {
        /* We're not timing this connection out during this pass, but as we
         * sweep, we need to determine the soonest that another sweep might be
         * needed. */
        UPDATE_STINP_IF_SP_SMTO_IS_SOONER(tp);
      }
    }
    TP_UNLOCK(tp);

    ct_prev = ct;
    if (ct == NULL) {
      ct = global_ctm.cons;
    } else {
      ct = ct->next;
    }
  } /* end of loop across each tracked connection */

  if (global_ctm.n_cons == 0) {
    /* All tracked connections were timed out. */
    global_ctm.soonest_next_connection_might_timeout = 0;
  } else {
    assert (soonest_timeout_in_next_pass > 0);
    global_ctm.soonest_next_connection_might_timeout =
      soonest_timeout_in_next_pass;
  }

  return status;
} /* CTM_apply_timeout */

static PyObject *pyob_CTM_halt(PyObject *self) {
  PyObject *timeout_thread_py = NULL;
  int status = -1;

  if (global_ctm.timeout_thread_py == NULL) { RETURN_PY_NONE; }

  LEAVE_GIL_WITHOUT_AFFECTING_DB
  CTM_LOCK;
  /* Critical section within these brackets: */
  {
    assert (NOT_RUNNING_IN_CONNECTION_TIMEOUT_THREAD);

    status = ConnectionTracker_release(&global_ctm.cons);
    /* The ConnectionTracker_release call should never fail, because
     * CConnection_untrack doesn't actually do anything. */
    assert (status == 0);
    assert (global_ctm.cons == NULL);

    /* We take local responsibility for the artificial reference created by
     * the ConnectionTimeoutThread in pyob_ConnectionTimeoutThread_main. */
    timeout_thread_py = global_ctm.timeout_thread_py;

    global_ctm.ctt_should_stop = TRUE;

    WAKE_TIMEOUT_THREAD;
  }
  CTM_UNLOCK;
  ENTER_GIL_WITHOUT_AFFECTING_DB

  if (status == 0) {
    assert (timeout_thread_py != NULL);
    {
      PyObject *join_result = PyObject_CallMethod(timeout_thread_py,
          "join", NULL
        );
      if (join_result != NULL) {
        ASSERT_CTT_REFS_ARE_CLEAR;
        Py_DECREF(join_result);
      } else {
        status = -1;
      }
    }
    Py_DECREF(timeout_thread_py);
  }

  if (status == 0) {
    RETURN_PY_NONE;
  } else {
    raise_exception(OperationalError, "Unable to cleanly stop"
        " ConnectionTimeoutThread."
      );
    return NULL;
  }
} /* pyob_CTM_halt */
/**** ConnectionTimeoutManager MEMBER FUNC DEFS AND SUPPORTING FUNCS: END ****/

/******************** ConnectionTimeoutThread DEFS: BEGIN ********************/
static PlatformThreadFuncReturnType THREAD_FUNC_MODIFIER
  ConnectionTimeoutThread_main(void *context)
{
  /* The GIL *IS NOT HELD* when this function is called, although this
   * function's subordinates sometimes acquire it (via the PyThreadState*
   * context). */
  PyThreadState *tstate = (PyThreadState *) context;
  assert (tstate != NULL);

  CTM_LOCK;
  for (;;) {
    while (global_ctm.n_cons == 0 && !global_ctm.ctt_should_stop) {
      /* The CTM lock should be held at this point. */
      debug_print("CTT will now wait indefinitely for new connection.\n");

      /* At this point, no connections with timeout enabled have been
       * registered.  This thread will wait until one of the following occurs:
       *   - a connection with timeout enabled arrives in the tracker (via
       *     CTM_add)
       *   - this thread is asked to terminate itself (via pyob_CTM_halt)    */
      #ifdef PLATFORM_WINDOWS
        /* Note:  Compared to pthread condition variables, Windows Event
         * objects contain many pitfalls for the unwary.  Foremost among them
         * is that the process of releasing the lock associated with the event,
         * then waiting for the event, is not an atomic operation, whereas
         * pthread_cond_wait performs those steps atomically.
         *   Generally speaking, that problem can be addressed by using either
         * a busy-wait loop or the atomic function SignalObjectAndWait, but the
         * latter is not available on Win9x.
         *   The lack of atomicity doesn't actually matter in this case,
         * because we know that there is exactly one thread waiting on the
         * Event (that is, ConnectionTimeoutThread). */
        CTM_UNLOCK;
        WaitForSingleObject(global_ctm.reconsider_wait_interval, INFINITE);
        CTM_LOCK;
      #else
        pthread_cond_wait(&global_ctm.reconsider_wait_interval,
            &global_ctm.lock
          );
      #endif
      /* The CTM lock should be held at this point. */
      debug_print("CTT awakened from indefinite wait for new connection.\n");
    }

    /* The CTM lock should be held at this point. */
    if (global_ctm.ctt_should_stop) {
      /* This thread, the ConnectionTimeoutThread, has been ordered to
       * terminate itself. */
      debug_print("CTT was ordered to terminate itself.\n");
      CLEAR_CTT_REFS;
      CTM_UNLOCK;
      break;
    }

    assert (global_ctm.n_cons > 0);
    assert (global_ctm.soonest_next_connection_might_timeout > 0);
    debug_print1("CTT now has %d con(s) to process.\n",
        (int) global_ctm.n_cons
      );

    if (global_ctm.soonest_next_connection_might_timeout <= time_millis()) {
      int timeout_result;
      debug_print("CTT calling CTM_apply_timeout.\n");
      timeout_result = CTM_apply_timeout(tstate);
      debug_print("CTT finished CTM_apply_timeout call.\n");

      /* At present, CTM_apply_timeout never indicates that an error occurred,
       * because this thread needs to remain active even if a user-supplied
       * callback raises an exception, or an attempt to close a connection
       * raises an exception. */
      assert (timeout_result == 0);

      /* Having just finished a call to CTM_apply_timeout, we yield the CTM
       * lock briefly.  This prevents the ConnectionTimeoutThread from hogging
       * the CTM if the client programmer has requested extremely short
       * timeouts, yet the connections with such timeouts are remaining active.
       * (The test suite provoked such behavior, although a real application is
       * unlikely to). */
      CTM_UNLOCK;
      sleep_millis(10);
      CTM_LOCK;
    }

    { /* CTM_add updates global_ctm.soonest_next_connection_might_timeout if
       * the newly arrived connection might require attention sooner than any
       * previously tracked connection.  Upon being awakened when CTM_add sets
       * the even, then, the ConnectionTimeoutThread need only examine
       * global_ctm.soonest_next_connection_might_timeout to determine how long
       * it should go back to sleep, if at all. */
      boolean wait_ended_because_new_connection_arrived = TRUE;
      while (wait_ended_because_new_connection_arrived) {
        /* If the CTM_apply_timeout call or a Connection's non-timeout closure
         * caused there to be no more connections to monitor, we can
         * immediately go back to waiting (indefinitely) for the
         * reconsider_wait_interval event.
         * We also need to exit this timed-wait loop if this thread has been
         * ordered to terminate. */
        if (global_ctm.n_cons == 0 || global_ctm.ctt_should_stop) { break; }

        /* Wait until the soonest point in time that one of the connections
         * currently tracked by the CTM might possibly time out.
         * If at any time during that waiting period a new connection is added
         * to the tracker, stop waiting and reorient to accomodate its timeout
         * period, if it requires action sooner than any of the previously
         * tracked connections. */
        assert (global_ctm.soonest_next_connection_might_timeout > 0);
        {
          LONG_LONG max_wait_ms_LL = (
                global_ctm.soonest_next_connection_might_timeout
              - time_millis()
            );
          long max_wait_ms;

          if (max_wait_ms_LL <= 0) {
            break;
          } else if (max_wait_ms_LL < 10) {
            /* Wait for no fewer than 10ms: */
            max_wait_ms_LL = 10;
          }

          /* Validation code should have ensured that no connection's timeout
           * period was longer than a certain threshold, which must be less
           * than LONG_MAX. */
          assert (max_wait_ms_LL <= LONG_MAX);
          max_wait_ms = (long) max_wait_ms_LL;
          debug_print1("CTT will now 'sleep' up to %ld ms.\n", max_wait_ms);

          #ifdef PLATFORM_WINDOWS
          {
            DWORD wait_result;

            CTM_UNLOCK;
            wait_result = WaitForSingleObject(
                global_ctm.reconsider_wait_interval, (DWORD) max_wait_ms
              );
            CTM_LOCK;

            assert (wait_result != WAIT_FAILED);
            wait_ended_because_new_connection_arrived = (boolean)
              (wait_result != WAIT_TIMEOUT);
          }
          #else
          {
            int wait_result;
            struct timespec abstime;

            millis_into_future_to_abstime(max_wait_ms, &abstime);
            wait_result = pthread_cond_timedwait(
                &global_ctm.reconsider_wait_interval, &global_ctm.lock,
                &abstime
              );

            assert (wait_result != EINVAL);
            assert (wait_result != EPERM);
            wait_ended_because_new_connection_arrived = (boolean)
              (wait_result != ETIMEDOUT);
          }
          #endif
        } /* end of scope block for wait time calculation */
      } /* end of while(wait_ended_because_new_connection_arrived) loop */
    } /* end of scope block for wait_ended_because_new_connection_arrived */
  } /* end of for(;;) loop */

  return THREAD_FUNC_RETURN_SUCCESS;
} /* ConnectionTimeoutThread_main */

static PyObject *pyob_ConnectionTimeoutThread_main(
    PyObject *self, PyObject *args
  )
{
  /* The GIL *IS HELD* when this function is called. */
  PyThreadState *tstate = PyThreadState_Get();
  boolean main_succeeded = FALSE;

  PyObject *py_ctt_ref;
  PyObject *started_event;
  PyObject *event_set_result = NULL;

  if (!PyArg_ParseTuple(args, "OO", &py_ctt_ref, &started_event)) {
    return NULL;
  }

  CTM_LOCK;

  /* At this point, no CTT thread should be running. */
  assert (global_ctm.timeout_thread_py == NULL);

  global_ctm.timeout_thread_py = py_ctt_ref;
  /* Create an artificial reference to self so that pyob_CTM_halt can be sure
   * the Python Thread object will not be garbage collected before
   * pyob_CTM_halt has joined it. */
  Py_INCREF(global_ctm.timeout_thread_py);
  py_ctt_ref = NULL;

  global_ctm.timeout_thread = Thread_current_ref();
  global_ctm.timeout_thread_id = Thread_current_id();
  debug_print1("CTT thread ID is %ld\n", global_ctm.timeout_thread_id);

  CTM_UNLOCK;

  /* Now that we've filled in the relevant global_ctm fields, call
   * event.set() to allow the thread that started this thread to proceed. */
  event_set_result = PyObject_CallMethod(started_event, "set", NULL);
  if (event_set_result == NULL) { goto fail; }
  Py_DECREF(event_set_result);

  /* Release the GIL before calling the "real" main function of this thread.
   * That function does enter the GIL at times, but most of the time it
   * operates GIL-free. */
  LEAVE_GIL_WITHOUT_AFFECTING_DB
  main_succeeded = (boolean) (
      ConnectionTimeoutThread_main(tstate) == THREAD_FUNC_RETURN_SUCCESS
    );
  ENTER_GIL_WITHOUT_AFFECTING_DB

  if (main_succeeded) {
    RETURN_PY_NONE;
  }
  /* Else, fall through to fail: */

  fail:
    assert (PyErr_Occurred());
    return NULL;
} /* pyob_ConnectionTimeoutThread_main */
/********************* ConnectionTimeoutThread DEFS: END *********************/

/*** ConnectionTimeoutParams MEMBER FUNC DEFS AND SUPPORTING FUNCS: BEGIN ****/
static ConnectionTimeoutParams *ConnectionTimeoutParams_create(
    long period, PyObject *py_callback_before, PyObject *py_callback_after
  )
{
  /* The GIL *IS HELD* when this function is called. */
  ConnectionTimeoutParams *tp = NULL;

  /* The range of period and type of the callbacks should have been validated
   * already; the checks below should be redundant: */
  assert (TIMEOUT_PERIOD_IS_IN_RANGE(period));
  assert (py_callback_before == NULL || PyCallable_Check(py_callback_before));
  assert (py_callback_after  == NULL || PyCallable_Check(py_callback_after ));

  tp = kimem_main_malloc(sizeof(ConnectionTimeoutParams));
  if (tp == NULL) { goto fail; }

  tp->state = CONOP_ACTIVE;
  tp->connected_at = 0;
  tp->last_active = 0;
  tp->timeout_period = period;
  tp->soonest_might_time_out = 0;

  Py_XINCREF(py_callback_before);
  tp->py_callback_before = py_callback_before;

  Py_XINCREF(py_callback_after);
  tp->py_callback_after = py_callback_after;

  tp->lock = PyThread_allocate_lock();
  if (tp->lock == NULL) { goto fail; }

  tp->owner = THREAD_ID_NONE;

  return tp;

  fail:
    assert (PyErr_Occurred());
    if (tp != NULL) {
      _ConnectionTimeoutParams_destroy_(&tp, FALSE);
    }
    return NULL;
} /* ConnectionTimeoutParams_create */

static int _ConnectionTimeoutParams_destroy_(
    ConnectionTimeoutParams **tp_, boolean should_destroy_lock
  )
{
  /* The GIL *IS HELD* when this function is called. */
  ConnectionTimeoutParams *tp = *tp_;

  Py_XDECREF(tp->py_callback_before);
  Py_XDECREF(tp->py_callback_after);

  if (should_destroy_lock) {
    PyThread_free_lock(tp->lock);
  }

  kimem_main_free(tp);
  *tp_ = NULL;

  return 0;
} /* _ConnectionTimeoutParams_destroy_ */

static int ConnectionTimeoutParams_destroy(ConnectionTimeoutParams **tp_) {
  /* The GIL *IS HELD* when this function is called. */
  return _ConnectionTimeoutParams_destroy_(tp_, TRUE);
} /* ConnectionTimeoutParams_destroy */

static ConnectionOpState ConnectionTimeoutParams_trans_while_already_locked(
    ConnectionTimeoutParams *tp,
    ConnectionOpState expected_old_state, ConnectionOpState requested_new_state
  )
{
  /* The GIL *MIGHT OR MIGHT NOT BE HELD* when this function is called
   * (therefore, this function should not attempt to acquire
   * global_ctm.lock because of deadlock risk). */

  assert (tp != NULL);
  assert (CURRENT_THREAD_OWNS_TP(tp));

  if (tp->state == expected_old_state) {
    tp->state = requested_new_state;
    if (requested_new_state == CONOP_IDLE) {
      /* We're going from a state of some kind of activity into one of
       * idleness, so we need to update tp's activity stamps. */
      _ConnectionTimeoutParams_touch(tp);
    }
  }

  return tp->state;
} /* ConnectionTimeoutParams_trans_while_already_locked */

static ConnectionOpState ConnectionTimeoutParams_trans(
    ConnectionTimeoutParams *tp,
    ConnectionOpState expected_old_state, ConnectionOpState requested_new_state
  )
{
  /* The GIL *IS HELD* when this function is called. */
  ConnectionOpState achieved_state;

  assert (tp != NULL);
  assert (!CURRENT_THREAD_OWNS_TP(tp));

  ACQUIRE_TP_WITH_GIL_HELD(tp);
  /* Critical section within these brackets: */
  {
    achieved_state = ConnectionTimeoutParams_trans_while_already_locked(tp,
        expected_old_state, requested_new_state
      );
  }
  TP_UNLOCK(tp);

  return achieved_state;
} /* ConnectionTimeoutParams_trans */

static void _ConnectionTimeoutParams_touch(ConnectionTimeoutParams *tp) {
  /* tp->lock must be held when this function is called. */
  tp->last_active = time_millis();
  tp->soonest_might_time_out = tp->last_active + tp->timeout_period;
} /* _ConnectionTimeoutParams_touch */

static double ConnectionTimeoutParams_active_secs(ConnectionTimeoutParams *tp) {
  return ((double) (tp->last_active - tp->connected_at)) / 1000.0;
} /* ConnectionTimeoutParams_active_secs */

static double ConnectionTimeoutParams_idle_secs(ConnectionTimeoutParams *tp) {
  return ((double) (time_millis() - tp->last_active)) / 1000.0;
} /* ConnectionTimeoutParams_idle_secs */

/**** ConnectionTimeoutParams MEMBER FUNC DEFS AND SUPPORTING FUNCS: END *****/

/****** CConnection ACTIVATION AND DEACTIVATION INFRASTRUCTURE: BEGIN ********/
static int Connection_activate(CConnection *con,
    const boolean con_tp_already_locked,
    const boolean allow_transparent_resumption
  )
{
  /* The GIL must be held when this function is called. */
  int status = 0;


  if (!Connection_timeout_enabled(con)) {
    if (con->state != CON_STATE_OPEN) {
      raise_exception(ProgrammingError, "Invalid connection state.  The"
          " connection must be open to perform this operation."
        );
      status = -1;
    }
  } else {
    ConnectionTimeoutParams *tp = con->timeout;
    ConnectionOpState achieved_state;

    assert (tp != NULL);
    assert (NOT_RUNNING_IN_CONNECTION_TIMEOUT_THREAD);

    /* In checked build, verify that the caller's claim about lock ownership
     * (delivered via boolean con_tp_already_locked) matches reality: */
    assert (
          con_tp_already_locked
        ?  CURRENT_THREAD_OWNS_CON_TP(con)
        : !CURRENT_THREAD_OWNS_CON_TP(con)
      );
    if (!con_tp_already_locked) {
      ACQUIRE_TP_WITH_GIL_HELD(tp);
    }
    assert (CURRENT_THREAD_OWNS_CON_TP(con));

    achieved_state = ConnectionTimeoutParams_trans_while_already_locked(tp,
        CONOP_IDLE, CONOP_ACTIVE
      );

    switch (achieved_state) {
      case CONOP_ACTIVE:
        /* Everything's fine. */
        break;

      case CONOP_TIMED_OUT_TRANSPARENTLY:
        if (allow_transparent_resumption) {
          /* Temporarily disassociate tp from con, then call
           * Connection_attach_from_members, which will reassociate tp and con
           * iff it's successful. */
          assert (tp == con->timeout);
          con->timeout = NULL;
          tp->state = CONOP_ACTIVE;
          status = Connection_attach_from_members(con, tp);

          if (status != 0) {
            PyObject *ex_type;
            PyObject *ex_value;
            PyObject *ex_traceback;
            PyObject *prev_msg = NULL;
            PyObject *new_msg = NULL;

            assert (PyErr_Occurred());
            PyErr_Fetch(&ex_type, &ex_value, &ex_traceback);

            prev_msg = PyObject_Str(ex_value);
            if (prev_msg != NULL) {
              new_msg = PyString_FromFormat("Attempt to reattach"
                  " transparently-timed-out connection failed with error:  %s",
                  PyString_AS_STRING(prev_msg)
                );
            }
            if (new_msg == NULL) {
              /* We didn't succeed in creating the new error message (probably
               * due to low memory), so restore the previous exception.
               * (PyErr_Restore reclaims our owned reference to each of its
               * arguments.) */
              PyErr_Restore(ex_type, ex_value, ex_traceback);
            } else {
              raise_exception(OperationalError, PyString_AS_STRING(new_msg));
              Py_DECREF(new_msg);
              /* The previous exception objects are now obsolete: */
              Py_XDECREF(ex_type);
              Py_XDECREF(ex_value);
              Py_XDECREF(ex_traceback);
            }

            Py_XDECREF(prev_msg);
          } else {
            const ConnectionOpState achieved_state =
              ConnectionTimeoutParams_trans_while_already_locked(tp,
                  CONOP_IDLE, CONOP_ACTIVE
                );
            if (achieved_state != CONOP_ACTIVE) {
              PyObject *err_msg;

              status = -1;
              {
                const char *achieved_state_desc = ConnectionOpState_describe(
                    achieved_state
                  );
                assert (achieved_state_desc != NULL);
                err_msg = PyString_FromFormat("Unable to reactivate"
                    " transparently-timed-out connection:  Could not"
                    " transition from state IDLE to ACTIVE (achieved state %s"
                    " instead).", achieved_state_desc
                  );
              }
              if (err_msg != NULL) {
                raise_exception(OperationalError, PyString_AS_STRING(err_msg));
                Py_DECREF(err_msg);
              }
            }
          }

          if (status == 0) {
            /* tp and con should've been reassociated: */
            assert (con->timeout == tp);
            assert (tp->state == CONOP_ACTIVE);
          } else {
            assert (PyErr_Occurred());
            if (con->timeout == NULL) {
              /* Connection_attach_from_members did not succeed, so it didn't
               * associate tp with con.  We must make the reassociation so that
               * the failure to transparently reconnect con is recorded: */
              con->timeout = tp;
            }
            tp->state = CONOP_TIMED_OUT_NONTRANSPARENTLY;
          }
          break;
        } /* Else (!allow_transparent_resumption), fall through to next: */

      case CONOP_TIMED_OUT_NONTRANSPARENTLY:
        status = -1;
        raise_exception(ConnectionTimedOut, "A transaction was still"
            " unresolved when this connection timed out, so it cannot be"
            " transparently reactivated."
          );
        break;

      case CONOP_IDLE:
        status = -1;
        raise_exception(OperationalError, "Unable to activate idle"
            " connection."
          );
        break;

      case CONOP_PERMANENTLY_CLOSED:
        status = -1;
        raise_exception(ProgrammingError, "Cannot operate on a permanently"
            " closed connection."
          );
        break;
    }

    if (!con_tp_already_locked) {
      TP_UNLOCK(tp);
    }
  }

  return status;
} /* Connection_activate */

static PyObject *Connection__read_activity_stamps(
    PyObject *self, PyObject *args
  )
{
  /* The GIL must be held when this function is called. */
  /* This function returns a 2-tuple of Python ints (or longs) of the form:
   *   (last_active, soonest_might_time_out)
   * If timeout is not enabled for the connection, this method returns None.
   * This function is not part of the public Connection API; it is intended
   * solely for verification purposes (see test_connection_timeouts.py in the
   * test suite). */
  PyObject *py_ret; /* 2-tuple. */
  CConnection *con;

  if (!PyArg_ParseTuple(args, "O!", &ConnectionType, &con)) { return NULL; }

  if (!Connection_timeout_enabled(con)) { RETURN_PY_NONE; }

  ACQUIRE_CON_TP_WITH_GIL_HELD(con);
  py_ret = Py_BuildValue("LL",
      con->timeout->last_active, con->timeout->soonest_might_time_out
    );
  TP_UNLOCK(con->timeout);

  return py_ret;
} /* Connection__read_activity_stamps */
/******* CConnection ACTIVATION AND DEACTIVATION INFRASTRUCTURE: END *********/
