PageRenderTime 54ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/src/infrastructure/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php

http://github.com/facebook/phabricator
PHP | 415 lines | 313 code | 68 blank | 34 comment | 33 complexity | 729f9d2075789b429fce1ae785fad23c MD5 | raw file
Possible License(s): JSON, MPL-2.0-no-copyleft-exception, Apache-2.0, BSD-3-Clause, LGPL-2.0, MIT, LGPL-2.1, LGPL-3.0
  1. <?php
  2. abstract class AphrontBaseMySQLDatabaseConnection
  3. extends AphrontDatabaseConnection {
  4. private $configuration;
  5. private $connection;
  6. private $connectionPool = array();
  7. private $lastResult;
  8. private $nextError;
  9. const CALLERROR_QUERY = 777777;
  10. const CALLERROR_CONNECT = 777778;
  11. abstract protected function connect();
  12. abstract protected function rawQuery($raw_query);
  13. abstract protected function rawQueries(array $raw_queries);
  14. abstract protected function fetchAssoc($result);
  15. abstract protected function getErrorCode($connection);
  16. abstract protected function getErrorDescription($connection);
  17. abstract protected function closeConnection();
  18. abstract protected function freeResult($result);
  19. public function __construct(array $configuration) {
  20. $this->configuration = $configuration;
  21. }
  22. public function __clone() {
  23. $this->establishConnection();
  24. }
  25. public function openConnection() {
  26. $this->requireConnection();
  27. }
  28. public function close() {
  29. if ($this->lastResult) {
  30. $this->lastResult = null;
  31. }
  32. if ($this->connection) {
  33. $this->closeConnection();
  34. $this->connection = null;
  35. }
  36. }
  37. public function escapeColumnName($name) {
  38. return '`'.str_replace('`', '``', $name).'`';
  39. }
  40. public function escapeMultilineComment($comment) {
  41. // These can either terminate a comment, confuse the hell out of the parser,
  42. // make MySQL execute the comment as a query, or, in the case of semicolon,
  43. // are quasi-dangerous because the semicolon could turn a broken query into
  44. // a working query plus an ignored query.
  45. static $map = array(
  46. '--' => '(DOUBLEDASH)',
  47. '*/' => '(STARSLASH)',
  48. '//' => '(SLASHSLASH)',
  49. '#' => '(HASH)',
  50. '!' => '(BANG)',
  51. ';' => '(SEMICOLON)',
  52. );
  53. $comment = str_replace(
  54. array_keys($map),
  55. array_values($map),
  56. $comment);
  57. // For good measure, kill anything else that isn't a nice printable
  58. // character.
  59. $comment = preg_replace('/[^\x20-\x7F]+/', ' ', $comment);
  60. return '/* '.$comment.' */';
  61. }
  62. public function escapeStringForLikeClause($value) {
  63. $value = addcslashes($value, '\%_');
  64. $value = $this->escapeUTF8String($value);
  65. return $value;
  66. }
  67. protected function getConfiguration($key, $default = null) {
  68. return idx($this->configuration, $key, $default);
  69. }
  70. private function establishConnection() {
  71. $host = $this->getConfiguration('host');
  72. $database = $this->getConfiguration('database');
  73. $profiler = PhutilServiceProfiler::getInstance();
  74. $call_id = $profiler->beginServiceCall(
  75. array(
  76. 'type' => 'connect',
  77. 'host' => $host,
  78. 'database' => $database,
  79. ));
  80. // If we receive these errors, we'll retry the connection up to the
  81. // retry limit. For other errors, we'll fail immediately.
  82. $retry_codes = array(
  83. // "Connection Timeout"
  84. 2002 => true,
  85. // "Unable to Connect"
  86. 2003 => true,
  87. );
  88. $max_retries = max(1, $this->getConfiguration('retries', 3));
  89. for ($attempt = 1; $attempt <= $max_retries; $attempt++) {
  90. try {
  91. $conn = $this->connect();
  92. $profiler->endServiceCall($call_id, array());
  93. break;
  94. } catch (AphrontQueryException $ex) {
  95. $code = $ex->getCode();
  96. if (($attempt < $max_retries) && isset($retry_codes[$code])) {
  97. $message = pht(
  98. 'Retrying database connection to "%s" after connection '.
  99. 'failure (attempt %d; "%s"; error #%d): %s',
  100. $host,
  101. $attempt,
  102. get_class($ex),
  103. $code,
  104. $ex->getMessage());
  105. // See T13403. If we're silenced with the "@" operator, don't log
  106. // this connection attempt. This keeps things quiet if we're
  107. // running a setup workflow like "bin/config" and expect that the
  108. // database credentials will often be incorrect.
  109. if (error_reporting()) {
  110. phlog($message);
  111. }
  112. } else {
  113. $profiler->endServiceCall($call_id, array());
  114. throw $ex;
  115. }
  116. }
  117. }
  118. $this->connection = $conn;
  119. }
  120. protected function requireConnection() {
  121. if (!$this->connection) {
  122. if ($this->connectionPool) {
  123. $this->connection = array_pop($this->connectionPool);
  124. } else {
  125. $this->establishConnection();
  126. }
  127. }
  128. return $this->connection;
  129. }
  130. protected function beginAsyncConnection() {
  131. $connection = $this->requireConnection();
  132. $this->connection = null;
  133. return $connection;
  134. }
  135. protected function endAsyncConnection($connection) {
  136. if ($this->connection) {
  137. $this->connectionPool[] = $this->connection;
  138. }
  139. $this->connection = $connection;
  140. }
  141. public function selectAllResults() {
  142. $result = array();
  143. $res = $this->lastResult;
  144. if ($res == null) {
  145. throw new Exception(pht('No query result to fetch from!'));
  146. }
  147. while (($row = $this->fetchAssoc($res))) {
  148. $result[] = $row;
  149. }
  150. return $result;
  151. }
  152. public function executeQuery(PhutilQueryString $query) {
  153. $display_query = $query->getMaskedString();
  154. $raw_query = $query->getUnmaskedString();
  155. $this->lastResult = null;
  156. $retries = max(1, $this->getConfiguration('retries', 3));
  157. while ($retries--) {
  158. try {
  159. $this->requireConnection();
  160. $is_write = $this->checkWrite($raw_query);
  161. $profiler = PhutilServiceProfiler::getInstance();
  162. $call_id = $profiler->beginServiceCall(
  163. array(
  164. 'type' => 'query',
  165. 'config' => $this->configuration,
  166. 'query' => $display_query,
  167. 'write' => $is_write,
  168. ));
  169. $result = $this->rawQuery($raw_query);
  170. $profiler->endServiceCall($call_id, array());
  171. if ($this->nextError) {
  172. $result = null;
  173. }
  174. if ($result) {
  175. $this->lastResult = $result;
  176. break;
  177. }
  178. $this->throwQueryException($this->connection);
  179. } catch (AphrontConnectionLostQueryException $ex) {
  180. $can_retry = ($retries > 0);
  181. if ($this->isInsideTransaction()) {
  182. // Zero out the transaction state to prevent a second exception
  183. // ("program exited with open transaction") from being thrown, since
  184. // we're about to throw a more relevant/useful one instead.
  185. $state = $this->getTransactionState();
  186. while ($state->getDepth()) {
  187. $state->decreaseDepth();
  188. }
  189. $can_retry = false;
  190. }
  191. if ($this->isHoldingAnyLock()) {
  192. $this->forgetAllLocks();
  193. $can_retry = false;
  194. }
  195. $this->close();
  196. if (!$can_retry) {
  197. throw $ex;
  198. }
  199. }
  200. }
  201. }
  202. public function executeRawQueries(array $raw_queries) {
  203. if (!$raw_queries) {
  204. return array();
  205. }
  206. $is_write = false;
  207. foreach ($raw_queries as $key => $raw_query) {
  208. $is_write = $is_write || $this->checkWrite($raw_query);
  209. $raw_queries[$key] = rtrim($raw_query, "\r\n\t ;");
  210. }
  211. $profiler = PhutilServiceProfiler::getInstance();
  212. $call_id = $profiler->beginServiceCall(
  213. array(
  214. 'type' => 'multi-query',
  215. 'config' => $this->configuration,
  216. 'queries' => $raw_queries,
  217. 'write' => $is_write,
  218. ));
  219. $results = $this->rawQueries($raw_queries);
  220. $profiler->endServiceCall($call_id, array());
  221. return $results;
  222. }
  223. protected function processResult($result) {
  224. if (!$result) {
  225. try {
  226. $this->throwQueryException($this->requireConnection());
  227. } catch (Exception $ex) {
  228. return $ex;
  229. }
  230. } else if (is_bool($result)) {
  231. return $this->getAffectedRows();
  232. }
  233. $rows = array();
  234. while (($row = $this->fetchAssoc($result))) {
  235. $rows[] = $row;
  236. }
  237. $this->freeResult($result);
  238. return $rows;
  239. }
  240. protected function checkWrite($raw_query) {
  241. // NOTE: The opening "(" allows queries in the form of:
  242. //
  243. // (SELECT ...) UNION (SELECT ...)
  244. $is_write = !preg_match('/^[(]*(SELECT|SHOW|EXPLAIN)\s/', $raw_query);
  245. if ($is_write) {
  246. if ($this->getReadOnly()) {
  247. throw new Exception(
  248. pht(
  249. 'Attempting to issue a write query on a read-only '.
  250. 'connection (to database "%s")!',
  251. $this->getConfiguration('database')));
  252. }
  253. AphrontWriteGuard::willWrite();
  254. return true;
  255. }
  256. return false;
  257. }
  258. protected function throwQueryException($connection) {
  259. if ($this->nextError) {
  260. $errno = $this->nextError;
  261. $error = pht('Simulated error.');
  262. $this->nextError = null;
  263. } else {
  264. $errno = $this->getErrorCode($connection);
  265. $error = $this->getErrorDescription($connection);
  266. }
  267. $this->throwQueryCodeException($errno, $error);
  268. }
  269. private function throwCommonException($errno, $error) {
  270. $message = pht('#%d: %s', $errno, $error);
  271. switch ($errno) {
  272. case 2013: // Connection Dropped
  273. throw new AphrontConnectionLostQueryException($message);
  274. case 2006: // Gone Away
  275. $more = pht(
  276. 'This error may occur if your configured MySQL "wait_timeout" or '.
  277. '"max_allowed_packet" values are too small. This may also indicate '.
  278. 'that something used the MySQL "KILL <process>" command to kill '.
  279. 'the connection running the query.');
  280. throw new AphrontConnectionLostQueryException("{$message}\n\n{$more}");
  281. case 1213: // Deadlock
  282. throw new AphrontDeadlockQueryException($message);
  283. case 1205: // Lock wait timeout exceeded
  284. throw new AphrontLockTimeoutQueryException($message);
  285. case 1062: // Duplicate Key
  286. // NOTE: In some versions of MySQL we get a key name back here, but
  287. // older versions just give us a key index ("key 2") so it's not
  288. // portable to parse the key out of the error and attach it to the
  289. // exception.
  290. throw new AphrontDuplicateKeyQueryException($message);
  291. case 1044: // Access denied to database
  292. case 1142: // Access denied to table
  293. case 1143: // Access denied to column
  294. case 1227: // Access denied (e.g., no SUPER for SHOW SLAVE STATUS).
  295. throw new AphrontAccessDeniedQueryException($message);
  296. case 1045: // Access denied (auth)
  297. throw new AphrontInvalidCredentialsQueryException($message);
  298. case 1146: // No such table
  299. case 1049: // No such database
  300. case 1054: // Unknown column "..." in field list
  301. throw new AphrontSchemaQueryException($message);
  302. }
  303. // TODO: 1064 is syntax error, and quite terrible in production.
  304. return null;
  305. }
  306. protected function throwConnectionException($errno, $error, $user, $host) {
  307. $this->throwCommonException($errno, $error);
  308. $message = pht(
  309. 'Attempt to connect to %s@%s failed with error #%d: %s.',
  310. $user,
  311. $host,
  312. $errno,
  313. $error);
  314. throw new AphrontConnectionQueryException($message, $errno);
  315. }
  316. protected function throwQueryCodeException($errno, $error) {
  317. $this->throwCommonException($errno, $error);
  318. $message = pht(
  319. '#%d: %s',
  320. $errno,
  321. $error);
  322. throw new AphrontQueryException($message, $errno);
  323. }
  324. /**
  325. * Force the next query to fail with a simulated error. This should be used
  326. * ONLY for unit tests.
  327. */
  328. public function simulateErrorOnNextQuery($error) {
  329. $this->nextError = $error;
  330. return $this;
  331. }
  332. /**
  333. * Check inserts for characters outside of the BMP. Even with the strictest
  334. * settings, MySQL will silently truncate data when it encounters these, which
  335. * can lead to data loss and security problems.
  336. */
  337. protected function validateUTF8String($string) {
  338. if (phutil_is_utf8($string)) {
  339. return;
  340. }
  341. throw new AphrontCharacterSetQueryException(
  342. pht(
  343. 'Attempting to construct a query using a non-utf8 string when '.
  344. 'utf8 is expected. Use the `%%B` conversion to escape binary '.
  345. 'strings data.'));
  346. }
  347. }