@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
at recaptime-dev/main 744 lines 20 kB view raw
1<?php 2 3final class PhabricatorDatabaseRef 4 extends Phobject { 5 6 const STATUS_OKAY = 'okay'; 7 const STATUS_FAIL = 'fail'; 8 const STATUS_AUTH = 'auth'; 9 const STATUS_REPLICATION_CLIENT = 'replication-client'; 10 11 const REPLICATION_OKAY = 'okay'; 12 const REPLICATION_MASTER_REPLICA = 'master-replica'; 13 const REPLICATION_REPLICA_NONE = 'replica-none'; 14 const REPLICATION_SLOW = 'replica-slow'; 15 const REPLICATION_NOT_REPLICATING = 'not-replicating'; 16 17 const KEY_HEALTH = 'cluster.db.health'; 18 const KEY_REFS = 'cluster.db.refs'; 19 const KEY_INDIVIDUAL = 'cluster.db.individual'; 20 21 private $host; 22 private $port; 23 private $user; 24 private $pass; 25 private $disabled; 26 private $isMaster; 27 private $isIndividual; 28 29 private $connectionLatency; 30 private $connectionStatus; 31 private $connectionMessage; 32 private $connectionException; 33 34 private $replicaStatus; 35 private $replicaMessage; 36 private $replicaDelay; 37 38 private $healthRecord; 39 private $didFailToConnect; 40 41 private $isDefaultPartition; 42 private $applicationMap = array(); 43 private $masterRef; 44 private $replicaRefs = array(); 45 private $usePersistentConnections; 46 47 public function setHost($host) { 48 $this->host = $host; 49 return $this; 50 } 51 52 public function getHost() { 53 return $this->host; 54 } 55 56 public function setPort($port) { 57 $this->port = $port; 58 return $this; 59 } 60 61 public function getPort() { 62 return $this->port; 63 } 64 65 public function setUser($user) { 66 $this->user = $user; 67 return $this; 68 } 69 70 public function getUser() { 71 return $this->user; 72 } 73 74 public function setPass(PhutilOpaqueEnvelope $pass) { 75 $this->pass = $pass; 76 return $this; 77 } 78 79 public function getPass() { 80 return $this->pass; 81 } 82 83 public function setIsMaster($is_master) { 84 $this->isMaster = $is_master; 85 return $this; 86 } 87 88 public function getIsMaster() { 89 return $this->isMaster; 90 } 91 92 public function setDisabled($disabled) { 93 $this->disabled = $disabled; 94 return $this; 95 } 96 97 public function getDisabled() { 98 return $this->disabled; 99 } 100 101 public function setConnectionLatency($connection_latency) { 102 $this->connectionLatency = $connection_latency; 103 return $this; 104 } 105 106 public function getConnectionLatency() { 107 return $this->connectionLatency; 108 } 109 110 public function setConnectionStatus($connection_status) { 111 $this->connectionStatus = $connection_status; 112 return $this; 113 } 114 115 public function getConnectionStatus() { 116 if ($this->connectionStatus === null) { 117 throw new PhutilInvalidStateException('queryAll'); 118 } 119 120 return $this->connectionStatus; 121 } 122 123 public function setConnectionMessage($connection_message) { 124 $this->connectionMessage = $connection_message; 125 return $this; 126 } 127 128 public function getConnectionMessage() { 129 return $this->connectionMessage; 130 } 131 132 public function setReplicaStatus($replica_status) { 133 $this->replicaStatus = $replica_status; 134 return $this; 135 } 136 137 public function getReplicaStatus() { 138 return $this->replicaStatus; 139 } 140 141 public function setReplicaMessage($replica_message) { 142 $this->replicaMessage = $replica_message; 143 return $this; 144 } 145 146 public function getReplicaMessage() { 147 return $this->replicaMessage; 148 } 149 150 public function setReplicaDelay($replica_delay) { 151 $this->replicaDelay = $replica_delay; 152 return $this; 153 } 154 155 public function getReplicaDelay() { 156 return $this->replicaDelay; 157 } 158 159 public function setIsIndividual($is_individual) { 160 $this->isIndividual = $is_individual; 161 return $this; 162 } 163 164 public function getIsIndividual() { 165 return $this->isIndividual; 166 } 167 168 public function setIsDefaultPartition($is_default_partition) { 169 $this->isDefaultPartition = $is_default_partition; 170 return $this; 171 } 172 173 public function getIsDefaultPartition() { 174 return $this->isDefaultPartition; 175 } 176 177 public function setUsePersistentConnections($use_persistent_connections) { 178 $this->usePersistentConnections = $use_persistent_connections; 179 return $this; 180 } 181 182 public function getUsePersistentConnections() { 183 return $this->usePersistentConnections; 184 } 185 186 public function setApplicationMap(array $application_map) { 187 $this->applicationMap = $application_map; 188 return $this; 189 } 190 191 public function getApplicationMap() { 192 return $this->applicationMap; 193 } 194 195 public function getPartitionStateForCommit() { 196 $state = PhabricatorEnv::getEnvConfig('cluster.databases'); 197 foreach ($state as $key => $value) { 198 // Don't store passwords, since we don't care if they differ and 199 // users may find it surprising. 200 unset($state[$key]['pass']); 201 } 202 203 return phutil_json_encode($state); 204 } 205 206 public function setMasterRef(PhabricatorDatabaseRef $master_ref) { 207 $this->masterRef = $master_ref; 208 return $this; 209 } 210 211 public function getMasterRef() { 212 return $this->masterRef; 213 } 214 215 public function addReplicaRef(PhabricatorDatabaseRef $replica_ref) { 216 $this->replicaRefs[] = $replica_ref; 217 return $this; 218 } 219 220 public function getReplicaRefs() { 221 return $this->replicaRefs; 222 } 223 224 public function getDisplayName() { 225 return $this->getRefKey(); 226 } 227 228 public function getRefKey() { 229 $host = $this->getHost(); 230 231 $port = $this->getPort(); 232 if ($port) { 233 return "{$host}:{$port}"; 234 } 235 236 return $host; 237 } 238 239 public static function getConnectionStatusMap() { 240 return array( 241 self::STATUS_OKAY => array( 242 'icon' => 'fa-exchange', 243 'color' => 'green', 244 'label' => pht('Okay'), 245 ), 246 self::STATUS_FAIL => array( 247 'icon' => 'fa-times', 248 'color' => 'red', 249 'label' => pht('Failed'), 250 ), 251 self::STATUS_AUTH => array( 252 'icon' => 'fa-key', 253 'color' => 'red', 254 'label' => pht('Invalid Credentials'), 255 ), 256 self::STATUS_REPLICATION_CLIENT => array( 257 'icon' => 'fa-eye-slash', 258 'color' => 'yellow', 259 'label' => pht('Missing Permission'), 260 ), 261 ); 262 } 263 264 public static function getReplicaStatusMap() { 265 return array( 266 self::REPLICATION_OKAY => array( 267 'icon' => 'fa-download', 268 'color' => 'green', 269 'label' => pht('Okay'), 270 ), 271 self::REPLICATION_MASTER_REPLICA => array( 272 'icon' => 'fa-database', 273 'color' => 'red', 274 'label' => pht('Replicating Master'), 275 ), 276 self::REPLICATION_REPLICA_NONE => array( 277 'icon' => 'fa-download', 278 'color' => 'red', 279 'label' => pht('Not A Replica'), 280 ), 281 self::REPLICATION_SLOW => array( 282 'icon' => 'fa-hourglass', 283 'color' => 'red', 284 'label' => pht('Slow Replication'), 285 ), 286 self::REPLICATION_NOT_REPLICATING => array( 287 'icon' => 'fa-exclamation-triangle', 288 'color' => 'red', 289 'label' => pht('Not Replicating'), 290 ), 291 ); 292 } 293 294 public static function getClusterRefs() { 295 $cache = PhabricatorCaches::getRequestCache(); 296 297 $refs = $cache->getKey(self::KEY_REFS); 298 if (!$refs) { 299 $refs = self::newRefs(); 300 $cache->setKey(self::KEY_REFS, $refs); 301 } 302 303 return $refs; 304 } 305 306 public static function getLiveIndividualRef() { 307 $cache = PhabricatorCaches::getRequestCache(); 308 309 $ref = $cache->getKey(self::KEY_INDIVIDUAL); 310 if (!$ref) { 311 $ref = self::newIndividualRef(); 312 $cache->setKey(self::KEY_INDIVIDUAL, $ref); 313 } 314 315 return $ref; 316 } 317 318 public static function newRefs() { 319 $default_port = PhabricatorEnv::getEnvConfig('mysql.port'); 320 $default_port = nonempty($default_port, 3306); 321 322 $default_user = PhabricatorEnv::getEnvConfig('mysql.user'); 323 324 $default_pass = PhabricatorEnv::getEnvConfig('mysql.pass'); 325 $default_pass = phutil_string_cast($default_pass); 326 $default_pass = new PhutilOpaqueEnvelope($default_pass); 327 328 $config = PhabricatorEnv::getEnvConfig('cluster.databases'); 329 330 return id(new PhabricatorDatabaseRefParser()) 331 ->setDefaultPort($default_port) 332 ->setDefaultUser($default_user) 333 ->setDefaultPass($default_pass) 334 ->newRefs($config); 335 } 336 337 public static function queryAll() { 338 $refs = self::getActiveDatabaseRefs(); 339 return self::queryRefs($refs); 340 } 341 342 private static function queryRefs(array $refs) { 343 foreach ($refs as $ref) { 344 $conn = $ref->newManagementConnection(); 345 346 $t_start = microtime(true); 347 $replica_status = false; 348 try { 349 $replica_status = queryfx_one($conn, 'SHOW REPLICA STATUS'); 350 $ref->setConnectionStatus(self::STATUS_OKAY); 351 } catch (AphrontAccessDeniedQueryException $ex) { 352 $ref->setConnectionStatus(self::STATUS_REPLICATION_CLIENT); 353 $ref->setConnectionMessage( 354 pht( 355 'No permission to run "SHOW REPLICA STATUS". Grant this user '. 356 '"REPLICATION CLIENT" permission to allow this server to '. 357 'monitor replica health.')); 358 } catch (AphrontInvalidCredentialsQueryException $ex) { 359 $ref->setConnectionStatus(self::STATUS_AUTH); 360 $ref->setConnectionMessage($ex->getMessage()); 361 } catch (AphrontQueryException $ex) { 362 $ref->setConnectionStatus(self::STATUS_FAIL); 363 364 $class = get_class($ex); 365 $message = $ex->getMessage(); 366 $ref->setConnectionMessage( 367 pht( 368 '%s: %s', 369 get_class($ex), 370 $ex->getMessage())); 371 } 372 $t_end = microtime(true); 373 $ref->setConnectionLatency($t_end - $t_start); 374 375 if ($replica_status !== false) { 376 $is_replica = (bool)$replica_status; 377 if ($ref->getIsMaster() && $is_replica) { 378 $ref->setReplicaStatus(self::REPLICATION_MASTER_REPLICA); 379 $ref->setReplicaMessage( 380 pht( 381 'This host has a "master" role, but is replicating data from '. 382 'another host ("%s")!', 383 idx($replica_status, 'Master_Host'))); 384 } else if (!$ref->getIsMaster() && !$is_replica) { 385 $ref->setReplicaStatus(self::REPLICATION_REPLICA_NONE); 386 $ref->setReplicaMessage( 387 pht( 388 'This host has a "replica" role, but is not replicating data '. 389 'from a master (no output from "SHOW REPLICA STATUS").')); 390 } else { 391 $ref->setReplicaStatus(self::REPLICATION_OKAY); 392 } 393 394 if ($is_replica) { 395 $latency = idx($replica_status, 'Seconds_Behind_Master'); 396 if (!phutil_nonempty_string($latency)) { 397 $ref->setReplicaStatus(self::REPLICATION_NOT_REPLICATING); 398 } else { 399 $latency = (int)$latency; 400 $ref->setReplicaDelay($latency); 401 if ($latency > 30) { 402 $ref->setReplicaStatus(self::REPLICATION_SLOW); 403 $ref->setReplicaMessage( 404 pht( 405 'This replica is lagging far behind the master. Data is at '. 406 'risk!')); 407 } 408 } 409 } 410 } 411 } 412 413 return $refs; 414 } 415 416 public function newManagementConnection() { 417 return $this->newConnection( 418 array( 419 'retries' => 0, 420 'timeout' => 2, 421 )); 422 } 423 424 public function newApplicationConnection($database) { 425 return $this->newConnection( 426 array( 427 'database' => $database, 428 )); 429 } 430 431 public function isSevered() { 432 // If we only have an individual database, never sever our connection to 433 // it, at least for now. It's possible that using the same severing rules 434 // might eventually make sense to help alleviate load-related failures, 435 // but we should wait for all the cluster stuff to stabilize first. 436 if ($this->getIsIndividual()) { 437 return false; 438 } 439 440 if ($this->didFailToConnect) { 441 return true; 442 } 443 444 $record = $this->getHealthRecord(); 445 $is_healthy = $record->getIsHealthy(); 446 if (!$is_healthy) { 447 return true; 448 } 449 450 return false; 451 } 452 453 public function isReachable(AphrontDatabaseConnection $connection) { 454 $record = $this->getHealthRecord(); 455 $should_check = $record->getShouldCheck(); 456 457 if ($this->isSevered() && !$should_check) { 458 return false; 459 } 460 461 $this->connectionException = null; 462 try { 463 $connection->openConnection(); 464 $reachable = true; 465 } catch (AphrontSchemaQueryException $ex) { 466 // We get one of these if the database we're trying to select does not 467 // exist. In this case, just re-throw the exception. This is expected 468 // during first-time setup, when databases like "config" will not exist 469 // yet. 470 throw $ex; 471 } catch (Exception $ex) { 472 $this->connectionException = $ex; 473 $reachable = false; 474 } 475 476 if ($should_check) { 477 $record->didHealthCheck($reachable); 478 } 479 480 if (!$reachable) { 481 $this->didFailToConnect = true; 482 } 483 484 return $reachable; 485 } 486 487 public function checkHealth() { 488 $health = $this->getHealthRecord(); 489 490 $should_check = $health->getShouldCheck(); 491 if ($should_check) { 492 // This does an implicit health update. 493 $connection = $this->newManagementConnection(); 494 $this->isReachable($connection); 495 } 496 497 return $this; 498 } 499 500 private function getHealthRecordCacheKey() { 501 $host = $this->getHost(); 502 $port = $this->getPort(); 503 $key = self::KEY_HEALTH; 504 505 return "{$key}({$host}, {$port})"; 506 } 507 508 public function getHealthRecord() { 509 if (!$this->healthRecord) { 510 $this->healthRecord = new PhabricatorClusterServiceHealthRecord( 511 $this->getHealthRecordCacheKey()); 512 } 513 return $this->healthRecord; 514 } 515 516 public function getConnectionException() { 517 return $this->connectionException; 518 } 519 520 public static function getActiveDatabaseRefs() { 521 $refs = array(); 522 523 foreach (self::getMasterDatabaseRefs() as $ref) { 524 $refs[] = $ref; 525 } 526 527 foreach (self::getReplicaDatabaseRefs() as $ref) { 528 $refs[] = $ref; 529 } 530 531 return $refs; 532 } 533 534 public static function getAllMasterDatabaseRefs() { 535 $refs = self::getClusterRefs(); 536 537 if (!$refs) { 538 return array(self::getLiveIndividualRef()); 539 } 540 541 $masters = array(); 542 foreach ($refs as $ref) { 543 if ($ref->getIsMaster()) { 544 $masters[] = $ref; 545 } 546 } 547 548 return $masters; 549 } 550 551 public static function getMasterDatabaseRefs() { 552 $refs = self::getAllMasterDatabaseRefs(); 553 return self::getEnabledRefs($refs); 554 } 555 556 public function isApplicationHost($database) { 557 return isset($this->applicationMap[$database]); 558 } 559 560 public function loadRawMySQLConfigValue($key) { 561 $conn = $this->newManagementConnection(); 562 563 try { 564 $value = queryfx_one($conn, 'SELECT @@%C', $key); 565 566 // NOTE: Although MySQL allows us to escape configuration values as if 567 // they are column names, the escaping is included in the column name 568 // of the return value: if we select "@@`x`", we get back a column named 569 // "@@`x`", not "@@x" as we might expect. 570 $value = head($value); 571 572 } catch (AphrontQueryException $ex) { 573 $value = null; 574 } 575 576 return $value; 577 } 578 579 public static function getMasterDatabaseRefForApplication($application) { 580 $masters = self::getMasterDatabaseRefs(); 581 582 $application_master = null; 583 $default_master = null; 584 foreach ($masters as $master) { 585 if ($master->isApplicationHost($application)) { 586 $application_master = $master; 587 break; 588 } 589 if ($master->getIsDefaultPartition()) { 590 $default_master = $master; 591 } 592 } 593 594 if ($application_master) { 595 $masters = array($application_master); 596 } else if ($default_master) { 597 $masters = array($default_master); 598 } else { 599 $masters = array(); 600 } 601 602 $masters = self::getEnabledRefs($masters); 603 $master = head($masters); 604 605 return $master; 606 } 607 608 public static function newIndividualRef() { 609 $default_user = PhabricatorEnv::getEnvConfig('mysql.user'); 610 $default_pass = new PhutilOpaqueEnvelope( 611 PhabricatorEnv::getEnvConfig('mysql.pass')); 612 $default_host = PhabricatorEnv::getEnvConfig('mysql.host'); 613 $default_port = PhabricatorEnv::getEnvConfig('mysql.port'); 614 615 return id(new self()) 616 ->setUser($default_user) 617 ->setPass($default_pass) 618 ->setHost($default_host) 619 ->setPort($default_port) 620 ->setIsIndividual(true) 621 ->setIsMaster(true) 622 ->setIsDefaultPartition(true) 623 ->setUsePersistentConnections(false); 624 } 625 626 public static function getAllReplicaDatabaseRefs() { 627 $refs = self::getClusterRefs(); 628 629 if (!$refs) { 630 return array(); 631 } 632 633 $replicas = array(); 634 foreach ($refs as $ref) { 635 if ($ref->getIsMaster()) { 636 continue; 637 } 638 639 $replicas[] = $ref; 640 } 641 642 return $replicas; 643 } 644 645 public static function getReplicaDatabaseRefs() { 646 $refs = self::getAllReplicaDatabaseRefs(); 647 return self::getEnabledRefs($refs); 648 } 649 650 private static function getEnabledRefs(array $refs) { 651 foreach ($refs as $key => $ref) { 652 if ($ref->getDisabled()) { 653 unset($refs[$key]); 654 } 655 } 656 return $refs; 657 } 658 659 public static function getReplicaDatabaseRefForApplication($application) { 660 $replicas = self::getReplicaDatabaseRefs(); 661 662 $application_replicas = array(); 663 $default_replicas = array(); 664 foreach ($replicas as $replica) { 665 $master = $replica->getMasterRef(); 666 667 if ($master->isApplicationHost($application)) { 668 $application_replicas[] = $replica; 669 } 670 671 if ($master->getIsDefaultPartition()) { 672 $default_replicas[] = $replica; 673 } 674 } 675 676 if ($application_replicas) { 677 $replicas = $application_replicas; 678 } else { 679 $replicas = $default_replicas; 680 } 681 682 $replicas = self::getEnabledRefs($replicas); 683 684 // TODO: We may have multiple replicas to choose from, and could make 685 // more of an effort to pick the "best" one here instead of always 686 // picking the first one. Once we've picked one, we should try to use 687 // the same replica for the rest of the request, though. 688 689 return head($replicas); 690 } 691 692 private function newConnection(array $options) { 693 // If we believe the database is unhealthy, don't spend as much time 694 // trying to connect to it, since it's likely to continue to fail and 695 // hammering it can only make the problem worse. 696 $record = $this->getHealthRecord(); 697 if ($record->getIsHealthy()) { 698 $default_retries = 3; 699 $default_timeout = 10; 700 } else { 701 $default_retries = 0; 702 $default_timeout = 2; 703 } 704 705 $spec = $options + array( 706 'user' => $this->getUser(), 707 'pass' => $this->getPass(), 708 'host' => $this->getHost(), 709 'port' => $this->getPort(), 710 'database' => null, 711 'retries' => $default_retries, 712 'timeout' => $default_timeout, 713 'persistent' => $this->getUsePersistentConnections(), 714 ); 715 716 $is_cli = (php_sapi_name() == 'cli'); 717 718 $use_persistent = false; 719 if (!empty($spec['persistent']) && !$is_cli) { 720 $use_persistent = true; 721 } 722 unset($spec['persistent']); 723 724 $connection = self::newRawConnection($spec); 725 726 // If configured, use persistent connections. See T11672 for details. 727 if ($use_persistent) { 728 $connection->setPersistent($use_persistent); 729 } 730 731 // Unless this is a script running from the CLI, prevent any query from 732 // running for more than 30 seconds. See T10849 for details. 733 if (!$is_cli) { 734 $connection->setQueryTimeout(30); 735 } 736 737 return $connection; 738 } 739 740 public static function newRawConnection(array $options) { 741 return new AphrontMySQLiDatabaseConnection($options); 742 } 743 744}