add database-aware MySQL pooling

This commit is contained in:
udo
2026-07-18 12:55:48 +00:00
parent 418c5812dc
commit 810c19b042
10 changed files with 83 additions and 31 deletions
+1 -1
View File
@@ -160,7 +160,7 @@ def defined_symbols(path: Path, llvm_nm: str) -> list[str]:
def main() -> int: def main() -> int:
ap = argparse.ArgumentParser() ap = argparse.ArgumentParser()
ap.add_argument("wasm", type=Path) ap.add_argument("wasm", type=Path)
ap.add_argument("--abi-version", default="6") ap.add_argument("--abi-version", default="7")
ap.add_argument("--llvm-nm", default=None) ap.add_argument("--llvm-nm", default=None)
ap.add_argument("--verbose", action="store_true") ap.add_argument("--verbose", action="store_true")
args = ap.parse_args() args = ap.parse_args()
+1 -1
View File
@@ -11,7 +11,7 @@ PP_FN="$4"
WASM_FN="$5" WASM_FN="$5"
SDK=${WASI_SDK:-/opt/wasi-sdk} SDK=${WASI_SDK:-/opt/wasi-sdk}
ABI_VERSION=${UCE_UNIT_ABI_VERSION:-6} ABI_VERSION=${UCE_UNIT_ABI_VERSION:-7}
ROOT=$(pwd) ROOT=$(pwd)
OBJ_FN="$DEST_DIR/$PP_FN.wasm.o" OBJ_FN="$DEST_DIR/$PP_FN.wasm.o"
ABI_TMP="$DEST_DIR/$PP_FN.uce-abi.txt" ABI_TMP="$DEST_DIR/$PP_FN.uce-abi.txt"
+36 -8
View File
@@ -18,10 +18,11 @@ cache_dir=""
http_host="${UCE_TEST_HTTP_HOST:-uce.openfu.com}" http_host="${UCE_TEST_HTTP_HOST:-uce.openfu.com}"
test_user="uce_pool_$$" test_user="uce_pool_$$"
test_database="uce_pool_$$" test_database="uce_pool_$$"
test_database_other="uce_pool_other_$$"
test_password=$(printf '%s' "$test_name-$(date +%s%N)" | sha256sum | cut -c1-32) test_password=$(printf '%s' "$test_name-$(date +%s%N)" | sha256sum | cut -c1-32)
cleanup() { cleanup() {
mariadb -e "DROP DATABASE IF EXISTS \`$test_database\`; DROP USER IF EXISTS '$test_user'@'127.0.0.1'" >/dev/null 2>&1 || true mariadb -e "DROP DATABASE IF EXISTS \`$test_database\`; DROP DATABASE IF EXISTS \`$test_database_other\`; DROP USER IF EXISTS '$test_user'@'127.0.0.1'" >/dev/null 2>&1 || true
rm -rf "$source_dir" rm -rf "$source_dir"
if [[ -n "$cache_dir" ]]; then if [[ -n "$cache_dir" ]]; then
rm -rf "$cache_dir" rm -rf "$cache_dir"
@@ -30,14 +31,21 @@ cleanup() {
trap cleanup EXIT trap cleanup EXIT
mkdir -p "$source_dir" mkdir -p "$source_dir"
cache_dir="$bin_directory$(realpath "$source_dir")" cache_dir="$bin_directory$(realpath "$source_dir")"
mariadb -e "DROP DATABASE IF EXISTS \`$test_database\`; DROP USER IF EXISTS '$test_user'@'127.0.0.1'; CREATE DATABASE \`$test_database\`; CREATE USER '$test_user'@'127.0.0.1' IDENTIFIED BY '$test_password'; GRANT ALL ON \`$test_database\`.* TO '$test_user'@'127.0.0.1'" mariadb -e "DROP DATABASE IF EXISTS \`$test_database\`; DROP DATABASE IF EXISTS \`$test_database_other\`; DROP USER IF EXISTS '$test_user'@'127.0.0.1'; CREATE DATABASE \`$test_database\`; CREATE DATABASE \`$test_database_other\`; CREATE USER '$test_user'@'127.0.0.1' IDENTIFIED BY '$test_password'; GRANT ALL ON \`$test_database\`.* TO '$test_user'@'127.0.0.1'; GRANT ALL ON \`$test_database_other\`.* TO '$test_user'@'127.0.0.1'; CREATE TABLE \`$test_database\`.pool_identity (label VARCHAR(16) NOT NULL); INSERT INTO \`$test_database\`.pool_identity VALUES ('primary'); CREATE TABLE \`$test_database_other\`.pool_identity (label VARCHAR(16) NOT NULL); INSERT INTO \`$test_database_other\`.pool_identity VALUES ('other')"
printf '%s\n' \ printf '%s\n' \
'RENDER(Request& context)' \ 'RENDER(Request& context)' \
'{' \ '{' \
" MySQL* db = mysql_connect(\"127.0.0.1\", \"$test_user\", \"$test_password\");" \ " MySQL* db = mysql_connect(\"127.0.0.1\", \"$test_user\", \"$test_password\", \"$test_database\");" \
' if(db == 0 || mysql_error(db) != "") { context.set_status(500, "MySQL connect failed"); print("connect-failed"); return; }' \ " MySQL* other = mysql_connect(\"127.0.0.1\", \"$test_user\", \"$test_password\", \"$test_database_other\");" \
" mysql_query(db, \"USE \\\`$test_database\\\`\");" \ " MySQL* unselected = mysql_connect(\"127.0.0.1\", \"$test_user\", \"$test_password\");" \
' if(db == 0 || other == 0 || unselected == 0 || !mysql_connected(db) || !mysql_connected(other) || !mysql_connected(unselected)) { context.set_status(500, "MySQL connect failed"); print("connect-failed"); return; }' \
' String primary_database, primary_label, other_database, other_label;' \
' mysql_query(db, "SELECT DATABASE() AS db, label FROM pool_identity").each([&](DValue row, String key) { primary_database = row["db"].to_string(); primary_label = row["label"].to_string(); });' \
' mysql_query(other, "SELECT DATABASE() AS db, label FROM pool_identity").each([&](DValue row, String key) { other_database = row["db"].to_string(); other_label = row["label"].to_string(); });' \
' String unselected_database;' \
' mysql_query(unselected, "SELECT DATABASE() AS db").each([&](DValue row, String key) { unselected_database = row["db"].to_string(); });' \
" bool database_clean = primary_database == \"$test_database\" && primary_label == \"primary\" && other_database == \"$test_database_other\" && other_label == \"other\" && unselected_database == \"\";" \
' DValue marker_rows = mysql_query(db, "SELECT @uce_pool_marker AS marker");' \ ' DValue marker_rows = mysql_query(db, "SELECT @uce_pool_marker AS marker");' \
' String marker; marker_rows.each([&](DValue row, String key) { marker = row["marker"].to_string(); });' \ ' String marker; marker_rows.each([&](DValue row, String key) { marker = row["marker"].to_string(); });' \
' bool marker_clean = marker == "";' \ ' bool marker_clean = marker == "";' \
@@ -45,19 +53,39 @@ printf '%s\n' \
' bool temp_clean = mysql_error(db) != "";' \ ' bool temp_clean = mysql_error(db) != "";' \
" mysql_query(db, \"SET @uce_pool_marker='dirty'\");" \ " mysql_query(db, \"SET @uce_pool_marker='dirty'\");" \
' mysql_query(db, "CREATE TEMPORARY TABLE uce_persistent_temp (id INT PRIMARY KEY)");' \ ' mysql_query(db, "CREATE TEMPORARY TABLE uce_persistent_temp (id INT PRIMARY KEY)");' \
" mysql_query(db, \"USE \\\`$test_database_other\\\`\");" \
" mysql_query(other, \"USE \\\`$test_database\\\`\");" \
" mysql_query(unselected, \"USE \\\`$test_database\\\`\");" \
' DValue perf = request_perf();' \ ' DValue perf = request_perf();' \
' String source;' \ ' String source;' \
' perf["mysql_operations"].each([&](DValue operation, String key) { if(operation["op"].to_string() == "connect") source = operation["source"].to_string(); });' \ ' perf["mysql_operations"].each([&](DValue operation, String key) { if(operation["op"].to_string() == "connect" && operation["source"].to_string() == "worker") source = "worker"; });' \
' print(perf["worker_pid"].to_string(), "|", source, "|", marker_clean ? "clean" : "dirty", "|", temp_clean ? "clean" : "dirty");' \ ' print(perf["worker_pid"].to_string(), "|", source, "|", marker_clean ? "clean" : "dirty", "|", temp_clean ? "clean" : "dirty", "|", database_clean ? "clean" : "dirty");' \
' mysql_disconnect(unselected);' \
' mysql_disconnect(other);' \
' mysql_disconnect(db);' \ ' mysql_disconnect(db);' \
'}' >"$source_dir/test.uce" '}' >"$source_dir/test.uce"
printf '%s\n' \
'RENDER(Request& context)' \
'{' \
" MySQL* db = mysql_connect(\"127.0.0.1\", \"$test_user\", \"$test_password\", \"${test_database}_missing\");" \
' String error = mysql_error(db);' \
' print(!mysql_connected(db) && error != "" ? "database-selection-failed" : "database-selection-was-ignored");' \
' mysql_disconnect(db);' \
'}' >"$source_dir/failure.uce"
failure=$(curl -fsS --max-time 10 -H "Host: $http_host" "http://127.0.0.1/$test_name/failure.uce")
if [[ "$failure" != "database-selection-failed" ]]; then
echo "Unknown initial database did not surface a connection failure: $failure" >&2
exit 1
fi
reused=0 reused=0
for _ in $(seq 1 160); do for _ in $(seq 1 160); do
output=$(curl -fsS --max-time 10 -H "Host: $http_host" "http://127.0.0.1/$test_name/test.uce") output=$(curl -fsS --max-time 10 -H "Host: $http_host" "http://127.0.0.1/$test_name/test.uce")
if [[ "$output" == *"|worker|"* ]]; then if [[ "$output" == *"|worker|"* ]]; then
reused=$((reused + 1)) reused=$((reused + 1))
if [[ "$output" != *"|worker|clean|clean" ]]; then if [[ "$output" != *"|worker|clean|clean|clean" ]]; then
echo "Persistent MySQL reuse leaked cross-request state: $output" >&2 echo "Persistent MySQL reuse leaked cross-request state: $output" >&2
exit 1 exit 1
fi fi
+4 -3
View File
@@ -1,21 +1,22 @@
:sig :sig
MySQL* mysql_connect(String host = "localhost", String username = "root", String password = "") MySQL* mysql_connect(String host = "localhost", String username = "root", String password = "", String database = "")
:params :params
host : host name of the MySQL server host : host name of the MySQL server
username : user name username : user name
password : password password : password
database : optional initial database name
return value : pointer to the MySQL connection struct return value : pointer to the MySQL connection struct
:see :see
>mysql >mysql
:content :content
Establishes a connection to a MySQL server and returns a request-owned pointer to the connection struct. A failed connection also returns a wrapper so that `mysql_error()` can report the failure; use `mysql_connected()` before issuing optional work that requires a live server handle. Establishes a connection to a MySQL server and returns a request-owned pointer to the connection struct. When `database` is non-empty, the server selects it as part of connection establishment and restores it after every cross-request connection reset. A failed connection also returns a wrapper so that `mysql_error()` can report the failure; use `mysql_connected()` before issuing optional work that requires a live server handle.
This connection handle is then used with helpers such as `mysql_query()`, `mysql_error()`, and `mysql_disconnect()`. This connection handle is then used with helpers such as `mysql_query()`, `mysql_error()`, and `mysql_disconnect()`.
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 and `mysql_disconnect()` releases that lease. At request cleanup UCE returns the server connection to the current worker's persistent pool, then resets it before cross-request reuse. The pool holds up to `MYSQL_PERSISTENT_POOL_SIZE` connections per worker (default 8); set the size to 0 to disable cross-request reuse. 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, credentials, and database reuse one server connection within the current request. Database identity is part of both request-local and worker-persistent pool keys. Each call creates a lease and `mysql_disconnect()` releases that lease. At request cleanup UCE returns the server connection to the current worker's persistent pool, then resets it before cross-request reuse. The pool holds up to `MYSQL_PERSISTENT_POOL_SIZE` connections per worker (default 8); set the size to 0 to disable cross-request reuse. Never store a `MySQL*` in globals, sessions, or other state that can outlive the current request.
:example :example
MySQL* db = mysql_connect(); MySQL* db = mysql_connect();
+1 -1
View File
@@ -16,7 +16,7 @@
namespace { namespace {
const u64 UCE_UNIT_ABI_VERSION = 10; const u64 UCE_UNIT_ABI_VERSION = 11;
struct SharedUnitFilesystemState struct SharedUnitFilesystemState
{ {
+18 -4
View File
@@ -3,7 +3,7 @@
#include <stdlib.h> #include <stdlib.h>
#include "mysql-connector.h" #include "mysql-connector.h"
// Same-credential mysql_connect() calls lease one request-local connection. // Same-target mysql_connect() calls lease one request-local connection.
// mysql_disconnect() releases a lease; cleanup_mysql_connections() owns the // mysql_disconnect() releases a lease; cleanup_mysql_connections() owns the
// actual close at request end, including exception/fatal recovery paths. // actual close at request end, including exception/fatal recovery paths.
static void mysql_register_request_connection(MySQL* db) static void mysql_register_request_connection(MySQL* db)
@@ -15,8 +15,12 @@ static void mysql_register_request_connection(MySQL* db)
connections.push_back(db); connections.push_back(db);
} }
bool MySQL::connect(String host, String username, String password) bool MySQL::connect(String host, String username, String password, String database)
{ {
request_host = host;
request_username = username;
request_password = password;
request_database = database;
// Register regardless of outcome: tracking is about the wrapper's // Register regardless of outcome: tracking is about the wrapper's
// lifetime, not the connection's. disconnect()/~MySQL() unregister. // lifetime, not the connection's. disconnect()/~MySQL() unregister.
mysql_register_request_connection(this); mysql_register_request_connection(this);
@@ -32,7 +36,7 @@ bool MySQL::connect(String host, String username, String password)
} }
if (mysql_real_connect((MYSQL*)connection, host.c_str(), username.c_str(), password.c_str(), if (mysql_real_connect((MYSQL*)connection, host.c_str(), username.c_str(), password.c_str(),
NULL, 0, NULL, 0) == NULL) database == "" ? NULL : database.c_str(), 0, NULL, 0) == NULL)
{ {
auto e = mysql_error((MYSQL*)connection); auto e = mysql_error((MYSQL*)connection);
fprintf(stderr, "%s\n", e); fprintf(stderr, "%s\n", e);
@@ -59,7 +63,17 @@ bool MySQL::connect(String host, String username, String password)
bool MySQL::reset_connection() bool MySQL::reset_connection()
{ {
if(!connection || mysql_reset_connection((MYSQL*)connection) != 0) if(!connection)
return(false);
MYSQL* mysql = (MYSQL*)connection;
String selected_database = mysql->db == NULL ? "" : mysql->db;
// RESET CONNECTION deliberately preserves the selected database. An
// unqualified lease cannot safely inherit one selected by its prior request.
if(request_database == "" && selected_database != "")
return(false);
if(mysql_reset_connection(mysql) != 0)
return(false);
if(request_database != "" && selected_database != request_database && mysql_select_db(mysql, request_database.c_str()) != 0)
return(false); return(false);
_preload_next_error_code = 0; _preload_next_error_code = 0;
affected_rows = 0; affected_rows = 0;
+6 -4
View File
@@ -27,10 +27,11 @@ struct MySQL {
String request_host; String request_host;
String request_username; String request_username;
String request_password; String request_password;
String request_database;
std::vector<MySQLFieldInfo> field_info; std::vector<MySQLFieldInfo> field_info;
bool connect(String host = "localhost", String username = "root", String password = ""); bool connect(String host = "localhost", String username = "root", String password = "", String database = "");
bool reset_connection(); bool reset_connection();
void disconnect(); void disconnect();
String error(); String error();
@@ -46,14 +47,14 @@ struct MySQL {
}; };
inline MySQL* mysql_connect(String host = "localhost", String username = "root", String password = "") inline MySQL* mysql_connect(String host = "localhost", String username = "root", String password = "", String database = "")
{ {
if(context) if(context)
{ {
for(void* raw : context->resources.mysql_connections) for(void* raw : context->resources.mysql_connections)
{ {
MySQL* db = (MySQL*)raw; MySQL* db = (MySQL*)raw;
if(db && db->request_pooled && db->connection && db->request_host == host && db->request_username == username && db->request_password == password) if(db && db->request_pooled && db->connection && db->request_host == host && db->request_username == username && db->request_password == password && db->request_database == database)
{ {
db->request_leases++; db->request_leases++;
return(db); return(db);
@@ -67,7 +68,8 @@ inline MySQL* mysql_connect(String host = "localhost", String username = "root",
db->request_host = host; db->request_host = host;
db->request_username = username; db->request_username = username;
db->request_password = password; db->request_password = password;
db->connect(host, username, password); db->request_database = database;
db->connect(host, username, password, database);
return(db); return(db);
} }
+7 -2
View File
@@ -146,13 +146,18 @@ static DValue wasm_mysql_call(DValue request)
return(wasm_sized_hostcall(request, uce_host_mysql)); return(wasm_sized_hostcall(request, uce_host_mysql));
} }
bool MySQL::connect(String host, String username, String password) bool MySQL::connect(String host, String username, String password, String database)
{ {
request_host = host;
request_username = username;
request_password = password;
request_database = database;
DValue request; DValue request;
request["op"] = "connect"; request["op"] = "connect";
request["host"] = host; request["host"] = host;
request["username"] = username; request["username"] = username;
request["password"] = password; request["password"] = password;
request["database"] = database;
DValue response = wasm_mysql_call(request); DValue response = wasm_mysql_call(request);
u64 handle = response["handle"].to_u64(); u64 handle = response["handle"].to_u64();
connection = (void*)(uintptr_t)handle; connection = (void*)(uintptr_t)handle;
@@ -804,7 +809,7 @@ void uce_free(void* ptr)
u32 uce_wasm_core_abi_version() u32 uce_wasm_core_abi_version()
{ {
return(6); return(7);
} }
int uce_wasm_core_init() int uce_wasm_core_init()
+2 -2
View File
@@ -254,7 +254,7 @@ int main(int argc, char** argv)
CHECK(call_i32(core, "uce_wasm_core_init") == 0, "core init failed"); CHECK(call_i32(core, "uce_wasm_core_init") == 0, "core init failed");
call_i32(core, "uce_wasm_core_reset_request"); call_i32(core, "uce_wasm_core_reset_request");
CHECK(call_i32(core, "uce_wasm_core_abi_version") == 6, "unexpected ABI version"); CHECK(call_i32(core, "uce_wasm_core_abi_version") == 7, "unexpected ABI version");
wasm_memory_t* memory = core.memory(); wasm_memory_t* memory = core.memory();
int32_t root = call_i32(core, "uce_dv_root"); int32_t root = call_i32(core, "uce_dv_root");
@@ -302,7 +302,7 @@ int main(int argc, char** argv)
int32_t output_ptr = call_i32(core, "uce_wasm_output_data"); int32_t output_ptr = call_i32(core, "uce_wasm_output_data");
CHECK(read_bytes(memory, output_ptr, output_len) == out, "output plumbing mismatch"); CHECK(read_bytes(memory, output_ptr, output_len) == out, "output plumbing mismatch");
printf("W1 core.wasm smoke: abi=6 encoded=%d output=%d\n", encoded_len, output_len); printf("W1 core.wasm smoke: abi=7 encoded=%d output=%d\n", encoded_len, output_len);
printf("W1 EXIT CRITERION: PASS\n"); printf("W1 EXIT CRITERION: PASS\n");
return(0); return(0);
} }
+7 -5
View File
@@ -968,14 +968,14 @@ public:
delete db; delete db;
} }
MySQL* mysql_checkout(const String& host, const String& username, const String& password, bool& reused, bool& persistent) MySQL* mysql_checkout(const String& host, const String& username, const String& password, const String& database, bool& reused, bool& persistent)
{ {
reused = false; reused = false;
persistent = false; persistent = false;
for(size_t i = 0; i < mysql_persistent_pool.size(); i++) for(size_t i = 0; i < mysql_persistent_pool.size(); i++)
{ {
MySQL* db = mysql_persistent_pool[i]; MySQL* db = mysql_persistent_pool[i];
if(!db || !db->connection || db->request_host != host || db->request_username != username || db->request_password != password) if(!db || !db->connection || db->request_host != host || db->request_username != username || db->request_password != password || db->request_database != database)
continue; continue;
if(db->reset_connection()) if(db->reset_connection())
{ {
@@ -1000,7 +1000,8 @@ public:
db->request_host = host; db->request_host = host;
db->request_username = username; db->request_username = username;
db->request_password = password; db->request_password = password;
if(!db->connect(host, username, password) || !db->connection) db->request_database = database;
if(!db->connect(host, username, password, database) || !db->connection)
return(db); return(db);
if(persistent) if(persistent)
{ {
@@ -3705,9 +3706,10 @@ private:
String host = request["host"].to_string(); String host = request["host"].to_string();
String username = request["username"].to_string(); String username = request["username"].to_string();
String password = request["password"].to_string(); String password = request["password"].to_string();
String database = request["database"].to_string();
MySQL* db = 0; MySQL* db = 0;
for(auto* pooled : self->mysql_request_pool) for(auto* pooled : self->mysql_request_pool)
if(pooled && pooled->connection && pooled->request_host == host && pooled->request_username == username && pooled->request_password == password) if(pooled && pooled->connection && pooled->request_host == host && pooled->request_username == username && pooled->request_password == password && pooled->request_database == database)
{ {
db = pooled; db = pooled;
connection_source = "request"; connection_source = "request";
@@ -3718,7 +3720,7 @@ private:
{ {
bool reused = false; bool reused = false;
bool persistent = false; bool persistent = false;
db = self->worker.mysql_checkout(host, username, password, reused, persistent); db = self->worker.mysql_checkout(host, username, password, database, reused, persistent);
connection_source = reused ? "worker" : "new"; connection_source = reused ? "worker" : "new";
ok = db && db->connection; ok = db && db->connection;
if(ok && db->connection) if(ok && db->connection)