Path: blob/master/src/infrastructure/cluster/PhabricatorDatabaseRef.php
12241 views
<?php12final class PhabricatorDatabaseRef3extends Phobject {45const STATUS_OKAY = 'okay';6const STATUS_FAIL = 'fail';7const STATUS_AUTH = 'auth';8const STATUS_REPLICATION_CLIENT = 'replication-client';910const REPLICATION_OKAY = 'okay';11const REPLICATION_MASTER_REPLICA = 'master-replica';12const REPLICATION_REPLICA_NONE = 'replica-none';13const REPLICATION_SLOW = 'replica-slow';14const REPLICATION_NOT_REPLICATING = 'not-replicating';1516const KEY_HEALTH = 'cluster.db.health';17const KEY_REFS = 'cluster.db.refs';18const KEY_INDIVIDUAL = 'cluster.db.individual';1920private $host;21private $port;22private $user;23private $pass;24private $disabled;25private $isMaster;26private $isIndividual;2728private $connectionLatency;29private $connectionStatus;30private $connectionMessage;31private $connectionException;3233private $replicaStatus;34private $replicaMessage;35private $replicaDelay;3637private $healthRecord;38private $didFailToConnect;3940private $isDefaultPartition;41private $applicationMap = array();42private $masterRef;43private $replicaRefs = array();44private $usePersistentConnections;4546public function setHost($host) {47$this->host = $host;48return $this;49}5051public function getHost() {52return $this->host;53}5455public function setPort($port) {56$this->port = $port;57return $this;58}5960public function getPort() {61return $this->port;62}6364public function setUser($user) {65$this->user = $user;66return $this;67}6869public function getUser() {70return $this->user;71}7273public function setPass(PhutilOpaqueEnvelope $pass) {74$this->pass = $pass;75return $this;76}7778public function getPass() {79return $this->pass;80}8182public function setIsMaster($is_master) {83$this->isMaster = $is_master;84return $this;85}8687public function getIsMaster() {88return $this->isMaster;89}9091public function setDisabled($disabled) {92$this->disabled = $disabled;93return $this;94}9596public function getDisabled() {97return $this->disabled;98}99100public function setConnectionLatency($connection_latency) {101$this->connectionLatency = $connection_latency;102return $this;103}104105public function getConnectionLatency() {106return $this->connectionLatency;107}108109public function setConnectionStatus($connection_status) {110$this->connectionStatus = $connection_status;111return $this;112}113114public function getConnectionStatus() {115if ($this->connectionStatus === null) {116throw new PhutilInvalidStateException('queryAll');117}118119return $this->connectionStatus;120}121122public function setConnectionMessage($connection_message) {123$this->connectionMessage = $connection_message;124return $this;125}126127public function getConnectionMessage() {128return $this->connectionMessage;129}130131public function setReplicaStatus($replica_status) {132$this->replicaStatus = $replica_status;133return $this;134}135136public function getReplicaStatus() {137return $this->replicaStatus;138}139140public function setReplicaMessage($replica_message) {141$this->replicaMessage = $replica_message;142return $this;143}144145public function getReplicaMessage() {146return $this->replicaMessage;147}148149public function setReplicaDelay($replica_delay) {150$this->replicaDelay = $replica_delay;151return $this;152}153154public function getReplicaDelay() {155return $this->replicaDelay;156}157158public function setIsIndividual($is_individual) {159$this->isIndividual = $is_individual;160return $this;161}162163public function getIsIndividual() {164return $this->isIndividual;165}166167public function setIsDefaultPartition($is_default_partition) {168$this->isDefaultPartition = $is_default_partition;169return $this;170}171172public function getIsDefaultPartition() {173return $this->isDefaultPartition;174}175176public function setUsePersistentConnections($use_persistent_connections) {177$this->usePersistentConnections = $use_persistent_connections;178return $this;179}180181public function getUsePersistentConnections() {182return $this->usePersistentConnections;183}184185public function setApplicationMap(array $application_map) {186$this->applicationMap = $application_map;187return $this;188}189190public function getApplicationMap() {191return $this->applicationMap;192}193194public function getPartitionStateForCommit() {195$state = PhabricatorEnv::getEnvConfig('cluster.databases');196foreach ($state as $key => $value) {197// Don't store passwords, since we don't care if they differ and198// users may find it surprising.199unset($state[$key]['pass']);200}201202return phutil_json_encode($state);203}204205public function setMasterRef(PhabricatorDatabaseRef $master_ref) {206$this->masterRef = $master_ref;207return $this;208}209210public function getMasterRef() {211return $this->masterRef;212}213214public function addReplicaRef(PhabricatorDatabaseRef $replica_ref) {215$this->replicaRefs[] = $replica_ref;216return $this;217}218219public function getReplicaRefs() {220return $this->replicaRefs;221}222223public function getDisplayName() {224return $this->getRefKey();225}226227public function getRefKey() {228$host = $this->getHost();229230$port = $this->getPort();231if ($port !== null && strlen($port)) {232return "{$host}:{$port}";233}234235return $host;236}237238public static function getConnectionStatusMap() {239return array(240self::STATUS_OKAY => array(241'icon' => 'fa-exchange',242'color' => 'green',243'label' => pht('Okay'),244),245self::STATUS_FAIL => array(246'icon' => 'fa-times',247'color' => 'red',248'label' => pht('Failed'),249),250self::STATUS_AUTH => array(251'icon' => 'fa-key',252'color' => 'red',253'label' => pht('Invalid Credentials'),254),255self::STATUS_REPLICATION_CLIENT => array(256'icon' => 'fa-eye-slash',257'color' => 'yellow',258'label' => pht('Missing Permission'),259),260);261}262263public static function getReplicaStatusMap() {264return array(265self::REPLICATION_OKAY => array(266'icon' => 'fa-download',267'color' => 'green',268'label' => pht('Okay'),269),270self::REPLICATION_MASTER_REPLICA => array(271'icon' => 'fa-database',272'color' => 'red',273'label' => pht('Replicating Master'),274),275self::REPLICATION_REPLICA_NONE => array(276'icon' => 'fa-download',277'color' => 'red',278'label' => pht('Not A Replica'),279),280self::REPLICATION_SLOW => array(281'icon' => 'fa-hourglass',282'color' => 'red',283'label' => pht('Slow Replication'),284),285self::REPLICATION_NOT_REPLICATING => array(286'icon' => 'fa-exclamation-triangle',287'color' => 'red',288'label' => pht('Not Replicating'),289),290);291}292293public static function getClusterRefs() {294$cache = PhabricatorCaches::getRequestCache();295296$refs = $cache->getKey(self::KEY_REFS);297if (!$refs) {298$refs = self::newRefs();299$cache->setKey(self::KEY_REFS, $refs);300}301302return $refs;303}304305public static function getLiveIndividualRef() {306$cache = PhabricatorCaches::getRequestCache();307308$ref = $cache->getKey(self::KEY_INDIVIDUAL);309if (!$ref) {310$ref = self::newIndividualRef();311$cache->setKey(self::KEY_INDIVIDUAL, $ref);312}313314return $ref;315}316317public static function newRefs() {318$default_port = PhabricatorEnv::getEnvConfig('mysql.port');319$default_port = nonempty($default_port, 3306);320321$default_user = PhabricatorEnv::getEnvConfig('mysql.user');322323$default_pass = PhabricatorEnv::getEnvConfig('mysql.pass');324$default_pass = phutil_string_cast($default_pass);325$default_pass = new PhutilOpaqueEnvelope($default_pass);326327$config = PhabricatorEnv::getEnvConfig('cluster.databases');328329return id(new PhabricatorDatabaseRefParser())330->setDefaultPort($default_port)331->setDefaultUser($default_user)332->setDefaultPass($default_pass)333->newRefs($config);334}335336public static function queryAll() {337$refs = self::getActiveDatabaseRefs();338return self::queryRefs($refs);339}340341private static function queryRefs(array $refs) {342foreach ($refs as $ref) {343$conn = $ref->newManagementConnection();344345$t_start = microtime(true);346$replica_status = false;347try {348$replica_status = queryfx_one($conn, 'SHOW SLAVE STATUS');349$ref->setConnectionStatus(self::STATUS_OKAY);350} catch (AphrontAccessDeniedQueryException $ex) {351$ref->setConnectionStatus(self::STATUS_REPLICATION_CLIENT);352$ref->setConnectionMessage(353pht(354'No permission to run "SHOW SLAVE STATUS". Grant this user '.355'"REPLICATION CLIENT" permission to allow this server to '.356'monitor replica health.'));357} catch (AphrontInvalidCredentialsQueryException $ex) {358$ref->setConnectionStatus(self::STATUS_AUTH);359$ref->setConnectionMessage($ex->getMessage());360} catch (AphrontQueryException $ex) {361$ref->setConnectionStatus(self::STATUS_FAIL);362363$class = get_class($ex);364$message = $ex->getMessage();365$ref->setConnectionMessage(366pht(367'%s: %s',368get_class($ex),369$ex->getMessage()));370}371$t_end = microtime(true);372$ref->setConnectionLatency($t_end - $t_start);373374if ($replica_status !== false) {375$is_replica = (bool)$replica_status;376if ($ref->getIsMaster() && $is_replica) {377$ref->setReplicaStatus(self::REPLICATION_MASTER_REPLICA);378$ref->setReplicaMessage(379pht(380'This host has a "master" role, but is replicating data from '.381'another host ("%s")!',382idx($replica_status, 'Master_Host')));383} else if (!$ref->getIsMaster() && !$is_replica) {384$ref->setReplicaStatus(self::REPLICATION_REPLICA_NONE);385$ref->setReplicaMessage(386pht(387'This host has a "replica" role, but is not replicating data '.388'from a master (no output from "SHOW SLAVE STATUS").'));389} else {390$ref->setReplicaStatus(self::REPLICATION_OKAY);391}392393if ($is_replica) {394$latency = idx($replica_status, 'Seconds_Behind_Master');395if (!strlen($latency)) {396$ref->setReplicaStatus(self::REPLICATION_NOT_REPLICATING);397} else {398$latency = (int)$latency;399$ref->setReplicaDelay($latency);400if ($latency > 30) {401$ref->setReplicaStatus(self::REPLICATION_SLOW);402$ref->setReplicaMessage(403pht(404'This replica is lagging far behind the master. Data is at '.405'risk!'));406}407}408}409}410}411412return $refs;413}414415public function newManagementConnection() {416return $this->newConnection(417array(418'retries' => 0,419'timeout' => 2,420));421}422423public function newApplicationConnection($database) {424return $this->newConnection(425array(426'database' => $database,427));428}429430public function isSevered() {431// If we only have an individual database, never sever our connection to432// it, at least for now. It's possible that using the same severing rules433// might eventually make sense to help alleviate load-related failures,434// but we should wait for all the cluster stuff to stabilize first.435if ($this->getIsIndividual()) {436return false;437}438439if ($this->didFailToConnect) {440return true;441}442443$record = $this->getHealthRecord();444$is_healthy = $record->getIsHealthy();445if (!$is_healthy) {446return true;447}448449return false;450}451452public function isReachable(AphrontDatabaseConnection $connection) {453$record = $this->getHealthRecord();454$should_check = $record->getShouldCheck();455456if ($this->isSevered() && !$should_check) {457return false;458}459460$this->connectionException = null;461try {462$connection->openConnection();463$reachable = true;464} catch (AphrontSchemaQueryException $ex) {465// We get one of these if the database we're trying to select does not466// exist. In this case, just re-throw the exception. This is expected467// during first-time setup, when databases like "config" will not exist468// yet.469throw $ex;470} catch (Exception $ex) {471$this->connectionException = $ex;472$reachable = false;473}474475if ($should_check) {476$record->didHealthCheck($reachable);477}478479if (!$reachable) {480$this->didFailToConnect = true;481}482483return $reachable;484}485486public function checkHealth() {487$health = $this->getHealthRecord();488489$should_check = $health->getShouldCheck();490if ($should_check) {491// This does an implicit health update.492$connection = $this->newManagementConnection();493$this->isReachable($connection);494}495496return $this;497}498499private function getHealthRecordCacheKey() {500$host = $this->getHost();501$port = $this->getPort();502$key = self::KEY_HEALTH;503504return "{$key}({$host}, {$port})";505}506507public function getHealthRecord() {508if (!$this->healthRecord) {509$this->healthRecord = new PhabricatorClusterServiceHealthRecord(510$this->getHealthRecordCacheKey());511}512return $this->healthRecord;513}514515public function getConnectionException() {516return $this->connectionException;517}518519public static function getActiveDatabaseRefs() {520$refs = array();521522foreach (self::getMasterDatabaseRefs() as $ref) {523$refs[] = $ref;524}525526foreach (self::getReplicaDatabaseRefs() as $ref) {527$refs[] = $ref;528}529530return $refs;531}532533public static function getAllMasterDatabaseRefs() {534$refs = self::getClusterRefs();535536if (!$refs) {537return array(self::getLiveIndividualRef());538}539540$masters = array();541foreach ($refs as $ref) {542if ($ref->getIsMaster()) {543$masters[] = $ref;544}545}546547return $masters;548}549550public static function getMasterDatabaseRefs() {551$refs = self::getAllMasterDatabaseRefs();552return self::getEnabledRefs($refs);553}554555public function isApplicationHost($database) {556return isset($this->applicationMap[$database]);557}558559public function loadRawMySQLConfigValue($key) {560$conn = $this->newManagementConnection();561562try {563$value = queryfx_one($conn, 'SELECT @@%C', $key);564565// NOTE: Although MySQL allows us to escape configuration values as if566// they are column names, the escaping is included in the column name567// of the return value: if we select "@@`x`", we get back a column named568// "@@`x`", not "@@x" as we might expect.569$value = head($value);570571} catch (AphrontQueryException $ex) {572$value = null;573}574575return $value;576}577578public static function getMasterDatabaseRefForApplication($application) {579$masters = self::getMasterDatabaseRefs();580581$application_master = null;582$default_master = null;583foreach ($masters as $master) {584if ($master->isApplicationHost($application)) {585$application_master = $master;586break;587}588if ($master->getIsDefaultPartition()) {589$default_master = $master;590}591}592593if ($application_master) {594$masters = array($application_master);595} else if ($default_master) {596$masters = array($default_master);597} else {598$masters = array();599}600601$masters = self::getEnabledRefs($masters);602$master = head($masters);603604return $master;605}606607public static function newIndividualRef() {608$default_user = PhabricatorEnv::getEnvConfig('mysql.user');609$default_pass = new PhutilOpaqueEnvelope(610PhabricatorEnv::getEnvConfig('mysql.pass'));611$default_host = PhabricatorEnv::getEnvConfig('mysql.host');612$default_port = PhabricatorEnv::getEnvConfig('mysql.port');613614return id(new self())615->setUser($default_user)616->setPass($default_pass)617->setHost($default_host)618->setPort($default_port)619->setIsIndividual(true)620->setIsMaster(true)621->setIsDefaultPartition(true)622->setUsePersistentConnections(false);623}624625public static function getAllReplicaDatabaseRefs() {626$refs = self::getClusterRefs();627628if (!$refs) {629return array();630}631632$replicas = array();633foreach ($refs as $ref) {634if ($ref->getIsMaster()) {635continue;636}637638$replicas[] = $ref;639}640641return $replicas;642}643644public static function getReplicaDatabaseRefs() {645$refs = self::getAllReplicaDatabaseRefs();646return self::getEnabledRefs($refs);647}648649private static function getEnabledRefs(array $refs) {650foreach ($refs as $key => $ref) {651if ($ref->getDisabled()) {652unset($refs[$key]);653}654}655return $refs;656}657658public static function getReplicaDatabaseRefForApplication($application) {659$replicas = self::getReplicaDatabaseRefs();660661$application_replicas = array();662$default_replicas = array();663foreach ($replicas as $replica) {664$master = $replica->getMasterRef();665666if ($master->isApplicationHost($application)) {667$application_replicas[] = $replica;668}669670if ($master->getIsDefaultPartition()) {671$default_replicas[] = $replica;672}673}674675if ($application_replicas) {676$replicas = $application_replicas;677} else {678$replicas = $default_replicas;679}680681$replicas = self::getEnabledRefs($replicas);682683// TODO: We may have multiple replicas to choose from, and could make684// more of an effort to pick the "best" one here instead of always685// picking the first one. Once we've picked one, we should try to use686// the same replica for the rest of the request, though.687688return head($replicas);689}690691private function newConnection(array $options) {692// If we believe the database is unhealthy, don't spend as much time693// trying to connect to it, since it's likely to continue to fail and694// hammering it can only make the problem worse.695$record = $this->getHealthRecord();696if ($record->getIsHealthy()) {697$default_retries = 3;698$default_timeout = 10;699} else {700$default_retries = 0;701$default_timeout = 2;702}703704$spec = $options + array(705'user' => $this->getUser(),706'pass' => $this->getPass(),707'host' => $this->getHost(),708'port' => $this->getPort(),709'database' => null,710'retries' => $default_retries,711'timeout' => $default_timeout,712'persistent' => $this->getUsePersistentConnections(),713);714715$is_cli = (php_sapi_name() == 'cli');716717$use_persistent = false;718if (!empty($spec['persistent']) && !$is_cli) {719$use_persistent = true;720}721unset($spec['persistent']);722723$connection = self::newRawConnection($spec);724725// If configured, use persistent connections. See T11672 for details.726if ($use_persistent) {727$connection->setPersistent($use_persistent);728}729730// Unless this is a script running from the CLI, prevent any query from731// running for more than 30 seconds. See T10849 for details.732if (!$is_cli) {733$connection->setQueryTimeout(30);734}735736return $connection;737}738739public static function newRawConnection(array $options) {740if (extension_loaded('mysqli')) {741return new AphrontMySQLiDatabaseConnection($options);742} else {743return new AphrontMySQLDatabaseConnection($options);744}745}746747}748749750