1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
|
/* Transactor framework, a wrapper for safely retryable transactions.
*
* DO NOT INCLUDE THIS FILE DIRECTLY; include pqxx/transactor instead.
*
* Copyright (c) 2000-2019, Jeroen T. Vermeulen.
*
* See COPYING for copyright license. If you did not receive a file called
* COPYING with this source code, please notify the distributor of this mistake,
* or contact the author.
*/
#ifndef PQXX_H_TRANSACTOR
#define PQXX_H_TRANSACTOR
#include "pqxx/compiler-public.hxx"
#include "pqxx/compiler-internal-pre.hxx"
#include "pqxx/connection_base.hxx"
#include "pqxx/transaction.hxx"
// Methods tested in eg. test module test01 are marked with "//[t01]".
namespace pqxx
{
/**
* @defgroup transactor Transactor framework
*
* Sometimes a transaction can fail for completely transient reasons, such as a
* conflict with another transaction in SERIALIZABLE isolation. The right way
* to handle those failures is often just to re-run the transaction from
* scratch.
*
* For example, your REST API might be handling each HTTP request in its own
* database transaction, and if this kind of transient failure happens, you
* simply want to "replay" the whole request, in a fresh transaction.
*
* You won't necessarily want to execute the exact same SQL commands with the
* exact same data. Some of your SQL statements may depend on state that can
* vary between retries. So instead of dumbly replaying the SQL, you re-run
* the same application code that produced those SQL commands.
*
* The transactor framework makes it a little easier for you to do this safely,
* and avoid typical pitfalls. You encapsulate the work that you want to do
* into a callable that you pass to the @c perform function.
*
* Here's how it works. You write your transaction code as a lambda or
* function, which creates its own transaction object, does its work, and
* commits at the end. You pass that callback to @c pqxx::perform, which runs
* it for you.
*
* If there's a failure inside your callback, there will be an exception. Your
* transaction object goes out of scope and gets destroyed, so that it aborts
* implicitly. Seeing this, @c perform tries running your callback again. It
* stops doing that when the callback succeeds, or when it has failed too many
* times, or when there's an error that leaves the database in an unknown
* state, such as a lost connection just while we're waiting for the database
* to confirm a commit. It all depends on the type of exception.
*
* The callback takes no arguments. If you're using lambdas, the easy way to
* pass arguments is for the lambda to "capture" them from your variables. Or,
* if you're using functions, you may want to use @c std::bind.
*
* Once your callback succeeds, it can return a result, and @c perform will
* return that result back to you.
*/
//@{
/// Simple way to execute a transaction with automatic retry.
/**
* Executes your transaction code as a callback. Repeats it until it completes
* normally, or it throws an error other than the few libpqxx-generated
* exceptions that the framework understands, or after a given number of failed
* attempts, or if the transaction ends in an "in-doubt" state.
*
* (An in-doubt state is one where libpqxx cannot determine whether the server
* finally committed a transaction or not. This can happen if the network
* connection to the server is lost just while we're waiting for its reply to
* a "commit" statement. The server may have completed the commit, or not, but
* it can't tell you because there's no longer a connection.
*
* Using this still takes a bit of care. If your callback makes use of data
* from the database, you'll probably have to query that data within your
* callback. If the attempt to perform your callback fails, and the framework
* tries again, you'll be in a new transaction and the data in the database may
* have changed under your feet.
*
* Also be careful about changing variables or data structures from within
* your callback. The run may still fail, and perhaps get run again. The
* ideal way to do it (in most cases) is to return your result from your
* callback, and change your program's data state only after @c perform
* completes successfully.
*
* @param callback Transaction code that can be called with no arguments.
* @param attempts Maximum number of times to attempt performing callback.
* Must be greater than zero.
* @return Whatever your callback returns.
*/
template<typename TRANSACTION_CALLBACK>
inline auto perform(const TRANSACTION_CALLBACK &callback, int attempts=3)
-> decltype(callback())
{
if (attempts <= 0)
throw std::invalid_argument{
"Zero or negative number of attempts passed to pqxx::perform()."};
for (; attempts > 0; --attempts)
{
try
{
return callback();
}
catch (const in_doubt_error &)
{
// Not sure whether transaction went through or not. The last thing in
// the world that we should do now is try again!
throw;
}
catch (const statement_completion_unknown &)
{
// Not sure whether our last statement succeeded. Don't risk running it
// again.
throw;
}
catch (const broken_connection &)
{
// Connection failed. Definitely worth retrying.
if (attempts <= 1) throw;
continue;
}
catch (const transaction_rollback &)
{
// Some error that may well be transient, such as serialization failure
// or deadlock. Worth retrying.
if (attempts <= 1) throw;
continue;
}
}
throw pqxx::internal_error{"No outcome reached on perform()."};
}
/// @deprecated Pre-C++11 wrapper for automatically retrying transactions.
/**
* Pass an object of your transactor-based class to connection_base::perform()
* to execute the transaction code embedded in it.
*
* connection_base::perform() is actually a template, specializing itself to any
* transactor type you pass to it. This means you will have to pass it a
* reference of your object's ultimate static type; runtime polymorphism is
* not allowed. Hence the absence of virtual methods in transactor. The
* exact methods to be called at runtime *must* be resolved at compile time.
*
* Your transactor-derived class must define a copy constructor. This will be
* used to create a "clean" copy of your transactor for every attempt that
* perform() makes to run it.
*/
template<typename TRANSACTION=transaction<read_committed>> class transactor
{
public:
using argument_type = TRANSACTION;
PQXX_DEPRECATED explicit transactor( //[t04]
const std::string &TName="transactor") :
m_name{TName} { }
/// Overridable transaction definition; insert your database code here
/** The operation will be retried if the connection to the backend is lost or
* the operation fails, but not if the connection is broken in such a way as
* to leave the library in doubt as to whether the operation succeeded. In
* that case, an in_doubt_error will be thrown.
*
* Recommended practice is to allow this operator to modify only the
* transactor itself, and the dedicated transaction object it is passed as an
* argument. This is what makes side effects, retrying etc. controllable in
* the transactor framework.
* @param T Dedicated transaction context created to perform this operation.
*/
void operator()(TRANSACTION &T); //[t04]
// Overridable member functions, called by connection_base::perform() if an
// attempt to run transaction fails/succeeds, respectively, or if the
// connection is lost at just the wrong moment, goes into an indeterminate
// state. Use these to patch up runtime state to match events, if needed, or
// to report failure conditions.
/// Optional overridable function to be called if transaction is aborted
/** This need not imply complete failure; the transactor will automatically
* retry the operation a number of times before giving up. on_abort() will be
* called for each of the failed attempts.
*
* One parameter is passed in by the framework: an error string describing why
* the transaction failed. This will also be logged to the connection's
* notice processor.
*/
void on_abort(const char[]) noexcept {} //[t13]
/// Optional overridable function to be called after successful commit
/** If your on_commit() throws an exception, the actual back-end transaction
* will remain committed, so any changes in the database remain regardless of
* how this function terminates.
*/
void on_commit() {} //[t07]
/// Overridable function to be called when "in doubt" about outcome
/** This may happen if the connection to the backend is lost while attempting
* to commit. In that case, the backend may have committed the transaction
* but is unable to confirm this to the frontend; or the transaction may have
* failed, causing it to be rolled back, but again without acknowledgement to
* the client program. The best way to deal with this situation is typically
* to wave red flags in the user's face and ask him to investigate.
*
* The robusttransaction class is intended to reduce the chances of this
* error occurring, at a certain cost in performance.
* @see robusttransaction
*/
void on_doubt() noexcept {} //[t13]
/// The transactor's name.
std::string name() const { return m_name; } //[t13]
private:
std::string m_name;
};
template<typename TRANSACTOR>
inline void connection_base::perform(
const TRANSACTOR &T,
int Attempts)
{
if (Attempts <= 0) return;
bool Done = false;
// Make attempts to perform T
do
{
--Attempts;
// Work on a copy of T2 so we can restore the starting situation if need be
TRANSACTOR T2{T};
try
{
typename TRANSACTOR::argument_type X{*this, T2.name()};
T2(X);
X.commit();
Done = true;
}
catch (const in_doubt_error &)
{
// Not sure whether transaction went through or not. The last thing in
// the world that we should do now is retry.
T2.on_doubt();
throw;
}
catch (const std::exception &e)
{
// Could be any kind of error.
T2.on_abort(e.what());
if (Attempts <= 0) throw;
continue;
}
catch (...)
{
// Don't try to forge ahead if we don't even know what happened
T2.on_abort("Unknown exception");
throw;
}
T2.on_commit();
} while (not Done);
}
} // namespace pqxx
//@}
#include "pqxx/compiler-internal-post.hxx"
#endif
|