Reuse reset MySQL connections per worker
This commit is contained in:
parent
b04ce3d005
commit
6a02c07bcb
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
71
scripts/test_mysql_persistent_pool.sh
Executable file
71
scripts/test_mysql_persistent_pool.sh
Executable 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"
|
||||
@ -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");
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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
|
||||
);
|
||||
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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 = '\'');
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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++;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user