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

#ifdef ENABLE_DB_EVENT_SUPPORT

/* This file is designed to be directly included in _kievents.c; the code here
 * has been placed in this file for organizational rather than compilational
 * purposes.
 *
 * Notes:
 *   - NO CODE IN THIS FILE *EVER* HOLDS THE GIL, OR MANIPULATES PYTHON OBJECTS
 *     IN ANY WAY.                                                           */

#define REQUIRE_EXECUTING_IN_EVENT_OP_THREAD(EventOpThreadContext_) \
  assert (Thread_ids_equal( \
      Thread_current_id(), (EventOpThreadContext_)->event_op_thread_id \
    ));

static int EventOpThreadContext_close_all_except_admin_comm_objects(
    EventOpThreadContext *self
  );

static int EventOpThreadContext_record_error(
    EventOpThreadContext *self, const char *preamble
  );

/************************* EventOpNode METHODS:BEGIN *************************/
static void EventOpNode_del(void *_n) {
  /* EventOpNodes can have complex payloads, the components of which might need
   * to be freed. */
  EventOpNode *n = (EventOpNode *) _n;
  assert (n != NULL);

  if (n->payload != NULL) {
    switch (n->op_code) {
      case OP_RECORD_AND_REREGISTER: {
          EventCallbackOutputNode *cbn =
            (EventCallbackOutputNode *) n->payload;
          if (cbn->updated_buf != NULL) {
            kimem_plain_free(cbn->updated_buf);
          }
        } break;

      case OP_CONNECT: {
          ConnParamsNode *p = (ConnParamsNode *) n->payload;
          if (p->dsn != NULL) {
            kimem_plain_free(DV_STR(p->dsn));
          }
          if (p->dpb != NULL) {
            kimem_plain_free(DV_STR(p->dpb));
          }
        } break;

      /* Didn't use 'default:' here because want compiler warning if new opcode
       * is added but this switch isn't updated to handle it. */
      case OP_DIE:
      case OP_CALLBACK_ERROR:
      case OP_REGISTER:
        break;
    }

    kimem_plain_free(DV_VOID(n->payload));
  }

  /* Note that this function frees the memory of the EventOpNode struct
   * itself, leaving the caller holding an invalid pointer. */
  kimem_plain_free(n);
} /* EventOpNode_del */
/************************** EventOpNode METHODS:END **************************/

/************************* EventOpQueue METHODS:BEGIN ************************/
 /* EventOpQueue doesn't actually exist as a type distinct from
  * ThreadSafeFIFOQueue, but for organizational purposes, it's convenient to
  * pretend that it is a distinct type. */

static int EventOpQueue_request(ThreadSafeFIFOQueue *self,
    EventOpThreadOpCode op_code, int tag, void *payload
  )
{
  EventOpNode *n = (EventOpNode *) kimem_plain_malloc(sizeof(EventOpNode));
  if (n == NULL) { goto fail; }

  n->op_code = op_code;
  n->tag = tag;
  n->payload = payload;

  if (ThreadSafeFIFOQueue_put(self, n, EventOpNode_del) != 0) { goto fail; }

  return 0;

  fail:
    if (n != NULL) {
      /* Notice that we free n itself, but we don't call EventOpNode_del, which
       * would free n->payload.  Since this call failed, the caller is
       * responsible for freeing n->payload. */
      kimem_plain_free(n);
    }

    return -1;
} /* EventOpQueue_request */

/************************** EventOpQueue METHODS:END *************************/

/*********************** AdminResponseNode METHODS:BEGIN *********************/
static void AdminResponseNode_del(void *_n) {
  AdminResponseNode *n = (AdminResponseNode *) _n;
  assert (n != NULL);

  if (n->message != NULL) {
    kimem_plain_free(DV_STR(n->message));
  }

  /* Note that this function frees the memory of the AdminResponseNode struct
   * itself, leaving the caller holding an invalid pointer. */
  kimem_plain_free(n);
} /* AdminResponseNode_del */
/************************ AdminResponseNode METHODS:END **********************/

/*********************** AdminResponseQueue METHODS:BEGIN ********************/
 /* AdminResponseQueue doesn't actually exist as a type distinct from
  * ThreadSafeFIFOQueue, but for organizational purposes, it's convenient to
  * pretend that it is a distinct type. */
static int AdminResponseQueue_post(ThreadSafeFIFOQueue *self,
    EventOpThreadOpCode op_code, long status,
    ISC_STATUS sql_error_code, char *msg
  )
{
  AdminResponseNode *n = (AdminResponseNode *) kimem_plain_malloc(
      sizeof(AdminResponseNode)
    );
  if (n == NULL) { goto fail; }

  n->op_code = op_code;
  n->status = status;
  n->sql_error_code = sql_error_code;

  if (msg == NULL) {
    n->message = NULL;
  } else {
    const size_t msg_len = strlen(msg);
    if (msg_len > 0) {
      n->message = kimem_plain_malloc(msg_len + 1);
      if (n->message == NULL) { goto fail; }
      strncpy(DV_STR(n->message), msg, msg_len + 1);
      assert (n->message[msg_len] == '\0');
    }
  }

  if (ThreadSafeFIFOQueue_put(self, n, AdminResponseNode_del) != 0) {
    goto fail;
  }

  return 0;

  fail:
    if (n != NULL) {
      /* Notice that we free n itself, but *not* n->message.  Since this call
       * failed, the caller is responsible for freeing n->message. */
      kimem_plain_free(n);
    }

    return -1;
} /* AdminResponseQueue_post */

static int AdminResponseQueue_require(ThreadSafeFIFOQueue *self,
    EventOpThreadOpCode op_code, long status,
    ISC_STATUS *sql_error_code, char **message, long timeout_millis
  )
{
  /* This method gets an AdminResponseNode from the queue, potentially waiting
   * up to timeout_millis.  A timeout is considered failure.
   *
   * Once the node has been retrieved, if its op_code and status members do not
   * match the provided values, this function will set *message to the node's
   * message, then release the node (but not its message, obviously) and return
   * -1.  Note that the node's message might be NULL.
   *
   * Otherwise, both the node and its message will be released, and this
   * function will return 0. */
  int res = -1;
  AdminResponseNode *n = NULL;
  WaitResult wait_res;

  assert (self != NULL);
  assert (*sql_error_code == 0);
  assert (*message == NULL);

  wait_res = ThreadSafeFIFOQueue_get(self, timeout_millis, (void **) &n);

  if (wait_res == WR_WAIT_OK) {
    assert (n != NULL);
    res = (n->op_code == op_code && n->status == status) ? 0 : -1;
  }
  if (n != NULL) {
    if (res != 0) {
      *sql_error_code = DV_ISC_STATUS(n->sql_error_code);
      *message = DV_STR(n->message);
    } else {
      if (n->message != NULL) {
        kimem_plain_free(DV_STR(n->message));
        n->message = NULL;
      }
    }
    kimem_plain_free(n);
    n = NULL;
  }

  return res;
} /* AdminResponseQueue_require */
/************************ AdminResponseQueue METHODS:END *********************/

/************************ EventFiredNode METHODS:BEGIN ***********************/
static void EventFiredNode_del(void *_n) {
  assert (_n != NULL);

  /* At present, the counts member is a fixed-size array, so it doesn't need to
   * be freed separately from the EventFiredNode struct. */

  /* Note that this function frees the memory of the EventFiredNode struct
   * itself, leaving the caller holding an invalid pointer. */
  kimem_plain_free(_n);
} /* EventFiredNode_del */
/************************* EventFiredNode METHODS:END ************************/

/************************ EventFiredQueue METHODS:BEGIN **********************/
 /* EventFiredQueue doesn't actually exist as a type distinct from
  * ThreadSafeFIFOQueue, but for organizational purposes, it's convenient to
  * pretend that it is a distinct type. */

static WaitResult EventFiredQueue_get(ThreadSafeFIFOQueue *self,
    long timeout_millis, EventFiredNode **n
  )
{
  WaitResult wait_res = WR_WAIT_ERROR;

  assert (self != NULL);
  assert (*n == NULL);

  wait_res = ThreadSafeFIFOQueue_get(self, timeout_millis, (void **) n);
  assert (wait_res != WR_WAIT_OK ? *n == NULL : TRUE);

  return wait_res;
} /* EventFiredQueue_get */

static int EventFiredQueue_post(ThreadSafeFIFOQueue *self,
    int block_number, ISC_ULONG *counts
  )
{
  int res = -1;
  int i;
  EventFiredNode *count_node = (EventFiredNode *)
    kimem_plain_malloc(sizeof(EventFiredNode));
  if (count_node == NULL) { goto fail; }

  count_node->block_number = block_number;
  for (i = 0; i < EVENT_BLOCK_SIZE; i++) {
    count_node->counts[i] = (long) counts[i];
  }

  if (ThreadSafeFIFOQueue_put(self, count_node, EventFiredNode_del) != 0) {
    goto fail;
  }

  res = 0;
  goto exit;

  fail:
    assert (res == -1);
    if (count_node != NULL) {
      kimem_plain_free(count_node);
    }
    /* Fall through to exit: */

  exit:
    return res;
} /* EventFiredQueue_post */

/************************* EventFiredQueue METHODS:END ***********************/

/****************** EventCallbackThreadContext METHODS:BEGIN *****************/
static int EventCallbackThreadContext_init(EventCallbackThreadContext *self,
    ThreadSafeFIFOQueue *op_q, int block_number
  )
{
  /* This method is called by the thread that creates the EventConduit, *not*
   * by the EventCallbackThread itself (the EventCallbackThread needs its
   * context to be initialized before it can run). */
  assert (self->state == ECALL_UNINITIALIZED);
  self->state = ECALL_DUMMY;

  self->block_number = block_number;

  self->op_thread_id = 0;

  self->op_q = op_q;

  if (Mutex_init(&self->lock) != 0) { goto fail; }

  return 0;

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

static int EventCallbackThreadContext_close(EventCallbackThreadContext *self) {
  /* This method is called by the thread that's closing the EventConduit, *not*
   * by the EventCallbackThread itself.  And obviously this method isn't
   * thread-safe. */
  self->state = ECALL_DEAD;
  self->op_q = NULL;

  if (Mutex_close(&self->lock) != 0) { return -1; }

  return 0;
} /* EventCallbackThreadContext_close */

static void EventCallbackThreadContext__event_callback(
    EventCallbackThreadContext *self,
    const unsigned short updated_buf_len,
    const UPDATED_BUF_SIGNEDNESS char *updated_buf
  )
{
  /* In all but one of its iterations in a given context, this function is
   * called by a thread (the EventCallbackThread) that's started and managed by
   * the DB client library.  Such calls simply pass notification of the event
   * to the EventOpThread by posting an EventCallbackOutputNode to its op_q.
   *
   * In the exceptional case, this function is executed by the EventOpThread
   * when it calls isc_cancel_events. */
  boolean succeeded = FALSE;
  EventCallbackOutputNode *n = NULL;

  if (Mutex_lock(&self->lock) != 0) { return; }
  /* Critical section within these brackets: */
  {
    if (Thread_ids_equal(Thread_current_id(), self->op_thread_id)) {
      self->state = ECALL_DEAD;
      succeeded = TRUE;
    } else if (self->state == ECALL_DEAD) {
      /* The EventOpThread must've cancelled the registration while we (the
       * EventCallbackThread) were waiting for self->lock; just exit. */
      succeeded = TRUE;
    } else {
      assert (self->state == ECALL_DUMMY || self->state == ECALL_NORMAL);

      n = kimem_plain_malloc(sizeof(EventCallbackOutputNode));
      if (n != NULL) {
        boolean allocation_succeeded = TRUE;

        n->block_number = self->block_number;

        if (updated_buf_len == 0) {
          n->updated_buf = NULL;
        } else {
          n->updated_buf = kimem_plain_malloc(updated_buf_len);
          allocation_succeeded = (n->updated_buf != NULL);
          if (allocation_succeeded) {
            memcpy(n->updated_buf, updated_buf, updated_buf_len);
          }
        }
        if (allocation_succeeded) {
          /* Enqueue the op request (note that we send self->state in the
           * event op node's ->tag so that the EventOpThread will know
           * whether it's receiving a dummy notification or the real thing). */
          if (EventOpQueue_request(DV_Q(self->op_q), OP_RECORD_AND_REREGISTER,
                self->state, n
              ) == 0
             )
          {
            succeeded = TRUE;
            if (self->state == ECALL_DUMMY) { self->state = ECALL_NORMAL; }
          } else {
            assert (succeeded == FALSE);
            self->state = ECALL_DEAD;
          }
        }
      }
    }

    if (!succeeded) {
      if (n != NULL) {
        if (n->updated_buf != NULL) {
          kimem_plain_free(n->updated_buf);
        }
        kimem_plain_free(n);
      }

      /* If the op queue has already been cancelled, then obviously the
       * EventOpThread (which spends most of its time waiting on that very
       * queue) knows that the event registration that this callback was
       * attempting to support is not longer valid.  Otherwise, post an
       * OP_CALLBACK_ERROR. */
      if (!ThreadSafeFIFOQueue_is_cancelled(DV_Q(self->op_q))) {
        if (EventOpQueue_request(DV_Q(self->op_q), OP_CALLBACK_ERROR,
              NO_TAG, NULL
            ) != 0
           )
        {
          /* In a low memory situation, it's entirely possible that posting a
           * failure message to the op queue will fail.  In such a case, we
           * resort to forcibly cancelling the op queue so its listener (the
           * EventOpThread) won't deadlock.
           *
           * If even that cancellation attempt doesn't work, we resort to
           * taking down the entire process (it's better to do something
           * drastic than to let a deadlock go undiagnosed). */
          if (ThreadSafeFIFOQueue_cancel(DV_Q(self->op_q)) != 0) {
            fprintf(stderr, "EventCallbackThreadContext__event_callback"
                " killing process after fatal error to avoid deadlock.\n"
              );
            exit(1);
          }
        }
      }
    }
  }
  Mutex_unlock(&self->lock);
} /* EventCallbackThreadContext__event_callback */

/******************* EventCallbackThreadContext METHODS:END ******************/

/********************* EventOpThreadContext METHODS:BEGIN ********************/
static int EventOpThreadContext_init(EventOpThreadContext *self,
    /* Pointer to the queue into which EventFiredNodes should be posted. */
    ThreadSafeFIFOQueue *event_q,
    int n_event_blocks
  )
{
  /* This method is called by the thread that creates the EventConduit, *not*
   * by the EventOpThread itself (the EventOpThread needs its context to be
   * initialized before it can run). */
  int i;
  PlatformMutexType *lock = &self->lock;
  ThreadSafeFIFOQueue *op_q = &self->op_q;
  ThreadSafeFIFOQueue *admin_response_q = &self->admin_response_q;

  boolean init_lock = FALSE;
  boolean init_op_q = FALSE;
  boolean init_admin_response_q = FALSE;
  int init_n_erbs = 0;

  /* Nullify self's fields: */
  self->state = OPTHREADSTATE_NONE;
  self->event_op_thread_id = 0;
  self->n_event_blocks = n_event_blocks;
  self->er_blocks = NULL;

  self->db_handle = NULL_DB_HANDLE;

  self->error_info = NULL;

  self->event_q = event_q;

  /* Initialize self's fields: */

  /* lock: */
  if (Mutex_init(lock) != 0) { goto fail; }
  init_lock = TRUE;

  /* op_q: */
  if (ThreadSafeFIFOQueue_init(op_q) != 0) { goto fail; }
  init_op_q = TRUE;

  /* admin_response_q: */
  if (ThreadSafeFIFOQueue_init(admin_response_q) != 0) { goto fail; }
  init_admin_response_q = TRUE;

  /* er_blocks: */
  self->er_blocks = kimem_plain_malloc(
      sizeof(EventRequestBlock) * n_event_blocks
    );
  if (self->er_blocks == NULL) { goto fail; }

  for (i = 0; i < n_event_blocks; i++) {
    EventRequestBlock *erb = DV_ERB(self->er_blocks + i);
    erb->event_id = NULL_EVENT_ID;
    erb->req_buf = NULL;
    erb->req_buf_len = -1;
    erb->callback_ctx.state = ECALL_UNINITIALIZED;
  }

  for (i = 0; i < n_event_blocks; i++) {
    EventRequestBlock *erb = DV_ERB(self->er_blocks + i);
    if (EventCallbackThreadContext_init(DV_CALCTX(&erb->callback_ctx), op_q, i)
        != 0
       )
    { goto fail; }
    ++init_n_erbs;
  }

  return 0;

  fail:
    if (init_lock) { Mutex_close(lock); }
    if (init_op_q) { ThreadSafeFIFOQueue_close(op_q); }
    if (init_admin_response_q) { ThreadSafeFIFOQueue_close(admin_response_q); }
    if (self->er_blocks != NULL) {
      for (i = 0; i < init_n_erbs; i++) {
        EventRequestBlock *erb = DV_ERB(self->er_blocks + i);
        EventCallbackThreadContext_close(DV_CALCTX(&erb->callback_ctx));
      }
      kimem_plain_free(DV_ERB(self->er_blocks));
    }

    return -1;
} /* EventOpThreadContext_init */

static int EventOpThreadContext_change_state_while_already_locked(
    EventOpThreadContext *self, EventOpThreadState new_state, char *msg
  )
{
  /* The caller must already hold self->lock. */
  #ifndef NDEBUG
  const EventOpThreadState old_state = self->state;
  #endif
  ISC_STATUS sql_error_code = 0;
  assert (new_state > old_state);
  self->state = new_state;


  switch (new_state) {
    case OPTHREADSTATE_FATALLY_WOUNDED:
      EventOpThreadContext_close_all_except_admin_comm_objects(self);
      /* This non-routine death requires an explicit post to avoid stalling the
       * administrative thread(s): */

      if (self->error_info != NULL && self->error_info->msg != NULL) {
        msg = self->error_info->msg;
        sql_error_code = self->error_info->code;
      }
      if (msg == NULL) {
        msg = "EventOpThread encountered unspecified fatal error.";
      }

      if (AdminResponseQueue_post(&self->admin_response_q, OP_DIE, -1,
            sql_error_code, msg
          ) != 0
         )
      {
        /* Posting to the admin response queue didn't work (perhaps because of
         * a low memory condition), so cancel it, if that hasn't already been
         * done by another thread. */
        if (!ThreadSafeFIFOQueue_is_cancelled(&self->admin_response_q)) {
          if (ThreadSafeFIFOQueue_cancel(&self->admin_response_q) != 0) {
            /* Not even cancelling worked, so we know something is dreadfully
             * wrong.  Kill the process to avoid deadlock. */
            fprintf(stderr,
                "EventOpThreadContext_change_state_while_already_locked"
                " killing process after fatal error to avoid deadlock.\n"
              );
            exit(1);
          }
        }
      }
      break;

    case OPTHREADSTATE_DEAD:
      EventOpThreadContext_close_all_except_admin_comm_objects(self);
      /* It is the EventConduit's responsibility to release our most basic
       * members via EventOpThreadContext_close. */
      break;

    default:
      break;
  }

  return 0;
} /* EventOpThreadContext_change_state_while_already_locked */

static int EventOpThreadContext_change_state(EventOpThreadContext *self,
    EventOpThreadState new_state, char *msg
  )
{
  int res = -1;

  if (Mutex_lock(&self->lock) != 0) { goto exit; }
  /* Critical section within these brackets: */
  {
    res = EventOpThreadContext_change_state_while_already_locked(
        self, new_state, msg
      );
  }
  if (Mutex_unlock(&self->lock) != 0) { goto exit; }

  exit:
    return res;
} /* EventOpThreadContext_change_state */

static boolean EventOpThreadContext_has_state(EventOpThreadContext *self,
    EventOpThreadState requested_state
  )
{
  /* This function has the obvious limitation that the state could change
   * between the time the calling thread reads it and the time the calling
   * thread acts upon it.  However, this function is only used by the
   * EventOpThread's supervising thread in situations where this potential for
   * inconsistency is not a problem. */
  boolean has_requested_state;

  if (Mutex_lock(&self->lock) != 0) { goto fail; }
  /* Critical section within these brackets: */
  {
    has_requested_state = (boolean) (self->state == requested_state);
  }
  if (Mutex_unlock(&self->lock) != 0) { goto fail; }

  return has_requested_state;

  fail:
    return FALSE;
} /* EventOpThreadContext_has_state */

static int EventOpThreadContext_free_er_blocks(EventOpThreadContext *self) {
  /* The caller should hold the GDAL while calling. */
  int i;

  if (self->er_blocks != NULL) {
    for (i = 0; i < self->n_event_blocks; i++) {
      EventRequestBlock *erb = DV_ERB(self->er_blocks + i);

      /* event_id: */
      if (erb->event_id != NULL_EVENT_ID) {
        isc_cancel_events(DV_STATVEC(self->sv),
            DV_DB_HANDLE_PTR(&self->db_handle), DV_ISC_LONG_PTR(&erb->event_id)
          );
        if (DB_API_ERROR(self->sv)) {
          EventOpThreadContext_record_error(self,
              "EventOpThreadContext_free_er_blocks: "
            );
          goto fail;
        }
        erb->event_id = NULL_EVENT_ID;
      }

      /* req_buf: */
      if (erb->req_buf != NULL) {
        /* The kimem_db_client_* functions require that the GDAL be held when
         * they're called, but the function we're currently in places the same
         * requirement on its callers, so there's we needn't acquire the GDAL
         * here. */
        kimem_db_client_free(DV_STR(erb->req_buf));
        erb->req_buf = NULL;
      }
      /* req_buf_len: */
      erb->req_buf_len = -1;

      if (EventCallbackThreadContext_close(DV_CALCTX(&erb->callback_ctx))
          != 0
         )
      { goto fail; }
    }

    kimem_plain_free(DV_ERB(self->er_blocks));
    self->er_blocks = NULL;
  }

  return 0;

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

static int EventOpThreadContext_close_DB_API_members(
    EventOpThreadContext *self
  )
{
  /* This function assumes that the caller is already holding self->lock, and
   * that it's running in the EventOpThread itself.
   *
   * It is safe to call this function multiple times on the same
   * EventOpThreadContext. */
  int res = -1;

  REQUIRE_EXECUTING_IN_EVENT_OP_THREAD(self);

  ENTER_GDAL_WITHOUT_LEAVING_PYTHON

  if (EventOpThreadContext_free_er_blocks(self) != 0) { goto fail; }
  /* db_handle: */
  if (self->db_handle != NULL_DB_HANDLE) {
    #ifdef FIREBIRD_1_0_ONLY
      /* See lengthy note about workaround for apparent FB 1.0.x bug in the
       * 'fail' block of _kievents.c/pyob_EventConduit_create. */
      if (self->state != OPTHREADSTATE_FATALLY_WOUNDED) {
    #endif
    ENTER_GCDL_WITHOUT_LEAVING_PYTHON
    /* YYY:2006.08.08:
     *   With FB 1.0, 1.5 or 2.0, a crash sometimes occurs when this thread
     * attempts to detach its connection, *even though* this is the same thread
     * that created the connection, and the only thread to ever perform any
     * operations on it.
     *   It is widely agreed that the FB 1.5.x client library has a bug that can
     * cause problems such as this; it seems to me that the FB 1.0 and 2.0
     * client libraries have the same.
     *   I've observed this problem on both Win32 and Linux.
     *   In the KInterbasDB test suite,
     * __fb_client_lib_event_on_isc_detach_database.py is a simple example of
     * code that provokes the problem. */
    isc_detach_database(DV_STATVEC(self->sv), DV_DB_HANDLE_PTR(&self->db_handle));
    LEAVE_GCDL_WITHOUT_ENTERING_PYTHON

    if (DB_API_ERROR(self->sv)) {
      EventOpThreadContext_record_error(self,
          "EventOpThreadContext_close_DB_API_members: "
        );
      goto fail;
    }
    #ifdef FIREBIRD_1_0_ONLY
      }
    #endif
    self->db_handle = NULL_DB_HANDLE;
  }

  res = 0;
  goto exit;

  fail:
    assert (res == -1);
    /* Fall through to exit. */

  exit:
    LEAVE_GDAL_WITHOUT_ENTERING_PYTHON
    return res;
} /* EventOpThreadContext_close_DB_API_members */

static int EventOpThreadContext_reject_all_requests(
    EventOpThreadContext *self
  )
{
  if (ThreadSafeFIFOQueue_cancel(&self->op_q) != 0) { goto fail; }

  /* Note that we do *not* close self->admin_response_q.  That's only closed
   * when the supervising thread is finished with this EventOpThreadContext
   * entirely. */

  if (ThreadSafeFIFOQueue_cancel(DV_Q(self->event_q)) != 0) { goto fail; }

  return 0;

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

static int EventOpThreadContext_close_all_except_admin_comm_objects(
    EventOpThreadContext *self
  )
{
  if (EventOpThreadContext_reject_all_requests(self) != 0) { goto fail; }
  if (EventOpThreadContext_close_DB_API_members(self) != 0) { goto fail; }

  return 0;

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

static int EventOpThreadContext_close(EventOpThreadContext *self) {
  /* EventOpThreadContext_close_all_except_admin_comm_objects should've been
   * called previously, and it should've caused all of the EventRequestBlocks
   * to be released. */
  assert (self->er_blocks == NULL);

  if (ThreadSafeFIFOQueue_cancel(&self->admin_response_q) != 0) { goto fail; }

  if (Mutex_close(&self->lock) != 0) { goto fail; }

  if (self->error_info != NULL) {
    NonPythonSQLErrorInfo_destroy(self->error_info);
    self->error_info = NULL;
  }

  return 0;

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

static int EventOpThreadContext_record_error(
    EventOpThreadContext *self, const char *preamble
  )
{
  /* The caller should hold the GDAL while calling. */

  NonPythonSQLErrorInfo *se = extract_sql_error_without_python(
      DV_STATVEC(self->sv), preamble
    );
  if (se == NULL) { goto fail; }

  if (self->error_info != NULL) {
    NonPythonSQLErrorInfo_destroy(self->error_info);
  }
  self->error_info = se;
  return 0;

  fail:
    return -1;
}

/********************** EventOpThreadContext METHODS:END *********************/

/************************ EventOpThread METHODS:BEGIN ************************/
 /* The functions in this section are to called only by the EventOpThread. */
static int EventOpThread_connect(EventOpThreadContext *ctx, EventOpNode *n,
    char **msg
  )
{
  int res = -1;
  ConnParamsNode *cp = (ConnParamsNode *) n->payload;

  assert (cp != NULL);
  assert (cp->dsn != NULL);
  assert (cp->dsn_len > 0);
  assert (cp->dpb != NULL);
  assert (cp->dpb_len > 0);

  if (Mutex_lock(&ctx->lock) != 0) { goto fail; }
  /* Critical section within these brackets: */
  {
    REQUIRE_EXECUTING_IN_EVENT_OP_THREAD(ctx);
    assert (ctx->db_handle == NULL_DB_HANDLE);

    ENTER_GDAL_WITHOUT_LEAVING_PYTHON /* This thread never holds the GIL. */
    ENTER_GCDL_WITHOUT_LEAVING_PYTHON

    isc_attach_database(DV_STATVEC(ctx->sv),
      cp->dsn_len, DV_STR(cp->dsn),
      DV_DB_HANDLE_PTR(&ctx->db_handle),
      cp->dpb_len, DV_STR(cp->dpb)
    );

    LEAVE_GCDL_WITHOUT_ENTERING_PYTHON
    LEAVE_GDAL_WITHOUT_ENTERING_PYTHON /* This thread never holds the GIL. */

    if (DB_API_ERROR(ctx->sv)) {
      ENTER_GDAL_WITHOUT_LEAVING_PYTHON
      EventOpThreadContext_record_error(ctx, "EventOpThread_connect: ");
      LEAVE_GDAL_WITHOUT_ENTERING_PYTHON
      goto unlock;
    }

    assert (ctx->db_handle != NULL_DB_HANDLE);
    EventOpThreadContext_change_state_while_already_locked(ctx,
        OPTHREADSTATE_READY, NULL
      );

    res = 0;
  }
  unlock:
    if (Mutex_unlock(&ctx->lock) != 0) { goto fail; }

  goto exit;

  fail:
    assert (res == -1);
    /* Fall through to exit: */

  exit:
    return res;
} /* EventOpThread_connect */

static int EventOpThread_register(
    EventOpThreadContext *ctx, int block_number
  )
{
  int res = -1;

  if (Mutex_lock(&ctx->lock) != 0) { goto fail; }
  /* Critical section within these brackets: */
  {
    EventRequestBlock *erb;
    REQUIRE_EXECUTING_IN_EVENT_OP_THREAD(ctx);
    erb = DV_ERB(ctx->er_blocks + block_number);

    ENTER_GDAL_WITHOUT_LEAVING_PYTHON /* This thread never holds the GIL. */
    isc_que_events(DV_STATVEC(ctx->sv),
        DV_DB_HANDLE_PTR(&ctx->db_handle), DV_ISC_LONG_PTR(&erb->event_id),
        erb->req_buf_len,
        #ifdef FIREBIRD_2_0_OR_LATER
          (ISC_UCHAR *)
        #endif
        DV_STR(erb->req_buf),
        (EVENT_CALLBACK_FUNCTION) EventCallbackThreadContext__event_callback,
        DV_CALCTX(&erb->callback_ctx)
      );
    LEAVE_GDAL_WITHOUT_ENTERING_PYTHON /* This thread never holds the GIL. */

    if (DB_API_ERROR(ctx->sv)) {
      ENTER_GDAL_WITHOUT_LEAVING_PYTHON
      EventOpThreadContext_record_error(ctx, "EventOpThread_register: ");
      LEAVE_GDAL_WITHOUT_ENTERING_PYTHON
      goto unlock;
    }
    res = 0;
  }
  unlock:
    if (Mutex_unlock(&ctx->lock) != 0) { goto fail; }

  goto exit;

  fail:
    assert (res == -1);
    /* Fall through to exit: */

  exit:
    return res;
} /* EventOpThread_register */

static int EventOpThread_record_and_reregister(EventOpThreadContext *ctx,
    EventOpNode *n
  )
{
  int res = -1;
  ISC_ULONG counts_dest[EVENT_BLOCK_SIZE];
  EventCallbackOutputNode *cb_node = (EventCallbackOutputNode *) n->payload;

  /* The payload is an EventCallbackOutputNode bearing (in its 'counts' member)
   * a buffer that contains event counts updated to reflect the event firing
   * that caused this EventOpNode to be created.
   *
   * While in the critical section, we use isc_event_counts to calculate the
   * difference between the event counts before and after this firing; that
   * difference is stored temporarily in counts_dest (would've used ctx->sv, as
   * recommended by docs, but Firebird's x86-64 port is incompatible with that
   * approach).
   *
   * isc_event_counts also updates the appropriate EventRequestBlock's req_buf
   * to reflect the new counts, so that buffer will be ready for another call
   * to isc_que_events. */
  if (Mutex_lock(&ctx->lock) != 0) { goto fail; }
  /* Critical section within these brackets: */
  {
    EventRequestBlock *erb;
    REQUIRE_EXECUTING_IN_EVENT_OP_THREAD(ctx);

    /* If the payload is NULL or the EventCallbackThread encountered any other
     * error, this thread needs to die also. */
    if (     cb_node == NULL
        || !(n->tag == ECALL_DUMMY || n->tag == ECALL_NORMAL)
       )
    { goto fail_with_unlock; }

    assert (
           cb_node->block_number >= 0
        && cb_node->block_number <= ctx->n_event_blocks
      );
    assert (cb_node->updated_buf != NULL);

    erb = DV_ERB(ctx->er_blocks + cb_node->block_number);

    /* isc_event_counts (implemented as gds__event_counts in jrd/utl.cpp) is a
     * simple function; the only lock we need to hold while calling it is a
     * lock on ctx. */
    isc_event_counts(counts_dest, erb->req_buf_len,
        #ifdef FIREBIRD_2_0_OR_LATER
          (ISC_UCHAR *)
        #endif
        DV_STR(erb->req_buf),
        cb_node->updated_buf
      );

    /* erb->req_buf must be updated via isc_event_counts even after the dummy
     * run, but of course we don't post to the event_q after the dummy run. */
    if (n->tag != ECALL_DUMMY) {
      if (EventFiredQueue_post(DV_Q(ctx->event_q), cb_node->block_number,
            counts_dest
          ) != 0
         )
      { goto fail_with_unlock; }
    } else {
      /* Don't report back to the thread supervising the EventOpThread until
       * the dummy run associated with the last EventRequestBlock completes.
       * There's no deadlock danger in this strategy, because any callback that
       * encounters an error will take the necessary measures to ensure that
       * the EventOpThread is informed, and the EventOpThread will in turn make
       * sure the supervising thread is informed. */
      if (cb_node->block_number == ctx->n_event_blocks - 1) {
        if (AdminResponseQueue_post(&ctx->admin_response_q,
              OP_REGISTER, 0, 0, NULL
            ) != 0
           )
        { goto fail_with_unlock; }
      }
    }
  }
  if (Mutex_unlock(&ctx->lock) != 0) { goto fail; }

  /* We must not hold ctx->lock while calling EventOpThread_register. */
  if (EventOpThread_register(ctx, cb_node->block_number) != 0) { goto fail; }

  res = 0; /* Only now do we consider this operation a success. */
  goto exit;

  fail_with_unlock:
    Mutex_unlock(&ctx->lock);
    /* Fall through to fail: */

  fail:
    assert (res == -1);
    /* Fall through to exit: */

  exit:
    return res;
} /* EventOpThread_record_and_reregister */

static PlatformThreadFuncReturnType THREAD_FUNC_MODIFIER EventOpThread_main(
    void *_context
  )
{
  EventOpThreadContext *ctx = (EventOpThreadContext *) _context;
  PlatformThreadFuncReturnType status = THREAD_FUNC_RETURN_FAILURE;

  EventOpNode *n = NULL;
  char *msg = NULL;

  /* Some operations are only supposed to run once; these boolean flags are
   * used to detect a second request for such an op. */
  boolean OP_CONNECT_has_already_been_requested = FALSE;
  boolean OP_REGISTER_has_already_been_requested = FALSE;
  boolean OP_DIE_has_already_been_requested = FALSE;

  int i;

  if (Mutex_lock(&ctx->lock) != 0) { goto die_with_error; }
  /* Critical section within these brackets: */
  {
    REQUIRE_EXECUTING_IN_EVENT_OP_THREAD(ctx);
    assert (ctx->state == OPTHREADSTATE_NONE);
    EventOpThreadContext_change_state_while_already_locked(
        ctx, OPTHREADSTATE_WAITING_FOR_CONNECTION_REQUEST, NULL
      );

    {
      const PlatformThreadIdType thread_id = Thread_current_id();
      for (i = 0; i < ctx->n_event_blocks; i++) {
        EventRequestBlock *erb = DV_ERB(ctx->er_blocks + i);
        erb->callback_ctx.op_thread_id = thread_id;
      }
    }
  }
  if (Mutex_unlock(&ctx->lock) != 0) { goto die_with_error; }

  #define _RELEASE_EVENT_OP_NODE_IF_NECESSARY(node) \
    if (node != NULL) { \
      EventOpNode_del(node); \
      node = NULL; \
    }

  /* Note that ctx->lock is not *directly* manipulated within this loop (though
   * it is manipulated by functions that the code here calls). */
  for (;;) {
    assert (n == NULL);

    if (ThreadSafeFIFOQueue_get(&ctx->op_q, WAIT_INFINITELY_LONG, (void **) &n)
        != WR_WAIT_OK
       )
    { goto die_with_error; }

    switch(n->op_code) {
      case OP_RECORD_AND_REREGISTER: /* The routine case. */
        if (EventOpThread_record_and_reregister(ctx, n) != 0) {
          goto die_with_error;
        }
        break;

      case OP_CALLBACK_ERROR:
        {
          ISC_STATUS sql_error_code = 0;
          char *msg = "EventOpThread detected fatal error in EventCallbackThread.";
          if (ctx->error_info != NULL && ctx->error_info->msg != NULL) {
            sql_error_code = ctx->error_info->code;
            msg = ctx->error_info->msg;
          }
          AdminResponseQueue_post(&ctx->admin_response_q, OP_CALLBACK_ERROR, -1,
              sql_error_code, msg
            );
          goto die_with_error;
        }
        break;

      case OP_CONNECT: /* Should happen only once per EventOpThread. */
        if (OP_CONNECT_has_already_been_requested) { goto die_with_error; }
        OP_CONNECT_has_already_been_requested = TRUE;

        if (EventOpThread_connect(ctx, n, &msg) != 0) { goto die_with_error; }

        if (AdminResponseQueue_post(&ctx->admin_response_q, OP_CONNECT, 0,
              0, NULL
            ) != 0
           )
        { goto die_with_error; }
        break;

      case OP_REGISTER: /* Should happen only once per EventOpThread. */
        if (OP_REGISTER_has_already_been_requested) { goto die_with_error; }
        OP_REGISTER_has_already_been_requested = TRUE;

        for (i = 0; i < ctx->n_event_blocks; i++) {
          if (EventOpThread_register(ctx, i) != 0) { goto die_with_error; }
        }
        /* Notice that we don't immediately indicate to the admin thread that
         * the OP_REGISTER was successful.  Instead, we move to the next loop
         * iteration and wait for the EventCallbackThread to indicate the
         * success of the dummy run in each EventCallbackThreadContext; only
         * once we know that outcome do we report back to the waiting admin
         * thread (that reporting back is performed in
         * EventOpThread_record_and_reregister). */
        break;

      case OP_DIE: /* Should happen only once per EventOpThread. */
        if (OP_DIE_has_already_been_requested) { goto die_with_error; }
        OP_DIE_has_already_been_requested = TRUE;

        if (EventOpThreadContext_change_state(ctx, OPTHREADSTATE_DEAD,
              "EventOpThread received OP_DIE request."
            ) != 0
           )
        { goto die_with_error; }
        AdminResponseQueue_post(&ctx->admin_response_q, OP_DIE, 0, 0, NULL);

        status = THREAD_FUNC_RETURN_SUCCESS;
        goto exit;
    }

    msg = NULL;
    _RELEASE_EVENT_OP_NODE_IF_NECESSARY(n);
  }

  die_with_error:
    assert (status == THREAD_FUNC_RETURN_FAILURE);
    EventOpThreadContext_change_state(ctx, OPTHREADSTATE_FATALLY_WOUNDED, msg);

  exit:
    _RELEASE_EVENT_OP_NODE_IF_NECESSARY(n);
    return status;
} /* EventOpThread_main */
/************************* EventOpThread METHODS:END *************************/

#endif /* ENABLE_DB_EVENT_SUPPORT */
