merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

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:
Msrc/backenddb/pg.c | 9+++++++++
Msrc/backenddb/test_merchantdb.c | 142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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));