Differences Between: [Versions 311 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 and 403]
1 <?php 2 /** 3 * ADOdb Load Balancer 4 * 5 * ADOdbLoadBalancer is a class that allows the user to do read/write splitting 6 * and load balancing across multiple servers. It can handle and load balance 7 * any number of write capable (AKA: master) or readonly (AKA: slave) connections, 8 * including dealing with connection failures and retrying queries on a different 9 * connection instead. 10 * 11 * This file is part of ADOdb, a Database Abstraction Layer library for PHP. 12 * 13 * @package ADOdb 14 * @link https://adodb.org Project's web site and documentation 15 * @link https://github.com/ADOdb/ADOdb Source code and issue tracker 16 * 17 * The ADOdb Library is dual-licensed, released under both the BSD 3-Clause 18 * and the GNU Lesser General Public Licence (LGPL) v2.1 or, at your option, 19 * any later version. This means you can use it in proprietary products. 20 * See the LICENSE.md file distributed with this source code for details. 21 * @license BSD-3-Clause 22 * @license LGPL-2.1-or-later 23 * 24 * @copyright 2016 Mike Benoit and the ADOdb community 25 */ 26 27 /** 28 * Class ADOdbLoadBalancer 29 */ 30 class ADOdbLoadBalancer 31 { 32 /** 33 * @var bool Once a write or readonly connection is made, stick to that connection for the entire request. 34 */ 35 public $enable_sticky_sessions = true; 36 37 38 /** 39 * @var bool|array All connections to each database. 40 */ 41 protected $connections = false; 42 43 /** 44 * @var bool|array Just connections to the write capable database. 45 */ 46 protected $connections_write = false; 47 48 /** 49 * @var bool|array Just connections to the readonly database. 50 */ 51 protected $connections_readonly = false; 52 53 /** 54 * @var array Counts of all connections and their types. 55 */ 56 protected $total_connections = array('all' => 0, 'write' => 0, 'readonly' => 0); 57 58 /** 59 * @var array Weights of all connections for each type. 60 */ 61 protected $total_connection_weights = array('all' => 0, 'write' => 0, 'readonly' => 0); 62 63 /** 64 * @var bool When in transactions, always use this connection. 65 */ 66 protected $pinned_connection_id = false; 67 68 /** 69 * @var array Last connection_id for each database type. 70 */ 71 protected $last_connection_id = array('write' => false, 'readonly' => false, 'all' => false); 72 73 /** 74 * @var bool Session variables that must be maintained across all connections, ie: SET TIME ZONE. 75 */ 76 protected $session_variables = false; 77 78 /** 79 * @var bool Called immediately after connecting to any DB. 80 */ 81 protected $user_defined_session_init_sql = false; 82 83 84 /** 85 * Defines SQL queries that are executed each time a new database connection is established. 86 * 87 * @param $sql 88 * @return bool 89 */ 90 public function setSessionInitSQL($sql) 91 { 92 $this->user_defined_session_init_sql[] = $sql; 93 94 return true; 95 } 96 97 /** 98 * Adds a new database connection to the pool, but no actual connection is made until its needed. 99 * 100 * @param $obj 101 * @return bool 102 * @throws Exception 103 */ 104 public function addConnection($obj) 105 { 106 if ($obj instanceof ADOdbLoadBalancerConnection) { 107 $this->connections[] = $obj; 108 end($this->connections); 109 $i = key($this->connections); 110 111 $this->total_connections[$obj->type]++; 112 $this->total_connections['all']++; 113 114 $this->total_connection_weights[$obj->type] += abs($obj->weight); 115 $this->total_connection_weights['all'] += abs($obj->weight); 116 117 if ($obj->type == 'write') { 118 $this->connections_write[] = $i; 119 } else { 120 $this->connections_readonly[] = $i; 121 } 122 123 return true; 124 } 125 126 throw new Exception('Connection object is not an instance of ADOdbLoadBalancerConnection'); 127 } 128 129 /** 130 * Removes a database connection from the pool. 131 * 132 * @param $i 133 * @return bool 134 */ 135 public function removeConnection($i) 136 { 137 if (isset($this->connections[$i])) { 138 $obj = $this->connections[ $i ]; 139 140 $this->total_connections[ $obj->type ]--; 141 $this->total_connections['all']--; 142 143 $this->total_connection_weights[ $obj->type ] -= abs($obj->weight); 144 $this->total_connection_weights['all'] -= abs($obj->weight); 145 146 if ($obj->type == 'write') { 147 unset($this->connections_write[array_search($i, $this->connections_write)]); 148 // Reindex array. 149 $this->connections_write = array_values($this->connections_write); 150 } else { 151 unset($this->connections_readonly[array_search($i, $this->connections_readonly)]); 152 // Reindex array. 153 $this->connections_readonly = array_values($this->connections_readonly); 154 } 155 156 // Remove any sticky connections as well. 157 if ($this->last_connection_id[$obj->type] == $i) { 158 $this->last_connection_id[$obj->type] = false; 159 } 160 161 unset($this->connections[$i]); 162 163 return true; 164 } 165 166 return false; 167 } 168 169 /** 170 * Returns a database connection of the specified type. 171 * 172 * Takes into account the connection weight for load balancing. 173 * 174 * @param string $type Type of database connection, either: 'write' capable or 'readonly' 175 * @return bool|int|string 176 */ 177 private function getConnectionByWeight($type) 178 { 179 if ($type == 'readonly') { 180 $total_weight = $this->total_connection_weights['all']; 181 } else { 182 $total_weight = $this->total_connection_weights['write']; 183 } 184 185 $i = false; 186 if (is_array($this->connections)) { 187 $n = 0; 188 $num = mt_rand(0, $total_weight); 189 foreach ($this->connections as $i => $connection_obj) { 190 if ($connection_obj->weight > 0 && ($type == 'readonly' || $connection_obj->type == 'write')) { 191 $n += $connection_obj->weight; 192 if ($n >= $num) { 193 break; 194 } 195 } 196 } 197 } 198 199 return $i; 200 } 201 202 /** 203 * Returns the proper database connection when taking into account sticky sessions and load balancing. 204 * 205 * @param $type 206 * @return bool|int|mixed|string 207 */ 208 public function getLoadBalancedConnection($type) 209 { 210 if ($this->total_connections == 0) { 211 $connection_id = 0; 212 } else { 213 if ($this->enable_sticky_sessions == true && $this->last_connection_id[$type] !== false) { 214 $connection_id = $this->last_connection_id[$type]; 215 } else { 216 if ($type == 'write' && $this->total_connections['write'] == 1) { 217 $connection_id = $this->connections_write[0]; 218 } else { 219 $connection_id = $this->getConnectionByWeight($type); 220 } 221 } 222 } 223 224 return $connection_id; 225 } 226 227 /** 228 * Returns the ADODB connection object by connection_id. 229 * 230 * Ensures that it's connected and the session variables are executed. 231 * 232 * @param $connection_id 233 * @return bool|ADOConnection 234 * @throws Exception 235 */ 236 private function _getConnection($connection_id) 237 { 238 if (isset($this->connections[$connection_id])) { 239 $connection_obj = $this->connections[$connection_id]; 240 /** @var ADOConnection $adodb_obj */ 241 $adodb_obj = $connection_obj->getADOdbObject(); 242 if (is_object($adodb_obj) && $adodb_obj->_connectionID == false) { 243 try { 244 if ($connection_obj->persistent_connection == true) { 245 $adodb_obj->Pconnect( 246 $connection_obj->host, 247 $connection_obj->user, 248 $connection_obj->password, 249 $connection_obj->database 250 ); 251 } else { 252 $adodb_obj->Connect( 253 $connection_obj->host, 254 $connection_obj->user, 255 $connection_obj->password, 256 $connection_obj->database 257 ); 258 } 259 } catch (Exception $e) { 260 // Connection error, see if there are other connections to try still. 261 throw $e; // No connections left, reThrow exception so application can catch it. 262 } 263 264 if (is_array($this->user_defined_session_init_sql)) { 265 foreach ($this->user_defined_session_init_sql as $session_init_sql) { 266 $adodb_obj->Execute($session_init_sql); 267 } 268 } 269 $this->executeSessionVariables($adodb_obj); 270 } 271 272 return $adodb_obj; 273 } else { 274 throw new Exception('Unable to return Connection object...'); 275 } 276 } 277 278 /** 279 * Returns the ADODB connection object by database type. 280 * 281 * Ensures that it's connected and the session variables are executed. 282 * 283 * @param string $type 284 * @param null $pin_connection 285 * @return ADOConnection|bool 286 * @throws Exception 287 */ 288 public function getConnection($type = 'write', $pin_connection = null) 289 { 290 while (($type == 'write' && $this->total_connections['write'] > 0) 291 || ($type == 'readonly' && $this->total_connections['all'] > 0) 292 ) { 293 if ($this->pinned_connection_id !== false) { 294 $connection_id = $this->pinned_connection_id; 295 } else { 296 $connection_id = $this->getLoadBalancedConnection($type); 297 } 298 299 if ($connection_id !== false) { 300 try { 301 $adodb_obj = $this->_getConnection($connection_id); 302 // $connection_obj = $this->connections[$connection_id]; 303 break; 304 } catch (Exception $e) { 305 // Connection error, see if there are other connections to try still. 306 $this->removeConnection($connection_id); 307 if ( ($type == 'write' && $this->total_connections['write'] == 0) 308 || ($type == 'readonly' && $this->total_connections['all'] == 0) 309 ) { 310 throw $e; 311 } 312 } 313 } else { 314 throw new Exception('Connection ID is invalid!'); 315 } 316 } 317 318 $this->last_connection_id[$type] = $connection_id; 319 320 if ($pin_connection === true) { 321 $this->pinned_connection_id = $connection_id; 322 } elseif ($pin_connection === false && $adodb_obj->transOff <= 1) { 323 // UnPin connection only if we are 1 level deep in a transaction. 324 $this->pinned_connection_id = false; 325 326 // When unpinning connection, reset last_connection_id so readonly 327 // queries don't get stuck on the write capable connection. 328 $this->last_connection_id['write'] = false; 329 $this->last_connection_id['readonly'] = false; 330 } 331 332 return $adodb_obj; 333 } 334 335 /** 336 * This is a hack to work around pass by reference error. 337 * 338 * Parameter 1 to ADOConnection::GetInsertSQL() expected to be a reference, 339 * value given in adodb-loadbalancer.inc.php on line 83 340 * 341 * @param $arr 342 * @return array 343 */ 344 private function makeValuesReferenced($arr) 345 { 346 $refs = array(); 347 348 foreach ($arr as $key => $value) { 349 $refs[$key] = &$arr[$key]; 350 } 351 352 return $refs; 353 } 354 355 /** 356 * Allow setting session variables that are maintained across connections. 357 * 358 * Its important that these are set using name/value, so it can determine 359 * if the same variable is set multiple times causing bloat/clutter when 360 * new connections are established. For example if the time_zone is set to 361 * many different ones through the course of a single connection, a new 362 * connection should only set it to the most recent value. 363 * 364 * @param $name 365 * @param $value 366 * @param bool $execute_immediately 367 * @return array|bool|mixed 368 * @throws Exception 369 */ 370 public function setSessionVariable($name, $value, $execute_immediately = true) 371 { 372 $this->session_variables[$name] = $value; 373 374 if ($execute_immediately == true) { 375 return $this->executeSessionVariables(); 376 } else { 377 return true; 378 } 379 } 380 381 /** 382 * Executes the session variables on a given ADODB object. 383 * 384 * @param ADOConnection|bool $adodb_obj 385 * @return array|bool|mixed 386 * @throws Exception 387 */ 388 private function executeSessionVariables($adodb_obj = false) 389 { 390 if (is_array($this->session_variables)) { 391 $sql = ''; 392 foreach ($this->session_variables as $name => $value) { 393 // $sql .= 'SET SESSION '. $name .' '. $value; 394 // MySQL uses: SET SESSION foo_bar='foo' 395 // PGSQL uses: SET SESSION foo_bar 'foo' 396 // So leave it up to the user to pass the proper value with '=' if needed. 397 // This may be a candidate to move into ADOdb proper. 398 $sql .= 'SET SESSION ' . $name . ' ' . $value; 399 } 400 401 if ($adodb_obj !== false) { 402 return $adodb_obj->Execute($sql); 403 } else { 404 return $this->ClusterExecute($sql); 405 } 406 } 407 408 return false; 409 } 410 411 /** 412 * Executes the same SQL QUERY on the entire cluster of connections. 413 * Would be used for things like SET SESSION TIME ZONE calls and such. 414 * 415 * @param $sql 416 * @param bool $inputarr 417 * @param bool $return_all_results 418 * @param bool $existing_connections_only 419 * @return array|bool|mixed 420 * @throws Exception 421 */ 422 public function clusterExecute( 423 $sql, 424 $inputarr = false, 425 $return_all_results = false, 426 $existing_connections_only = true 427 ) { 428 if (is_array($this->connections) && count($this->connections) > 0) { 429 foreach ($this->connections as $key => $connection_obj) { 430 if ($existing_connections_only == false 431 || ($existing_connections_only == true 432 && $connection_obj->getADOdbObject()->_connectionID !== false 433 ) 434 ) { 435 $adodb_obj = $this->_getConnection($key); 436 if (is_object($adodb_obj)) { 437 $result_arr[] = $adodb_obj->Execute($sql, $inputarr); 438 } 439 } 440 } 441 442 if (isset($result_arr) && $return_all_results == true) { 443 return $result_arr; 444 } else { 445 // Loop through all results checking to see if they match, if they do return the first one 446 // otherwise return an array of all results. 447 if (isset($result_arr)) { 448 foreach ($result_arr as $result) { 449 if ($result == false) { 450 return $result_arr; 451 } 452 } 453 454 return $result_arr[0]; 455 } else { 456 // When using lazy connections, there are cases where 457 // setSessionVariable() is called early on, but there are 458 // no connections to execute the queries on yet. 459 // This captures that case and forces a RETURN TRUE to occur. 460 // As likely the queries will be executed as soon as a 461 // connection is established. 462 return true; 463 } 464 } 465 } 466 467 return false; 468 } 469 470 /** 471 * Determines if a SQL query is read-only or not. 472 * 473 * @param string $sql SQL Query to test. 474 * @return bool 475 */ 476 public function isReadOnlyQuery($sql) 477 { 478 if ( stripos($sql, 'SELECT') === 0 479 && stripos($sql, 'FOR UPDATE') === false 480 && stripos($sql, ' INTO ') === false 481 && stripos($sql, 'LOCK IN') === false 482 ) { 483 return true; 484 } 485 486 return false; 487 } 488 489 /** 490 * Use this instead of __call() as it significantly reduces the overhead of call_user_func_array(). 491 * 492 * @param $sql 493 * @param bool $inputarr 494 * @return array|bool|mixed 495 * @throws Exception 496 */ 497 public function execute($sql, $inputarr = false) 498 { 499 $type = 'write'; 500 $pin_connection = null; 501 502 // Prevent leading spaces from causing isReadOnlyQuery/stripos from failing. 503 $sql = trim($sql); 504 505 // SELECT queries that can write and therefore must be run on a write capable connection. 506 // SELECT ... FOR UPDATE; 507 // SELECT ... INTO ... 508 // SELECT .. LOCK IN ... (MYSQL) 509 if ($this->isReadOnlyQuery($sql) == true) { 510 $type = 'readonly'; 511 } elseif (stripos($sql, 'SET') === 0) { 512 // SET SQL statements should likely use setSessionVariable() instead, 513 // so state is properly maintained across connections, especially when they are lazily created. 514 return $this->ClusterExecute($sql, $inputarr); 515 } 516 517 $adodb_obj = $this->getConnection($type, $pin_connection); 518 if ($adodb_obj !== false) { 519 return $adodb_obj->Execute($sql, $inputarr); 520 } 521 522 return false; 523 } 524 525 /** 526 * Magic method to intercept method and callback to the proper ADODB object for write/readonly connections. 527 * 528 * @param string $method ADODB method to call. 529 * @param array $args Arguments to the ADODB method. 530 * @return bool|mixed 531 * @throws Exception 532 */ 533 public function __call($method, $args) 534 { 535 $type = 'write'; 536 $pin_connection = null; 537 538 // Intercept specific methods to determine if they are read-only or not. 539 $method = strtolower($method); 540 switch ($method) { 541 // case 'execute': // This is the direct overloaded function above instead. 542 case 'getone': 543 case 'getrow': 544 case 'getall': 545 case 'getcol': 546 case 'getassoc': 547 case 'selectlimit': 548 if ($this->isReadOnlyQuery(trim($args[0])) == true) { 549 $type = 'readonly'; 550 } 551 break; 552 case 'cachegetone': 553 case 'cachegetrow': 554 case 'cachegetall': 555 case 'cachegetcol': 556 case 'cachegetassoc': 557 case 'cacheexecute': 558 case 'cacheselect': 559 case 'pageexecute': 560 case 'cachepageexecute': 561 $type = 'readonly'; 562 break; 563 // case 'ignoreerrors': 564 // // When ignoreerrors is called, PIN to the connection until its called again. 565 // if (!isset($args[0]) || (isset($args[0]) && $args[0] == FALSE)) { 566 // $pin_connection = TRUE; 567 // } else { 568 // $pin_connection = FALSE; 569 // } 570 // break; 571 572 // Manual transactions 573 case 'begintrans': 574 case 'settransactionmode': 575 $pin_connection = true; 576 break; 577 case 'rollbacktrans': 578 case 'committrans': 579 $pin_connection = false; 580 break; 581 // Smart transactions 582 case 'starttrans': 583 $pin_connection = true; 584 break; 585 case 'completetrans': 586 case 'failtrans': 587 // getConnection() will only unpin the transaction if we're exiting the last nested transaction 588 $pin_connection = false; 589 break; 590 591 // Functions that don't require any connection and therefore 592 // shouldn't force a connection be established before they run. 593 case 'qstr': 594 case 'escape': 595 case 'binddate': 596 case 'bindtimestamp': 597 case 'setfetchmode': 598 $type = false; // No connection necessary. 599 break; 600 601 // Default to assuming write connection is required to be on the safe side. 602 default: 603 break; 604 } 605 606 if ($type === false) { 607 if (is_array($this->connections) && count($this->connections) > 0) { 608 foreach ($this->connections as $key => $connection_obj) { 609 $adodb_obj = $connection_obj->getADOdbObject(); 610 return call_user_func_array(array($adodb_obj, $method), $this->makeValuesReferenced($args)); // Just makes the function call on the first object. 611 } 612 } 613 } else { 614 $adodb_obj = $this->getConnection($type, $pin_connection); 615 if (is_object($adodb_obj)) { 616 $result = call_user_func_array(array($adodb_obj, $method), $this->makeValuesReferenced($args)); 617 618 return $result; 619 } 620 } 621 return false; 622 } 623 624 /** 625 * Magic method to proxy property getter calls back to the proper ADODB object currently in use. 626 * 627 * @param $property 628 * @return mixed 629 * @throws Exception 630 */ 631 public function __get($property) 632 { 633 if (is_array($this->connections) && count($this->connections) > 0) { 634 foreach ($this->connections as $key => $connection_obj) { 635 // Just returns the property from the first object. 636 return $connection_obj->getADOdbObject()->$property; 637 } 638 } 639 640 return false; 641 } 642 643 /** 644 * Magic method to proxy property setter calls back to the proper ADODB object currently in use. 645 * 646 * @param $property 647 * @param $value 648 * @return mixed 649 * @throws Exception 650 */ 651 public function __set($property, $value) 652 { 653 // Special function to set object properties on all objects 654 // without initiating a connection to the database. 655 if (is_array($this->connections) && count($this->connections) > 0) { 656 foreach ($this->connections as $key => $connection_obj) { 657 $connection_obj->getADOdbObject()->$property = $value; 658 } 659 660 return true; 661 } 662 663 return false; 664 } 665 666 /** 667 * Override the __clone() magic method. 668 */ 669 private function __clone() 670 { 671 } 672 } 673 674 /** 675 * Class ADOdbLoadBalancerConnection 676 */ 677 class ADOdbLoadBalancerConnection 678 { 679 /** 680 * @var bool ADOdb drive name. 681 */ 682 protected $driver = false; 683 684 /** 685 * @var bool ADODB object. 686 */ 687 protected $adodb_obj = false; 688 689 /** 690 * @var string Type of connection, either 'write' capable or 'readonly' 691 */ 692 public $type = 'write'; 693 694 /** 695 * @var int Weight of connection, lower receives less queries, higher receives more queries. 696 */ 697 public $weight = 1; 698 699 /** 700 * @var bool Determines if the connection persistent. 701 */ 702 public $persistent_connection = false; 703 704 /** 705 * @var string Database connection host 706 */ 707 public $host = ''; 708 709 /** 710 * @var string Database connection user 711 */ 712 public $user = ''; 713 714 /** 715 * @var string Database connection password 716 */ 717 public $password = ''; 718 719 /** 720 * @var string Database connection database name 721 */ 722 public $database = ''; 723 724 /** 725 * ADOdbLoadBalancerConnection constructor to setup the ADODB object. 726 * 727 * @param $driver 728 * @param string $type 729 * @param int $weight 730 * @param bool $persistent_connection 731 * @param string $argHostname 732 * @param string $argUsername 733 * @param string $argPassword 734 * @param string $argDatabaseName 735 */ 736 public function __construct( 737 $driver, 738 $type = 'write', 739 $weight = 1, 740 $persistent_connection = false, 741 $argHostname = '', 742 $argUsername = '', 743 $argPassword = '', 744 $argDatabaseName = '' 745 ) { 746 if ($type !== 'write' && $type !== 'readonly') { 747 return false; 748 } 749 750 $this->adodb_obj = ADONewConnection($driver); 751 752 $this->type = $type; 753 $this->weight = $weight; 754 $this->persistent_connection = $persistent_connection; 755 756 $this->host = $argHostname; 757 $this->user = $argUsername; 758 $this->password = $argPassword; 759 $this->database = $argDatabaseName; 760 761 return true; 762 } 763 764 /** 765 * Returns the ADODB object for this connection. 766 * 767 * @return bool 768 */ 769 public function getADOdbObject() 770 { 771 return $this->adodb_obj; 772 } 773 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body