Kea  1.5.0
mysql_connection.cc
Go to the documentation of this file.
1 // Copyright (C) 2012-2018 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 
27 const int MYSQL_DEFAULT_CONNECTION_TIMEOUT = 5; // seconds
28 
29 MySqlTransaction::MySqlTransaction(MySqlConnection& conn)
30  : conn_(conn), committed_(false) {
31  conn_.startTransaction();
32 }
33 
35  // Rollback if the MySqlTransaction::commit wasn't explicitly
36  // called.
37  if (!committed_) {
38  conn_.rollback();
39  }
40 }
41 
42 void
44  conn_.commit();
45  committed_ = true;
46 }
47 
48 
49 // Open the database using the parameters passed to the constructor.
50 
51 void
53 
54  // Set up the values of the parameters
55  const char* host = "localhost";
56  string shost;
57  try {
58  shost = getParameter("host");
59  host = shost.c_str();
60  } catch (...) {
61  // No host. Fine, we'll use "localhost"
62  }
63 
64  unsigned int port = 0;
65  string sport;
66  try {
67  sport = getParameter("port");
68  } catch (...) {
69  // No port parameter, we are going to use the default port.
70  sport = "";
71  }
72 
73  if (sport.size() > 0) {
74  // Port was given, so try to convert it to an integer.
75 
76  try {
77  port = boost::lexical_cast<unsigned int>(sport);
78  } catch (...) {
79  // Port given but could not be converted to an unsigned int.
80  // Just fall back to the default value.
81  port = 0;
82  }
83 
84  // The port is only valid when it is in the 0..65535 range.
85  // Again fall back to the default when the given value is invalid.
86  if (port > numeric_limits<uint16_t>::max()) {
87  port = 0;
88  }
89  }
90 
91  const char* user = NULL;
92  string suser;
93  try {
94  suser = getParameter("user");
95  user = suser.c_str();
96  } catch (...) {
97  // No user. Fine, we'll use NULL
98  }
99 
100  const char* password = NULL;
101  string spassword;
102  try {
103  spassword = getParameter("password");
104  password = spassword.c_str();
105  } catch (...) {
106  // No password. Fine, we'll use NULL
107  }
108 
109  const char* name = NULL;
110  string sname;
111  try {
112  sname = getParameter("name");
113  name = sname.c_str();
114  } catch (...) {
115  // No database name. Throw a "NoName" exception
116  isc_throw(NoDatabaseName, "must specify a name for the database");
117  }
118 
119  unsigned int connect_timeout = MYSQL_DEFAULT_CONNECTION_TIMEOUT;
120  string stimeout;
121  try {
122  stimeout = getParameter("connect-timeout");
123  } catch (...) {
124  // No timeout parameter, we are going to use the default timeout.
125  stimeout = "";
126  }
127 
128  if (stimeout.size() > 0) {
129  // Timeout was given, so try to convert it to an integer.
130 
131  try {
132  connect_timeout = boost::lexical_cast<unsigned int>(stimeout);
133  } catch (...) {
134  // Timeout given but could not be converted to an unsigned int. Set
135  // the connection timeout to an invalid value to trigger throwing
136  // of an exception.
137  connect_timeout = 0;
138  }
139 
140  // The timeout is only valid if greater than zero, as depending on the
141  // database, a zero timeout might signify something like "wait
142  // indefinitely".
143  //
144  // The check below also rejects a value greater than the maximum
145  // integer value. The lexical_cast operation used to obtain a numeric
146  // value from a string can get confused if trying to convert a negative
147  // integer to an unsigned int: instead of throwing an exception, it may
148  // produce a large positive value.
149  if ((connect_timeout == 0) ||
150  (connect_timeout > numeric_limits<int>::max())) {
151  isc_throw(DbInvalidTimeout, "database connection timeout (" <<
152  stimeout << ") must be an integer greater than 0");
153  }
154  }
155 
156  // Set options for the connection:
157  //
158  // Set options for the connection:
159  // Make sure auto_reconnect is OFF! Enabling it leaves us with an unusable
160  // connection after a reconnect as among other things, it drops all our
161  // pre-compiled statements.
162  my_bool auto_reconnect = MLM_FALSE;
163  int result = mysql_options(mysql_, MYSQL_OPT_RECONNECT, &auto_reconnect);
164  if (result != 0) {
165  isc_throw(DbOpenError, "unable to set auto-reconnect option: " <<
166  mysql_error(mysql_));
167  }
168 
169  // Make sure we have a large idle time window ... say 30 days...
170  const char *wait_time = "SET SESSION wait_timeout = 30 * 86400";
171  result = mysql_options(mysql_, MYSQL_INIT_COMMAND, wait_time);
172  if (result != 0) {
173  isc_throw(DbOpenError, "unable to set wait_timeout " <<
174  mysql_error(mysql_));
175  }
176 
177  // Set SQL mode options for the connection: SQL mode governs how what
178  // constitutes insertable data for a given column, and how to handle
179  // invalid data. We want to ensure we get the strictest behavior and
180  // to reject invalid data with an error.
181  const char *sql_mode = "SET SESSION sql_mode ='STRICT_ALL_TABLES'";
182  result = mysql_options(mysql_, MYSQL_INIT_COMMAND, sql_mode);
183  if (result != 0) {
184  isc_throw(DbOpenError, "unable to set SQL mode options: " <<
185  mysql_error(mysql_));
186  }
187 
188  // Connection timeout, the amount of time taken for the client to drop
189  // the connection if the server is not responding.
190  result = mysql_options(mysql_, MYSQL_OPT_CONNECT_TIMEOUT, &connect_timeout);
191  if (result != 0) {
192  isc_throw(DbOpenError, "unable to set database connection timeout: " <<
193  mysql_error(mysql_));
194  }
195 
196  // Open the database.
197  //
198  // The option CLIENT_FOUND_ROWS is specified so that in an UPDATE,
199  // the affected rows are the number of rows found that match the
200  // WHERE clause of the SQL statement, not the rows changed. The reason
201  // here is that MySQL apparently does not update a row if data has not
202  // changed and so the "affected rows" (retrievable from MySQL) is zero.
203  // This makes it hard to distinguish whether the UPDATE changed no rows
204  // because no row matching the WHERE clause was found, or because a
205  // row was found but no data was altered.
206  MYSQL* status = mysql_real_connect(mysql_, host, user, password, name,
207  port, NULL, CLIENT_FOUND_ROWS);
208  if (status != mysql_) {
209  isc_throw(DbOpenError, mysql_error(mysql_));
210  }
211 }
212 
213 
214 // Prepared statement setup. The textual form of an SQL statement is stored
215 // in a vector of strings (text_statements_) and is used in the output of
216 // error messages. The SQL statement is also compiled into a "prepared
217 // statement" (stored in statements_), which avoids the overhead of compilation
218 // during use. As prepared statements have resources allocated to them, the
219 // class destructor explicitly destroys them.
220 
221 void
222 MySqlConnection::prepareStatement(uint32_t index, const char* text) {
223  // Validate that there is space for the statement in the statements array
224  // and that nothing has been placed there before.
225  if ((index >= statements_.size()) || (statements_[index] != NULL)) {
226  isc_throw(InvalidParameter, "invalid prepared statement index (" <<
227  static_cast<int>(index) << ") or indexed prepared " <<
228  "statement is not null");
229  }
230 
231  // All OK, so prepare the statement
232  text_statements_[index] = std::string(text);
233  statements_[index] = mysql_stmt_init(mysql_);
234  if (statements_[index] == NULL) {
235  isc_throw(DbOperationError, "unable to allocate MySQL prepared "
236  "statement structure, reason: " << mysql_error(mysql_));
237  }
238 
239  int status = mysql_stmt_prepare(statements_[index], text, strlen(text));
240  if (status != 0) {
241  isc_throw(DbOperationError, "unable to prepare MySQL statement <" <<
242  text << ">, reason: " << mysql_error(mysql_));
243  }
244 }
245 
246 void
248  const TaggedStatement* end_statement) {
249  // Created the MySQL prepared statements for each DML statement.
250  for (const TaggedStatement* tagged_statement = start_statement;
251  tagged_statement != end_statement; ++tagged_statement) {
252  if (tagged_statement->index >= statements_.size()) {
253  statements_.resize(tagged_statement->index + 1, NULL);
254  text_statements_.resize(tagged_statement->index + 1,
255  std::string(""));
256  }
257  prepareStatement(tagged_statement->index,
258  tagged_statement->text);
259  }
260 }
261 
263  statements_.clear();
264  text_statements_.clear();
265 }
266 
269  // Free up the prepared statements, ignoring errors. (What would we do
270  // about them? We're destroying this object and are not really concerned
271  // with errors on a database connection that is about to go away.)
272  for (int i = 0; i < statements_.size(); ++i) {
273  if (statements_[i] != NULL) {
274  (void) mysql_stmt_close(statements_[i]);
275  statements_[i] = NULL;
276  }
277  }
278  statements_.clear();
279  text_statements_.clear();
280 }
281 
282 // Time conversion methods.
283 //
284 // Note that the MySQL TIMESTAMP data type (used for "expire") converts data
285 // from the current timezone to UTC for storage, and from UTC to the current
286 // timezone for retrieval.
287 //
288 // This causes no problems providing that:
289 // a) cltt is given in local time
290 // b) We let the system take care of timezone conversion when converting
291 // from a time read from the database into a local time.
292 void
294  MYSQL_TIME& output_time) {
295  MySqlBinding::convertToDatabaseTime(input_time, output_time);
296 }
297 
298 void
300  const uint32_t valid_lifetime,
301  MYSQL_TIME& expire) {
302  MySqlBinding::convertToDatabaseTime(cltt, valid_lifetime, expire);
303 }
304 
305 void
307  uint32_t valid_lifetime, time_t& cltt) {
308  MySqlBinding::convertFromDatabaseTime(expire, valid_lifetime, cltt);
309 }
310 
311 void
314  // We create prepared statements for all other queries, but MySQL
315  // don't support prepared statements for START TRANSACTION.
316  int status = mysql_query(mysql_, "START TRANSACTION");
317  if (status != 0) {
318  isc_throw(DbOperationError, "unable to start transaction, "
319  "reason: " << mysql_error(mysql_));
320  }
321 }
322 
323 void
326  if (mysql_commit(mysql_) != 0) {
327  isc_throw(DbOperationError, "commit failed: "
328  << mysql_error(mysql_));
329  }
330 }
331 
332 void
335  if (mysql_rollback(mysql_) != 0) {
336  isc_throw(DbOperationError, "rollback failed: "
337  << mysql_error(mysql_));
338  }
339 }
340 
341 
342 } // namespace isc::db
343 } // 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,...
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...
#define DB_LOG_DEBUG(LEVEL, MESSAGE)
Macros.
Definition: db_log.h:115
void commit()
Commits transaction.
static void convertFromDatabaseTime(const MYSQL_TIME &expire, uint32_t valid_lifetime, time_t &cltt)
Convert Database Time to Lease Times.
Exception thrown on failure to open database.
#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.
static void convertFromDatabaseTime(const MYSQL_TIME &expire, uint32_t valid_lifetime, time_t &cltt)
Converts Database Time to Lease Times.
std::string getParameter(const std::string &name) const
Returns value of a connection parameter.
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.h:40
Defines the logger used by the top-level component of kea-dhcp-ddns.
std::vector< MYSQL_STMT * > statements_
Prepared statements.
void startTransaction()
Starts Transaction.
void rollback()
Rollback Transactions.
void commit()
Commit Transactions.
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.