diff --git a/scripts/check_unit_wasm.py b/scripts/check_unit_wasm.py index 887b459..afaef9b 100755 --- a/scripts/check_unit_wasm.py +++ b/scripts/check_unit_wasm.py @@ -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() diff --git a/scripts/compile_wasm_unit b/scripts/compile_wasm_unit index ed1cacb..89e6519 100755 --- a/scripts/compile_wasm_unit +++ b/scripts/compile_wasm_unit @@ -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" diff --git a/scripts/test_mysql_persistent_pool.sh b/scripts/test_mysql_persistent_pool.sh index dbbc002..336e2d5 100755 --- a/scripts/test_mysql_persistent_pool.sh +++ b/scripts/test_mysql_persistent_pool.sh @@ -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 diff --git a/site/doc/pages/mysql_connect.txt b/site/doc/pages/mysql_connect.txt index b69222a..a7f51a0 100644 --- a/site/doc/pages/mysql_connect.txt +++ b/site/doc/pages/mysql_connect.txt @@ -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(); diff --git a/src/lib/compiler.cpp b/src/lib/compiler.cpp index fb62c92..d6a6021 100644 --- a/src/lib/compiler.cpp +++ b/src/lib/compiler.cpp @@ -16,7 +16,7 @@ namespace { -const u64 UCE_UNIT_ABI_VERSION = 10; +const u64 UCE_UNIT_ABI_VERSION = 11; struct SharedUnitFilesystemState { diff --git a/src/lib/mysql-connector.cpp b/src/lib/mysql-connector.cpp index 34bd542..7104093 100644 --- a/src/lib/mysql-connector.cpp +++ b/src/lib/mysql-connector.cpp @@ -3,7 +3,7 @@ #include #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; diff --git a/src/lib/mysql-connector.h b/src/lib/mysql-connector.h index 87ab875..ca332ef 100644 --- a/src/lib/mysql-connector.h +++ b/src/lib/mysql-connector.h @@ -27,10 +27,11 @@ struct MySQL { String request_host; String request_username; String request_password; + String request_database; std::vector 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); } diff --git a/src/wasm/core.cpp b/src/wasm/core.cpp index e645503..5283094 100644 --- a/src/wasm/core.cpp +++ b/src/wasm/core.cpp @@ -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() diff --git a/src/wasm/w1_smoke.cpp b/src/wasm/w1_smoke.cpp index e31ac14..cc2a0b3 100644 --- a/src/wasm/w1_smoke.cpp +++ b/src/wasm/w1_smoke.cpp @@ -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); } diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp index 2006952..910f25e 100644 --- a/src/wasm/worker.cpp +++ b/src/wasm/worker.cpp @@ -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)