Reuse reset MySQL connections per worker

This commit is contained in:
udo 2026-07-16 17:00:28 +00:00
parent b04ce3d005
commit 6a02c07bcb
11 changed files with 185 additions and 10 deletions

View File

@ -192,6 +192,7 @@ WASM_CORE_PATH=<UCE_REPO_ROOT>/bin/wasm/core.wasm
WASM_MEMORY_LIMIT_BYTES=536870912
WASM_EPOCH_DEADLINE_TICKS=200
WASM_EPOCH_PERIOD_MS=50
MYSQL_PERSISTENT_POOL_SIZE=8
WORKER_COUNT=4
MAX_MEMORY=16777216
@ -211,6 +212,7 @@ Important settings:
- `BIN_DIRECTORY` stores generated C++, wasm artifacts, compile output, and runtime caches.
- `TMP_UPLOAD_PATH` and `SESSION_PATH` must be writable by the runtime.
- `SESSION_COOKIE_SECURE=1` adds the `Secure` attribute to UCE-managed session cookies and should be used for HTTPS-only deployments. Leave it `0` only for local/plain-HTTP development.
- `MYSQL_PERSISTENT_POOL_SIZE` caps credential-keyed connections retained by each Wasm worker. The default `8` is clamped to `64`; set it to `0` to restore request-lifetime connections. Cached sessions are reset before reuse.
- `HTTP_PORT` is the built-in HTTP/WebSocket listener used for WebSocket upgrade traffic and direct local probes. Bind/firewall it for local access only; nginx/Apache should be the public entry point.
- `WS_BROKER_OUTBOUND_TIMEOUT_SECONDS` controls how long a forwarded WS message can remain queued in the broker before being dropped (default `30`). Set to `0` to disable the timeout.
- `WASM_COMPILE_SCRIPT` must point to `scripts/compile_wasm_unit` unless you provide an equivalent compiler. Relative paths are resolved from the runtime root/`COMPILER_SYS_PATH`. That script calls `scripts/check_unit_wasm.py` after linking each unit and uses the pinned WASI SDK on every deployment host.

View File

@ -42,6 +42,7 @@ WASM_CORE_PATH=bin/wasm/core.wasm
WASM_MEMORY_LIMIT_BYTES=536870912
WASM_EPOCH_DEADLINE_TICKS=200
WASM_EPOCH_PERIOD_MS=50
MYSQL_PERSISTENT_POOL_SIZE=8
# ENABLE THE BACKGROUND PROACTIVE COMPILER LOOP
PROACTIVE_COMPILE_ENABLED=1

View File

@ -71,5 +71,6 @@ if [[ "$action" == "run" ]]; then
scripts/test_nested_component_props.sh
scripts/test_password_hashing.sh
scripts/test_mysql_epoch_refresh.sh
scripts/test_mysql_persistent_pool.sh
scripts/test_log_timeliness.sh
fi

View File

@ -0,0 +1,71 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
test_name="mysql-persistent-pool-test-$$"
settings_file="${UCE_SETTINGS_FILE:-/etc/uce/settings.cfg}"
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r "$settings_file" ]]; then
configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' "$settings_file")
if [[ -n "${configured_site_directory:-}" ]]; then
site_directory="$configured_site_directory"
fi
fi
source_dir="$site_directory/$test_name"
bin_directory=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' "$settings_file" 2>/dev/null || true)
bin_directory="${bin_directory:-/tmp/uce/work}"
cache_dir=""
http_host="${UCE_TEST_HTTP_HOST:-uce.openfu.com}"
test_user="uce_pool_$$"
test_database="uce_pool_$$"
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
rm -rf "$source_dir"
if [[ -n "$cache_dir" ]]; then
rm -rf "$cache_dir"
fi
}
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'"
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\\\`\");" \
' 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 == "";' \
' mysql_query(db, "SELECT id FROM uce_persistent_temp LIMIT 1");' \
' 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)");' \
' 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");' \
' mysql_disconnect(db);' \
'}' >"$source_dir/test.uce"
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
echo "Persistent MySQL reuse leaked cross-request state: $output" >&2
exit 1
fi
fi
done
if [[ "$reused" -eq 0 ]]; then
echo "Persistent MySQL pool did not produce a worker reuse in 160 requests" >&2
exit 1
fi
echo "Persistent MySQL pool reset passed across $reused reused requests"

View File

@ -99,6 +99,7 @@ RENDER(Request& context)
print("Hostcalls: ", (u64)perf["hostcall_count"].to_u64(), " / ", (u64)perf["hostcall_us"].to_u64(), " us\n");
print("MySQL hostcalls: ", (u64)perf["mysql_hostcall_count"].to_u64(), " / ", (u64)perf["mysql_hostcall_us"].to_u64(), " us\n");
print("MySQL operations: ", json_encode(perf["mysql_operations"]), " / dropped ", (u64)perf["mysql_operations_dropped"].to_u64(), "\n");
print("MySQL connections: ", (u64)perf["mysql_connection_open_count"].to_u64(), " new / ", (u64)perf["mysql_connection_reuse_count"].to_u64(), " worker reuse / ", (u64)perf["mysql_request_pool_hit_count"].to_u64(), " request reuse\n");
print("Memcache hostcalls: ", (u64)perf["memcache_hostcall_count"].to_u64(), " / ", (u64)perf["memcache_hostcall_us"].to_u64(), " us\n");
print("Component resolves: ", (u64)perf["component_resolve_count"].to_u64(), " / ", (u64)perf["component_resolve_us"].to_u64(), " us\n");
print("Output buffer size: ", context.ob->str().length(), "\n");

View File

@ -9,7 +9,9 @@ return value : performance snapshot for the active request/workspace
>time_precise
:content
Returns a DValue with timing and process metadata such as worker pid, parent pid, request count, request start times, native dispatch, workspace setup and birth, context application, guest execution, and hostcall timing. Hostcall totals include component resolution; MySQL, memcache, and component resolution also expose their own count and microsecond fields. `mysql_operations` is an ordered, query-text-free list of up to 64 logical MySQL operations and their microsecond durations; `mysql_operations_dropped` reports any overflow. The profiling hostcall itself is excluded from those totals so repeated snapshots remain comparable.
Returns a DValue with timing and process metadata such as worker pid, parent pid, request count, request start times, native dispatch, workspace setup and birth, context application, guest execution, and hostcall timing. Hostcall totals include component resolution; MySQL, memcache, and component resolution also expose their own count and microsecond fields. `mysql_operations` is an ordered, query-text-free list of up to 64 logical MySQL operations and their microsecond durations. A connect operation also identifies its source as `new`, cross-request `worker`, or same-request `request`; the corresponding `mysql_connection_open_count`, `mysql_connection_reuse_count`, and `mysql_request_pool_hit_count` fields provide totals. `mysql_operations_dropped` reports any overflow. The profiling hostcall itself is excluded from those totals so repeated snapshots remain comparable.
Wasm FastCGI workers retain up to `MYSQL_PERSISTENT_POOL_SIZE` credential-keyed MySQL connections (default `8`; set `0` to disable). UCE calls the client library's connection-reset operation before another request receives a cached connection, clearing transactions, temporary tables, session variables, and selected databases while avoiding a new authentication handshake. Same-request leases continue to share state until request cleanup.
:example
DValue perf = request_perf();

View File

@ -149,7 +149,7 @@ RENDER(Request& context)
String mysql_operations = json_encode(mysql_perf["mysql_operations"]);
mark(
"request_perf() exposes bounded query-text-free MySQL operations",
mysql_perf["mysql_operation_count"].to_u64() > 0 && mysql_operations.find("\"connect\"") != String::npos && mysql_operations.find("\"us\"") != String::npos && mysql_operations.find("SHOW DATABASES") == String::npos && mysql_perf["mysql_operations_dropped"].to_u64() == 0 ? "pass" : "fail",
mysql_perf["mysql_operation_count"].to_u64() > 0 && mysql_operations.find("\"connect\"") != String::npos && mysql_operations.find("\"source\"") != String::npos && mysql_operations.find("\"us\"") != String::npos && mysql_operations.find("SHOW DATABASES") == String::npos && (mysql_error_text != "" || mysql_perf["mysql_connection_open_count"].to_u64() + mysql_perf["mysql_connection_reuse_count"].to_u64() > 0) && mysql_perf["mysql_operations_dropped"].to_u64() == 0 ? "pass" : "fail",
mysql_operations
);

View File

@ -8,7 +8,7 @@
// actual close at request end, including exception/fatal recovery paths.
static void mysql_register_request_connection(MySQL* db)
{
if(!context || !db)
if(!context || !db || db->worker_persistent)
return;
auto& connections = context->resources.mysql_connections;
if(std::find(connections.begin(), connections.end(), (void*)db) == connections.end())
@ -57,6 +57,21 @@ bool MySQL::connect(String host, String username, String password)
return(true);
}
bool MySQL::reset_connection()
{
if(!connection || mysql_reset_connection((MYSQL*)connection) != 0)
return(false);
_preload_next_error_code = 0;
affected_rows = 0;
field_count = 0;
row_count = 0;
insert_id = 0;
statement_info = "reset";
field_info.clear();
request_leases = 0;
return(true);
}
String MySQL::escape(String raw, char quote_char)
{
return(mysql_escape(raw, quote_char));

View File

@ -22,6 +22,7 @@ struct MySQL {
String statement_info = ""; //
bool request_cleanup_delete = false;
bool request_pooled = false;
bool worker_persistent = false;
u32 request_leases = 0;
String request_host;
String request_username;
@ -30,6 +31,7 @@ struct MySQL {
std::vector<MySQLFieldInfo> field_info;
bool connect(String host = "localhost", String username = "root", String password = "");
bool reset_connection();
void disconnect();
String error();
String escape(String raw, char quote_char = '\'');

View File

@ -52,6 +52,7 @@ static String wasm_backend_ensure_started(Request* context)
wc.write_roots.push_back(cfg[key]);
wc.memory_limit = (int64_t)to_u64(cfg["WASM_MEMORY_LIMIT_BYTES"], 512ull * 1024 * 1024);
wc.epoch_deadline_ticks = to_u64(cfg["WASM_EPOCH_DEADLINE_TICKS"], 200);
wc.mysql_persistent_pool_size = std::min<u64>(to_u64(cfg["MYSQL_PERSISTENT_POOL_SIZE"], 8), 64);
wc.verbose = to_bool(cfg["WASM_BACKEND_VERBOSE"], false);
// UCE_HOSTCALL_BLOCKLIST: comma-separated uce_host_* names (with or without
// the "uce_host_" prefix) the sysadmin disables; each blocked call traps into

View File

@ -92,6 +92,7 @@ struct WasmWorkerConfig
int64_t memory_limit = 512ll * 1024 * 1024;
u32 table_headroom = 4096;
u64 epoch_deadline_ticks = 200; // ticker period × ticks = CPU budget
u64 mysql_persistent_pool_size = 8;
bool verbose = false;
// uce_host_* names (bare, without the "uce_host_" prefix) the sysadmin has
// disabled via UCE_HOSTCALL_BLOCKLIST. A blocked hostcall resolves to a trap
@ -102,6 +103,7 @@ struct WasmWorkerConfig
struct WasmMySQLOperation
{
String op;
String source;
u64 elapsed_us = 0;
};
@ -119,6 +121,9 @@ struct WasmRequestProfile
u64 mysql_hostcall_total_us = 0;
u64 mysql_operation_count = 0;
u64 mysql_operations_dropped = 0;
u64 mysql_connection_open_count = 0;
u64 mysql_connection_reuse_count = 0;
u64 mysql_request_pool_hit_count = 0;
std::vector<WasmMySQLOperation> mysql_operations;
u64 memcache_hostcall_count = 0;
u64 memcache_hostcall_total_us = 0;
@ -625,6 +630,62 @@ public:
{
}
#ifdef UCE_WASM_HOST_CONNECTORS
~WasmWorker()
{
for(auto* db : mysql_persistent_pool)
delete db;
}
MySQL* mysql_checkout(const String& host, const String& username, const String& password, 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)
continue;
if(db->reset_connection())
{
if(i + 1 < mysql_persistent_pool.size())
{
mysql_persistent_pool.erase(mysql_persistent_pool.begin() + i);
mysql_persistent_pool.push_back(db);
}
reused = true;
persistent = true;
return(db);
}
delete db;
mysql_persistent_pool.erase(mysql_persistent_pool.begin() + i);
break;
}
MySQL* db = new MySQL();
persistent = cfg.mysql_persistent_pool_size > 0;
db->worker_persistent = persistent;
db->request_pooled = true;
db->request_host = host;
db->request_username = username;
db->request_password = password;
if(!db->connect(host, username, password) || !db->connection)
return(db);
if(persistent)
{
while(mysql_persistent_pool.size() >= cfg.mysql_persistent_pool_size)
{
delete mysql_persistent_pool.front();
mysql_persistent_pool.erase(mysql_persistent_pool.begin());
}
mysql_persistent_pool.push_back(db);
}
return(db);
}
std::vector<MySQL*> mysql_persistent_pool;
#endif
String init()
{
std::vector<u8> bytes;
@ -831,6 +892,7 @@ public:
std::vector<SQLite*> sqlite_handles;
std::vector<MySQL*> mysql_handles;
std::vector<MySQL*> mysql_request_pool;
std::vector<MySQL*> mysql_request_owned;
#endif
~WasmWorkspace()
{
@ -848,6 +910,9 @@ public:
if(db)
delete db; // ~SQLite disconnects
for(auto* db : mysql_request_pool)
if(db)
db->request_leases = 0;
for(auto* db : mysql_request_owned)
if(db)
delete db; // ~MySQL disconnects
#endif
@ -1780,11 +1845,16 @@ private:
response["mysql_hostcall_us"] = (f64)self->mysql_hostcall_total_us;
response["mysql_operation_count"] = (f64)self->mysql_operation_count;
response["mysql_operations_dropped"] = (f64)self->mysql_operations_dropped;
response["mysql_connection_open_count"] = (f64)self->mysql_connection_open_count;
response["mysql_connection_reuse_count"] = (f64)self->mysql_connection_reuse_count;
response["mysql_request_pool_hit_count"] = (f64)self->mysql_request_pool_hit_count;
response["mysql_operations"].set_array();
for(auto& operation : self->mysql_operations)
{
DValue item;
item["op"] = operation.op;
if(operation.source != "")
item["source"] = operation.source;
item["us"] = (f64)operation.elapsed_us;
response["mysql_operations"].push(item);
}
@ -2500,6 +2570,7 @@ private:
DValue request, response;
String decode_error;
String op;
String connection_source;
auto operation_started = std::chrono::steady_clock::now();
if(ucb_decode(encoded, request, &decode_error))
{
@ -2514,19 +2585,23 @@ private:
if(pooled && pooled->connection && pooled->request_host == host && pooled->request_username == username && pooled->request_password == password)
{
db = pooled;
connection_source = "request";
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);
bool reused = false;
bool persistent = false;
db = self->worker.mysql_checkout(host, username, password, reused, persistent);
connection_source = reused ? "worker" : "new";
ok = db && db->connection;
if(ok && db->connection)
{
self->mysql_request_pool.push_back(db);
if(!persistent)
self->mysql_request_owned.push_back(db);
}
}
u64 handle = 0;
if(ok && db->connection)
@ -2534,11 +2609,14 @@ private:
db->request_leases++;
self->mysql_handles.push_back(db);
handle = self->mysql_handles.size();
if(connection_source == "new") self->mysql_connection_open_count++;
else if(connection_source == "worker") self->mysql_connection_reuse_count++;
else if(connection_source == "request") self->mysql_request_pool_hit_count++;
}
response["handle"] = (f64)handle;
response["error_code"] = (f64)db->_preload_next_error_code;
response["statement_info"] = db->error();
if(handle == 0)
if(handle == 0 && db)
delete db;
}
else if(op == "escape")
@ -2571,6 +2649,7 @@ private:
{
WasmMySQLOperation operation;
operation.op = op;
operation.source = connection_source;
operation.elapsed_us = (u64)std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - operation_started).count();
self->mysql_operation_count++;