SSPCPP-670 - Session Cleanup for Database Session Storage can cause performance issues
[shibboleth/cpp-sp.git] / odbc-store / odbc-store.cpp
1 /**\r
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
6  *\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
10  * License at\r
11  *\r
12  * http://www.apache.org/licenses/LICENSE-2.0\r
13  *\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
19  */\r
20 \r
21 /**\r
22  * odbc-store.cpp\r
23  *\r
24  * Storage Service using ODBC.\r
25  */\r
26 \r
27 #if defined (_MSC_VER) || defined(__BORLANDC__)\r
28 # include "config_win32.h"\r
29 #else\r
30 # include "config.h"\r
31 #endif\r
32 \r
33 #ifdef WIN32\r
34 # define _CRT_NONSTDC_NO_DEPRECATE 1\r
35 # define _CRT_SECURE_NO_DEPRECATE 1\r
36 #endif\r
37 \r
38 #ifdef WIN32\r
39 # define ODBCSTORE_EXPORTS __declspec(dllexport)\r
40 #else\r
41 # define ODBCSTORE_EXPORTS\r
42 #endif\r
43 \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
52 \r
53 #include <sql.h>\r
54 #include <sqlext.h>\r
55 \r
56 #include <boost/lexical_cast.hpp>\r
57 #include <boost/algorithm/string.hpp>\r
58 \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
64 \r
65 #define PLUGIN_VER_MAJOR 1\r
66 #define PLUGIN_VER_MINOR 1\r
67 \r
68 #define LONGDATA_BUFLEN 16384\r
69 \r
70 #define COLSIZE_CONTEXT 255\r
71 #define COLSIZE_ID 255\r
72 #define COLSIZE_STRING_VALUE 255\r
73 \r
74 #define STRING_TABLE "strings"\r
75 #define TEXT_TABLE "texts"\r
76 \r
77 /* table definitions\r
78 CREATE TABLE version (\r
79     major int NOT nullptr,\r
80     minor int NOT nullptr\r
81     )\r
82 \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
90     )\r
91 \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
99     )\r
100 */\r
101 \r
102 namespace {\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
110 \r
111     // RAII for ODBC handles\r
112     struct ODBCConn {\r
113         ODBCConn(SQLHDBC conn) : handle(conn), autoCommit(true) {}\r
114         ~ODBCConn() {\r
115             if (handle != SQL_NULL_HDBC) {\r
116                 SQLRETURN sr = SQL_SUCCESS;\r
117                 if (!autoCommit)\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
123             }\r
124         }\r
125         operator SQLHDBC() {return handle;}\r
126         SQLHDBC handle;\r
127         bool autoCommit;\r
128     };\r
129 \r
130     class ODBCStorageService : public StorageService\r
131     {\r
132     public:\r
133         ODBCStorageService(const DOMElement* e);\r
134         virtual ~ODBCStorageService();\r
135 \r
136         const Capabilities& getCapabilities() const {\r
137             return m_caps;\r
138         }\r
139 \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
142         }\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
145         }\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
148         }\r
149         bool deleteString(const char* context, const char* key) {\r
150             return deleteRow(STRING_TABLE, context, key);\r
151         }\r
152 \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
155         }\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
158         }\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
161         }\r
162         bool deleteText(const char* context, const char* key) {\r
163             return deleteRow(TEXT_TABLE, context, key);\r
164         }\r
165 \r
166         void reap(const char* context) {\r
167             reap(STRING_TABLE, context);\r
168             reap(TEXT_TABLE, context);\r
169         }\r
170 \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
174         }\r
175 \r
176         void deleteContext(const char* context) {\r
177             deleteContext(STRING_TABLE, context);\r
178             deleteContext(TEXT_TABLE, context);\r
179         }\r
180          \r
181 \r
182     private:\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
187 \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
191 \r
192         SQLHDBC getHDBC();\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
196 \r
197         static void* cleanup_fn(void*); \r
198         void cleanup();\r
199 \r
200         Category& m_log;\r
201         Capabilities m_caps;\r
202         int m_cleanupInterval;\r
203         scoped_ptr<CondWait> shutdown_wait;\r
204         Thread* cleanup_thread;\r
205         bool shutdown;\r
206 \r
207         SQLHENV m_henv;\r
208         string m_connstring;\r
209         long m_isolation;\r
210         bool m_wideVersion;\r
211         vector<SQLINTEGER> m_retries;\r
212     };\r
213 \r
214     StorageService* ODBCStorageServiceFactory(const DOMElement* const & e)\r
215     {\r
216         return new ODBCStorageService(e);\r
217     }\r
218 \r
219     // convert SQL timestamp to time_t \r
220     time_t timeFromTimestamp(SQL_TIMESTAMP_STRUCT expires)\r
221     {\r
222         time_t ret;\r
223         struct tm t;\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
230         t.tm_isdst=0;\r
231 #if defined(HAVE_TIMEGM)\r
232         ret = timegm(&t);\r
233 #else\r
234         ret = mktime(&t) - timezone;\r
235 #endif\r
236         return (ret);\r
237     }\r
238 \r
239     // conver time_t to SQL string\r
240     void timestampFromTime(time_t t, char* ret)\r
241     {\r
242 #ifdef HAVE_GMTIME_R\r
243         struct tm res;\r
244         struct tm* ptime=gmtime_r(&t,&res);\r
245 #else\r
246         struct tm* ptime=gmtime(&t);\r
247 #endif\r
248         strftime(ret,32,"{ts '%Y-%m-%d %H:%M:%S'}",ptime);\r
249     }\r
250 \r
251     class SQLString {\r
252         const char* m_src;\r
253         string m_copy;\r
254     public:\r
255         SQLString(const char* src) : m_src(src) {\r
256             if (strchr(src, '\'')) {\r
257                 m_copy = src;\r
258                 replace_all(m_copy, "'", "''");\r
259             }\r
260         }\r
261 \r
262         operator const char*() const {\r
263             return tostr();\r
264         }\r
265 \r
266         const char* tostr() const {\r
267             return m_copy.empty() ? m_src : m_copy.c_str();\r
268         }\r
269     };\r
270 };\r
271 \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
276 {\r
277 #ifdef _DEBUG\r
278     xmltooling::NDC ndc("ODBCStorageService");\r
279 #endif\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
289     else\r
290         throw XMLToolingException("Unknown transaction isolationLevel property.");\r
291 \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
295 \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
299 \r
300         // Specify ODBC 3.x\r
301         SQLSetEnvAttr(m_henv, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0);\r
302 \r
303         m_log.info("ODBC initialized");\r
304     }\r
305 \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
312     }\r
313     m_connstring = arg.get();\r
314 \r
315     // Connect and check version.\r
316     ODBCConn conn(getHDBC());\r
317     pair<SQLINTEGER,SQLINTEGER> v = getVersion(conn);\r
318 \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
324     }\r
325     \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
329     }\r
330 \r
331     // Load any retry errors to check.\r
332     e = XMLHelper::getNextSiblingElement(e, RetryOnError);\r
333     while (e) {\r
334         if (e->hasChildNodes()) {\r
335             try {\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
339             }\r
340             catch (XMLException&) {\r
341                 m_log.error("skipping non-numeric ODBC retry code");\r
342             }\r
343         }\r
344         e = XMLHelper::getNextSiblingElement(e, RetryOnError);\r
345     }\r
346 \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
351     }\r
352     else {\r
353         m_log.info("no cleanup interval configured, no cleanup thread will be started");\r
354     }\r
355 }\r
356 \r
357 ODBCStorageService::~ODBCStorageService()\r
358 {\r
359     shutdown = true;\r
360     if (shutdown_wait.get()) {\r
361         shutdown_wait->signal();\r
362     }\r
363     if (cleanup_thread) {\r
364         cleanup_thread->join(nullptr);\r
365     }\r
366     if (m_henv != SQL_NULL_HANDLE) {\r
367         SQLFreeHandle(SQL_HANDLE_ENV, m_henv);\r
368     }\r
369 }\r
370 \r
371 pair<bool,bool> ODBCStorageService::log_error(SQLHANDLE handle, SQLSMALLINT htype, const char* checkfor)\r
372 {\r
373     SQLSMALLINT  i = 0;\r
374     SQLINTEGER   native;\r
375     SQLCHAR      state[7];\r
376     SQLCHAR      text[256];\r
377     SQLSMALLINT  len;\r
378     SQLRETURN    ret;\r
379 \r
380     pair<bool,bool> res = make_pair(false,false);\r
381     do {\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
388                 res.second = true;\r
389         }\r
390     } while(SQL_SUCCEEDED(ret));\r
391     return res;\r
392 }\r
393 \r
394 SQLHDBC ODBCStorageService::getHDBC()\r
395 {\r
396 #ifdef _DEBUG\r
397     xmltooling::NDC ndc("getHDBC");\r
398 #endif\r
399 \r
400     // Get a handle.\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
407     }\r
408 \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
415     }\r
416 \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
422     }\r
423 \r
424     return handle;\r
425 }\r
426 \r
427 SQLHSTMT ODBCStorageService::getHSTMT(SQLHDBC conn)\r
428 {\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
435     }\r
436     return hstmt;\r
437 }\r
438 \r
439 pair<SQLINTEGER,SQLINTEGER> ODBCStorageService::getVersion(SQLHDBC conn)\r
440 {\r
441     // Grab the version number from the database.\r
442     SQLHSTMT stmt = getHSTMT(conn);\r
443     \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
449     }\r
450 \r
451     SQLINTEGER major;\r
452     SQLINTEGER minor;\r
453     SQLBindCol(stmt, 1, SQL_C_SLONG, &major, 0, nullptr);\r
454     SQLBindCol(stmt, 2, SQL_C_SLONG, &minor, 0, nullptr);\r
455 \r
456     if ((sr = SQLFetch(stmt)) != SQL_NO_DATA)\r
457         return make_pair(major,minor);\r
458 \r
459     m_log.error("no rows returned in version query");\r
460     throw IOException("ODBC StorageService failed to read version from database.");\r
461 }\r
462 \r
463 bool ODBCStorageService::createRow(const char* table, const char* context, const char* key, const char* value, time_t expiration)\r
464 {\r
465 #ifdef _DEBUG\r
466     xmltooling::NDC ndc("createRow");\r
467 #endif\r
468 \r
469     char timebuf[32];\r
470     timestampFromTime(expiration, timebuf);\r
471 \r
472     // Get statement handle.\r
473     ODBCConn conn(getHDBC());\r
474     SQLHSTMT stmt = getHSTMT(conn);\r
475 \r
476     string q  = string("INSERT INTO ") + table + " VALUES (?,?," + timebuf + ",1,?)";\r
477 \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
483     }\r
484     m_log.debug("SQLPrepare succeeded. SQL: %s", q.c_str());\r
485 \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
492     }\r
493     m_log.debug("SQLBindParam succeeded (context = %s)", context);\r
494 \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
500     }\r
501     m_log.debug("SQLBindParam succeeded (key = %s)", key);\r
502 \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
505     else\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
511     }\r
512     m_log.debug("SQLBindParam succeeded (value = %s)", value);\r
513     \r
514     int attempts = 3;\r
515     pair<bool,bool> logres;\r
516     do {\r
517         logres = make_pair(false,false);\r
518         attempts--;\r
519         sr = SQLExecute(stmt);\r
520         if (SQL_SUCCEEDED(sr)) {\r
521             m_log.debug("SQLExecute of insert succeeded");\r
522             return true;\r
523         }\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
532                 continue;\r
533             }\r
534             return false;\r
535         }\r
536     } while (attempts && logres.first);\r
537 \r
538     throw IOException("ODBC StorageService failed to insert record.");\r
539 }\r
540 \r
541 int ODBCStorageService::readRow(const char *table, const char* context, const char* key, string* pvalue, time_t* pexpiration, int version)\r
542 {\r
543 #ifdef _DEBUG\r
544     xmltooling::NDC ndc("readRow");\r
545 #endif\r
546 \r
547     // Get statement handle.\r
548     ODBCConn conn(getHDBC());\r
549     SQLHSTMT stmt = getHSTMT(conn);\r
550 \r
551     // Prepare and exectute select statement.\r
552     char timebuf[32];\r
553     timestampFromTime(time(nullptr), timebuf);\r
554     SQLString scontext(context);\r
555     SQLString skey(key);\r
556     string q("SELECT version");\r
557     if (pexpiration)\r
558         q += ",expires";\r
559     if (pvalue) {\r
560         pvalue->erase();\r
561         q = q + ",CASE version WHEN " + lexical_cast<string>(version) + " THEN null ELSE value END";\r
562     }\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
566 \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
572     }\r
573 \r
574     SQLSMALLINT ver;\r
575     SQLINTEGER widever;\r
576     SQL_TIMESTAMP_STRUCT expiration;\r
577 \r
578     if (m_wideVersion)\r
579         SQLBindCol(stmt, 1, SQL_C_SLONG, &widever, 0, nullptr);\r
580     else\r
581         SQLBindCol(stmt, 1, SQL_C_SSHORT, &ver, 0, nullptr);\r
582     if (pexpiration)\r
583         SQLBindCol(stmt, 2, SQL_C_TYPE_TIMESTAMP, &expiration, 0, nullptr);\r
584 \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
588         return 0;\r
589     }\r
590 \r
591     if (pexpiration)\r
592         *pexpiration = timeFromTimestamp(expiration);\r
593 \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
598     }\r
599 \r
600     if (pvalue) {\r
601         SQLLEN len;\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
608             }\r
609             pvalue->append((char*)buf);\r
610         }\r
611     }\r
612     \r
613     return (m_wideVersion ? widever : ver);\r
614 }\r
615 \r
616 int ODBCStorageService::updateRow(const char *table, const char* context, const char* key, const char* value, time_t expiration, int version)\r
617 {\r
618 #ifdef _DEBUG\r
619     xmltooling::NDC ndc("updateRow");\r
620 #endif\r
621 \r
622     if (!value && !expiration)\r
623         throw IOException("ODBC StorageService given invalid update instructions.");\r
624 \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
632 \r
633     // First, fetch the current version for later, which also ensures the record still exists.\r
634     char timebuf[32];\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
640 \r
641     m_log.debug("SQL: %s", q.c_str());\r
642 \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
648     }\r
649 \r
650     SQLSMALLINT ver;\r
651     SQLINTEGER widever;\r
652     if (m_wideVersion)\r
653         SQLBindCol(stmt, 1, SQL_C_SLONG, &widever, 0, nullptr);\r
654     else\r
655         SQLBindCol(stmt, 1, SQL_C_SSHORT, &ver, 0, nullptr);\r
656     if ((sr = SQLFetch(stmt)) == SQL_NO_DATA) {\r
657         return 0;\r
658     }\r
659 \r
660     // Check version?\r
661     if (version > 0 && version != (m_wideVersion ? widever : ver)) {\r
662         return -1;\r
663     }\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
667     }\r
668 \r
669     SQLFreeHandle(SQL_HANDLE_STMT, stmt);\r
670     stmt = getHSTMT(conn);\r
671 \r
672     // Prepare and exectute update statement.\r
673     q = string("UPDATE ") + table + " SET ";\r
674 \r
675     if (value)\r
676         q = q + "value=?, version=version+1";\r
677 \r
678     if (expiration) {\r
679         timestampFromTime(expiration, timebuf);\r
680         if (value)\r
681             q += ',';\r
682         q = q + "expires = " + timebuf;\r
683     }\r
684 \r
685     q = q + " WHERE context='" + scontext.tostr() + "' AND id='" + skey.tostr() + "'";\r
686 \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
692     }\r
693     m_log.debug("SQLPrepare succeeded. SQL: %s", q.c_str());\r
694 \r
695     SQLLEN b_ind = SQL_NTS;\r
696     if (value) {\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
699         else\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
705         }\r
706         m_log.debug("SQLBindParam succeeded (value = %s)", value);\r
707     }\r
708 \r
709     int attempts = 3;\r
710     pair<bool,bool> logres;\r
711     do {\r
712         logres = make_pair(false,false);\r
713         attempts--;\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
720         }\r
721 \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
725 \r
726     throw IOException("ODBC StorageService failed to update record.");\r
727 }\r
728 \r
729 bool ODBCStorageService::deleteRow(const char *table, const char *context, const char* key)\r
730 {\r
731 #ifdef _DEBUG\r
732     xmltooling::NDC ndc("deleteRow");\r
733 #endif\r
734 \r
735     // Get statement handle.\r
736     ODBCConn conn(getHDBC());\r
737     SQLHSTMT stmt = getHSTMT(conn);\r
738 \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
744 \r
745     SQLRETURN sr = SQLExecDirect(stmt, (SQLCHAR*)q.c_str(), SQL_NTS);\r
746      if (sr == SQL_NO_DATA)\r
747         return false;\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
752     }\r
753 \r
754     return true;\r
755 }\r
756 \r
757 \r
758 void ODBCStorageService::cleanup()\r
759 {\r
760 #ifdef _DEBUG\r
761     xmltooling::NDC ndc("cleanup");\r
762 #endif\r
763 \r
764     scoped_ptr<Mutex> mutex(Mutex::create());\r
765 \r
766     mutex->lock();\r
767 \r
768     m_log.info("cleanup thread started... running every %d secs", m_cleanupInterval);\r
769 \r
770     while (!shutdown) {\r
771         shutdown_wait->timedwait(mutex.get(), m_cleanupInterval);\r
772         if (shutdown)\r
773             break;\r
774         try {\r
775             reap(nullptr);\r
776         }\r
777         catch (std::exception& ex) {\r
778             m_log.error("cleanup thread swallowed exception: %s", ex.what());\r
779         }\r
780     }\r
781 \r
782     m_log.info("cleanup thread exiting...");\r
783 \r
784     mutex->unlock();\r
785     Thread::exit(nullptr);\r
786 }\r
787 \r
788 void* ODBCStorageService::cleanup_fn(void* cache_p)\r
789 {\r
790   ODBCStorageService* cache = (ODBCStorageService*)cache_p;\r
791 \r
792 #ifndef WIN32\r
793   // First, let's block all signals\r
794   Thread::mask_all_signals();\r
795 #endif\r
796 \r
797   // Now run the cleanup process.\r
798   cache->cleanup();\r
799   return nullptr;\r
800 }\r
801 \r
802 void ODBCStorageService::updateContext(const char *table, const char* context, time_t expiration)\r
803 {\r
804 #ifdef _DEBUG\r
805     xmltooling::NDC ndc("updateContext");\r
806 #endif\r
807 \r
808     // Get statement handle.\r
809     ODBCConn conn(getHDBC());\r
810     SQLHSTMT stmt = getHSTMT(conn);\r
811 \r
812     char timebuf[32];\r
813     timestampFromTime(expiration, timebuf);\r
814 \r
815     char nowbuf[32];\r
816     timestampFromTime(time(nullptr), nowbuf);\r
817 \r
818     SQLString scontext(context);\r
819     string q = string("UPDATE ") + table + " SET expires = " + timebuf + " WHERE context='" + scontext.tostr() + "' AND expires > " + nowbuf;\r
820 \r
821     m_log.debug("SQL: %s", q.c_str());\r
822 \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
828     }\r
829 }\r
830 \r
831 void ODBCStorageService::reap(const char *table, const char* context)\r
832 {\r
833 #ifdef _DEBUG\r
834     xmltooling::NDC ndc("reap");\r
835 #endif\r
836 \r
837     // Get statement handle.\r
838     ODBCConn conn(getHDBC());\r
839     SQLHSTMT stmt = getHSTMT(conn);\r
840 \r
841     // Prepare and execute delete statement.\r
842     char nowbuf[32];\r
843     timestampFromTime(time(nullptr), nowbuf);\r
844     string q;\r
845     if (context) {\r
846         SQLString scontext(context);\r
847         q = string("DELETE FROM ") + table + " WHERE context='" + scontext.tostr() + "' AND expires <= " + nowbuf;\r
848     }\r
849     else {\r
850         q = string("DELETE FROM ") + table + " WHERE expires <= " + nowbuf;\r
851     }\r
852     m_log.debug("SQL: %s", q.c_str());\r
853 \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
859     }\r
860 }\r
861 \r
862 void ODBCStorageService::deleteContext(const char *table, const char* context)\r
863 {\r
864 #ifdef _DEBUG\r
865     xmltooling::NDC ndc("deleteContext");\r
866 #endif\r
867 \r
868     // Get statement handle.\r
869     ODBCConn conn(getHDBC());\r
870     SQLHSTMT stmt = getHSTMT(conn);\r
871 \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
876 \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
882     }\r
883 }\r
884 \r
885 extern "C" int ODBCSTORE_EXPORTS xmltooling_extension_init(void*)\r
886 {\r
887     // Register this SS type\r
888     XMLToolingConfig::getConfig().StorageServiceManager.registerFactory("ODBC", ODBCStorageServiceFactory);\r
889     return 0;\r
890 }\r
891 \r
892 extern "C" void ODBCSTORE_EXPORTS xmltooling_extension_term()\r
893 {\r
894     XMLToolingConfig::getConfig().StorageServiceManager.deregisterFactory("ODBC");\r
895 }\r