Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1pPjHl-0000he-Cr for pgsql-hackers@arkaria.postgresql.org; Wed, 08 Feb 2023 12:10:49 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.92) (envelope-from ) id 1pPjHk-0001PC-8e for pgsql-hackers@arkaria.postgresql.org; Wed, 08 Feb 2023 12:10:48 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1pPjHj-0001P2-UC for pgsql-hackers@lists.postgresql.org; Wed, 08 Feb 2023 12:10:47 +0000 Received: from new4-smtp.messagingengine.com ([66.111.4.230]) by magus.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1pPjHg-0006dD-Vx for pgsql-hackers@postgresql.org; Wed, 08 Feb 2023 12:10:47 +0000 Received: from compute2.internal (compute2.nyi.internal [10.202.2.46]) by mailnew.nyi.internal (Postfix) with ESMTP id E70CF58208B; Wed, 8 Feb 2023 07:10:41 -0500 (EST) Received: from mailfrontend1 ([10.202.2.162]) by compute2.internal (MEProxy); Wed, 08 Feb 2023 07:10:41 -0500 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:cc:content-transfer-encoding :content-type:date:date:feedback-id:feedback-id:from:from :in-reply-to:in-reply-to:message-id:mime-version:reply-to:sender :subject:subject:to:to:x-me-proxy:x-me-proxy:x-me-sender :x-me-sender:x-sasl-enc; s=fm1; t=1675858241; x=1675865441; bh=E ohKMwPYbggKaM+OTfPj+Y+1MrpVQexsT3RwXq7rNIw=; b=tExFU+LckvhvOHiy6 n0fnQcVx54AK7wYt1bkNqvV6KhylQl3Dc7iRfWoE7R1QjhF7BLInuI8zcSoITCA9 Cn+QfcSkPcBvBQXiL/f8mVk0SkoEKHgYDf3HFeBxtzXo1mGFPWOqKjl+638rakvn +bFiK7OMASVjJe7EsUi7WVPion0/qHl6sKsM2wfysEeBxaue3wJC8BrBcxEaNbEq NT0aLcP+w0e2/AweoSV45ST4Y/yV6biK38cUMesIZ18AurPy3ciX83BiTkL93d1n zk/IcBOrD9689M5vn3G0qfFrhgY6W4Qfu205shgx1W3RwE9RTH+tseQAm60voaOX 2Q9KQ== X-ME-Sender: X-ME-Received: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedvhedrudehtddgfeehucetufdoteggodetrfdotf fvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfqfgfvpdfurfetoffkrfgpnffqhgen uceurghilhhouhhtmecufedttdenucesvcftvggtihhpihgvnhhtshculddquddttddmne cujfgurhepfffhvfevuffkgggtugfgjgesmhekreertddtjeenucfhrhhomheptehlvhgr rhhoucfjvghrrhgvrhgruceorghlvhhhvghrrhgvsegrlhhvhhdrnhhoqdhiphdrohhrgh eqnecuggftrfgrthhtvghrnhepudelkeekgfdttdettdekgfduvdeiffelteegjeeihfet heekveeftedtuddvhfeunecuffhomhgrihhnpegvnhhtvghrphhrihhsvggusgdrtghomh enucevlhhushhtvghrufhiiigvpedtnecurfgrrhgrmhepmhgrihhlfhhrohhmpegrlhhv hhgvrhhrvgesrghlvhhhrdhnohdqihhprdhorhhg X-ME-Proxy: Feedback-ID: ia2694551:Fastmail Received: by mail.messagingengine.com (Postfix) with ESMTPA; Wed, 8 Feb 2023 07:10:41 -0500 (EST) Received: by perhan.alvh.no-ip.org (Postfix, from userid 1000) id 100449C; Wed, 8 Feb 2023 13:10:40 +0100 (CET) Date: Wed, 8 Feb 2023 13:10:40 +0100 From: Alvaro Herrera To: Yugo NAGATA Cc: Tom Lane , Fabien COELHO , pgsql-hackers@postgresql.org Subject: Re: pgbench: using prepared BEGIN statement in a pipeline could cause an error Message-ID: <20230208121040.gbxn4ixd52ujxlzc@alvherre.pgsql> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="udzl2jqxwo77y7hd" Content-Disposition: inline Content-Transfer-Encoding: 8bit In-Reply-To: <20230208120928.fy3jcpjvzq76zqhb@alvherre.pgsql> List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --udzl2jqxwo77y7hd Content-Type: text/plain; charset=utf-8 Content-Disposition: inline Content-Transfer-Encoding: 8bit On 2023-Feb-08, Alvaro Herrera wrote: > I propose instead the following: each command is prepared just before > it's executed, as previously, and if we see a \startpipeline, then we > prepare all commands starting with the one just after, and until the > \endpipeline. Here's the patch. -- Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/ --udzl2jqxwo77y7hd Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v7-0001-Prepare-commands-in-pipelines-in-advance.patch" From 6d8938009b246463efe4104f5312e37b28b55235 Mon Sep 17 00:00:00 2001 From: Alvaro Herrera Date: Wed, 8 Feb 2023 13:01:47 +0100 Subject: [PATCH v7] Prepare commands in pipelines in advance --- src/bin/pgbench/pgbench.c | 147 +++++++++++++------ src/bin/pgbench/t/001_pgbench_with_server.pl | 17 +++ 2 files changed, 117 insertions(+), 47 deletions(-) diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c index 508ed218e8..a66fd51f92 100644 --- a/src/bin/pgbench/pgbench.c +++ b/src/bin/pgbench/pgbench.c @@ -628,7 +628,8 @@ typedef struct pg_time_usec_t txn_begin; /* used for measuring schedule lag times */ pg_time_usec_t stmt_begin; /* used for measuring statement latencies */ - bool prepared[MAX_SCRIPTS]; /* whether client prepared the script */ + /* whether client prepared each command of each script */ + bool **prepared; /* * For processing failures and repeating transactions with serialization @@ -739,6 +740,7 @@ static const char *QUERYMODE[] = {"simple", "extended", "prepared"}; * aset do gset on all possible queries of a combined query (\;). * expr Parsed expression, if needed. * stats Time spent in this command. + * prepname The name that this command is prepared under, in prepare mode * retries Number of retries after a serialization or deadlock error in the * current command. * failures Number of errors in the current command that were not retried. @@ -754,6 +756,7 @@ typedef struct Command char *varprefix; PgBenchExpr *expr; SimpleStats stats; + char *prepname; int64 retries; int64 failures; } Command; @@ -3006,13 +3009,6 @@ runShellCommand(Variables *variables, char *variable, char **argv, int argc) return true; } -#define MAX_PREPARE_NAME 32 -static void -preparedStatementName(char *buffer, int file, int state) -{ - sprintf(buffer, "P%d_%d", file, state); -} - /* * Report the abortion of the client when processing SQL commands. */ @@ -3053,6 +3049,87 @@ chooseScript(TState *thread) return i - 1; } +/* + * Prepare the SQL command from st->use_file at command_num. The name of the + * new prepared statement is stored in command->prepname. + */ +static void +prepareCommand(CState *st, int command_num) +{ + Command *command = sql_script[st->use_file].commands[command_num]; + static int prepnr = 0; + + /* No prepare for non-SQL commands */ + if (command->type != SQL_COMMAND) + return; + + if (!st->prepared) + { + st->prepared = pg_malloc(sizeof(bool *) * num_scripts); + for (int i = 0; i < num_scripts; i++) + { + ParsedScript *script = &sql_script[i]; + int numcmds; + + for (numcmds = 0; script->commands[numcmds] != NULL; numcmds++) + ; + st->prepared[i] = pg_malloc0(numcmds); + } + } + + if (!st->prepared[st->use_file][command_num]) + { + PGresult *res; + + if (!command->prepname) + command->prepname = psprintf("P%d_%d", st->use_file, prepnr++); + pg_log_debug("client %d preparing %s", st->id, command->prepname); + res = PQprepare(st->con, command->prepname, + command->argv[0], command->argc - 1, NULL); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_log_error("%s", PQerrorMessage(st->con)); + PQclear(res); + st->prepared[st->use_file][command_num] = true; + } +} + +/* + * Prepare all the commands in the script that come after the \startpipeline + * that's at position st->command, and the first \endpipeline we find. + * + * (This sets the ->prepared flag for each relevant command, but doesn't move + * the st->command counter) + */ +static void +prepareCommandsInPipeline(CState *st) +{ + int j; + Command **commands = sql_script[st->use_file].commands; + + Assert(commands[st->command]->type == META_COMMAND && + commands[st->command]->meta == META_STARTPIPELINE); + + /* + * We set the 'prepared' flag on the \startpipeline itself to flag that we + * don't need to do this next time, even though we don't actually prepare + * it. + */ + if (st->prepared && + st->prepared[st->use_file][st->command]) + return; + + for (j = st->command + 1; commands[j] != NULL; j++) + { + if (commands[j]->type == META_COMMAND && + commands[j]->meta == META_ENDPIPELINE) + break; + + prepareCommand(st, j); + } + + st->prepared[st->use_file][st->command] = true; +} + /* Send a SQL command, using the chosen querymode */ static bool sendCommand(CState *st, Command *command) @@ -3083,49 +3160,14 @@ sendCommand(CState *st, Command *command) } else if (querymode == QUERY_PREPARED) { - char name[MAX_PREPARE_NAME]; const char *params[MAX_ARGS]; - if (!st->prepared[st->use_file]) - { - int j; - Command **commands = sql_script[st->use_file].commands; - - for (j = 0; commands[j] != NULL; j++) - { - PGresult *res; - - if (commands[j]->type != SQL_COMMAND) - continue; - preparedStatementName(name, st->use_file, j); - if (PQpipelineStatus(st->con) == PQ_PIPELINE_OFF) - { - res = PQprepare(st->con, name, - commands[j]->argv[0], commands[j]->argc - 1, NULL); - if (PQresultStatus(res) != PGRES_COMMAND_OK) - pg_log_error("%s", PQerrorMessage(st->con)); - PQclear(res); - } - else - { - /* - * In pipeline mode, we use asynchronous functions. If a - * server-side error occurs, it will be processed later - * among the other results. - */ - if (!PQsendPrepare(st->con, name, - commands[j]->argv[0], commands[j]->argc - 1, NULL)) - pg_log_error("%s", PQerrorMessage(st->con)); - } - } - st->prepared[st->use_file] = true; - } + prepareCommand(st, st->command); getQueryParams(&st->variables, command, params); - preparedStatementName(name, st->use_file, st->command); - pg_log_debug("client %d sending %s", st->id, name); - r = PQsendQueryPrepared(st->con, name, command->argc - 1, + pg_log_debug("client %d sending %s", st->id, command->prepname); + r = PQsendQueryPrepared(st->con, command->prepname, command->argc - 1, params, NULL, NULL, 0); } else /* unknown sql mode */ @@ -3597,7 +3639,8 @@ advanceConnectionState(TState *thread, CState *st, StatsData *agg) thread->conn_duration += now - start; /* Reset session-local state */ - memset(st->prepared, 0, sizeof(st->prepared)); + pg_free(st->prepared); + st->prepared = NULL; } /* @@ -4360,6 +4403,16 @@ executeMetaCommand(CState *st, pg_time_usec_t *now) return CSTATE_ABORTED; } + /* + * If we're in prepared-query mode, we need to prepare all the + * commands that are inside the pipeline. This solves the problem + * that running BEGIN ISOLATION LEVEL SERIALIZABLE would fail, because + * by the time the command is executed, a snapshot would have already + * been acquired, so an error would be thrown. + */ + if (querymode == QUERY_PREPARED) + prepareCommandsInPipeline(st); + if (PQpipelineStatus(st->con) != PQ_PIPELINE_OFF) { commandFailed(st, "startpipeline", "already in pipeline mode"); diff --git a/src/bin/pgbench/t/001_pgbench_with_server.pl b/src/bin/pgbench/t/001_pgbench_with_server.pl index 4bf508ea96..dfdeb1f70f 100644 --- a/src/bin/pgbench/t/001_pgbench_with_server.pl +++ b/src/bin/pgbench/t/001_pgbench_with_server.pl @@ -839,6 +839,23 @@ select 1 \gset f } }); +# Working \startpipeline in prepared query mode with serializable +$node->pgbench( + '-t 1 -n -M prepared', + 0, + [ qr{type: .*/001_pgbench_pipeline_serializable}, qr{actually processed: 1/1} ], + [], + 'working \startpipeline with serializable', + { + '001_pgbench_pipeline_serializable' => q{ +-- test startpipeline with serializable +\startpipeline +BEGIN ISOLATION LEVEL SERIALIZABLE; +} . "select 1;\n" x 10 . q{ +END; +\endpipeline +} + }); # trigger many expression errors my @errors = ( -- 2.30.2 --udzl2jqxwo77y7hd--