2 * Licensed to the University Corporation for Advanced Internet
\r
3 * Development, Inc. (UCAID) under one or more contributor license
\r
4 * agreements. See the NOTICE file distributed with this work for
\r
5 * additional information regarding copyright ownership.
\r
7 * UCAID licenses this file to you under the Apache License,
\r
8 * Version 2.0 (the "License"); you may not use this file except
\r
9 * in compliance with the License. You may obtain a copy of the
\r
12 * http://www.apache.org/licenses/LICENSE-2.0
\r
14 * Unless required by applicable law or agreed to in writing,
\r
15 * software distributed under the License is distributed on an
\r
16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
\r
17 * either express or implied. See the License for the specific
\r
18 * language governing permissions and limitations under the License.
\r
24 * Storage Service using ODBC.
\r
27 #if defined (_MSC_VER) || defined(__BORLANDC__)
\r
28 # include "config_win32.h"
\r
30 # include "config.h"
\r
34 # define _CRT_NONSTDC_NO_DEPRECATE 1
\r
35 # define _CRT_SECURE_NO_DEPRECATE 1
\r
39 # define ODBCSTORE_EXPORTS __declspec(dllexport)
\r
41 # define ODBCSTORE_EXPORTS
\r
44 #include <xmltooling/logging.h>
\r
45 #include <xmltooling/unicode.h>
\r
46 #include <xmltooling/XMLToolingConfig.h>
\r
47 #include <xmltooling/util/NDC.h>
\r
48 #include <xmltooling/util/StorageService.h>
\r
49 #include <xmltooling/util/Threads.h>
\r
50 #include <xmltooling/util/XMLHelper.h>
\r
51 #include <xercesc/util/XMLUniDefs.hpp>
\r
56 #include <boost/lexical_cast.hpp>
\r
57 #include <boost/algorithm/string.hpp>
\r
59 using namespace xmltooling::logging;
\r
60 using namespace xmltooling;
\r
61 using namespace xercesc;
\r
62 using namespace boost;
\r
63 using namespace std;
\r
65 #define PLUGIN_VER_MAJOR 1
\r
66 #define PLUGIN_VER_MINOR 1
\r
68 #define LONGDATA_BUFLEN 16384
\r
70 #define COLSIZE_CONTEXT 255
\r
71 #define COLSIZE_ID 255
\r
72 #define COLSIZE_STRING_VALUE 255
\r
74 #define STRING_TABLE "strings"
\r
75 #define TEXT_TABLE "texts"
\r
77 /* table definitions
\r
78 CREATE TABLE version (
\r
79 major int NOT nullptr,
\r
80 minor int NOT nullptr
\r
83 CREATE TABLE strings (
\r
84 context varchar(255) not null,
\r
85 id varchar(255) not null,
\r
86 expires datetime not null,
\r
87 version int not null,
\r
88 value varchar(255) not null,
\r
89 PRIMARY KEY (context, id)
\r
92 CREATE TABLE texts (
\r
93 context varchar(255) not null,
\r
94 id varchar(255) not null,
\r
95 expires datetime not null,
\r
96 version int not null,
\r
97 value text not null,
\r
98 PRIMARY KEY (context, id)
\r
103 static const XMLCh cleanupInterval[] = UNICODE_LITERAL_15(c,l,e,a,n,u,p,I,n,t,e,r,v,a,l);
\r
104 static const XMLCh isolationLevel[] = UNICODE_LITERAL_14(i,s,o,l,a,t,i,o,n,L,e,v,e,l);
\r
105 static const XMLCh ConnectionString[] = UNICODE_LITERAL_16(C,o,n,n,e,c,t,i,o,n,S,t,r,i,n,g);
\r
106 static const XMLCh RetryOnError[] = UNICODE_LITERAL_12(R,e,t,r,y,O,n,E,r,r,o,r);
\r
107 static const XMLCh contextSize[] = UNICODE_LITERAL_11(c,o,n,t,e,x,t,S,i,z,e);
\r
108 static const XMLCh keySize[] = UNICODE_LITERAL_7(k,e,y,S,i,z,e);
\r
109 static const XMLCh stringSize[] = UNICODE_LITERAL_10(s,t,r,i,n,g,S,i,z,e);
\r
111 // RAII for ODBC handles
\r
113 ODBCConn(SQLHDBC conn) : handle(conn), autoCommit(true) {}
\r
115 if (handle != SQL_NULL_HDBC) {
\r
116 SQLRETURN sr = SQL_SUCCESS;
\r
118 sr = SQLSetConnectAttr(handle, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_ON, 0);
\r
119 SQLDisconnect(handle);
\r
120 SQLFreeHandle(SQL_HANDLE_DBC, handle);
\r
121 if (!SQL_SUCCEEDED(sr))
\r
122 throw IOException("Failed to commit connection and return to auto-commit mode.");
\r
125 operator SQLHDBC() {return handle;}
\r
130 class ODBCStorageService : public StorageService
\r
133 ODBCStorageService(const DOMElement* e);
\r
134 virtual ~ODBCStorageService();
\r
136 const Capabilities& getCapabilities() const {
\r
140 bool createString(const char* context, const char* key, const char* value, time_t expiration) {
\r
141 return createRow(STRING_TABLE, context, key, value, expiration);
\r
143 int readString(const char* context, const char* key, string* pvalue=nullptr, time_t* pexpiration=nullptr, int version=0) {
\r
144 return readRow(STRING_TABLE, context, key, pvalue, pexpiration, version);
\r
146 int updateString(const char* context, const char* key, const char* value=nullptr, time_t expiration=0, int version=0) {
\r
147 return updateRow(STRING_TABLE, context, key, value, expiration, version);
\r
149 bool deleteString(const char* context, const char* key) {
\r
150 return deleteRow(STRING_TABLE, context, key);
\r
153 bool createText(const char* context, const char* key, const char* value, time_t expiration) {
\r
154 return createRow(TEXT_TABLE, context, key, value, expiration);
\r
156 int readText(const char* context, const char* key, string* pvalue=nullptr, time_t* pexpiration=nullptr, int version=0) {
\r
157 return readRow(TEXT_TABLE, context, key, pvalue, pexpiration, version);
\r
159 int updateText(const char* context, const char* key, const char* value=nullptr, time_t expiration=0, int version=0) {
\r
160 return updateRow(TEXT_TABLE, context, key, value, expiration, version);
\r
162 bool deleteText(const char* context, const char* key) {
\r
163 return deleteRow(TEXT_TABLE, context, key);
\r
166 void reap(const char* context) {
\r
167 reap(STRING_TABLE, context);
\r
168 reap(TEXT_TABLE, context);
\r
171 void updateContext(const char* context, time_t expiration) {
\r
172 updateContext(STRING_TABLE, context, expiration);
\r
173 updateContext(TEXT_TABLE, context, expiration);
\r
176 void deleteContext(const char* context) {
\r
177 deleteContext(STRING_TABLE, context);
\r
178 deleteContext(TEXT_TABLE, context);
\r
183 bool createRow(const char *table, const char* context, const char* key, const char* value, time_t expiration);
\r
184 int readRow(const char *table, const char* context, const char* key, string* pvalue, time_t* pexpiration, int version);
\r
185 int updateRow(const char *table, const char* context, const char* key, const char* value, time_t expiration, int version);
\r
186 bool deleteRow(const char *table, const char* context, const char* key);
\r
188 void reap(const char* table, const char* context);
\r
189 void updateContext(const char* table, const char* context, time_t expiration);
\r
190 void deleteContext(const char* table, const char* context);
\r
193 SQLHSTMT getHSTMT(SQLHDBC);
\r
194 pair<SQLINTEGER,SQLINTEGER> getVersion(SQLHDBC);
\r
195 pair<bool,bool> log_error(SQLHANDLE handle, SQLSMALLINT htype, const char* checkfor=nullptr);
\r
197 static void* cleanup_fn(void*);
\r
201 Capabilities m_caps;
\r
202 int m_cleanupInterval;
\r
203 scoped_ptr<CondWait> shutdown_wait;
\r
204 Thread* cleanup_thread;
\r
208 string m_connstring;
\r
210 bool m_wideVersion;
\r
211 vector<SQLINTEGER> m_retries;
\r
214 StorageService* ODBCStorageServiceFactory(const DOMElement* const & e)
\r
216 return new ODBCStorageService(e);
\r
219 // convert SQL timestamp to time_t
\r
220 time_t timeFromTimestamp(SQL_TIMESTAMP_STRUCT expires)
\r
224 t.tm_sec=expires.second;
\r
225 t.tm_min=expires.minute;
\r
226 t.tm_hour=expires.hour;
\r
227 t.tm_mday=expires.day;
\r
228 t.tm_mon=expires.month-1;
\r
229 t.tm_year=expires.year-1900;
\r
231 #if defined(HAVE_TIMEGM)
\r
234 ret = mktime(&t) - timezone;
\r
239 // conver time_t to SQL string
\r
240 void timestampFromTime(time_t t, char* ret)
\r
242 #ifdef HAVE_GMTIME_R
\r
244 struct tm* ptime=gmtime_r(&t,&res);
\r
246 struct tm* ptime=gmtime(&t);
\r
248 strftime(ret,32,"{ts '%Y-%m-%d %H:%M:%S'}",ptime);
\r
255 SQLString(const char* src) : m_src(src) {
\r
256 if (strchr(src, '\'')) {
\r
258 replace_all(m_copy, "'", "''");
\r
262 operator const char*() const {
\r
266 const char* tostr() const {
\r
267 return m_copy.empty() ? m_src : m_copy.c_str();
\r
272 ODBCStorageService::ODBCStorageService(const DOMElement* e) : m_log(Category::getInstance("XMLTooling.StorageService")),
\r
273 m_caps(XMLHelper::getAttrInt(e, 255, contextSize), XMLHelper::getAttrInt(e, 255, keySize), XMLHelper::getAttrInt(e, 255, stringSize)),
\r
274 m_cleanupInterval(XMLHelper::getAttrInt(e, 900, cleanupInterval)),
\r
275 cleanup_thread(nullptr), shutdown(false), m_henv(SQL_NULL_HENV), m_isolation(SQL_TXN_SERIALIZABLE), m_wideVersion(false)
\r
278 xmltooling::NDC ndc("ODBCStorageService");
\r
280 string iso(XMLHelper::getAttrString(e, "SERIALIZABLE", isolationLevel));
\r
281 if (iso == "SERIALIZABLE")
\r
282 m_isolation = SQL_TXN_SERIALIZABLE;
\r
283 else if (iso == "REPEATABLE_READ")
\r
284 m_isolation = SQL_TXN_REPEATABLE_READ;
\r
285 else if (iso == "READ_COMMITTED")
\r
286 m_isolation = SQL_TXN_READ_COMMITTED;
\r
287 else if (iso == "READ_UNCOMMITTED")
\r
288 m_isolation = SQL_TXN_READ_UNCOMMITTED;
\r
290 throw XMLToolingException("Unknown transaction isolationLevel property.");
\r
292 if (m_henv == SQL_NULL_HENV) {
\r
293 // Enable connection pooling.
\r
294 SQLSetEnvAttr(SQL_NULL_HANDLE, SQL_ATTR_CONNECTION_POOLING, (void*)SQL_CP_ONE_PER_HENV, 0);
\r
296 // Allocate the environment.
\r
297 if (!SQL_SUCCEEDED(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &m_henv)))
\r
298 throw XMLToolingException("ODBC failed to initialize.");
\r
300 // Specify ODBC 3.x
\r
301 SQLSetEnvAttr(m_henv, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0);
\r
303 m_log.info("ODBC initialized");
\r
306 // Grab connection string from the configuration.
\r
307 e = e ? XMLHelper::getFirstChildElement(e, ConnectionString) : nullptr;
\r
308 auto_ptr_char arg(e ? e->getTextContent() : nullptr);
\r
309 if (!arg.get() || !*arg.get()) {
\r
310 SQLFreeHandle(SQL_HANDLE_ENV, m_henv);
\r
311 throw XMLToolingException("ODBC StorageService requires ConnectionString element in configuration.");
\r
313 m_connstring = arg.get();
\r
315 // Connect and check version.
\r
316 ODBCConn conn(getHDBC());
\r
317 pair<SQLINTEGER,SQLINTEGER> v = getVersion(conn);
\r
319 // Make sure we've got the right version.
\r
320 if (v.first != PLUGIN_VER_MAJOR) {
\r
321 SQLFreeHandle(SQL_HANDLE_ENV, m_henv);
\r
322 m_log.crit("unknown database version: %d.%d", v.first, v.second);
\r
323 throw XMLToolingException("Unknown database version for ODBC StorageService.");
\r
326 if (v.first > 1 || v.second > 0) {
\r
327 m_log.info("using 32-bit int type for version fields in tables");
\r
328 m_wideVersion = true;
\r
331 // Load any retry errors to check.
\r
332 e = XMLHelper::getNextSiblingElement(e, RetryOnError);
\r
334 if (e->hasChildNodes()) {
\r
336 int code = XMLString::parseInt(e->getTextContent());
\r
337 m_retries.push_back(code);
\r
338 m_log.info("will retry operations when native ODBC error (%d) is returned", code);
\r
340 catch (XMLException&) {
\r
341 m_log.error("skipping non-numeric ODBC retry code");
\r
344 e = XMLHelper::getNextSiblingElement(e, RetryOnError);
\r
347 if (m_cleanupInterval > 0) {
\r
348 // Initialize the cleanup thread
\r
349 shutdown_wait.reset(CondWait::create());
\r
350 cleanup_thread = Thread::create(&cleanup_fn, (void*)this);
\r
353 m_log.info("no cleanup interval configured, no cleanup thread will be started");
\r
357 ODBCStorageService::~ODBCStorageService()
\r
360 if (shutdown_wait.get()) {
\r
361 shutdown_wait->signal();
\r
363 if (cleanup_thread) {
\r
364 cleanup_thread->join(nullptr);
\r
366 if (m_henv != SQL_NULL_HANDLE) {
\r
367 SQLFreeHandle(SQL_HANDLE_ENV, m_henv);
\r
371 pair<bool,bool> ODBCStorageService::log_error(SQLHANDLE handle, SQLSMALLINT htype, const char* checkfor)
\r
380 pair<bool,bool> res = make_pair(false,false);
\r
382 ret = SQLGetDiagRec(htype, handle, ++i, state, &native, text, sizeof(text), &len);
\r
383 if (SQL_SUCCEEDED(ret)) {
\r
384 m_log.error("ODBC Error: %s:%ld:%ld:%s", state, i, native, text);
\r
385 for (vector<SQLINTEGER>::const_iterator n = m_retries.begin(); !res.first && n != m_retries.end(); ++n)
\r
386 res.first = (*n == native);
\r
387 if (checkfor && !strcmp(checkfor, (const char*)state))
\r
390 } while(SQL_SUCCEEDED(ret));
\r
394 SQLHDBC ODBCStorageService::getHDBC()
\r
397 xmltooling::NDC ndc("getHDBC");
\r
401 SQLHDBC handle = SQL_NULL_HDBC;
\r
402 SQLRETURN sr = SQLAllocHandle(SQL_HANDLE_DBC, m_henv, &handle);
\r
403 if (!SQL_SUCCEEDED(sr) || handle == SQL_NULL_HDBC) {
\r
404 m_log.error("failed to allocate connection handle");
\r
405 log_error(m_henv, SQL_HANDLE_ENV);
\r
406 throw IOException("ODBC StorageService failed to allocate a connection handle.");
\r
409 sr = SQLDriverConnect(handle,nullptr,(SQLCHAR*)m_connstring.c_str(),m_connstring.length(),nullptr,0,nullptr,SQL_DRIVER_NOPROMPT);
\r
410 if (!SQL_SUCCEEDED(sr)) {
\r
411 m_log.error("failed to connect to database");
\r
412 log_error(handle, SQL_HANDLE_DBC);
\r
413 SQLFreeHandle(SQL_HANDLE_DBC, handle);
\r
414 throw IOException("ODBC StorageService failed to connect to database.");
\r
417 sr = SQLSetConnectAttr(handle, SQL_ATTR_TXN_ISOLATION, (SQLPOINTER)m_isolation, 0);
\r
418 if (!SQL_SUCCEEDED(sr)) {
\r
419 SQLDisconnect(handle);
\r
420 SQLFreeHandle(SQL_HANDLE_DBC, handle);
\r
421 throw IOException("ODBC StorageService failed to set transaction isolation level.");
\r
427 SQLHSTMT ODBCStorageService::getHSTMT(SQLHDBC conn)
\r
429 SQLHSTMT hstmt = SQL_NULL_HSTMT;
\r
430 SQLRETURN sr = SQLAllocHandle(SQL_HANDLE_STMT, conn, &hstmt);
\r
431 if (!SQL_SUCCEEDED(sr) || hstmt == SQL_NULL_HSTMT) {
\r
432 m_log.error("failed to allocate statement handle");
\r
433 log_error(conn, SQL_HANDLE_DBC);
\r
434 throw IOException("ODBC StorageService failed to allocate a statement handle.");
\r
439 pair<SQLINTEGER,SQLINTEGER> ODBCStorageService::getVersion(SQLHDBC conn)
\r
441 // Grab the version number from the database.
\r
442 SQLHSTMT stmt = getHSTMT(conn);
\r
444 SQLRETURN sr = SQLExecDirect(stmt, (SQLCHAR*)"SELECT major,minor FROM version", SQL_NTS);
\r
445 if (!SQL_SUCCEEDED(sr)) {
\r
446 m_log.error("failed to read version from database");
\r
447 log_error(stmt, SQL_HANDLE_STMT);
\r
448 throw IOException("ODBC StorageService failed to read version from database.");
\r
453 SQLBindCol(stmt, 1, SQL_C_SLONG, &major, 0, nullptr);
\r
454 SQLBindCol(stmt, 2, SQL_C_SLONG, &minor, 0, nullptr);
\r
456 if ((sr = SQLFetch(stmt)) != SQL_NO_DATA)
\r
457 return make_pair(major,minor);
\r
459 m_log.error("no rows returned in version query");
\r
460 throw IOException("ODBC StorageService failed to read version from database.");
\r
463 bool ODBCStorageService::createRow(const char* table, const char* context, const char* key, const char* value, time_t expiration)
\r
466 xmltooling::NDC ndc("createRow");
\r
470 timestampFromTime(expiration, timebuf);
\r
472 // Get statement handle.
\r
473 ODBCConn conn(getHDBC());
\r
474 SQLHSTMT stmt = getHSTMT(conn);
\r
476 string q = string("INSERT INTO ") + table + " VALUES (?,?," + timebuf + ",1,?)";
\r
478 SQLRETURN sr = SQLPrepare(stmt, (SQLCHAR*)q.c_str(), SQL_NTS);
\r
479 if (!SQL_SUCCEEDED(sr)) {
\r
480 m_log.error("SQLPrepare failed (t=%s, c=%s, k=%s)", table, context, key);
\r
481 log_error(stmt, SQL_HANDLE_STMT);
\r
482 throw IOException("ODBC StorageService failed to insert record.");
\r
484 m_log.debug("SQLPrepare succeeded. SQL: %s", q.c_str());
\r
486 SQLLEN b_ind = SQL_NTS;
\r
487 sr = SQLBindParam(stmt, 1, SQL_C_CHAR, SQL_VARCHAR, 255, 0, const_cast<char*>(context), &b_ind);
\r
488 if (!SQL_SUCCEEDED(sr)) {
\r
489 m_log.error("SQLBindParam failed (context = %s)", context);
\r
490 log_error(stmt, SQL_HANDLE_STMT);
\r
491 throw IOException("ODBC StorageService failed to insert record.");
\r
493 m_log.debug("SQLBindParam succeeded (context = %s)", context);
\r
495 sr = SQLBindParam(stmt, 2, SQL_C_CHAR, SQL_VARCHAR, 255, 0, const_cast<char*>(key), &b_ind);
\r
496 if (!SQL_SUCCEEDED(sr)) {
\r
497 m_log.error("SQLBindParam failed (key = %s)", key);
\r
498 log_error(stmt, SQL_HANDLE_STMT);
\r
499 throw IOException("ODBC StorageService failed to insert record.");
\r
501 m_log.debug("SQLBindParam succeeded (key = %s)", key);
\r
503 if (strcmp(table, TEXT_TABLE)==0)
\r
504 sr = SQLBindParam(stmt, 3, SQL_C_CHAR, SQL_LONGVARCHAR, strlen(value), 0, const_cast<char*>(value), &b_ind);
\r
506 sr = SQLBindParam(stmt, 3, SQL_C_CHAR, SQL_VARCHAR, 255, 0, const_cast<char*>(value), &b_ind);
\r
507 if (!SQL_SUCCEEDED(sr)) {
\r
508 m_log.error("SQLBindParam failed (value = %s)", value);
\r
509 log_error(stmt, SQL_HANDLE_STMT);
\r
510 throw IOException("ODBC StorageService failed to insert record.");
\r
512 m_log.debug("SQLBindParam succeeded (value = %s)", value);
\r
515 pair<bool,bool> logres;
\r
517 logres = make_pair(false,false);
\r
519 sr = SQLExecute(stmt);
\r
520 if (SQL_SUCCEEDED(sr)) {
\r
521 m_log.debug("SQLExecute of insert succeeded");
\r
524 m_log.error("insert record failed (t=%s, c=%s, k=%s)", table, context, key);
\r
525 logres = log_error(stmt, SQL_HANDLE_STMT, "23000");
\r
526 if (logres.second) {
\r
527 // Supposedly integrity violation.
\r
528 // Try and delete any expired record still hanging around until the final attempt.
\r
529 if (attempts > 0) {
\r
530 reap(table, context);
\r
531 logres.first = true; // force it to treat as a retryable error
\r
536 } while (attempts && logres.first);
\r
538 throw IOException("ODBC StorageService failed to insert record.");
\r
541 int ODBCStorageService::readRow(const char *table, const char* context, const char* key, string* pvalue, time_t* pexpiration, int version)
\r
544 xmltooling::NDC ndc("readRow");
\r
547 // Get statement handle.
\r
548 ODBCConn conn(getHDBC());
\r
549 SQLHSTMT stmt = getHSTMT(conn);
\r
551 // Prepare and exectute select statement.
\r
553 timestampFromTime(time(nullptr), timebuf);
\r
554 SQLString scontext(context);
\r
555 SQLString skey(key);
\r
556 string q("SELECT version");
\r
561 q = q + ",CASE version WHEN " + lexical_cast<string>(version) + " THEN null ELSE value END";
\r
563 q = q + " FROM " + table + " WHERE context='" + scontext.tostr() + "' AND id='" + skey.tostr() + "' AND expires > " + timebuf;
\r
564 if (m_log.isDebugEnabled())
\r
565 m_log.debug("SQL: %s", q.c_str());
\r
567 SQLRETURN sr=SQLExecDirect(stmt, (SQLCHAR*)q.c_str(), SQL_NTS);
\r
568 if (!SQL_SUCCEEDED(sr)) {
\r
569 m_log.error("error searching for (t=%s, c=%s, k=%s)", table, context, key);
\r
570 log_error(stmt, SQL_HANDLE_STMT);
\r
571 throw IOException("ODBC StorageService search failed.");
\r
575 SQLINTEGER widever;
\r
576 SQL_TIMESTAMP_STRUCT expiration;
\r
579 SQLBindCol(stmt, 1, SQL_C_SLONG, &widever, 0, nullptr);
\r
581 SQLBindCol(stmt, 1, SQL_C_SSHORT, &ver, 0, nullptr);
\r
583 SQLBindCol(stmt, 2, SQL_C_TYPE_TIMESTAMP, &expiration, 0, nullptr);
\r
585 if ((sr = SQLFetch(stmt)) == SQL_NO_DATA) {
\r
586 if (m_log.isDebugEnabled())
\r
587 m_log.debug("search returned no data (t=%s, c=%s, k=%s)", table, context, key);
\r
592 *pexpiration = timeFromTimestamp(expiration);
\r
594 if (version == (m_wideVersion ? widever : ver)) {
\r
595 if (m_log.isDebugEnabled())
\r
596 m_log.debug("versioned search detected no change (t=%s, c=%s, k=%s)", table, context, key);
\r
597 return version; // nothing's changed, so just echo back the version
\r
602 SQLCHAR buf[LONGDATA_BUFLEN];
\r
603 while ((sr = SQLGetData(stmt, (pexpiration ? 3 : 2), SQL_C_CHAR, buf, sizeof(buf), &len)) != SQL_NO_DATA) {
\r
604 if (!SQL_SUCCEEDED(sr)) {
\r
605 m_log.error("error while reading text field from result set");
\r
606 log_error(stmt, SQL_HANDLE_STMT);
\r
607 throw IOException("ODBC StorageService search failed to read data from result set.");
\r
609 pvalue->append((char*)buf);
\r
613 return (m_wideVersion ? widever : ver);
\r
616 int ODBCStorageService::updateRow(const char *table, const char* context, const char* key, const char* value, time_t expiration, int version)
\r
619 xmltooling::NDC ndc("updateRow");
\r
622 if (!value && !expiration)
\r
623 throw IOException("ODBC StorageService given invalid update instructions.");
\r
625 // Get statement handle. Disable auto-commit mode to wrap select + update.
\r
626 ODBCConn conn(getHDBC());
\r
627 SQLRETURN sr = SQLSetConnectAttr(conn, SQL_ATTR_AUTOCOMMIT, SQL_AUTOCOMMIT_OFF, 0);
\r
628 if (!SQL_SUCCEEDED(sr))
\r
629 throw IOException("ODBC StorageService failed to disable auto-commit mode.");
\r
630 conn.autoCommit = false;
\r
631 SQLHSTMT stmt = getHSTMT(conn);
\r
633 // First, fetch the current version for later, which also ensures the record still exists.
\r
635 timestampFromTime(time(nullptr), timebuf);
\r
636 SQLString scontext(context);
\r
637 SQLString skey(key);
\r
638 string q("SELECT version FROM ");
\r
639 q = q + table + " WHERE context='" + scontext.tostr() + "' AND id='" + skey.tostr() + "' AND expires > " + timebuf;
\r
641 m_log.debug("SQL: %s", q.c_str());
\r
643 sr = SQLExecDirect(stmt, (SQLCHAR*)q.c_str(), SQL_NTS);
\r
644 if (!SQL_SUCCEEDED(sr)) {
\r
645 m_log.error("error searching for (t=%s, c=%s, k=%s)", table, context, key);
\r
646 log_error(stmt, SQL_HANDLE_STMT);
\r
647 throw IOException("ODBC StorageService search failed.");
\r
651 SQLINTEGER widever;
\r
653 SQLBindCol(stmt, 1, SQL_C_SLONG, &widever, 0, nullptr);
\r
655 SQLBindCol(stmt, 1, SQL_C_SSHORT, &ver, 0, nullptr);
\r
656 if ((sr = SQLFetch(stmt)) == SQL_NO_DATA) {
\r
661 if (version > 0 && version != (m_wideVersion ? widever : ver)) {
\r
664 else if ((m_wideVersion && widever == INT_MAX) || (!m_wideVersion && ver == 32767)) {
\r
665 m_log.error("record version overflow (t=%s, c=%s, k=%s)", table, context, key);
\r
666 throw IOException("Version overflow, record in ODBC StorageService could not be updated.");
\r
669 SQLFreeHandle(SQL_HANDLE_STMT, stmt);
\r
670 stmt = getHSTMT(conn);
\r
672 // Prepare and exectute update statement.
\r
673 q = string("UPDATE ") + table + " SET ";
\r
676 q = q + "value=?, version=version+1";
\r
679 timestampFromTime(expiration, timebuf);
\r
682 q = q + "expires = " + timebuf;
\r
685 q = q + " WHERE context='" + scontext.tostr() + "' AND id='" + skey.tostr() + "'";
\r
687 sr = SQLPrepare(stmt, (SQLCHAR*)q.c_str(), SQL_NTS);
\r
688 if (!SQL_SUCCEEDED(sr)) {
\r
689 m_log.error("update of record failed (t=%s, c=%s, k=%s", table, context, key);
\r
690 log_error(stmt, SQL_HANDLE_STMT);
\r
691 throw IOException("ODBC StorageService failed to update record.");
\r
693 m_log.debug("SQLPrepare succeeded. SQL: %s", q.c_str());
\r
695 SQLLEN b_ind = SQL_NTS;
\r
697 if (strcmp(table, TEXT_TABLE)==0)
\r
698 sr = SQLBindParam(stmt, 1, SQL_C_CHAR, SQL_LONGVARCHAR, strlen(value), 0, const_cast<char*>(value), &b_ind);
\r
700 sr = SQLBindParam(stmt, 1, SQL_C_CHAR, SQL_VARCHAR, 255, 0, const_cast<char*>(value), &b_ind);
\r
701 if (!SQL_SUCCEEDED(sr)) {
\r
702 m_log.error("SQLBindParam failed (value = %s)", value);
\r
703 log_error(stmt, SQL_HANDLE_STMT);
\r
704 throw IOException("ODBC StorageService failed to update record.");
\r
706 m_log.debug("SQLBindParam succeeded (value = %s)", value);
\r
710 pair<bool,bool> logres;
\r
712 logres = make_pair(false,false);
\r
714 sr = SQLExecute(stmt);
\r
715 if (sr == SQL_NO_DATA)
\r
716 return 0; // went missing?
\r
717 else if (SQL_SUCCEEDED(sr)) {
\r
718 m_log.debug("SQLExecute of update succeeded");
\r
719 return (m_wideVersion ? widever : ver) + 1;
\r
722 m_log.error("update of record failed (t=%s, c=%s, k=%s)", table, context, key);
\r
723 logres = log_error(stmt, SQL_HANDLE_STMT);
\r
724 } while (attempts && logres.first);
\r
726 throw IOException("ODBC StorageService failed to update record.");
\r
729 bool ODBCStorageService::deleteRow(const char *table, const char *context, const char* key)
\r
732 xmltooling::NDC ndc("deleteRow");
\r
735 // Get statement handle.
\r
736 ODBCConn conn(getHDBC());
\r
737 SQLHSTMT stmt = getHSTMT(conn);
\r
739 // Prepare and execute delete statement.
\r
740 SQLString scontext(context);
\r
741 SQLString skey(key);
\r
742 string q = string("DELETE FROM ") + table + " WHERE context='" + scontext.tostr() + "' AND id='" + skey.tostr() + "'";
\r
743 m_log.debug("SQL: %s", q.c_str());
\r
745 SQLRETURN sr = SQLExecDirect(stmt, (SQLCHAR*)q.c_str(), SQL_NTS);
\r
746 if (sr == SQL_NO_DATA)
\r
748 else if (!SQL_SUCCEEDED(sr)) {
\r
749 m_log.error("error deleting record (t=%s, c=%s, k=%s)", table, context, key);
\r
750 log_error(stmt, SQL_HANDLE_STMT);
\r
751 throw IOException("ODBC StorageService failed to delete record.");
\r
758 void ODBCStorageService::cleanup()
\r
761 xmltooling::NDC ndc("cleanup");
\r
764 scoped_ptr<Mutex> mutex(Mutex::create());
\r
768 m_log.info("cleanup thread started... running every %d secs", m_cleanupInterval);
\r
770 while (!shutdown) {
\r
771 shutdown_wait->timedwait(mutex.get(), m_cleanupInterval);
\r
777 catch (std::exception& ex) {
\r
778 m_log.error("cleanup thread swallowed exception: %s", ex.what());
\r
782 m_log.info("cleanup thread exiting...");
\r
785 Thread::exit(nullptr);
\r
788 void* ODBCStorageService::cleanup_fn(void* cache_p)
\r
790 ODBCStorageService* cache = (ODBCStorageService*)cache_p;
\r
793 // First, let's block all signals
\r
794 Thread::mask_all_signals();
\r
797 // Now run the cleanup process.
\r
802 void ODBCStorageService::updateContext(const char *table, const char* context, time_t expiration)
\r
805 xmltooling::NDC ndc("updateContext");
\r
808 // Get statement handle.
\r
809 ODBCConn conn(getHDBC());
\r
810 SQLHSTMT stmt = getHSTMT(conn);
\r
813 timestampFromTime(expiration, timebuf);
\r
816 timestampFromTime(time(nullptr), nowbuf);
\r
818 SQLString scontext(context);
\r
819 string q = string("UPDATE ") + table + " SET expires = " + timebuf + " WHERE context='" + scontext.tostr() + "' AND expires > " + nowbuf;
\r
821 m_log.debug("SQL: %s", q.c_str());
\r
823 SQLRETURN sr = SQLExecDirect(stmt, (SQLCHAR*)q.c_str(), SQL_NTS);
\r
824 if ((sr != SQL_NO_DATA) && !SQL_SUCCEEDED(sr)) {
\r
825 m_log.error("error updating records (t=%s, c=%s)", table, context ? context : "all");
\r
826 log_error(stmt, SQL_HANDLE_STMT);
\r
827 throw IOException("ODBC StorageService failed to update context expiration.");
\r
831 void ODBCStorageService::reap(const char *table, const char* context)
\r
834 xmltooling::NDC ndc("reap");
\r
837 // Get statement handle.
\r
838 ODBCConn conn(getHDBC());
\r
839 SQLHSTMT stmt = getHSTMT(conn);
\r
841 // Prepare and execute delete statement.
\r
843 timestampFromTime(time(nullptr), nowbuf);
\r
846 SQLString scontext(context);
\r
847 q = string("DELETE FROM ") + table + " WHERE context='" + scontext.tostr() + "' AND expires <= " + nowbuf;
\r
850 q = string("DELETE FROM ") + table + " WHERE expires <= " + nowbuf;
\r
852 m_log.debug("SQL: %s", q.c_str());
\r
854 SQLRETURN sr = SQLExecDirect(stmt, (SQLCHAR*)q.c_str(), SQL_NTS);
\r
855 if ((sr != SQL_NO_DATA) && !SQL_SUCCEEDED(sr)) {
\r
856 m_log.error("error expiring records (t=%s, c=%s)", table, context ? context : "all");
\r
857 log_error(stmt, SQL_HANDLE_STMT);
\r
858 throw IOException("ODBC StorageService failed to purge expired records.");
\r
862 void ODBCStorageService::deleteContext(const char *table, const char* context)
\r
865 xmltooling::NDC ndc("deleteContext");
\r
868 // Get statement handle.
\r
869 ODBCConn conn(getHDBC());
\r
870 SQLHSTMT stmt = getHSTMT(conn);
\r
872 // Prepare and execute delete statement.
\r
873 SQLString scontext(context);
\r
874 string q = string("DELETE FROM ") + table + " WHERE context='" + scontext.tostr() + "'";
\r
875 m_log.debug("SQL: %s", q.c_str());
\r
877 SQLRETURN sr = SQLExecDirect(stmt, (SQLCHAR*)q.c_str(), SQL_NTS);
\r
878 if ((sr != SQL_NO_DATA) && !SQL_SUCCEEDED(sr)) {
\r
879 m_log.error("error deleting context (t=%s, c=%s)", table, context);
\r
880 log_error(stmt, SQL_HANDLE_STMT);
\r
881 throw IOException("ODBC StorageService failed to delete context.");
\r
885 extern "C" int ODBCSTORE_EXPORTS xmltooling_extension_init(void*)
\r
887 // Register this SS type
\r
888 XMLToolingConfig::getConfig().StorageServiceManager.registerFactory("ODBC", ODBCStorageServiceFactory);
\r
892 extern "C" void ODBCSTORE_EXPORTS xmltooling_extension_term()
\r
894 XMLToolingConfig::getConfig().StorageServiceManager.deregisterFactory("ODBC");
\r