xwords/xwords4/relay/dbmgr.cpp
Eric House 85d484a881 major mod to deal with devices that fail to receive ACK and then
reconnect.  I was putting both (i.e. the same device twice) in the
same game.  Now I detect this based on the seed being duplicated and
treat the device as having failed to ACK then proceed with the CONNECT
as if it were new.  Tested pretty heavily but only with two-device
games.
2011-06-20 18:13:15 -07:00

675 lines
20 KiB
C++

/* -*-mode: C; fill-column: 78; c-basic-offset: 4; -*- */
/*
* Copyright 2010 by Eric House (xwords@eehouse.org). All rights reserved.
*
* 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; either version 2
* of the License, or (at your option) any later version.
*
* 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include "dbmgr.h"
#include "mlock.h"
#include "xwrelay_priv.h"
#define DB_NAME "xwgames"
#define GAMES_TABLE "games"
#define MSGS_TABLE "msgs"
#define ARRAYSUM "sum_array(nPerDevice)"
static DBMgr* s_instance = NULL;
#define DELIM "\1"
static void formatParams( char* paramValues[], int nParams, const char* fmt,
char* buf, int bufLen, ... );
static int here_less_seed( const char* seeds, int perDeviceSum,
unsigned short seed );
/* static */ DBMgr*
DBMgr::Get()
{
if ( s_instance == NULL ) {
s_instance = new DBMgr();
}
return s_instance;
} /* Get */
DBMgr::DBMgr()
{
logf( XW_LOGINFO, "%s called", __func__ );
/* Now figure out what the largest cid currently is. There must be a way
to get postgres to do this for me.... */
/* const char* query = "SELECT cid FROM games ORDER BY cid DESC LIMIT 1"; */
/* PGresult* result = PQexec( m_pgconn, query ); */
/* if ( 0 == PQntuples( result ) ) { */
/* m_nextCID = 1; */
/* } else { */
/* char* value = PQgetvalue( result, 0, 0 ); */
/* m_nextCID = 1 + atoi( value ); */
/* } */
/* PQclear(result); */
/* logf( XW_LOGINFO, "%s: m_nextCID=%d", __func__, m_nextCID ); */
}
DBMgr::~DBMgr()
{
logf( XW_LOGINFO, "%s called", __func__ );
assert( s_instance == this );
s_instance = NULL;
}
void
DBMgr::AddNew( const char* cookie, const char* connName, CookieID cid,
int langCode, int nPlayersT, bool isPublic )
{
if ( !cookie ) cookie = "";
if ( !connName ) connName = "";
const char* command = "INSERT INTO " GAMES_TABLE
" (cid, room, connName, nTotal, lang, pub)"
" VALUES( $1, $2, $3, $4, $5, $6 )";
int nParams = 6;
char* paramValues[nParams];
char buf[512];
formatParams( paramValues, nParams,
"%d"DELIM"%s"DELIM"%s"DELIM"%d"DELIM"%d"DELIM"%s",
buf, sizeof(buf), cid, cookie, connName, nPlayersT,
langCode, isPublic?"TRUE":"FALSE" );
PGresult* result = PQexecParams( getThreadConn(), command,
nParams, NULL,
paramValues,
NULL, NULL, 0 );
if ( PGRES_COMMAND_OK != PQresultStatus(result) ) {
logf( XW_LOGERROR, "PQexec=>%s;%s", PQresStatus(PQresultStatus(result)),
PQresultErrorMessage(result) );
}
PQclear( result );
}
CookieID
DBMgr::FindGame( const char* connName, char* cookieBuf, int bufLen,
int* langP, int* nPlayersTP, int* nPlayersHP, bool* isDead )
{
CookieID cid = 0;
const char* fmt = "SELECT cid, room, lang, nTotal, nPerDevice, dead FROM "
GAMES_TABLE " WHERE connName = '%s'"
" LIMIT 1";
char query[256];
snprintf( query, sizeof(query), fmt, connName );
logf( XW_LOGINFO, "query: %s", query );
PGresult* result = PQexec( getThreadConn(), query );
if ( 1 == PQntuples( result ) ) {
cid = atoi( PQgetvalue( result, 0, 0 ) );
snprintf( cookieBuf, bufLen, "%s", PQgetvalue( result, 0, 1 ) );
*langP = atoi( PQgetvalue( result, 0, 2 ) );
*nPlayersTP = atoi( PQgetvalue( result, 0, 3 ) );
*nPlayersHP = atoi( PQgetvalue( result, 0, 4 ) );
*isDead = 't' == PQgetvalue( result, 0, 5 )[0];
}
PQclear( result );
logf( XW_LOGINFO, "%s(%s)=>%d", __func__, connName, cid );
return cid;
} /* FindGame */
bool
DBMgr::SeenSeed( const char* cookie, unsigned short seed,
int langCode, int nPlayersT, bool wantsPublic,
char* connNameBuf, int bufLen, int* nPlayersHP,
CookieID* cid )
{
int nParams = 5;
char* paramValues[nParams];
char buf[512];
formatParams( paramValues, nParams,
"%s"DELIM"%d"DELIM"%d"DELIM"%d"DELIM"%s", buf, sizeof(buf),
cookie, langCode, nPlayersT, seed,
wantsPublic?"TRUE":"FALSE" );
const char* cmd = "SELECT cid, connName, seeds, sum_array(nPerDevice) FROM "
GAMES_TABLE
" WHERE NOT dead"
" AND room ILIKE $1"
" AND lang = $2"
" AND nTotal = $3"
" AND $4 = ANY(seeds)"
" AND $5 = pub"
" ORDER BY ctime DESC"
" LIMIT 1";
PGresult* result = PQexecParams( getThreadConn(), cmd,
nParams, NULL,
paramValues,
NULL, NULL, 0 );
bool found = 1 == PQntuples( result );
if ( found ) {
*cid = atoi( PQgetvalue( result, 0, 0 ) );
*nPlayersHP = here_less_seed( PQgetvalue( result, 0, 2 ),
atoi( PQgetvalue( result, 0, 3 ) ),
seed );
snprintf( connNameBuf, bufLen, "%s", PQgetvalue( result, 0, 1 ) );
}
PQclear( result );
logf( XW_LOGINFO, "%s(%4X)=>%s", __func__, seed, found?"true":"false" );
return found;
}
CookieID
DBMgr::FindOpen( const char* cookie, int lang, int nPlayersT, int nPlayersH,
bool wantsPublic, char* connNameBuf, int bufLen,
int* nPlayersHP )
{
CookieID cid = 0;
int nParams = 5;
char* paramValues[nParams];
char buf[512];
formatParams( paramValues, nParams,
"%s"DELIM"%d"DELIM"%d"DELIM"%d"DELIM"%s", buf, sizeof(buf),
cookie, lang, nPlayersT, nPlayersH, wantsPublic?"TRUE":"FALSE" );
/* NOTE: ILIKE, for case-insensitive comparison, is a postgres extension
to SQL. */
const char* cmd = "SELECT cid, connName, sum_array(nPerDevice) FROM "
GAMES_TABLE
" WHERE NOT dead"
" AND room ILIKE $1"
" AND lang = $2"
" AND nTotal = $3"
" AND $4 <= nTotal-sum_array(nPerDevice)"
" AND $5 = pub"
" LIMIT 1";
PGresult* result = PQexecParams( getThreadConn(), cmd,
nParams, NULL,
paramValues,
NULL, NULL, 0 );
if ( 1 == PQntuples( result ) ) {
cid = atoi( PQgetvalue( result, 0, 0 ) );
snprintf( connNameBuf, bufLen, "%s", PQgetvalue( result, 0, 1 ) );
*nPlayersHP = atoi( PQgetvalue( result, 0, 2 ) );
/* cid may be 0, but should use game anyway */
}
PQclear( result );
logf( XW_LOGINFO, "%s=>%d", __func__, cid );
return cid;
} /* FindOpen */
bool
DBMgr::AllDevsAckd( const char* const connName )
{
const char* cmd = "SELECT ntotal=sum_array(nperdevice) AND 'A'=ALL(ack) from " GAMES_TABLE
" WHERE connName='%s'";
char query[256];
snprintf( query, sizeof(query), cmd, connName );
logf( XW_LOGINFO, "query: %s", query );
PGresult* result = PQexec( getThreadConn(), query );
int nTuples = PQntuples( result );
assert( nTuples <= 1 );
bool full = 't' == PQgetvalue( result, 0, 0 )[0];
PQclear( result );
logf( XW_LOGINFO, "%s=>%d", __func__, full );
return full;
}
HostID
DBMgr::AddDevice( const char* connName, HostID curID, int nToAdd,
unsigned short seed, bool ackd )
{
HostID newID = curID;
if ( newID == HOST_ID_NONE ) {
int arr[4] = {0};
readArray( connName, arr );
for ( newID = HOST_ID_SERVER; newID <= 4; ++newID ) {
if ( arr[newID-1] == 0 ) {
break;
}
}
}
assert( newID <= 4 );
const char* fmt = "UPDATE " GAMES_TABLE " SET nPerDevice[%d] = %d,"
" seeds[%d] = %d, mtimes[%d]='now', ack[%d]=\'%c\'"
" WHERE connName = '%s'";
char query[256];
snprintf( query, sizeof(query), fmt, newID, nToAdd, newID, seed, newID,
newID, ackd?'A':'a', connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
execSql( query );
return newID;
} /* AddDevice */
void
DBMgr::NoteAckd( const char* const connName, HostID id )
{
char query[256];
const char* fmt = "UPDATE " GAMES_TABLE " SET ack[%d]='A'"
" WHERE connName = '%s'";
snprintf( query, sizeof(query), fmt, id, connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
execSql( query );
}
bool
DBMgr::RmDeviceByHid( const char* connName, HostID hid )
{
const char* fmt = "UPDATE " GAMES_TABLE " SET nPerDevice[%d] = 0, "
"seeds[%d] = 0, ack[%d]='-', mtimes[%d]='now' WHERE connName = '%s'";
char query[256];
snprintf( query, sizeof(query), fmt, hid, hid, hid, hid, connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
return execSql( query );
}
void
DBMgr::RmDeviceBySeed( const char* const connName, unsigned short seed )
{
char seeds[128] = {0};
const char* fmt = "SELECT seeds FROM " GAMES_TABLE
" WHERE connName = '%s'"
" AND %d = ANY(seeds)";
char query[256];
snprintf( query, sizeof(query), fmt, connName, seed );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
PGresult* result = PQexec( getThreadConn(), query );
if ( 1 == PQntuples( result ) ) {
snprintf( seeds, sizeof(seeds), "%s", PQgetvalue( result, 0, 0 ) );
}
PQclear( result );
if ( 0 != seeds[0] ) {
char *saveptr = NULL;
int ii;
char* str;
for ( str = seeds, ii = 0; ; str = NULL, ++ii ) {
char* tok = strtok_r( str, "{},", &saveptr );
if ( NULL == tok ) {
break;
} else {
int asint = atoi( tok );
if ( asint == seed ) {
RmDeviceByHid( connName, ii + 1 );
break;
}
}
}
} else {
assert(0); /* but don't ship with this!!!! */
}
} /* RmDeviceSeed */
bool
DBMgr::HaveDevice( const char* connName, HostID hid, int seed )
{
bool found = false;
const char* fmt = "SELECT * from " GAMES_TABLE
" WHERE connName = '%s' AND seeds[%d] = %d";
char query[256];
snprintf( query, sizeof(query), fmt, connName, hid, seed );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
PGresult* result = PQexec( getThreadConn(), query );
found = 1 == PQntuples( result );
PQclear( result );
return found;
}
void
DBMgr::AddCID( const char* const connName, CookieID cid )
{
const char* fmt = "UPDATE " GAMES_TABLE " SET cid = %d "
" WHERE connName = '%s'";
char query[256];
snprintf( query, sizeof(query), fmt, cid, connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
execSql( query );
}
void
DBMgr::ClearCID( const char* connName )
{
const char* fmt = "UPDATE " GAMES_TABLE " SET cid = null "
"WHERE connName = '%s'";
char query[256];
snprintf( query, sizeof(query), fmt, connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
execSql( query );
}
void
DBMgr::RecordSent( const char* const connName, HostID hid, int nBytes )
{
assert( hid >= 0 && hid <= 4 );
const char* fmt = "UPDATE " GAMES_TABLE " SET"
" nsent = nsent + %d, mtimes[%d] = 'now'"
" WHERE connName = '%s'";
char query[256];
snprintf( query, sizeof(query), fmt, nBytes, hid, connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
execSql( query );
}
void
DBMgr::GetPlayerCounts( const char* const connName, int* nTotal, int* nHere )
{
const char* fmt = "SELECT ntotal, sum_array(nperdevice) FROM " GAMES_TABLE
" WHERE connName = '%s'";
char query[256];
snprintf( query, sizeof(query), fmt, connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
PGresult* result = PQexec( getThreadConn(), query );
assert( 1 == PQntuples( result ) );
*nTotal = atoi( PQgetvalue( result, 0, 0 ) );
*nHere = atoi( PQgetvalue( result, 0, 1 ) );
PQclear( result );
}
void
DBMgr::KillGame( const char* const connName, int hid )
{
const char* fmt = "UPDATE " GAMES_TABLE " SET dead = TRUE,"
" nperdevice[%d] = - nperdevice[%d]"
" WHERE connName = '%s'";
char query[256];
snprintf( query, sizeof(query), fmt, hid, hid, connName );
execSql( query );
}
void
DBMgr::ClearCIDs( void )
{
execSql( "UPDATE " GAMES_TABLE " set cid = null" );
}
void
DBMgr::PublicRooms( int lang, int nPlayers, int* nNames, string& names )
{
const char* fmt = "SELECT room, nTotal-sum_array(nPerDevice),"
" round( extract( epoch from age('now', ctime)))"
" FROM " GAMES_TABLE
" WHERE NOT dead"
" AND pub = TRUE"
" AND lang = %d"
" AND nTotal>sum_array(nPerDevice)"
" AND nTotal = %d";
char query[256];
snprintf( query, sizeof(query), fmt, lang, nPlayers );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
PGresult* result = PQexec( getThreadConn(), query );
int nTuples = PQntuples( result );
for ( int ii = 0; ii < nTuples; ++ii ) {
names.append( PQgetvalue( result, ii, 0 ) );
names.append( "/" );
names.append( PQgetvalue( result, ii, 1 ) );
names.append( "/" );
names.append( PQgetvalue( result, ii, 2 ) );
names.append( "\n" );
}
PQclear( result );
*nNames = nTuples;
}
int
DBMgr::PendingMsgCount( const char* connName, int hid )
{
int count = 0;
const char* fmt = "SELECT COUNT(*) FROM " MSGS_TABLE
" WHERE connName = '%s' AND hid = %d";
char query[256];
snprintf( query, sizeof(query), fmt, connName, hid );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
PGresult* result = PQexec( getThreadConn(), query );
if ( 1 == PQntuples( result ) ) {
count = atoi( PQgetvalue( result, 0, 0 ) );
}
PQclear( result );
return count;
}
bool
DBMgr::execSql( const char* query )
{
PGresult* result = PQexec( getThreadConn(), query );
bool ok = PGRES_COMMAND_OK == PQresultStatus(result);
if ( !ok ) {
logf( XW_LOGERROR, "PQexec=>%s;%s", PQresStatus(PQresultStatus(result)), PQresultErrorMessage(result) );
}
PQclear( result );
return ok;
}
void
DBMgr::readArray( const char* const connName, int arr[] ) /* len 4 */
{
const char* fmt = "SELECT nPerDevice FROM " GAMES_TABLE " WHERE connName='%s'";
char query[256];
snprintf( query, sizeof(query), fmt, connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
PGresult* result = PQexec( getThreadConn(), query );
assert( 1 == PQntuples( result ) );
const char* arrStr = PQgetvalue( result, 0, 0 );
sscanf( arrStr, "{%d,%d,%d,%d}", &arr[0], &arr[1], &arr[2], &arr[3] );
PQclear( result );
}
/*
id | connname | hid | msg
----+-----------+-----+---------
1 | abcd:1234 | 2 | xyzzx
2 | abcd:1234 | 2 | xyzzxxx
3 | abcd:1234 | 3 | xyzzxxx
*/
int
DBMgr::CountStoredMessages( const char* const connName, int hid )
{
const char* fmt = "SELECT count(*) FROM " MSGS_TABLE
" WHERE connname = '%s' ";
char query[256];
int len = snprintf( query, sizeof(query), fmt, connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
if ( hid != -1 ) {
snprintf( &query[len], sizeof(query)-len, "AND hid = %d",
hid );
}
PGresult* result = PQexec( getThreadConn(), query );
assert( 1 == PQntuples( result ) );
int count = atoi( PQgetvalue( result, 0, 0 ) );
PQclear( result );
return count;
}
int
DBMgr::CountStoredMessages( const char* const connName )
{
return CountStoredMessages( connName, -1 );
} /* CountStoredMessages */
void
DBMgr::StoreMessage( const char* const connName, int hid,
const unsigned char* buf, int len )
{
size_t newLen;
const char* fmt = "INSERT INTO " MSGS_TABLE " (connname, hid, msg, msglen)"
" VALUES( '%s', %d, E'%s', %d)";
unsigned char* bytes = PQescapeByteaConn( getThreadConn(), buf, len, &newLen );
assert( NULL != bytes );
char query[newLen+128];
unsigned int siz = snprintf( query, sizeof(query), fmt, connName, hid,
bytes, len );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
PQfreemem( bytes );
assert( siz < sizeof(query) );
execSql( query );
}
bool
DBMgr::GetNthStoredMessage( const char* const connName, int hid,
int nn, unsigned char* buf, size_t* buflen,
int* msgID )
{
const char* fmt = "SELECT id, msg, msglen FROM " MSGS_TABLE
" WHERE connName = '%s' AND hid = %d ORDER BY id LIMIT 1 OFFSET %d";
char query[256];
snprintf( query, sizeof(query), fmt, connName, hid, nn );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
PGresult* result = PQexec( getThreadConn(), query );
int nTuples = PQntuples( result );
assert( nTuples <= 1 );
bool found = nTuples == 1;
if ( found ) {
if ( NULL != msgID ) {
*msgID = atoi( PQgetvalue( result, 0, 0 ) );
}
size_t msglen = atoi( PQgetvalue( result, 0, 2 ) );
/* int len = PQgetlength( result, 0, 1 ); */
const unsigned char* from =
(const unsigned char* )PQgetvalue( result, 0, 1 );
size_t to_length;
unsigned char* bytes = PQunescapeBytea( from, &to_length );
assert( to_length <= *buflen );
memcpy( buf, bytes, to_length );
PQfreemem( bytes );
*buflen = to_length;
assert( 0 == msglen || to_length == msglen );
}
PQclear( result );
return found;
}
bool
DBMgr::GetStoredMessage( const char* const connName, int hid,
unsigned char* buf, size_t* buflen, int* msgID )
{
return GetNthStoredMessage( connName, hid, 0, buf, buflen, msgID );
}
void
DBMgr::RemoveStoredMessages( const int* msgIDs, int nMsgIDs )
{
char ids[1024];
int len = 0;
int ii;
assert( nMsgIDs > 0 );
for ( ii = 0; ; ) {
len += snprintf( ids + len, sizeof(ids) - len, "%d,", msgIDs[ii] );
if ( ++ii == nMsgIDs ) {
ids[len-1] = '\0'; /* overwrite last comma */
break;
}
}
const char* fmt = "DELETE from " MSGS_TABLE " WHERE id in (%s)";
char query[1024];
snprintf( query, sizeof(query), fmt, ids );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
execSql( query );
}
static void
formatParams( char* paramValues[], int nParams, const char* fmt, char* buf,
int bufLen, ... )
{
va_list ap;
va_start( ap, bufLen );
int len = vsnprintf( buf, bufLen, fmt, ap );
int ii, pnum;
for ( pnum = 0, ii = 0; ii < len && pnum < nParams; ++pnum ) {
paramValues[pnum] = &buf[ii];
for ( ; ii < len; ++ii ) {
if ( buf[ii] == DELIM[0] ) {
buf[ii] = '\0';
++ii;
break;
}
}
}
va_end(ap);
}
static int
here_less_seed( const char* seeds, int sumPerDevice, unsigned short seed )
{
logf( XW_LOGINFO, "%s: find %x in \"%s\", sub from \"%d\"", __func__,
seed, seeds, sumPerDevice );
return sumPerDevice - 1; /* FIXME */
}
static void
destr_function( void* conn )
{
logf( XW_LOGINFO, "%s()", __func__ );
PGconn* pgconn = (PGconn*)conn;
PQfinish( pgconn );
}
static pthread_key_t s_conn_key;
static void conn_key_alloc()
{
logf( XW_LOGINFO, "%s()", __func__ );
pthread_key_create( &s_conn_key, destr_function );
}
PGconn*
DBMgr::getThreadConn( void )
{
PGconn* conn = NULL;
static pthread_once_t key_once = PTHREAD_ONCE_INIT;
pthread_once( &key_once, conn_key_alloc );
conn = (PGconn*)pthread_getspecific( s_conn_key );
if ( NULL == conn ) {
conn = PQconnectdb( "dbname = " DB_NAME );
pthread_setspecific( s_conn_key, conn );
}
return conn;
}