diff --git a/site/doc/pages/mysql_connect.txt b/site/doc/pages/mysql_connect.txt index 6a4538c..3416a47 100644 --- a/site/doc/pages/mysql_connect.txt +++ b/site/doc/pages/mysql_connect.txt @@ -15,7 +15,7 @@ Establishes a connection to a MySQL server and returns a pointer to the connecti This connection handle is then used with helpers such as `mysql_query()`, `mysql_error()`, and `mysql_disconnect()`. -MySQL handles are request-scoped framework resources. If a request exits without calling `mysql_disconnect()`, UCE closes any remaining MySQL handles during request cleanup. Call `mysql_disconnect()` when you want to close early, but never store a `MySQL*` in globals, sessions, or other state that can outlive the current request. +MySQL handles are request-scoped framework resources. Repeated `mysql_connect()` calls with the same host and credentials reuse one server connection within the current request. Each call creates a lease; `mysql_disconnect()` releases that lease, and UCE closes the pooled server connection during request cleanup. Never store a `MySQL*` in globals, sessions, or other state that can outlive the current request. :example MySQL* db = mysql_connect(); diff --git a/site/doc/pages/mysql_disconnect.txt b/site/doc/pages/mysql_disconnect.txt index ecddfa8..ae261b8 100644 --- a/site/doc/pages/mysql_disconnect.txt +++ b/site/doc/pages/mysql_disconnect.txt @@ -10,7 +10,7 @@ m : pointer to an existing MySQL connection struct :content Closes an existing connection to a MySQL server. -Call this when you want to release a `MySQL*` connection handle before the request ends. UCE also closes any remaining MySQL handles automatically during request cleanup, including handles skipped by exceptions or fatal request recovery. A `MySQL*` is only valid inside the request that opened it; do not cache it across requests. +Releases the lease returned by `mysql_connect()`. The request-local server connection remains available for another same-credential `mysql_connect()` call and is closed by UCE during request cleanup, including after exceptions or fatal request recovery. A `MySQL*` is only valid inside the request that opened it; do not use it after release or cache it across requests. :example MySQL* db = mysql_connect(); diff --git a/src/lib/mysql-connector.cpp b/src/lib/mysql-connector.cpp index 0fe8797..0d4509d 100644 --- a/src/lib/mysql-connector.cpp +++ b/src/lib/mysql-connector.cpp @@ -3,10 +3,9 @@ #include #include "mysql-connector.h" -// MySQL handles are request-scoped framework resources. User code may call -// mysql_disconnect() to close early, but leaked/exception-skipped handles are -// still closed by cleanup_mysql_connections() at request end. Handles must not -// be cached across requests or stored in globals/sessions. +// Same-credential mysql_connect() calls lease one request-local connection. +// mysql_disconnect() releases a lease; cleanup_mysql_connections() owns the +// actual close at request end, including exception/fatal recovery paths. static void mysql_register_request_connection(MySQL* db) { if(!context || !db) @@ -16,6 +15,31 @@ static void mysql_register_request_connection(MySQL* db) connections.push_back(db); } +MySQL* mysql_connect(String host, String username, String password) +{ + if(context) + { + for(void* raw : context->resources.mysql_connections) + { + MySQL* db = (MySQL*)raw; + if(db && db->request_pooled && db->connection && db->request_host == host && db->request_username == username && db->request_password == password) + { + db->request_leases++; + return(db); + } + } + } + MySQL* db = new MySQL(); + db->request_cleanup_delete = true; + db->request_pooled = context != 0; + db->request_leases = 1; + db->request_host = host; + db->request_username = username; + db->request_password = password; + db->connect(host, username, password); + return(db); +} + bool MySQL::connect(String host, String username, String password) { // Register regardless of outcome: tracking is about the wrapper's @@ -325,6 +349,7 @@ void MySQL::disconnect() if(connection) mysql_close((MYSQL*)connection); connection = NULL; + request_leases = 0; if(context) { auto& connections = context->resources.mysql_connections; @@ -389,6 +414,7 @@ void cleanup_mysql_connections() MySQL* db = (MySQL*)context->resources.mysql_connections.back(); context->resources.mysql_connections.pop_back(); bool should_delete = db->request_cleanup_delete; + db->request_pooled = false; db->disconnect(); if(should_delete) delete db; diff --git a/src/lib/mysql-connector.h b/src/lib/mysql-connector.h index d37c84b..5ff9847 100644 --- a/src/lib/mysql-connector.h +++ b/src/lib/mysql-connector.h @@ -21,6 +21,11 @@ struct MySQL { u64 insert_id = 0; String statement_info = ""; // bool request_cleanup_delete = false; + bool request_pooled = false; + u32 request_leases = 0; + String request_host; + String request_username; + String request_password; std::vector field_info; @@ -33,26 +38,24 @@ struct MySQL { DValue query(String q, StringMap params); DValue get_pending_result(); - // Unregisters from request connection tracking, so stack-allocated instances - // cannot leave dangling pointers behind for request cleanup. Heap handles - // returned by mysql_connect() are also request-owned and auto-closed at the - // end of the request if user code does not call mysql_disconnect() first. + // Unregisters non-pooled instances from request tracking. mysql_connect() + // handles remain request-owned until cleanup after their leases are released. ~MySQL() { disconnect(); } }; -inline MySQL* mysql_connect(String host = "localhost", String username = "root", String password = "") -{ - MySQL* m = new MySQL(); - m->request_cleanup_delete = true; - m->connect(host, username, password); - return(m); -} +MySQL* mysql_connect(String host = "localhost", String username = "root", String password = ""); inline void mysql_disconnect(MySQL* m) { if(!m) return; + if(m->request_pooled) + { + if(m->request_leases > 0) + m->request_leases--; + return; + } m->disconnect(); delete m; } diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp index 206c72b..26945fa 100644 --- a/src/wasm/worker.cpp +++ b/src/wasm/worker.cpp @@ -804,6 +804,7 @@ public: // never cache these opaque handles across requests. std::vector sqlite_handles; std::vector mysql_handles; + std::vector mysql_request_pool; #endif ~WasmWorkspace() { @@ -820,7 +821,7 @@ public: for(auto* db : sqlite_handles) if(db) delete db; // ~SQLite disconnects - for(auto* db : mysql_handles) + for(auto* db : mysql_request_pool) if(db) delete db; // ~MySQL disconnects #endif @@ -2409,11 +2410,32 @@ private: String op = request["op"].to_string(); if(op == "connect") { - MySQL* db = new MySQL(); - bool ok = db->connect(request["host"].to_string(), request["username"].to_string(), request["password"].to_string()); + String host = request["host"].to_string(); + String username = request["username"].to_string(); + String password = request["password"].to_string(); + MySQL* db = 0; + for(auto* pooled : self->mysql_request_pool) + if(pooled && pooled->connection && pooled->request_host == host && pooled->request_username == username && pooled->request_password == password) + { + db = pooled; + break; + } + bool ok = db != 0; + if(!db) + { + db = new MySQL(); + db->request_pooled = true; + db->request_host = host; + db->request_username = username; + db->request_password = password; + ok = db->connect(host, username, password); + if(ok && db->connection) + self->mysql_request_pool.push_back(db); + } u64 handle = 0; if(ok && db->connection) { + db->request_leases++; self->mysql_handles.push_back(db); handle = self->mysql_handles.size(); } @@ -2443,7 +2465,8 @@ private: } else if(op == "disconnect" && db) { - delete db; + if(db->request_leases > 0) + db->request_leases--; self->mysql_handles[(size_t)handle - 1] = 0; } }