fix: stabilize runtime follow-up regressions

This commit is contained in:
udo
2026-06-11 23:03:07 +00:00
parent 7f757654b6
commit 20db669589
37 changed files with 906 additions and 144 deletions
+23 -21
View File
@@ -345,7 +345,7 @@ bool compiler_line_starts_fragmentable_entrypoint(String trimmed, String& kind)
String compiler_fragment_capture_prelude(String slot)
{
return("\nstruct __UceFragmentCapture { Request& context; String slot; __UceFragmentCapture(Request& c, String s) : context(c), slot(s) { ob_start(); } ~__UceFragmentCapture() { String html = ob_get_close(); if(html != \"\") context.call[\"fragments\"][slot] = context.call[\"fragments\"][slot].to_string() + html; } }; __UceFragmentCapture __uce_fragment_capture(context, " + compiler_cpp_string_literal(slot) + ");\n");
return("\nUceFragmentCapture __uce_fragment_capture(context, " + compiler_cpp_string_literal(slot) + ");\n");
}
String compiler_rewrite_fragment_attributes(String content)
@@ -365,7 +365,6 @@ String compiler_rewrite_fragment_attributes(String content)
}
String slot = (kind == "ONCE" ? "once" : "");
StringList pending;
bool has_fragment_attr = false;
u32 j = i + 1;
while(j < lines.size())
@@ -402,28 +401,31 @@ String compiler_rewrite_fragment_attributes(String content)
continue;
}
result += line;
if(j < lines.size())
result += "\n";
while(j < lines.size())
// The body must open on the next non-blank line after the attribute
// lines; anything else (e.g. literal text that merely starts with
// "ONCE(") is not an entry point and passes through untouched.
u32 body_index = j;
while(body_index < lines.size() && trim(lines[body_index]) == "")
body_index++;
bool body_opens = body_index < lines.size() && trim(lines[body_index]).rfind("{", 0) == 0;
if(!body_opens)
{
String body_line = lines[j];
auto brace_pos = body_line.find("{");
if(brace_pos == String::npos)
{
result += body_line;
if(j + 1 < lines.size())
result += "\n";
j++;
continue;
}
result += body_line.substr(0, brace_pos + 1) + compiler_fragment_capture_prelude(slot) + body_line.substr(brace_pos + 1);
if(j + 1 < lines.size())
result += line;
if(i + 1 < lines.size())
result += "\n";
i = j;
break;
continue;
}
result += line;
result += "\n";
for(u32 k = j; k < body_index; k++)
result += lines[k] + "\n";
String body_line = lines[body_index];
auto brace_pos = body_line.find("{");
result += body_line.substr(0, brace_pos + 1) + compiler_fragment_capture_prelude(slot) + body_line.substr(brace_pos + 1);
if(body_index + 1 < lines.size())
result += "\n";
i = body_index;
}
return(result);
}
+1 -1
View File
@@ -18,7 +18,7 @@ const char* UCE_CLI_SYMBOL = "__uce_cli";
const char* UCE_SERVE_HTTP_SYMBOL = "__uce_serve_http";
const char* UCE_ONCE_SYMBOL = "__uce_once";
const char* UCE_INIT_SYMBOL = "__uce_init";
const u64 UCE_UNIT_ABI_VERSION = 2;
const u64 UCE_UNIT_ABI_VERSION = 4;
struct SharedUnitFilesystemState
{
+71 -51
View File
@@ -20,7 +20,7 @@ TreePtr dtree_resolve_reference(TreePtr tree)
return(tree);
}
bool dtree_key_is_index(String key, s64 expected_index = -1)
bool dtree_key_is_index(String key)
{
if(key == "")
return(false);
@@ -29,8 +29,10 @@ bool dtree_key_is_index(String key, s64 expected_index = -1)
if(!isdigit(c))
return(false);
}
if(expected_index >= 0)
return(key == std::to_string(expected_index));
// Only canonical index strings count ("1", not "01"), so each numeric
// value has exactly one representation.
if(key.length() > 1 && key[0] == '0')
return(false);
return(true);
}
@@ -140,12 +142,25 @@ u64 dtree_clamp_to_u64_range(long double value)
}
void DTree::each(std::function <void (const DTree& t, String key)> f)
void DTree::each(std::function <void (const DTree& t, String key)> f) const
{
const DTree& target = deref();
switch(target.type)
{
case('M'):
// Lists iterate in numeric index order (string keys sort "10"
// before "2"), matching the json/xml/yaml encoders.
if(target.is_list())
{
for(u64 i = 0; i < target._map.size(); i++)
{
auto it = target._map.find(std::to_string(i));
if(it == target._map.end())
break;
f(it->second, it->first);
}
break;
}
for (auto it = target._map.begin(); it != target._map.end(); ++it)
{
f(it->second, it->first);
@@ -157,7 +172,7 @@ void DTree::each(std::function <void (const DTree& t, String key)> f)
}
}
bool DTree::is_array()
bool DTree::is_array() const
{
return(deref().type == 'M');
}
@@ -169,44 +184,45 @@ bool DTree::is_list() const
return(false);
if(target._map.size() == 0)
return(target._list_mode);
s64 expected_index = 0;
// The map iterates in string order ("10" before "2"), so the check must
// be order-independent: n unique canonical index keys with maximum n-1
// are exactly 0..n-1.
s64 max_index = -1;
for(const auto& entry : target._map)
{
if(!dtree_key_is_index(entry.first, expected_index))
if(!dtree_key_is_index(entry.first))
return(false);
expected_index += 1;
s64 index = strtoll(entry.first.c_str(), 0, 10);
if(index > max_index)
max_index = index;
}
return(true);
return(max_index == (s64)target._map.size() - 1);
}
String DTree::to_string()
String DTree::to_string(String default_value) const
{
const DTree& target = deref();
switch(target.type)
{
case('S'):
if(target._String == "")
return(default_value);
return(target._String);
break;
case('F'):
return(std::to_string(target._float));
break;
case('B'):
return(target._bool ? "(true)" : "(false)");
break;
case('M'):
return("");
break;
return(default_value);
case('P'):
return(std::to_string((u64)target._ptr));
break;
case('R'):
return("");
break;
return(default_value);
}
return("");
return(default_value);
}
s64 DTree::to_s64()
s64 DTree::to_s64(s64 default_value) const
{
const DTree& target = deref();
switch(target.type)
@@ -215,7 +231,7 @@ s64 DTree::to_s64()
{
f64 value = 0;
if(!dtree_string_to_f64_value(target._String, value))
return(0);
return(default_value);
return(dtree_clamp_to_s64_range((long double)value));
}
case('F'):
@@ -226,18 +242,18 @@ s64 DTree::to_s64()
{
const DTree* item = dtree_scalar_map_value(target);
if(item)
return(const_cast<DTree*>(item)->to_s64());
return(0);
return(item->to_s64(default_value));
return(default_value);
}
case('P'):
return(dtree_clamp_to_s64_range((long double)(u64)target._ptr));
case('R'):
return(0);
return(default_value);
}
return(0);
return(default_value);
}
u64 DTree::to_u64()
u64 DTree::to_u64(u64 default_value) const
{
const DTree& target = deref();
switch(target.type)
@@ -246,7 +262,7 @@ u64 DTree::to_u64()
{
f64 value = 0;
if(!dtree_string_to_f64_value(target._String, value))
return(0);
return(default_value);
return(dtree_clamp_to_u64_range((long double)value));
}
case('F'):
@@ -257,18 +273,18 @@ u64 DTree::to_u64()
{
const DTree* item = dtree_scalar_map_value(target);
if(item)
return(const_cast<DTree*>(item)->to_u64());
return(0);
return(item->to_u64(default_value));
return(default_value);
}
case('P'):
return((u64)target._ptr);
case('R'):
return(0);
return(default_value);
}
return(0);
return(default_value);
}
f64 DTree::to_f64()
f64 DTree::to_f64(f64 default_value) const
{
const DTree& target = deref();
switch(target.type)
@@ -277,7 +293,7 @@ f64 DTree::to_f64()
{
f64 value = 0;
if(!dtree_string_to_f64_value(target._String, value))
return(0);
return(default_value);
return(value);
}
case('F'):
@@ -288,18 +304,18 @@ f64 DTree::to_f64()
{
const DTree* item = dtree_scalar_map_value(target);
if(item)
return(const_cast<DTree*>(item)->to_f64());
return(0);
return(item->to_f64(default_value));
return(default_value);
}
case('P'):
return(dtree_clamp_to_f64_range((long double)(u64)target._ptr));
case('R'):
return(0);
return(default_value);
}
return(0);
return(default_value);
}
bool DTree::to_bool()
bool DTree::to_bool(bool default_value) const
{
const DTree& target = deref();
switch(target.type)
@@ -312,7 +328,11 @@ bool DTree::to_bool()
f64 numeric_value = 0;
if(dtree_string_to_f64_value(target._String, numeric_value))
return(numeric_value != 0);
return(dtree_trim(target._String) != "");
// Non-empty unparseable strings stay truthy; only a missing/empty
// value falls back to the default.
if(dtree_trim(target._String) != "")
return(true);
return(default_value);
}
case('F'):
return(target._float != 0);
@@ -322,18 +342,18 @@ bool DTree::to_bool()
{
const DTree* item = dtree_scalar_map_value(target);
if(item)
return(const_cast<DTree*>(item)->to_bool());
return(item->to_bool(default_value));
return(target._map.size() > 0);
}
case('P'):
return(target._ptr != 0);
case('R'):
return(false);
return(default_value);
}
return(false);
return(default_value);
}
StringMap DTree::to_stringmap()
StringMap DTree::to_stringmap() const
{
const DTree& target = deref();
StringMap result;
@@ -341,7 +361,7 @@ StringMap DTree::to_stringmap()
{
case('M'):
for(const auto& entry : target._map)
result[entry.first] = const_cast<DTree&>(entry.second.deref()).to_string();
result[entry.first] = entry.second.deref().to_string();
break;
case('S'):
if(dtree_trim(target._String) != "")
@@ -350,7 +370,7 @@ StringMap DTree::to_stringmap()
case('F'):
case('B'):
case('P'):
result["value"] = const_cast<DTree&>(target).to_string();
result["value"] = target.to_string();
break;
case('R'):
break;
@@ -358,7 +378,7 @@ StringMap DTree::to_stringmap()
return(result);
}
String DTree::to_json(char quote_char)
String DTree::to_json(char quote_char) const
{
const DTree& target = deref();
switch(target.type)
@@ -385,7 +405,7 @@ String DTree::to_json(char quote_char)
return("\"(unknown)\"");
}
String DTree::get_type_name()
String DTree::get_type_name() const
{
const DTree& target = deref();
switch(target.type)
@@ -412,7 +432,7 @@ String DTree::get_type_name()
return("unknown");
}
DTree DTree::get_by_path(String path, String delim)
DTree DTree::get_by_path(String path, String delim) const
{
const DTree* current = &deref();
if(path == "")
@@ -447,7 +467,7 @@ DTree DTree::get_by_path(String path, String delim)
return(current->deref());
}
bool DTree::is_reference()
bool DTree::is_reference() const
{
return(type == 'R');
}
@@ -778,12 +798,12 @@ String to_String(DTree t)
return(t.to_string());
}
String var_dump(DTree map, String prefix, String postfix)
String var_dump(const DTree& map, String prefix, String postfix)
{
String result = "";
if(!map.is_array())
return(map.to_string());
map.each([&] (DTree item, String key) {
map.each([&] (const DTree& item, String key) {
result += prefix + key + ": " + item.to_string() + postfix;
if(item.is_array())
result += var_dump(item, prefix + "\t");
+16 -13
View File
@@ -19,19 +19,22 @@ struct DTree {
void* _ptr;
std::map<String, DTree> _map;
void each(std::function <void (const DTree& t, String key)> f);
bool is_array();
// Read accessors are const and never create or modify nodes. The to_*
// conversions take an optional default that is returned when the value is
// missing (empty) or cannot be converted to the requested type.
void each(std::function <void (const DTree& t, String key)> f) const;
bool is_array() const;
bool is_list() const;
String to_string();
s64 to_s64();
u64 to_u64();
f64 to_f64();
bool to_bool();
StringMap to_stringmap();
String to_json(char quote_char = '"');
String get_type_name();
DTree get_by_path(String path, String delim = "/");
bool is_reference();
String to_string(String default_value = "") const;
s64 to_s64(s64 default_value = 0) const;
u64 to_u64(u64 default_value = 0) const;
f64 to_f64(f64 default_value = 0) const;
bool to_bool(bool default_value = false) const;
StringMap to_stringmap() const;
String to_json(char quote_char = '"') const;
String get_type_name() const;
DTree get_by_path(String path, String delim = "/") const;
bool is_reference() const;
DTree* reference_target();
const DTree* reference_target() const;
DTree& deref();
@@ -63,4 +66,4 @@ struct DTree {
};
String to_String(DTree t);
String var_dump(DTree map, String prefix = "", String postfix = "\n");
String var_dump(const DTree& map, String prefix = "", String postfix = "\n");
+13 -1
View File
@@ -745,7 +745,19 @@ StringMap split_http_headers(String s)
while(header_start < lines.size() && trim(lines[header_start]) == "")
header_start++;
if(header_start < lines.size() && lines[header_start].find(':') == String::npos)
// A header line has its colon before any whitespace ("Host: x"); a request
// line has a space first even when the URI contains colons
// ("GET /x.uce?t=12:30 HTTP/1.1").
bool first_line_is_header = false;
if(header_start < lines.size())
{
String first_line = trim(lines[header_start]);
size_t colon = first_line.find(':');
size_t space = first_line.find_first_of(" \t");
first_line_is_header = colon != String::npos && (space == String::npos || colon < space);
}
if(header_start < lines.size() && !first_line_is_header)
{
String request_line = trim(lines[header_start]);
result["REQUEST_METHOD"] = nibble(request_line, " ");
+20
View File
@@ -132,6 +132,26 @@ void ob_close();
String ob_get();
String ob_get_close();
// RAII capture used by the preprocessor's @fragment rewrite: buffers the
// handler's output and appends it to context.call["fragments"][slot].
struct UceFragmentCapture
{
Request& context;
String slot;
UceFragmentCapture(Request& c, String s) : context(c), slot(s)
{
ob_start();
}
~UceFragmentCapture()
{
String html = ob_get_close();
if(html != "")
context.call["fragments"][slot] = context.call["fragments"][slot].to_string() + html;
}
};
String safe_name(String raw);
String ascii_safe_name(String raw);
+29 -13
View File
@@ -3,16 +3,28 @@
#include <stdlib.h>
#include "mysql-connector.h"
static void mysql_register_request_connection(MySQL* db)
{
if(!context || !db)
return;
auto& connections = context->resources.mysql_connections;
if(std::find(connections.begin(), connections.end(), (void*)db) == connections.end())
connections.push_back(db);
}
bool MySQL::connect(String host, String username, String password)
{
// Register regardless of outcome: tracking is about the wrapper's
// lifetime, not the connection's. disconnect()/~MySQL() unregister.
mysql_register_request_connection(this);
//switch_to_system_alloc();
connection = mysql_init(NULL);
if (connection == NULL)
{
auto e = mysql_error((MYSQL*)connection);
fprintf(stderr, "%s\n", e);
fprintf(stderr, "mysql_init failed\n");
//switch_to_arena(context->mem);
statement_info.assign(e);
_preload_next_error_code = CR_OUT_OF_MEMORY;
statement_info = "mysql_init failed";
return(false);
}
@@ -21,9 +33,11 @@ bool MySQL::connect(String host, String username, String password)
{
auto e = mysql_error((MYSQL*)connection);
fprintf(stderr, "%s\n", e);
mysql_close((MYSQL*)connection);
//switch_to_arena(context->mem);
_preload_next_error_code = CR_UNKNOWN_ERROR;
statement_info.assign(e);
mysql_close((MYSQL*)connection);
connection = NULL;
//switch_to_arena(context->mem);
return(false);
}
@@ -37,8 +51,6 @@ bool MySQL::connect(String host, String username, String password)
*/
//switch_to_arena(context->mem);
statement_info = String("connected");
if(context)
context->resources.mysql_connections.push_back(this);
return(true);
}
@@ -192,6 +204,12 @@ DTree MySQL::query(String q)
statement_info = "mysql positional ? placeholders are not supported; use named :name placeholders";
return(DTree());
}
if(!connection)
{
_preload_next_error_code = CR_UNKNOWN_ERROR;
statement_info = "mysql connection is not open";
return(DTree());
}
_preload_next_error_code = mysql_query((MYSQL*)connection, q.c_str());
DTree result;
if(_preload_next_error_code == 0)
@@ -237,12 +255,8 @@ static bool mysql_has_unquoted_positional_placeholder(String query)
DTree MySQL::query(String q, StringMap params)
{
if(mysql_has_unquoted_positional_placeholder(q))
{
_preload_next_error_code = CR_UNKNOWN_ERROR;
statement_info = "mysql positional ? placeholders are not supported; use named :name placeholders";
return(DTree());
}
// Positional ? placeholders survive named substitution (values are always
// quoted by escape()), so the check in query(String) covers this path too.
return(query(
parse_query_parameters(q, params).c_str()
));
@@ -347,6 +361,8 @@ String MySQL::error()
_preload_next_error_code = 0;
return(p);
}
if(!connection)
return("");
const char* res = mysql_error((MYSQL*)connection);
if(res)
{
+4
View File
@@ -33,6 +33,10 @@ struct MySQL {
DTree query(String q, StringMap params);
DTree get_pending_result();
// Unregisters from the request's connection tracking, so stack-allocated
// instances cannot leave dangling pointers behind for request cleanup.
~MySQL() { disconnect(); }
};
MySQL* mysql_connect(String host = "localhost", String username = "root", String password = "")
+15 -3
View File
@@ -70,15 +70,21 @@ bool SQLite::connect(String path)
{
disconnect();
this->path = path;
// Register regardless of outcome: tracking is about the wrapper's
// lifetime, not the connection's. disconnect()/~SQLite() unregister.
sqlite_register_request_connection(this);
s32 rc = sqlite3_open_v2(path.c_str(), (sqlite3**)&connection, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, 0);
if(rc != SQLITE_OK)
{
set_error(rc, "sqlite open failed for " + path);
disconnect();
if(connection)
{
sqlite3_close((sqlite3*)connection);
connection = 0;
}
return(false);
}
sqlite3_busy_timeout((sqlite3*)connection, 5000);
sqlite_register_request_connection(this);
if(!apply_default_pragmas())
return(false);
statement_info = "connected";
@@ -278,9 +284,11 @@ SQLite* sqlite_connect(String path)
}
SQLite* db = new SQLite();
db->worker_cache = true;
if(db->connect(path) && db->connection)
{
db->worker_cache = true;
sqlite_worker_connection_cache[path] = db;
}
else
db->request_cleanup_delete = true;
return(db);
@@ -339,6 +347,10 @@ void cleanup_sqlite_connections()
context->resources.sqlite_connections.pop_back();
if(db->worker_cache)
{
// A page that ran BEGIN and faulted must not leak its transaction
// (and the WAL write lock) into the next request on this worker.
if(db->connection && !sqlite3_get_autocommit((sqlite3*)db->connection))
sqlite3_exec((sqlite3*)db->connection, "ROLLBACK", 0, 0, 0);
db->affected_rows = 0;
db->insert_id = 0;
db->error_code = SQLITE_OK;
+4
View File
@@ -17,6 +17,10 @@ struct SQLite {
DTree query(String q);
DTree query(String q, const StringMap& params);
// Unregisters from the request's connection tracking, so stack-allocated
// instances cannot leave dangling pointers behind for request cleanup.
~SQLite() { disconnect(); }
private:
void set_error(s32 code, String info = "");
bool apply_default_pragmas();
+13 -8
View File
@@ -17,17 +17,12 @@ constexpr f64 FILE_LOCK_WAIT_TIMEOUT_SECONDS = 3.0;
}
String capture_backtrace_string(u32 max_frames, u32 skip_frames)
String backtrace_frames_string(void* const* frames, size_t size, u32 skip_frames)
{
if(max_frames == 0)
return("");
std::vector<void*> frames(max_frames);
size_t size = backtrace(frames.data(), max_frames);
if(size == 0)
return("");
char** symbols = backtrace_symbols(frames.data(), size);
char** symbols = backtrace_symbols(frames, size);
if(!symbols)
return("");
@@ -38,7 +33,17 @@ String capture_backtrace_string(u32 max_frames, u32 skip_frames)
trace += "\n";
}
free(symbols);
return(trace);
return(trace);
}
String capture_backtrace_string(u32 max_frames, u32 skip_frames)
{
if(max_frames == 0)
return("");
std::vector<void*> frames(max_frames);
size_t size = backtrace(frames.data(), max_frames);
return(backtrace_frames_string(frames.data(), size, skip_frames));
}
String signal_name(int sig)
+1
View File
@@ -65,6 +65,7 @@ bool ws_send(String message, bool binary = false, String scope = "");
bool ws_send_to(String connection_id, String message, bool binary = false);
bool ws_close(String connection_id = "");
String backtrace_frames_string(void* const* frames, size_t size, u32 skip_frames = 0);
String capture_backtrace_string(u32 max_frames = 32, u32 skip_frames = 0);
String signal_name(int sig);
+3 -8
View File
@@ -366,14 +366,9 @@ DTree request_route_from_raw_path(String raw_path, String default_path)
void request_populate_context_params(Request& context, String default_path)
{
DTree route = request_query_route(context, default_path);
String script_url = request_script_url(context);
context.params["SCRIPT_URL"] = script_url;
context.params["BASE_URL"] = request_base_url_from_script_url(script_url);
context.params["ROUTE_PATH"] = route["l_path"].to_string();
context.params["ROUTE_PAGE"] = route["page"].to_string();
context.params["ROUTE_PATH_RAW"] = route["raw_path"].to_string();
context.params["ROUTE_VALID"] = route["valid"].to_bool() ? "1" : "0";
String raw_path;
parse_query(context.params["QUERY_STRING"], &raw_path);
request_populate_context_params_from_route(context, raw_path, default_path);
}
void request_populate_context_params_from_route(Request& context, String raw_path, String default_path)
+11 -4
View File
@@ -18,7 +18,10 @@ static sigjmp_buf request_fault_jmp;
static volatile sig_atomic_t request_fault_active = 0;
static volatile sig_atomic_t request_fault_signal = 0;
static Request* request_fault_request = 0;
static String request_fault_trace = "";
// Raw frame pointers only: the signal handler must not allocate, so frames are
// captured here and symbolized after the siglongjmp.
static void* request_fault_frames[64];
static volatile sig_atomic_t request_fault_frame_count = 0;
static int websocket_exec_fd = -1;
static String websocket_exec_read_buffer = "";
static std::deque<DTree> websocket_exec_pending_jobs;
@@ -105,7 +108,7 @@ void on_request_fault_signal(int sig)
request_fault_signal = sig;
if(request_fault_active && request_fault_request)
{
request_fault_trace = capture_backtrace_string(32, 1);
request_fault_frame_count = backtrace(request_fault_frames, 64);
siglongjmp(request_fault_jmp, 1);
}
on_segfault(sig);
@@ -887,7 +890,7 @@ int handle_complete(FastCGIRequest& request) {
request_fault_request = &request;
request_fault_active = 1;
request_fault_signal = 0;
request_fault_trace = "";
request_fault_frame_count = 0;
install_request_fault_handlers();
String failure_title = "";
@@ -898,7 +901,7 @@ int handle_complete(FastCGIRequest& request) {
{
failure_title = "fatal signal during request";
failure_details = "worker recovered before closing the upstream connection";
failure_trace = request_fault_trace;
failure_trace = backtrace_frames_string(request_fault_frames, request_fault_frame_count, 1);
}
else
{
@@ -1502,6 +1505,10 @@ void init_base_process()
int main(int argc, char** argv)
{
// Warm up libgcc's backtrace state so the first in-handler backtrace()
// after a fault does not allocate.
backtrace(request_fault_frames, 4);
init_base_process();
ensure_proactive_compiler();