From 18e642290d34eef824360a29b807c60373b8317c Mon Sep 17 00:00:00 2001 From: Yorhel Date: Sun, 23 Feb 2025 14:05:43 +0100 Subject: [PATCH] Some fixes and framework docs --- FU.pm | 307 ++++++++++++++++++++++++++++++++++++++++++++++++-- FU.xs | 2 +- FU/Pg.pm | 3 +- c/pgst.c | 1 + c/pgtypes.c | 13 ++- t/pgconnect.t | 2 +- t/pgtypes.t | 2 + 7 files changed, 313 insertions(+), 17 deletions(-) diff --git a/FU.pm b/FU.pm index b490b37..ec6be17 100644 --- a/FU.pm +++ b/FU.pm @@ -30,9 +30,8 @@ FU::Log::set_fmt(sub($msg) { ); }); -sub debug { state $v = 0; $v = $_[0] if @_; $v } -sub log_slow_pages { state $v = 0; $v = $_[0] if @_; $v } -sub log_queries { state $v = 0; $v = $_[0] if @_; $v } +sub debug { state $v = 0; $v = $_[0] if @_; $v } +sub log_slow_reqs { state $v = 0; $v = $_[0] if @_; $v } sub mime_types() { state $v = {qw{ 7z application/x-7z-compressed @@ -108,7 +107,22 @@ sub compress_mimes { state $v = {map +($_,1), qw{ our $INIT_DB; our $DB; -sub _connect_db { $DB = ref $INIT_DB eq 'CODE' ? $INIT_DB->() : FU::Pg->connect($INIT_DB) } +sub query_trace($st,@) { + $REQ->{trace_nsql}++; + $REQ->{trace_nsqlprep}++ if $st->prepare_time; + $REQ->{trace_nsqldirect}++ if !defined $st->prepare_time; + $REQ->{trace_sqlexec} += $st->exec_time; + $REQ->{trace_sqlprep} += $st->prepare_time if $st->prepare_time; + push $REQ->{trace_sql}->@*, { + query => $st->query, nrows => $st->nrows, + param_types => $st->param_types, param_values => $st->param_values, + exec_time => $st->exec_time, prepare_time => $st->prepare_time, + } if FU::debug; +} +sub _connect_db { + $DB = ref $INIT_DB eq 'CODE' ? $INIT_DB->() : FU::Pg->connect($INIT_DB); + $DB->query_trace(\&query_trace); +} sub init_db($info) { require FU::Pg; $INIT_DB = $info; @@ -119,7 +133,7 @@ sub init_db($info) { my @before_request; my @after_request; sub before_request :prototype(&) ($f) { push @before_request, $f } -sub after_request :prototype(&) ($f) { push @after_request, $f } +sub after_request :prototype(&) ($f) { unshift @after_request, $f } my %path_routes; @@ -281,7 +295,7 @@ sub _log_err($e) { log_write join "\n", 'IP: '.($REQ->{ip}||'-'), 'Headers:', (map " $_: $REQ->{hdr}{$_}", sort keys $REQ->{hdr}->%*), - 'ERROR:', $e =~ s/(^|\n)/ /rg; + 'ERROR:', $e =~ s/(^|\n)/\n /rg; # TODO: decoded body, if we have that. } else { log_write $e; @@ -332,8 +346,9 @@ sub _do_req($c) { } if ($err) { - fu->reset; my($code, $msg) = ref $err eq 'FU::err' ? $err->@* : (500, $err); + fu->reset; + fu->status($code); eval { ($onerr{$code} || $onerr{500})->($code, $msg); 1; @@ -344,11 +359,14 @@ sub _do_req($c) { fu->_flush($c->{fcgi_obj} || $c->{client_sock}); my $proc_ms = (time - $REQ->{trace_start}) * 1000; - log_write(sprintf "%.0fms %s-%s %s-%d\n", $proc_ms, + log_write(sprintf "%.0fms%s %s-%s %s-%d\n", $proc_ms, + $REQ->{trace_nsql} ? + sprintf ' (sql %.0f+%.0fms, %d/%d/%d)', + ($REQ->{trace_sqlexec}||0)*1000, ($REQ->{trace_sqlprep}||0)*1000, + $REQ->{trace_nsqldirect}||0, $REQ->{trace_nsqlprep}||0, $REQ->{trace_nsql} : '', $REQ->{status}, ($REQ->{reshdr}{'content-type'}//'-') =~ s/;.+$//r, $REQ->{reshdr}{'content-encoding'}//'bytes', length($REQ->{resbody}), - # TODO: SQL timings - ) if FU::debug || $proc_ms > (FU::log_slow_pages||1e10); + ) if FU::debug || $proc_ms > (FU::log_slow_reqs||1e10); } @@ -568,8 +586,8 @@ sub db { }; } -sub sql { fu->db->q(@_) } -sub SQL { fu->db->Q(@_) } +sub sql { shift->db->q(@_) } +sub SQL { shift->db->Q(@_) } @@ -803,6 +821,7 @@ Zstandard. =back + =head2 Framework Overview C is a mostly straightforward and conventional backend web framework. It @@ -840,10 +859,45 @@ certainly not great if you plan to transfer large files. The rest of this document is reference documentation; there's no easy introductionary cookbook-style docs yet, sorry about that. +Unless specifically mentioned otherwise, all methods and functions taking or +returning strings deal with perl Unicode strings, not raw bytes. + + =head2 Framework Configuration =over +=item FU::init_db($info) + +Set database configuration. C<$info> can either be a connection string for C<< +FU::Pg->connect() >> or a subroutine that returns a L connection. The +latter can be useful to set default parameters such as C, +C, C, etc. + +A C callback is registered after connection to collect +per-request performance metrics. If you want to register your own trace +callback, you'll want to have it call C to keep the +functionality. + +The configured database is used for C<< fu->db >> and related methods; you can +of course still manage alternative database connections in your own code if you +need that, but then that won't benefit from FU's integrated transaction +handling and performance tracing. + +=item FU::debug($enable) + +Enable or disable debug mode. Returns the current mode when no argument is +given. + +Debug mode currently only enables more verbose logging, but it may influence +other features in the future as well. You're of course free to use the debug +setting to enable or disable debugging features in your own code. + +=item FU::log_slow_reqs($ms) + +Enable logging of requests that took longer than C<$ms> milliseconds to +process. Can be set to 0 to disable such logging. + =item FU::monitor_path(@paths) Add filesystem paths to be monitored for changes when running in monitor mode @@ -870,14 +924,243 @@ restart loop. =back + =head2 Handlers & Routing +=over + +=item FU::get($path, $sub) + +=item FU::post($path, $sub) + +=item FU::delete($path, $sub) + +=item FU::options($path, $sub) + +=item FU::put($path, $sub) + +=item FU::patch($path, $sub) + +=item FU::query($path, $sub) + +Register a route handler for the given HTTP method and C<$path>. C<$path> can +either be a string, which is matched for equality with C<< fu->path >>, or a +regex that must fully match the request path. If the regex contains capture +groups, its contents are passed to C<$sub> as arguments. + + FU::get '/', sub { + # Here goes the code for the root path. + }; + + FU::post '/sub/path', sub { + # POST requests to '/sub/path' go here. + }; + + FU::get qr{/hello/(.+)}, sub($name) { + # "GET /hello/world" goes to this code, with $name='world'. + }; + +It is an error to register multiple handlers for the same method and path. This +is verified for exact paths, but if you register handlers with overlapping +regexes, it's not defined which one is actually called. + +=item FU::before_request($sub) + +Register a callback to be run when a request has been received but before it's +being routed to the main handler function. Callbacks are run in the order that +they are registered. If C<$sub> throws an error or calls C<< fu->done >>, any +later C callbacks are not run and no routing handler is called. + +=item FU::after_request($sub) + +Register a callback to be run after the routing handler has finished but before +the response is sent back to the client. Callbacks are run in reverse order +that they are registered. These callbacks are always run, even when a previous +C or the routing handler threw an error. + +=item FU::on_error($code, $sub) + +Register a callback to be run when the given error code (HTTP status code) is +generated. C<$sub> is called with the error code as arguments and should +generate a suitable error page to send to the client. Only one callback can be +registered for each code, calling this function another time with the same +C<$code> overwrites a previous callback. + +Internally, C can generate errors with code C<400>, C<404> and C<500>, but +C<< fu->error() >> can be used to generate other errors. If no callback exists +for a certain error code, C<500> is used as fallback. + +=back + + =head2 The 'fu' Object +While the C namespace is used for global configuration and utility +functions, the C object is intended for methods that deal with request +processing (although some are useful used outside of request handlers as well). + +The C object itself can be used to store request-local data. For example, +the following is a valid approach to handle user authentication: + + FU::before { + fu->{user} = authenticate_user_from_cookie_or_something(); + }; + + FU::get '/registered-users-only', sub { + fu->error(403) if !fu->{user}; + }; + +In addition to the request information and response generation methods +described in the sections below, it has a few utility methods: + +=over + +=item fu->debug + +Read-only alias of C. + +=item fu->db_conn + +Returns the current database handle, as set with C. This is +mainly useful for configuration, you generally shouldn't use this for running +queries inside a request handler, see C<< fu->db >> for that instead. + +=item fu->db + +Returns the database transaction for the current request. Starts a new +transaction if none is active. + +Transactions initiated this way are automatically committed when the request +has successfully been processed, or rolled back if there was an error. + +=item fu->sql($query, @params) + +Convenient short-hand for C<< fu->db->q($query, @params) >>. + +=item fu->SQL(@args) + +Convenient short-hand for C<< fu->db->Q(@args) >>. + +=back + =head2 Request Information +=over + +=item fu->path + +The path component of the request. E.g. if the request is for +C, this returns C. + +=item fu->method + +Upper-case request method, e.g. 'POST' or 'GET'. + +=item fu->header($name) + +Return the request header by the given C<$name>, or undef if the requests did +not have that header. Header name matching is case-insensitive. If the request +includes multiple headers with the same name, these are merged into a single +comma-separated value. + +=item fu->headers + +Return a hashref with all request headers. Keys are lower-cased header names. + +=item fu->ip + +Return the client IP address. + +=item fu->query() + +Return the raw query part of the request URI, e.g. +C this returns C. + +=item fu->query($name) + +Parses the raw query string with C in L and returns the +value with the given $name. + +=item fu->query($schema) + +I + +=item fu->formdata($name) + +=item fu->formdata($schema) + +Like C<< fu->query() >> but returns data from the POST request body. + +=back + +I Support C and file uploads. + +I Support JSON bodies. + +I Cookie parsing. + + =head2 Generating Responses +=over + +=item fu->done + +Throw an exception to indicate that the response is "done", i.e. the current +function will return and no further handlers (if any) are run. Only works if +you're not catching the exception elsewhere, of course. + +=item fu->error($code, $message) + +Throw an exception with a status code. If the exception is not caught +elsewhere, this ends up in running the appropriate C handler. + +C<$message> is optional and currently only used for logging. + +=item fu->reset + +Reset the response to an empty state, basically undoing all effects of the +methods below. + +=item fu->status($code) + +Set the HTTP status code for the response. Defaults to C<200> if not set and no +error is thrown. + +=item fu->add_header($name, $value) + +Add a response header, can be used to add multiple headers with the same name. + +=item fu->set_header($name, $value) + +Add a response header or overwrite the header with a new value if it already +exists. Set C<$value> to undef to remove a previously set header. + +=item fu->set_body($data) + +Set the (raw, binary) body of the response to C<$data>. This method is not very +convenient for writing dynamic responses, so usually you'll want to use a +templating system or L: + + use FU::XMLWriter ':html5_'; + + fu->set_body(html_ sub { + body_ sub { + h1_ "Hello, world!"; + }; + }); + +=back + +I Setting cookies. + +I JSON output. + +I Redirection responses. + +I Sending files. + + =head2 Running the Site When your script is done setting L and registering diff --git a/FU.xs b/FU.xs index d72d2de..ac46cc8 100644 --- a/FU.xs +++ b/FU.xs @@ -351,7 +351,7 @@ void columns(fupg_st *st) void nrows(fupg_st *st) CODE: - ST(0) = st->result ? fupg_exec_result(aTHX_ st->result) : &PL_sv_undef; + ST(0) = st->result ? sv_2mortal(newSViv(PQntuples(st->result))) : &PL_sv_undef; void query(fupg_st *st) CODE: diff --git a/FU/Pg.pm b/FU/Pg.pm index 22d8559..ecf17e0 100644 --- a/FU/Pg.pm +++ b/FU/Pg.pm @@ -372,8 +372,7 @@ returns. =item $st->nrows -Returns the number of rows the query returned, may return C for -C-style queries. +Number of rows returned by the query. =item $st->exec_time diff --git a/c/pgst.c b/c/pgst.c index 469147f..1442297 100644 --- a/c/pgst.c +++ b/c/pgst.c @@ -63,6 +63,7 @@ static SV *fupg_exec(pTHX_ fupg_conn *c, const char *sql) { fupg_st *st = safecalloc(1, sizeof(*st)); st->conn = c; SvREFCNT_inc(c->self); + st->cookie = c->cookie; st->query = savepv(sql); st->stflags = c->stflags; st->result = r; diff --git a/c/pgtypes.c b/c/pgtypes.c index e359326..6e33ff9 100644 --- a/c/pgtypes.c +++ b/c/pgtypes.c @@ -80,6 +80,16 @@ SENDFN(bool) { fustr_write_ch(out, SvTRUE(val) ? 1 : 0); } +RECVFN(void) { + RLEN(0); + (void)buf; + return &PL_sv_undef; +} + +SENDFN(void) { + (void)val; (void)out; +} + RECVFN(int2) { RLEN(2); return newSViv(fu_frombeI(16, buf)); @@ -562,8 +572,8 @@ SENDFN(timestamp) { /* 603 box */\ /* 604 polygon */\ /* 628 line */\ - B( 650, "cidr", inet )\ A( 629, "_line", 628 )\ + B( 650, "cidr", inet )\ A( 651, "_cidr", 650 )\ B( 700, "float4", float4)\ B( 701, "float8", float8)\ @@ -635,6 +645,7 @@ SENDFN(timestamp) { A( 2209, "_regoperator", 2204 )\ A( 2210, "_regclass", 2205 )\ A( 2211, "_regtype", 2206 )\ + B( 2278, "void", void )\ A( 2949, "_txid_snapshot", 2970 )\ B( 2950, "uuid", uuid )\ A( 2951, "_uuid", 2950 )\ diff --git a/t/pgconnect.t b/t/pgconnect.t index c4385d9..2a77cf6 100644 --- a/t/pgconnect.t +++ b/t/pgconnect.t @@ -428,7 +428,7 @@ subtest 'Tracing', sub { is_deeply $st->param_types, []; is_deeply $st->param_values, []; is_deeply $st->columns, []; - ok !defined $st->nrows; + is $st->nrows, 0; is $st->query, 'SET client_encoding=UTF8'; ok $st->exec_time > 0 && $st->exec_time < 1; ok !defined $st->prepare_time; diff --git a/t/pgtypes.t b/t/pgtypes.t index ac76c36..22b1f6e 100644 --- a/t/pgtypes.t +++ b/t/pgtypes.t @@ -40,6 +40,8 @@ sub f($type, $p_in) { ok !eval { $conn->q("SELECT \$1::$type", $p_in)->val; 1 }, "$test fail"; } +ok !defined $conn->q('SELECT pg_sleep(0)')->val; # void + v bool => true, undef, 1, 't'; v bool => false, undef, 0, 'f';