/* Copyright (C) 2014 InfiniDB, Inc.
   Copyright (C) 2016 MariaDB Corporation

   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public License
   as published by the Free Software Foundation; version 2 of
   the License.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
   MA 02110-1301, USA. */

/*****************************************************************************
 * $Id$
 *
 ****************************************************************************/

/** @file
 * This file contains the longer SessionManager stress tests.
 */

#include <string>
#include <typeinfo>
#include <sstream>
#include <stdexcept>
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <values.h>
#include <errno.h>
#include <signal.h>
#include "calpontsystemcatalog.h"
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include "sessionmanager.h"
#include <cppunit/extensions/HelperMacros.h>
#include <boost/thread.hpp>

using namespace std;
using namespace execplan;

int threadStop, threadNum;

static void* SMRunner(void* arg)
{
  SessionManager* sm;
  int op;
  uint32_t seed, sessionnum;
  SessionManager::TxnID tmp;
  int tNum = reinterpret_cast<int>(arg);

  struct entry
  {
    SessionManager::TxnID tid;
    uint32_t sessionnum;
    struct entry* next;
  };

  struct entry* e;
  struct entry* listHead = NULL;
  struct entry* listTail = NULL;

  cerr << "Thread " << tNum << " started." << endl;

  seed = time(NULL) % MAXINT;

  sm = new SessionManager();

  while (!threadStop)
  {
    sm->verifySize();
    op = rand_r(&seed) % 4;  // 0 = newTxnID, 1 = committed, 2 = getTxnID, 3 = delete sm; new sm
    sessionnum = rand_r(&seed);

    switch (op)
    {
      case 0:
        e = new struct entry;
        e->tid = sm->newTxnID(sessionnum, false);

        if (e->tid.valid == false)  // SM is full
        {
          delete e;
          break;
        }

        e->sessionnum = sessionnum;
        e->next = NULL;

        if (listTail != NULL)
          listTail->next = e;
        else
          listHead = e;

        listTail = e;
        break;

      case 1:
        if (listHead == NULL)
          continue;

        sm->committed(listHead->tid);
        e = listHead;
        listHead = listHead->next;

        if (listHead == NULL)
          listTail = NULL;

        delete e;
        break;

      case 2:
        if (listHead == NULL)
          continue;

        tmp = sm->getTxnID(listHead->sessionnum);
        CPPUNIT_ASSERT(tmp.valid == listHead->tid.valid == true);
        // there's some risk of collision here if 2 threads happen to choose the
        // same session number
        // CPPUNIT_ASSERT(tmp.id == listHead->tid.id);
        break;

      case 3:
        delete sm;
        sm = new SessionManager();
        break;

      default: cerr << "SMRunner: ??" << endl;
    };
  }

  while (listHead != NULL)
  {
    e = listHead;
    listHead = listHead->next;
    delete e;
  }

  delete sm;
  cerr << "Thread " << tNum << " exiting." << endl;
  pthread_exit(0);
}

class ExecPlanTest : public CppUnit::TestFixture
{
  CPPUNIT_TEST_SUITE(ExecPlanTest);
  CPPUNIT_TEST(sessionManager_3);
  unlink("/tmp/CalpontShm");
  CPPUNIT_TEST_SUITE_END();

 public:
  /*
   * destroySemaphores() and destroyShmseg() will print error messages
   * if there are no objects to destroy.  That's OK.
   */
  void destroySemaphores()
  {
    key_t semkey;
    char* semseed = "/usr/local/mariadb/columnstore/etc/Columnstore.xml";
    int sems, err;

    // 		semkey = ftok(semseed, 0x2149bdd2);   // these things must match in the SM constructor
    semkey = 0x2149bdd2;

    if (semkey == -1)
      perror("tdriver: ftok");

    sems = semget(semkey, 2, 0666);

    if (sems != -1)
    {
      err = semctl(sems, 0, IPC_RMID);

      if (err == -1)
        perror("tdriver: semctl");
    }
  }

  void destroyShmseg()
  {
    key_t shmkey;
    char* shmseed = "/usr/local/mariadb/columnstore/etc/Columnstore.xml";
    int shms, err;

    // 		shmkey = ftok(shmseed, 0x2149bdd2);   // these things much match in the SM constructor
    shmkey = 0x2149bdd2;

    if (shmkey == -1)
      perror("tdriver: ftok");

    shms = shmget(shmkey, 0, 0666);

    if (shms != -1)
    {
      err = shmctl(shms, IPC_RMID, NULL);

      if (err == -1 && errno != EINVAL)
      {
        perror("tdriver: shmctl");
        return;
      }
    }
  }

  /** This launches several threads to stress test the Session Manager
   */
  void sessionManager_3()
  {
    const int threadCount = 4;
    int i;
    pthread_t threads[threadCount];

    cerr << endl
         << "Multithreaded SessionManager test.  "
            "This runs for 2 minutes."
         << endl;

    destroySemaphores();
    destroyShmseg();
    unlink("/tmp/CalpontShm");

    threadStop = 0;

    for (i = 0; i < threadCount; i++)
    {
      if (pthread_create(&threads[i], NULL, SMRunner, reinterpret_cast<void*>(i + 1)) < 0)
        throw logic_error("Error creating threads for the Session Manager test");

      usleep(1000);
    }

    sleep(120);
    threadStop = 1;

    for (i = 0; i < threadCount; i++)
      pthread_join(threads[i], NULL);

    destroySemaphores();
    destroyShmseg();
  }
};

CPPUNIT_TEST_SUITE_REGISTRATION(ExecPlanTest);

#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/ui/text/TestRunner.h>

int main(int argc, char** argv)
{
  CppUnit::TextUi::TestRunner runner;
  CppUnit::TestFactoryRegistry& registry = CppUnit::TestFactoryRegistry::getRegistry();
  runner.addTest(registry.makeTest());
  bool wasSuccessful = runner.run("", false);
  return (wasSuccessful ? 0 : 1);
}
