Kea  1.9.9-git
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>
10 #include <exceptions/exceptions.h>
11 #include <mysql/mysql_connection.h>
12 
13 #include <boost/lexical_cast.hpp>
14 
15 #include <algorithm>
16 #include <stdint.h>
17 #include <string>
18 #include <limits>
19 
20 using namespace isc;
21 using namespace std;
22 
23 namespace isc {
24 namespace db {
25 
26 bool MySqlHolder::atexit_ = []{atexit([]{mysql_library_end();});return true;};
27 
29 const int MYSQL_DEFAULT_CONNECTION_TIMEOUT = 5; // seconds
30 
31 MySqlTransaction::MySqlTransaction(MySqlConnection& conn)
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 
44 void
46  conn_.commit();
47  committed_ = true;
48 }
49 
50 
51 // Open the database using the parameters passed to the constructor.
52 
53 void
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 
234 std::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 
317 void
318 MySqlConnection::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 
342 void
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.
388 void
390  MYSQL_TIME& output_time) {
391  MySqlBinding::convertToDatabaseTime(input_time, output_time);
392 }
393 
394 void
396  const uint32_t valid_lifetime,
397  MYSQL_TIME& expire) {
398  MySqlBinding::convertToDatabaseTime(cltt, valid_lifetime, expire);
399 }
400 
401 void
403  uint32_t valid_lifetime, time_t& cltt) {
404  MySqlBinding::convertFromDatabaseTime(expire, valid_lifetime, cltt);
405 }
406 
407 void
410  checkUnusable();
411  // We create prepared statements for all other queries, but MySQL
412  // don't support prepared statements for START TRANSACTION.
413  int status = mysql_query(mysql_, "START TRANSACTION");
414  if (status != 0) {
415  isc_throw(DbOperationError, "unable to start transaction, "
416  "reason: " << mysql_error(mysql_));
417  }
418 }
419 
420 void
423  checkUnusable();
424  if (mysql_commit(mysql_) != 0) {
425  isc_throw(DbOperationError, "commit failed: "
426  << mysql_error(mysql_));
427  }
428 }
429 
430 void
433  checkUnusable();
434  if (mysql_rollback(mysql_) != 0) {
435  isc_throw(DbOperationError, "rollback failed: "
436  << mysql_error(mysql_));
437  }
438 }
439 
440 } // namespace db
441 } // namespace isc
std::vector< std::string > text_statements_
Raw text of statements.
We want to reuse the database backend connection and exchange code for other uses, in particular for hook libraries.
bool my_bool
my_bool type in MySQL 8.x.
MySqlHolder mysql_
MySQL connection handle.
static void convertToDatabaseTime(const time_t input_time, MYSQL_TIME &output_time)
Convert time_t value to database time.
static void convertToDatabaseTime(const time_t input_time, MYSQL_TIME &output_time)
Converts time_t value to database time.
A generic exception that is thrown if a parameter given to a method or function is considered invalid...
static std::pair< uint32_t, uint32_t > getVersion(const ParameterMap &parameters)
Get the schema version.
void commit()
Commits transaction.
STL namespace.
static void convertFromDatabaseTime(const MYSQL_TIME &expire, uint32_t valid_lifetime, time_t &cltt)
Convert Database Time to Lease Times.
void checkUnusable()
Throws an exception if the connection is not usable.
Exception thrown on failure to open database.
int MysqlExecuteStatement(MYSQL_STMT *stmt)
Execute a prepared statement.
#define isc_throw(type, stream)
A shortcut macro to insert known values into exception arguments.
const my_bool MLM_FALSE
MySQL false value.
Exception thrown if name of database is not specified.
int version()
returns Kea hooks version.
static void convertFromDatabaseTime(const MYSQL_TIME &expire, uint32_t valid_lifetime, time_t &cltt)
Converts Database Time to Lease Times.
void clearStatements()
Clears prepared statements and text statements.
const int MYSQL_DEFAULT_CONNECTION_TIMEOUT
virtual ~MySqlConnection()
Destructor.
const int DB_DBG_TRACE_DETAIL
Database logging levels.
Definition: db_log.cc:21
Defines the logger used by the top-level component of kea-dhcp-ddns.
std::string getParameter(const std::string &name) const
Returns value of a connection parameter.
std::vector< MYSQL_STMT * > statements_
Prepared statements.
void startTransaction()
Starts Transaction.
void rollback()
Rollback Transactions.
void commit()
Commit Transactions.
std::map< std::string, std::string > ParameterMap
Database configuration parameter map.
void prepareStatement(uint32_t index, const char *text)
Prepare Single Statement.
void prepareStatements(const TaggedStatement *start_statement, const TaggedStatement *end_statement)
Prepare statements.
void openDatabase()
Open Database.
MySQL Selection Statements.
Exception thrown on failure to execute a database function.
Common MySQL Connector Pool.