commit b6cf0226759a12eb383fa9be0b62d6d2efbd787d
parent b6c749ca995b21144aa1a7196ac2228d3dc12f36
Author: Christian Grothoff <christian@grothoff.org>
Date: Wed, 5 Aug 2026 22:36:58 +0200
clear instance on reconnect to re-arm setting it
Diffstat:
2 files changed, 151 insertions(+), 0 deletions(-)
diff --git a/src/backenddb/pg.c b/src/backenddb/pg.c
@@ -56,6 +56,15 @@ reconnect_cb (struct TALER_MERCHANTDB_PostgresContext *pg,
GNUNET_PQ_EXECUTE_STATEMENT_END
};
+ /* The 'search_path' of the new connection is the one we set above,
+ so the instance the old connection was routed to is no longer
+ current; forget it, or TALER_MERCHANTDB_set_instance() would
+ consider the routing up-to-date and skip re-issuing it. */
+ GNUNET_free (pg->current_merchant_id);
+ pg->current_merchant_serial = 0;
+ memset (&pg->current_merchant_pub,
+ 0,
+ sizeof (pg->current_merchant_pub));
if (GNUNET_OK !=
GNUNET_PQ_exec_statements (pq,
es))
diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c
@@ -654,6 +654,146 @@ post_test_instances (struct TestInstances_Closure *cls)
/**
+ * Tests that a per-instance operation still resolves against the
+ * per-instance schema after the database connection was dropped and
+ * re-established.
+ *
+ * Regression test: reconnect_cb() re-issues 'SET search_path TO
+ * merchant' but used to leave @e current_merchant_id set, so the
+ * TALER_MERCHANTDB_set_instance() the dispatcher does for the next
+ * request found the instance already selected and returned without
+ * re-issuing the per-instance search_path. Every unqualified
+ * statement of that instance then hit schema 'merchant' and failed
+ * with 42P01 (HTTP 500) until a different instance was touched.
+ *
+ * @param instance the instance to run the test against.
+ * @return 0 when successful, 1 otherwise.
+ */
+static int
+test_reconnect_search_path (const struct InstanceData *instance)
+{
+ json_t *i18n = json_object ();
+ struct TALER_MERCHANTDB_PostgresContext *cpg;
+ uint64_t backend_pid;
+ uint64_t gen;
+ uint64_t cat;
+ char sql[128];
+ int ret = 1;
+
+ GNUNET_assert (NULL != i18n);
+ TEST_SET_INSTANCE (instance->instance.id,
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT);
+ if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ TALER_MERCHANTDB_insert_category (pg,
+ instance->instance.id,
+ "reconnect-before",
+ i18n,
+ &cat))
+ {
+ GNUNET_break (0);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Insert category before reconnect failed\n");
+ goto cleanup;
+ }
+ /* Find out which backend serves our connection. */
+ {
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_end
+ };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_uint64 ("pid",
+ &backend_pid),
+ GNUNET_PQ_result_spec_end
+ };
+
+ if ( (GNUNET_OK !=
+ GNUNET_PQ_prepare_anon (pg->conn,
+ "SELECT pg_backend_pid()::INT8 AS pid")) ||
+ (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "",
+ params,
+ rs)) )
+ {
+ GNUNET_break (0);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Failed to determine backend PID\n");
+ goto cleanup;
+ }
+ }
+ /* Kill our own backend from a second connection, the way a
+ PostgreSQL restart or a pgbouncer recycle would. */
+ GNUNET_snprintf (sql,
+ sizeof (sql),
+ "DO $$ BEGIN"
+ " PERFORM pg_terminate_backend(%llu);"
+ " END $$",
+ (unsigned long long) backend_pid);
+ cpg = TALER_MERCHANTDB_connect (test_cfg);
+ if (NULL == cpg)
+ {
+ GNUNET_break (0);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Failed to open second database connection\n");
+ goto cleanup;
+ }
+ {
+ struct GNUNET_PQ_ExecuteStatement es[] = {
+ GNUNET_PQ_make_execute (sql),
+ GNUNET_PQ_EXECUTE_STATEMENT_END
+ };
+ enum GNUNET_GenericReturnValue res;
+
+ res = GNUNET_PQ_exec_statements (cpg->conn,
+ es);
+ TALER_MERCHANTDB_disconnect (cpg);
+ if (GNUNET_OK != res)
+ {
+ GNUNET_break (0);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Failed to terminate our own backend\n");
+ goto cleanup;
+ }
+ }
+ gen = TMH_PG_prep_gen_;
+ for (unsigned int i = 0; i<10; i++)
+ {
+ GNUNET_PQ_reconnect_if_down (pg->conn);
+ if (gen != TMH_PG_prep_gen_)
+ break;
+ sleep (1);
+ }
+ if (gen == TMH_PG_prep_gen_)
+ {
+ GNUNET_break (0);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Database did not reconnect\n");
+ goto cleanup;
+ }
+ /* This is what the dispatcher does for the next request; it must
+ restore the per-instance search_path. */
+ TEST_SET_INSTANCE (instance->instance.id,
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT);
+ if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ TALER_MERCHANTDB_insert_category (pg,
+ instance->instance.id,
+ "reconnect-after",
+ i18n,
+ &cat))
+ {
+ GNUNET_break (0);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Insert category after reconnect failed\n");
+ goto cleanup;
+ }
+ ret = 0;
+cleanup:
+ json_decref (i18n);
+ return ret;
+}
+
+
+/**
* Function that tests instances.
*
* @param cls closure with config
@@ -762,6 +902,8 @@ run_test_instances (struct TestInstances_Closure *cls)
"Lookup account failed: account found where there is none\n");
return 1;
}
+ /* Test that a reconnect does not lose the per-instance search_path */
+ TEST_RET_ON_FAIL (test_reconnect_search_path (&cls->instances[0]));
/* Test instance private key deletion */
TEST_RET_ON_FAIL (test_delete_instance_private_key (&cls->instances[0],
GNUNET_DB_STATUS_SUCCESS_ONE_RESULT));