#include "__mysql.h" #include "Exception.h" #include #include #include namespace jet { namespace { struct PooledConnection { MYSQL *handle; std::string host, user, database; }; // Persistent, per-worker-thread connection cache - reused across // requests instead of a fresh mysql_real_connect() every time. // Safe with no locking: the apiserver's EPoll worker pool is a // fixed set of OS threads for the process's whole life (see // MysqlThreadScope's own doc comment - mysql_thread_init() runs // once per worker thread at thread start), so a thread_local // instance here means a given MYSQL* handle is only ever touched // by the one thread that created it, never concurrently accessed. // Keyed by sessionid, which is how this codebase's own / // / tags already address a specific connection // (Global::sessions is keyed the same way) - the cached host/user/ // database are kept alongside purely as a defensive check that a // reused entry really matches what THIS invocation asked for, // not a real expectation that sessionid is ever reused across // genuinely different backing databases in practice. thread_local std::unordered_map connectionPool; } __mysql::__mysql(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, this) { if(!keywordDefined("host")) throw coreutils::Exception("host must be specified for mysql tag."); // jet-1.4's spells the same attribute "name" instead of // "database" (e.g. SESSIONCTL.tag's own definition) - // accept either rather than requiring that real content to change. coreutils::ZString databaseKeyword = keywordDefined("database") ? "database" : "name"; if(!keywordDefined(databaseKeyword)) throw coreutils::Exception("database must be specified for mysql tag."); if(!keywordDefined("user")) throw coreutils::Exception("user must be specified for mysql tag."); if(!keywordDefined("password")) throw coreutils::Exception("password must be specified for mysql tag."); // Same double-indirection bug as the host/user/password/database // fix above - resolveKeyword("sessionid") already returns the // resolved VALUE (e.g. "_SESSIONCTL"), so wrapping it in keywords[...] // used that value as a map KEY instead, auto-vivifying an empty // string (no attribute is ever literally named "_SESSIONCTL"). // sessionId was therefore always empty regardless of what // sessionid= actually specified - confirmed live via gdb: two // real / tags with genuinely different sessionid // values ("" and "_SESSIONCTL") both registered under "", the // second colliding with "sessionid already exists.". sessionId = resolveKeyword("sessionid"); // mysql_real_connect() returns NULL on failure but does NOT itself // free the handle mysql_init() allocated - the caller is still // expected to mysql_close() that original handle to avoid leaking // it (MySQL C API docs). The previous code overwrote `mysql` with // the connect() return value directly, so on failure the only // pointer to the real handle was lost before ever being freed - a // real leak on every failed connection attempt (this member's // eventual value, NULL, is what a normal ~__mysql() would try to // mysql_close() anyway, which is a safe no-op, but that never even // runs here since the constructor throws below - see the // addSession() comment for why that matters more). // resolveKeyword() already returns the fully-resolved VALUE - the // previous keywords[resolveKeyword("host")] wrapped that resolved // VALUE ("localhost") back into a keywords[] lookup, i.e. used it // as a MAP KEY instead of the value itself. Since no attribute is // ever literally named "localhost"/the real username/etc., // std::map::operator[] silently auto-vivified an empty MString for // every one of the four parameters - mysql_real_connect() was // therefore always called with host/user/password/database all // empty, regardless of what the tag actually specified. Confirmed // live: MySQL's own error ("Access denied for user 'barant'@'localhost' // (using password: NO)") showed the password was genuinely empty, // not merely wrong. coreutils::MString hostVal = resolveKeyword("host"); coreutils::MString userVal = resolveKeyword("user"); coreutils::MString passwordVal = resolveKeyword("password"); coreutils::MString databaseVal = resolveKeyword(databaseKeyword); // Reuse a live connection this same worker thread already holds // for this sessionid, rather than paying mysql_real_connect()'s // full handshake cost on every single request. mysql_ping() is // what makes this safe - it's a genuine round trip to the server, // so a connection MySQL itself already dropped (its own // wait_timeout, or an admin-initiated restart like this project // already lived through once) is detected and discarded here // rather than handed to processContainer() to fail on later. std::string poolKey = sessionId.str(); auto pooled = connectionPool.find(poolKey); if(pooled != connectionPool.end() && pooled->second.host == hostVal.str() && pooled->second.user == userVal.str() && pooled->second.database == databaseVal.str()) { if(mysql_ping(pooled->second.handle) == 0) { mysql = pooled->second.handle; } else { mysql_close(pooled->second.handle); connectionPool.erase(pooled); } } if(!mysql) { MYSQL *handle = mysql_init(NULL); mysql = mysql_real_connect(handle, hostVal.c_str(), userVal.c_str(), passwordVal.c_str(), databaseVal.c_str(), 0, NULL, 0); if(!mysql) { std::string err = mysql_error(handle); mysql_close(handle); throw coreutils::Exception("database connection failed: " + err); } connectionPool[poolKey] = PooledConnection{mysql, hostVal.str(), userVal.str(), databaseVal.str()}; } // Registered only once the connection is confirmed live - a real, // reproducible crash (heap corruption - "free(): invalid pointer" // seen live, then two independent server-hang repros) traced back // to this: addSession() used to run BEFORE the connect attempt, so // a FAILED connection (which throws, right above) left a dangling // `this` pointer registered under sessionId in Global's session // map forever - ~__mysql() (which would normally call // removeSession()) never runs for a constructor that itself threw, // per ordinary C++ rules (only already-completed base-class // subobjects get destroyed during exception unwinding, not the // derived object whose constructor is still executing). The very // next tag construction using the same sessionId (e.g. two // product pages with an empty sessionid="" back to back, both // failing the same way) then collided with that stale entry. // Confirmed via a real, deterministic two-request repro before this // fix, and confirmed gone after it. global.addSession(sessionId, this); processContainer(container); } __mysql::~__mysql() { global.removeSession(sessionId); mysql_free_result(result); // mysql itself is deliberately NOT closed here anymore - see the // connectionPool doc comment above the constructor. It's owned by // this worker thread's persistent pool now and gets reused by // whatever request this thread handles next, instead of being // torn down and fully reconnected from scratch every time. } void __mysql::query(coreutils::MString query) { int rc = mysql_real_query(mysql, query.getData(), query.getLength()); result = mysql_store_result(mysql); if(result) { row = mysql_fetch_row(result); fieldLength = mysql_fetch_lengths(result); qFields = mysql_num_fields(result); // Built once per query, not once per column LOOKUP - see the // matching comment on getColumnValue() and on fieldIndex's own // declaration in __mysql.h. mysql_fetch_fields() returns the // whole field array in one call (mysql_fetch_field_direct() // looping ix=0..qFields-1, as getColumnValue() used to do on // every single named access, does the exact same work over and // over for a result set whose schema never changes after this // point). fieldIndex.clear(); MYSQL_FIELD *fields = mysql_fetch_fields(result); for(unsigned int ix = 0; ix < qFields; ++ix) fieldIndex[std::string(fields[ix].name)] = (int)ix; } else { // A failed/errored query (real content builds one from an unset // $[...] parameter, e.g. "where tasks.id=" with no value - genuine // MySQL syntax error) previously left row/qFields holding whatever // a PRIOR successful query on this same session object had set, // since only the success branch above ever touched them. A later // getColumnValue() call then looped 0..qFields (stale, nonzero) // calling mysql_fetch_field_direct() against this NULL `result` - // segfaulting (confirmed via gdb: SIGSEGV in // mysql_fetch_field_direct, called from getColumnValue) instead of // reaching the "column does not exist in session result." the // caller already throws/catches for a genuinely missing column. // Resetting both here makes a failed query present as "no row, no // columns" - clean and consistent regardless of what ran before it // on this session. row = NULL; qFields = 0; fieldIndex.clear(); } } void __mysql::nextRow() { row = mysql_fetch_row(result); fieldLength = mysql_fetch_lengths(result); } bool __mysql::hasRow() { return row != NULL; } coreutils::ZString __mysql::getColumnValue(coreutils::ZString column) { MYSQL_FIELD *field; if(column == "?#") { nbrOfColumns = (int)qFields; return nbrOfColumns; } else if(column.ifNext("#") || column.ifNext("?")) { // "#N"/"?N" (column name at 1-based position N) are the same // jet-1.4 syntax under two different prefixes - real, unmodified // 1.4 content uses both (barant.tag's own definition // writes $[?$[index]], never $[#$[index]]) - confirmed via a real // repro: admin.page's own Users/Products tables rendered every // column header and cell completely blank, since "?N" fell all // the way through to the ordinary column-VALUE lookup below // (nothing named "?3" exists in the row) and read as empty rather // than as "the name of column 3", per the jet-1.4-fallback #2 // empty-string-on-missing-column behavior elsewhere in this // codebase (see Tag.cpp). if(column.eod()) { nbrOfRows = (int)mysql_num_rows(result); return nbrOfRows; } else { int index = column.asInteger(); field = mysql_fetch_field_direct(result, index - 1); return coreutils::ZString(field->name); } } // A query that ran successfully but matched zero rows (a real, // valid query - e.g. cart.page's own aggregate/JOIN query against // an empty cart session - not a failed one, see query()'s own // row=NULL/qFields=0 reset above for that separate case) leaves // qFields correctly reflecting the real schema but `row` NULL. // The loop below finds a genuine matching column name and then // dereferences row[ix] - segfaulting (confirmed via gdb: SIGSEGV // right here) instead of reaching a catchable error. Reusing the // same "column does not exist" exception the loop's own fallthrough // already throws - no current row means no column value is // obtainable either way, and callers already expect and catch // this exact exception. if(!row) throw coreutils::Exception("column does not exist in session result."); // O(1) against the map query() already built for this result set, // instead of re-deriving field metadata (mysql_fetch_field_direct, // a real MySQL C API call) and string-comparing column names one // by one on every single named access - see fieldIndex's own doc // comment in __mysql.h for the full story. auto it = fieldIndex.find(column.str()); if(it != fieldIndex.end()) return coreutils::ZString(row[it->second], fieldLength[it->second]); throw coreutils::Exception("column does not exist in session result."); } }