Kea 1.9.11
mysql_connection.cc
Go to the documentation of this file.
1// Copyright (C) 2012-2021 Internet Systems Consortium, Inc. ("ISC")
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7#include <config.h>
8
9#include <database/db_log.h>
12
13#include <boost/lexical_cast.hpp>
14
15#include <algorithm>
16#include <stdint.h>
17#include <string>
18#include <limits>
19
20using namespace isc;
21using namespace std;
22
23namespace isc {
24namespace db {
25
26bool MySqlHolder::atexit_ = []{atexit([]{mysql_library_end();});return true;};
27
29const int MYSQL_DEFAULT_CONNECTION_TIMEOUT = 5; // seconds
30
32 : conn_(conn), committed_(false) {
33 conn_.startTransaction();
34}
35
37 // Rollback if the MySqlTransaction::commit wasn't explicitly
38 // called.
39 if (!committed_) {
40 conn_.rollback();
41 }
42}
43
44void
46 conn_.commit();
47 committed_ = true;
48}
49
50
51// Open the database using the parameters passed to the constructor.
52
53void
55 // Set up the values of the parameters
56 const char* host = "localhost";
57 string shost;
58 try {
59 shost = getParameter("host");
60 host = shost.c_str();
61 } catch (...) {
62 // No host. Fine, we'll use "localhost"
63 }
64
65 unsigned int port = 0;
66 string sport;
67 try {
68 sport = getParameter("port");
69 } catch (...) {
70 // No port parameter, we are going to use the default port.
71 sport = "";
72 }
73
74 if (sport.size() > 0) {
75 // Port was given, so try to convert it to an integer.
76
77 try {
78 port = boost::lexical_cast<unsigned int>(sport);
79 } catch (...) {
80 // Port given but could not be converted to an unsigned int.
81 // Just fall back to the default value.
82 port = 0;
83 }
84
85 // The port is only valid when it is in the 0..65535 range.
86 // Again fall back to the default when the given value is invalid.
87 if (port > numeric_limits<uint16_t>::max()) {
88 port = 0;
89 }
90 }
91
92 const char* user = NULL;
93 string suser;
94 try {
95 suser = getParameter("user");
96 user = suser.c_str();
97 } catch (...) {
98 // No user. Fine, we'll use NULL
99 }
100
101 const char* password = NULL;
102 string spassword;
103 try {
104 spassword = getParameter("password");
105 password = spassword.c_str();
106 } catch (...) {
107 // No password. Fine, we'll use NULL
108 }
109
110 const char* name = NULL;
111 string sname;
112 try {
113 sname = getParameter("name");
114 name = sname.c_str();
115 } catch (...) {
116 // No database name. Throw a "NoName" exception
117 isc_throw(NoDatabaseName, "must specify a name for the database");
118 }
119
120 unsigned int connect_timeout = MYSQL_DEFAULT_CONNECTION_TIMEOUT;
121 string stimeout;
122 try {
123 stimeout = getParameter("connect-timeout");
124 } catch (...) {
125 // No timeout parameter, we are going to use the default timeout.
126 stimeout = "";
127 }
128
129 if (stimeout.size() > 0) {
130 // Timeout was given, so try to convert it to an integer.
131
132 try {
133 connect_timeout = boost::lexical_cast<unsigned int>(stimeout);
134 } catch (...) {
135 // Timeout given but could not be converted to an unsigned int. Set
136 // the connection timeout to an invalid value to trigger throwing
137 // of an exception.
138 connect_timeout = 0;
139 }
140
141 // The timeout is only valid if greater than zero, as depending on the
142 // database, a zero timeout might signify something like "wait
143 // indefinitely".
144 //
145 // The check below also rejects a value greater than the maximum
146 // integer value. The lexical_cast operation used to obtain a numeric
147 // value from a string can get confused if trying to convert a negative
148 // integer to an unsigned int: instead of throwing an exception, it may
149 // produce a large positive value.
150 if ((connect_timeout == 0) ||
151 (connect_timeout > numeric_limits<int>::max())) {
152 isc_throw(DbInvalidTimeout, "database connection timeout (" <<
153 stimeout << ") must be an integer greater than 0");
154 }
155 }
156
157 // Set options for the connection:
158 //
159 // Set options for the connection:
160 // Make sure auto_reconnect is OFF! Enabling it leaves us with an unusable
161 // connection after a reconnect as among other things, it drops all our
162 // pre-compiled statements.
163 my_bool auto_reconnect = MLM_FALSE;
164 int result = mysql_options(mysql_, MYSQL_OPT_RECONNECT, &auto_reconnect);
165 if (result != 0) {
166 isc_throw(DbOpenError, "unable to set auto-reconnect option: " <<
167 mysql_error(mysql_));
168 }
169
170 // Make sure we have a large idle time window ... say 30 days...
171 const char *wait_time = "SET SESSION wait_timeout = 30 * 86400";
172 result = mysql_options(mysql_, MYSQL_INIT_COMMAND, wait_time);
173 if (result != 0) {
174 isc_throw(DbOpenError, "unable to set wait_timeout " <<
175 mysql_error(mysql_));
176 }
177
178 // Set SQL mode options for the connection: SQL mode governs how what
179 // constitutes insertable data for a given column, and how to handle
180 // invalid data. We want to ensure we get the strictest behavior and
181 // to reject invalid data with an error.
182 const char *sql_mode = "SET SESSION sql_mode ='STRICT_ALL_TABLES'";
183 result = mysql_options(mysql_, MYSQL_INIT_COMMAND, sql_mode);
184 if (result != 0) {
185 isc_throw(DbOpenError, "unable to set SQL mode options: " <<
186 mysql_error(mysql_));
187 }
188
189 // Connection timeout, the amount of time taken for the client to drop
190 // the connection if the server is not responding.
191 result = mysql_options(mysql_, MYSQL_OPT_CONNECT_TIMEOUT, &connect_timeout);
192 if (result != 0) {
193 isc_throw(DbOpenError, "unable to set database connection timeout: " <<
194 mysql_error(mysql_));
195 }
196
197 // Open the database.
198 //
199 // The option CLIENT_FOUND_ROWS is specified so that in an UPDATE,
200 // the affected rows are the number of rows found that match the
201 // WHERE clause of the SQL statement, not the rows changed. The reason
202 // here is that MySQL apparently does not update a row if data has not
203 // changed and so the "affected rows" (retrievable from MySQL) is zero.
204 // This makes it hard to distinguish whether the UPDATE changed no rows
205 // because no row matching the WHERE clause was found, or because a
206 // row was found but no data was altered.
207 MYSQL* status = mysql_real_connect(mysql_, host, user, password, name,
208 port, NULL, CLIENT_FOUND_ROWS);
209 if (status != mysql_) {
210 isc_throw(DbOpenError, mysql_error(mysql_));
211 }
212
213 // Enable autocommit. In case transaction is explicitly used, this
214 // setting will be overwritten for the transaction. However, there are
215 // cases when lack of autocommit could cause transactions to hang
216 // until commit or rollback is explicitly called. This already
217 // caused issues for some unit tests which were unable to cleanup
218 // the database after the test because of pending transactions.
219 // Use of autocommit will eliminate this problem.
220 my_bool autocommit_result = mysql_autocommit(mysql_, 1);
221 if (autocommit_result != 0) {
222 isc_throw(DbOperationError, mysql_error(mysql_));
223 }
224
225 // To avoid a flush to disk on every commit, the global parameter
226 // innodb_flush_log_at_trx_commit should be set to 2. This will cause the
227 // changes to be written to the log, but flushed to disk in the background
228 // every second. Setting the parameter to that value will speed up the
229 // system, but at the risk of losing data if the system crashes.
230}
231
232// Get schema version.
233
234std::pair<uint32_t, uint32_t>
236 // Get a connection.
237 MySqlConnection conn(parameters);
238
239 // Open the database.
240 conn.openDatabase();
241
242 // Allocate a new statement.
243 MYSQL_STMT *stmt = mysql_stmt_init(conn.mysql_);
244 if (stmt == NULL) {
245 isc_throw(DbOperationError, "unable to allocate MySQL prepared "
246 "statement structure, reason: " << mysql_error(conn.mysql_));
247 }
248
249 try {
250
251 // Prepare the statement from SQL text.
252 const char* version_sql = "SELECT version, minor FROM schema_version";
253 int status = mysql_stmt_prepare(stmt, version_sql, strlen(version_sql));
254 if (status != 0) {
255 isc_throw(DbOperationError, "unable to prepare MySQL statement <"
256 << version_sql << ">, reason: "
257 << mysql_error(conn.mysql_));
258 }
259
260 // Execute the prepared statement.
261 if (MysqlExecuteStatement(stmt) != 0) {
262 isc_throw(DbOperationError, "cannot execute schema version query <"
263 << version_sql << ">, reason: "
264 << mysql_errno(conn.mysql_));
265 }
266
267 // Bind the output of the statement to the appropriate variables.
268 MYSQL_BIND bind[2];
269 memset(bind, 0, sizeof(bind));
270
271 uint32_t version;
272 bind[0].buffer_type = MYSQL_TYPE_LONG;
273 bind[0].is_unsigned = 1;
274 bind[0].buffer = &version;
275 bind[0].buffer_length = sizeof(version);
276
277 uint32_t minor;
278 bind[1].buffer_type = MYSQL_TYPE_LONG;
279 bind[1].is_unsigned = 1;
280 bind[1].buffer = &minor;
281 bind[1].buffer_length = sizeof(minor);
282
283 if (mysql_stmt_bind_result(stmt, bind)) {
284 isc_throw(DbOperationError, "unable to bind result set for <"
285 << version_sql << ">, reason: "
286 << mysql_errno(conn.mysql_));
287 }
288
289 // Fetch the data.
290 if (mysql_stmt_fetch(stmt)) {
291 isc_throw(DbOperationError, "unable to bind result set for <"
292 << version_sql << ">, reason: "
293 << mysql_errno(conn.mysql_));
294 }
295
296 // Discard the statement and its resources
297 mysql_stmt_close(stmt);
298
299 return (std::make_pair(version, minor));
300
301 } catch (const std::exception&) {
302 // Avoid a memory leak on error.
303 mysql_stmt_close(stmt);
304
305 // Send the exception to the caller.
306 throw;
307 }
308}
309
310// Prepared statement setup. The textual form of an SQL statement is stored
311// in a vector of strings (text_statements_) and is used in the output of
312// error messages. The SQL statement is also compiled into a "prepared
313// statement" (stored in statements_), which avoids the overhead of compilation
314// during use. As prepared statements have resources allocated to them, the
315// class destructor explicitly destroys them.
316
317void
318MySqlConnection::prepareStatement(uint32_t index, const char* text) {
319 // Validate that there is space for the statement in the statements array
320 // and that nothing has been placed there before.
321 if ((index >= statements_.size()) || (statements_[index] != NULL)) {
322 isc_throw(InvalidParameter, "invalid prepared statement index (" <<
323 static_cast<int>(index) << ") or indexed prepared " <<
324 "statement is not null");
325 }
326
327 // All OK, so prepare the statement
328 text_statements_[index] = std::string(text);
329 statements_[index] = mysql_stmt_init(mysql_);
330 if (statements_[index] == NULL) {
331 isc_throw(DbOperationError, "unable to allocate MySQL prepared "
332 "statement structure, reason: " << mysql_error(mysql_));
333 }
334
335 int status = mysql_stmt_prepare(statements_[index], text, strlen(text));
336 if (status != 0) {
337 isc_throw(DbOperationError, "unable to prepare MySQL statement <" <<
338 text << ">, reason: " << mysql_error(mysql_));
339 }
340}
341
342void
344 const TaggedStatement* end_statement) {
345 // Created the MySQL prepared statements for each DML statement.
346 for (const TaggedStatement* tagged_statement = start_statement;
347 tagged_statement != end_statement; ++tagged_statement) {
348 if (tagged_statement->index >= statements_.size()) {
349 statements_.resize(tagged_statement->index + 1, NULL);
350 text_statements_.resize(tagged_statement->index + 1,
351 std::string(""));
352 }
353 prepareStatement(tagged_statement->index,
354 tagged_statement->text);
355 }
356}
357
359 statements_.clear();
360 text_statements_.clear();
361}
362
365 // Free up the prepared statements, ignoring errors. (What would we do
366 // about them? We're destroying this object and are not really concerned
367 // with errors on a database connection that is about to go away.)
368 for (int i = 0; i < statements_.size(); ++i) {
369 if (statements_[i] != NULL) {
370 (void) mysql_stmt_close(statements_[i]);
371 statements_[i] = NULL;
372 }
373 }
374 statements_.clear();
375 text_statements_.clear();
376}
377
378// Time conversion methods.
379//
380// Note that the MySQL TIMESTAMP data type (used for "expire") converts data
381// from the current timezone to UTC for storage, and from UTC to the current
382// timezone for retrieval.
383//
384// This causes no problems providing that:
385// a) cltt is given in local time
386// b) We let the system take care of timezone conversion when converting
387// from a time read from the database into a local time.
388void
390 MYSQL_TIME& output_time) {
391 MySqlBinding::convertToDatabaseTime(input_time, output_time);
392}
393
394void
396 const uint32_t valid_lifetime,
397 MYSQL_TIME& expire) {
398 MySqlBinding::convertToDatabaseTime(cltt, valid_lifetime, expire);
399}
400
401void
403 uint32_t valid_lifetime, time_t& cltt) {
404 MySqlBinding::convertFromDatabaseTime(expire, valid_lifetime, cltt);
405}
406
407void
409 // If it is nested transaction, do nothing.
410 if (++transaction_ref_count_ > 1) {
411 return;
412 }
413
416 // We create prepared statements for all other queries, but MySQL
417 // don't support prepared statements for START TRANSACTION.
418 int status = mysql_query(mysql_, "START TRANSACTION");
419 if (status != 0) {
420 isc_throw(DbOperationError, "unable to start transaction, "
421 "reason: " << mysql_error(mysql_));
422 }
423}
424
425bool
427 return (transaction_ref_count_ > 0);
428}
429
430void
432 if (transaction_ref_count_ <= 0) {
433 isc_throw(Unexpected, "commit called for not started transaction - coding error");
434 }
435
436 // When committing nested transaction, do nothing.
437 if (--transaction_ref_count_ > 0) {
438 return;
439 }
442 if (mysql_commit(mysql_) != 0) {
443 isc_throw(DbOperationError, "commit failed: "
444 << mysql_error(mysql_));
445 }
446}
447
448void
450 if (transaction_ref_count_ <= 0) {
451 isc_throw(Unexpected, "rollback called for not started transaction - coding error");
452 }
453
454 // When rolling back nested transaction, do nothing.
455 if (--transaction_ref_count_ > 0) {
456 return;
457 }
460 if (mysql_rollback(mysql_) != 0) {
461 isc_throw(DbOperationError, "rollback failed: "
462 << mysql_error(mysql_));
463 }
464}
465
466} // namespace db
467} // namespace isc
int version()
returns Kea hooks version.
A generic exception that is thrown if a parameter given to a method or function is considered invalid...
A generic exception that is thrown when an unexpected error condition occurs.
std::string getParameter(const std::string &name) const
Returns value of a connection parameter.
void checkUnusable()
Throws an exception if the connection is not usable.
std::map< std::string, std::string > ParameterMap
Database configuration parameter map.
Exception thrown on failure to open database.
Exception thrown on failure to execute a database function.
static void convertFromDatabaseTime(const MYSQL_TIME &expire, uint32_t valid_lifetime, time_t &cltt)
Converts Database Time to Lease Times.
static void convertToDatabaseTime(const time_t input_time, MYSQL_TIME &output_time)
Converts time_t value to database time.
Common MySQL Connector Pool.
MySqlHolder mysql_
MySQL connection handle.
void prepareStatement(uint32_t index, const char *text)
Prepare Single Statement.
std::vector< MYSQL_STMT * > statements_
Prepared statements.
bool isTransactionStarted() const
Checks if there is a transaction in progress.
std::vector< std::string > text_statements_
Raw text of statements.
static void convertToDatabaseTime(const time_t input_time, MYSQL_TIME &output_time)
Convert time_t value to database time.
static void convertFromDatabaseTime(const MYSQL_TIME &expire, uint32_t valid_lifetime, time_t &cltt)
Convert Database Time to Lease Times.
void commit()
Commits current transaction.
void openDatabase()
Open Database.
void prepareStatements(const TaggedStatement *start_statement, const TaggedStatement *end_statement)
Prepare statements.
static std::pair< uint32_t, uint32_t > getVersion(const ParameterMap &parameters)
Get the schema version.
int transaction_ref_count_
Reference counter for transactions.
void startTransaction()
Starts new transaction.
virtual ~MySqlConnection()
Destructor.
void clearStatements()
Clears prepared statements and text statements.
void rollback()
Rollbacks current transaction.
void commit()
Commits transaction.
MySqlTransaction(MySqlConnection &conn)
Constructor.
Exception thrown if name of database is not specified.
We want to reuse the database backend connection and exchange code for other uses,...
#define isc_throw(type, stream)
A shortcut macro to insert known values into exception arguments.
const int DB_DBG_TRACE_DETAIL
Database logging levels.
Definition: db_log.cc:21
const my_bool MLM_FALSE
MySQL false value.
const int MYSQL_DEFAULT_CONNECTION_TIMEOUT
@ MYSQL_START_TRANSACTION
Definition: db_log.h:61
@ MYSQL_ROLLBACK
Definition: db_log.h:63
@ MYSQL_COMMIT
Definition: db_log.h:62
bool my_bool
my_bool type in MySQL 8.x.
int MysqlExecuteStatement(MYSQL_STMT *stmt)
Execute a prepared statement.
Defines the logger used by the top-level component of kea-lfc.
MySQL Selection Statements.