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

View File

@ -160,7 +160,7 @@ def defined_symbols(path: Path, llvm_nm: str) -> list[str]:
def main() -> int:
ap = argparse.ArgumentParser()
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("--verbose", action="store_true")
args = ap.parse_args()

View File

@ -11,7 +11,7 @@ PP_FN="$4"
WASM_FN="$5"
SDK=${WASI_SDK:-/opt/wasi-sdk}
ABI_VERSION=${UCE_UNIT_ABI_VERSION:-6}
ABI_VERSION=${UCE_UNIT_ABI_VERSION:-7}
ROOT=$(pwd)
OBJ_FN="$DEST_DIR/$PP_FN.wasm.o"
ABI_TMP="$DEST_DIR/$PP_FN.uce-abi.txt"

View File

@ -18,10 +18,11 @@ cache_dir=""
http_host="${UCE_TEST_HTTP_HOST:-uce.openfu.com}"
test_user="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)
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"
if [[ -n "$cache_dir" ]]; then
rm -rf "$cache_dir"
@ -30,14 +31,21 @@ cleanup() {
trap cleanup EXIT
mkdir -p "$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' \
'RENDER(Request& context)' \
'{' \
" MySQL* db = mysql_connect(\"127.0.0.1\", \"$test_user\", \"$test_password\");" \
' if(db == 0 || mysql_error(db) != "") { context.set_status(500, "MySQL connect failed"); print("connect-failed"); return; }' \
" mysql_query(db, \"USE \\\`$test_database\\\`\");" \
" MySQL* db = mysql_connect(\"127.0.0.1\", \"$test_user\", \"$test_password\", \"$test_database\");" \
" MySQL* other = mysql_connect(\"127.0.0.1\", \"$test_user\", \"$test_password\", \"$test_database_other\");" \
" 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");' \
' String marker; marker_rows.each([&](DValue row, String key) { marker = row["marker"].to_string(); });' \
' bool marker_clean = marker == "";' \
@ -45,19 +53,39 @@ printf '%s\n' \
' bool temp_clean = mysql_error(db) != "";' \
" mysql_query(db, \"SET @uce_pool_marker='dirty'\");" \
' 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();' \
' String source;' \
' perf["mysql_operations"].each([&](DValue operation, String key) { if(operation["op"].to_string() == "connect") source = operation["source"].to_string(); });' \
' print(perf["worker_pid"].to_string(), "|", source, "|", marker_clean ? "clean" : "dirty", "|", temp_clean ? "clean" : "dirty");' \
' 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", "|", database_clean ? "clean" : "dirty");' \
' mysql_disconnect(unselected);' \
' mysql_disconnect(other);' \
' mysql_disconnect(db);' \
'}' >"$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
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")
if [[ "$output" == *"|worker|"* ]]; then
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
exit 1
fi

View File

@ -1,21 +1,22 @@
: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
host : host name of the MySQL server
username : user name
password : password
database : optional initial database name
return value : pointer to the MySQL connection struct
:see
>mysql
: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()`.
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
MySQL* db = mysql_connect();

View File

@ -16,7 +16,7 @@
namespace {
const u64 UCE_UNIT_ABI_VERSION = 10;
const u64 UCE_UNIT_ABI_VERSION = 11;
struct SharedUnitFilesystemState
{

View File

@ -3,7 +3,7 @@
#include <stdlib.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
// actual close at request end, including exception/fatal recovery paths.
static void mysql_register_request_connection(MySQL* db)
@ -15,8 +15,12 @@ static void mysql_register_request_connection(MySQL* 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
// lifetime, not the connection's. disconnect()/~MySQL() unregister.
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(),
NULL, 0, NULL, 0) == NULL)
database == "" ? NULL : database.c_str(), 0, NULL, 0) == NULL)
{
auto e = mysql_error((MYSQL*)connection);
fprintf(stderr, "%s\n", e);
@ -59,7 +63,17 @@ bool MySQL::connect(String host, String username, String password)
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);
_preload_next_error_code = 0;
affected_rows = 0;

View File

@ -27,10 +27,11 @@ struct MySQL {
String request_host;
String request_username;
String request_password;
String request_database;
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();
void disconnect();
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)
{
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)
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++;
return(db);
@ -67,7 +68,8 @@ inline MySQL* mysql_connect(String host = "localhost", String username = "root",
db->request_host = host;
db->request_username = username;
db->request_password = password;
db->connect(host, username, password);
db->request_database = database;
db->connect(host, username, password, database);
return(db);
}

View File

@ -146,13 +146,18 @@ static DValue wasm_mysql_call(DValue request)
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;
request["op"] = "connect";
request["host"] = host;
request["username"] = username;
request["password"] = password;
request["database"] = database;
DValue response = wasm_mysql_call(request);
u64 handle = response["handle"].to_u64();
connection = (void*)(uintptr_t)handle;
@ -804,7 +809,7 @@ void uce_free(void* ptr)
u32 uce_wasm_core_abi_version()
{
return(6);
return(7);
}
int uce_wasm_core_init()

View File

@ -254,7 +254,7 @@ int main(int argc, char** argv)
CHECK(call_i32(core, "uce_wasm_core_init") == 0, "core init failed");
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();
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");
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");
return(0);
}

View File

@ -968,14 +968,14 @@ public:
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;
persistent = false;
for(size_t i = 0; i < mysql_persistent_pool.size(); 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;
if(db->reset_connection())
{
@ -1000,7 +1000,8 @@ public:
db->request_host = host;
db->request_username = username;
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);
if(persistent)
{
@ -3705,9 +3706,10 @@ private:
String host = request["host"].to_string();
String username = request["username"].to_string();
String password = request["password"].to_string();
String database = request["database"].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)
if(pooled && pooled->connection && pooled->request_host == host && pooled->request_username == username && pooled->request_password == password && pooled->request_database == database)
{
db = pooled;
connection_source = "request";
@ -3718,7 +3720,7 @@ private:
{
bool reused = 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";
ok = db && db->connection;
if(ok && db->connection)