add database-aware MySQL pooling
This commit is contained in:
parent
418c5812dc
commit
810c19b042
@ -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()
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
|
||||
namespace {
|
||||
|
||||
const u64 UCE_UNIT_ABI_VERSION = 10;
|
||||
const u64 UCE_UNIT_ABI_VERSION = 11;
|
||||
|
||||
struct SharedUnitFilesystemState
|
||||
{
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user