Line data Source code
1 : /*
2 : This file is part of TALER
3 : (C) 2014--2025 Taler Systems SA
4 :
5 : TALER is free software; you can redistribute it and/or modify it under the
6 : terms of the GNU Lesser General Public License as published by the Free Software
7 : Foundation; either version 3, or (at your option) any later version.
8 :
9 : TALER is distributed in the hope that it will be useful, but WITHOUT ANY
10 : WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
11 : A PARTICULAR PURPOSE. See the GNU General Public License for more details.
12 :
13 : You should have received a copy of the GNU General Public License along with
14 : TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
15 : */
16 : /**
17 : * @file src/backenddb/start.c
18 : * @brief implementation of the start, rollback and commit functions
19 : * @author Christian Grothoff
20 : */
21 : #include <gnunet/gnunet_util_lib.h>
22 : #include "merchantdb_lib.h"
23 : #include "merchant-database/start.h"
24 : #include "merchant-database/preflight.h"
25 : #include "helper.h"
26 :
27 :
28 : enum GNUNET_GenericReturnValue
29 522 : TALER_MERCHANTDB_start (
30 : struct TALER_MERCHANTDB_PostgresContext *pg,
31 : const char *name)
32 : {
33 522 : struct GNUNET_PQ_ExecuteStatement es[] = {
34 522 : GNUNET_PQ_make_execute ("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
35 : GNUNET_PQ_EXECUTE_STATEMENT_END
36 : };
37 :
38 522 : GNUNET_assert (NULL != name);
39 522 : TALER_MERCHANTDB_preflight (pg);
40 522 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
41 : "Starting merchant DB transaction `%s'\n",
42 : name);
43 522 : if (GNUNET_OK !=
44 522 : GNUNET_PQ_exec_statements (pg->conn,
45 : es))
46 : {
47 0 : TALER_LOG_ERROR ("Failed to start transaction\n");
48 0 : GNUNET_break (0);
49 0 : return GNUNET_SYSERR;
50 : }
51 522 : pg->transaction_name = name;
52 522 : return GNUNET_OK;
53 : }
54 :
55 :
56 : enum GNUNET_GenericReturnValue
57 1 : TALER_MERCHANTDB_start_read_committed (
58 : struct TALER_MERCHANTDB_PostgresContext *pg,
59 : const char *name)
60 : {
61 1 : struct GNUNET_PQ_ExecuteStatement es[] = {
62 1 : GNUNET_PQ_make_execute ("START TRANSACTION ISOLATION LEVEL READ COMMITTED"),
63 : GNUNET_PQ_EXECUTE_STATEMENT_END
64 : };
65 :
66 1 : GNUNET_assert (NULL != name);
67 1 : TALER_MERCHANTDB_preflight (pg);
68 1 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
69 : "Starting merchant DB transaction %s (READ COMMITTED)\n",
70 : name);
71 1 : if (GNUNET_OK !=
72 1 : GNUNET_PQ_exec_statements (pg->conn,
73 : es))
74 : {
75 0 : TALER_LOG_ERROR ("Failed to start transaction\n");
76 0 : GNUNET_break (0);
77 0 : return GNUNET_SYSERR;
78 : }
79 1 : pg->transaction_name = name;
80 1 : return GNUNET_OK;
81 : }
82 :
83 :
84 : void
85 123 : TALER_MERCHANTDB_rollback (
86 : struct TALER_MERCHANTDB_PostgresContext *pg)
87 : {
88 123 : struct GNUNET_PQ_ExecuteStatement es[] = {
89 123 : GNUNET_PQ_make_execute ("ROLLBACK"),
90 : GNUNET_PQ_EXECUTE_STATEMENT_END
91 : };
92 :
93 123 : if (NULL == pg->transaction_name)
94 10 : return;
95 113 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
96 : "Rolling back merchant DB transaction `%s'\n",
97 : pg->transaction_name);
98 113 : GNUNET_break (GNUNET_OK ==
99 : GNUNET_PQ_exec_statements (pg->conn,
100 : es));
101 113 : pg->transaction_name = NULL;
102 : }
103 :
104 :
105 : enum GNUNET_DB_QueryStatus
106 410 : TALER_MERCHANTDB_commit (
107 : struct TALER_MERCHANTDB_PostgresContext *pg)
108 : {
109 410 : struct GNUNET_PQ_QueryParam params[] = {
110 : GNUNET_PQ_query_param_end
111 : };
112 : enum GNUNET_DB_QueryStatus qs;
113 :
114 410 : GNUNET_break (NULL != pg->transaction_name);
115 410 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
116 : "Committing transaction `%s'\n",
117 : pg->transaction_name);
118 410 : PREPARE (pg,
119 : "commit",
120 : "COMMIT");
121 : {
122 : PGresult *result;
123 :
124 410 : result = GNUNET_PQ_exec_prepared (pg->conn,
125 : "commit",
126 : params);
127 410 : qs = GNUNET_PQ_eval_result (pg->conn,
128 : "commit",
129 : result);
130 : /* PostgreSQL accepts COMMIT on a transaction it has already aborted: it
131 : silently rolls the transaction back and answers PGRES_COMMAND_OK with the
132 : command tag ROLLBACK. GNUNET_PQ_eval_result() cannot tell that apart from
133 : a real commit, so every caller took the success path for a transaction
134 : whose writes are gone. Report it as a soft error instead: sessions run
135 : SERIALIZABLE, so the overwhelmingly likely cause is an unhandled
136 : 40001/40P01 for which retrying is right; a caller with a retry loop gives
137 : up after MAX_RETRIES and fails visibly, one without fails immediately.
138 :
139 : The *success* path deliberately keeps returning exactly what
140 : GNUNET_PQ_eval_prepared_non_select() returned before. That function is
141 : GNUNET_PQ_exec_prepared() + GNUNET_PQ_eval_result() plus a
142 : strtol(PQcmdTuples()) step that only runs when the status is already
143 : GNUNET_DB_STATUS_SUCCESS_NO_RESULTS, and PQcmdTuples() on a COMMIT command
144 : tag is the empty string -- so that step yields 0, which is
145 : GNUNET_DB_STATUS_SUCCESS_NO_RESULTS again. */
146 410 : if ( (0 <= qs) &&
147 410 : (NULL != result) &&
148 410 : (0 == strcmp ("ROLLBACK",
149 410 : PQcmdStatus (result))) )
150 : {
151 1 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
152 : "Transaction `%s' was rolled back by the database instead of committed; a previous statement must have failed without being handled\n",
153 : pg->transaction_name);
154 1 : qs = GNUNET_DB_STATUS_SOFT_ERROR;
155 : }
156 410 : PQclear (result);
157 : }
158 410 : pg->transaction_name = NULL;
159 410 : return qs;
160 : }
|