3 final class PhabricatorDatabaseRef
6 const STATUS_OKAY
= 'okay';
7 const STATUS_FAIL
= 'fail';
8 const STATUS_AUTH
= 'auth';
9 const STATUS_REPLICATION_CLIENT
= 'replication-client';
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';
17 const KEY_HEALTH
= 'cluster.db.health';
18 const KEY_REFS
= 'cluster.db.refs';
19 const KEY_INDIVIDUAL
= 'cluster.db.individual';
27 private $isIndividual;
29 private $connectionLatency;
30 private $connectionStatus;
31 private $connectionMessage;
32 private $connectionException;
34 private $replicaStatus;
35 private $replicaMessage;
36 private $replicaDelay;
38 private $healthRecord;
39 private $didFailToConnect;
41 private $isDefaultPartition;
42 private $applicationMap = array();
44 private $replicaRefs = array();
45 private $usePersistentConnections;
47 public function setHost($host) {
52 public function getHost() {
56 public function setPort($port) {
61 public function getPort() {
65 public function setUser($user) {
70 public function getUser() {
74 public function setPass(PhutilOpaqueEnvelope
$pass) {
79 public function getPass() {
83 public function setIsMaster($is_master) {
84 $this->isMaster
= $is_master;
88 public function getIsMaster() {
89 return $this->isMaster
;
92 public function setDisabled($disabled) {
93 $this->disabled
= $disabled;
97 public function getDisabled() {
98 return $this->disabled
;
101 public function setConnectionLatency($connection_latency) {
102 $this->connectionLatency
= $connection_latency;
106 public function getConnectionLatency() {
107 return $this->connectionLatency
;
110 public function setConnectionStatus($connection_status) {
111 $this->connectionStatus
= $connection_status;
115 public function getConnectionStatus() {
116 if ($this->connectionStatus
=== null) {
117 throw new PhutilInvalidStateException('queryAll');
120 return $this->connectionStatus
;
123 public function setConnectionMessage($connection_message) {
124 $this->connectionMessage
= $connection_message;
128 public function getConnectionMessage() {
129 return $this->connectionMessage
;
132 public function setReplicaStatus($replica_status) {
133 $this->replicaStatus
= $replica_status;
137 public function getReplicaStatus() {
138 return $this->replicaStatus
;
141 public function setReplicaMessage($replica_message) {
142 $this->replicaMessage
= $replica_message;
146 public function getReplicaMessage() {
147 return $this->replicaMessage
;
150 public function setReplicaDelay($replica_delay) {
151 $this->replicaDelay
= $replica_delay;
155 public function getReplicaDelay() {
156 return $this->replicaDelay
;
159 public function setIsIndividual($is_individual) {
160 $this->isIndividual
= $is_individual;
164 public function getIsIndividual() {
165 return $this->isIndividual
;
168 public function setIsDefaultPartition($is_default_partition) {
169 $this->isDefaultPartition
= $is_default_partition;
173 public function getIsDefaultPartition() {
174 return $this->isDefaultPartition
;
177 public function setUsePersistentConnections($use_persistent_connections) {
178 $this->usePersistentConnections
= $use_persistent_connections;
182 public function getUsePersistentConnections() {
183 return $this->usePersistentConnections
;
186 public function setApplicationMap(array $application_map) {
187 $this->applicationMap
= $application_map;
191 public function getApplicationMap() {
192 return $this->applicationMap
;
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']);
203 return phutil_json_encode($state);
206 public function setMasterRef(PhabricatorDatabaseRef
$master_ref) {
207 $this->masterRef
= $master_ref;
211 public function getMasterRef() {
212 return $this->masterRef
;
215 public function addReplicaRef(PhabricatorDatabaseRef
$replica_ref) {
216 $this->replicaRefs
[] = $replica_ref;
220 public function getReplicaRefs() {
221 return $this->replicaRefs
;
224 public function getDisplayName() {
225 return $this->getRefKey();
228 public function getRefKey() {
229 $host = $this->getHost();
231 $port = $this->getPort();
233 return "{$host}:{$port}";
239 public static function getConnectionStatusMap() {
241 self
::STATUS_OKAY
=> array(
242 'icon' => 'fa-exchange',
244 'label' => pht('Okay'),
246 self
::STATUS_FAIL
=> array(
247 'icon' => 'fa-times',
249 'label' => pht('Failed'),
251 self
::STATUS_AUTH
=> array(
254 'label' => pht('Invalid Credentials'),
256 self
::STATUS_REPLICATION_CLIENT
=> array(
257 'icon' => 'fa-eye-slash',
259 'label' => pht('Missing Permission'),
264 public static function getReplicaStatusMap() {
266 self
::REPLICATION_OKAY
=> array(
267 'icon' => 'fa-download',
269 'label' => pht('Okay'),
271 self
::REPLICATION_MASTER_REPLICA
=> array(
272 'icon' => 'fa-database',
274 'label' => pht('Replicating Master'),
276 self
::REPLICATION_REPLICA_NONE
=> array(
277 'icon' => 'fa-download',
279 'label' => pht('Not A Replica'),
281 self
::REPLICATION_SLOW
=> array(
282 'icon' => 'fa-hourglass',
284 'label' => pht('Slow Replication'),
286 self
::REPLICATION_NOT_REPLICATING
=> array(
287 'icon' => 'fa-exclamation-triangle',
289 'label' => pht('Not Replicating'),
294 public static function getClusterRefs() {
295 $cache = PhabricatorCaches
::getRequestCache();
297 $refs = $cache->getKey(self
::KEY_REFS
);
299 $refs = self
::newRefs();
300 $cache->setKey(self
::KEY_REFS
, $refs);
306 public static function getLiveIndividualRef() {
307 $cache = PhabricatorCaches
::getRequestCache();
309 $ref = $cache->getKey(self
::KEY_INDIVIDUAL
);
311 $ref = self
::newIndividualRef();
312 $cache->setKey(self
::KEY_INDIVIDUAL
, $ref);
318 public static function newRefs() {
319 $default_port = PhabricatorEnv
::getEnvConfig('mysql.port');
320 $default_port = nonempty($default_port, 3306);
322 $default_user = PhabricatorEnv
::getEnvConfig('mysql.user');
324 $default_pass = PhabricatorEnv
::getEnvConfig('mysql.pass');
325 $default_pass = phutil_string_cast($default_pass);
326 $default_pass = new PhutilOpaqueEnvelope($default_pass);
328 $config = PhabricatorEnv
::getEnvConfig('cluster.databases');
330 return id(new PhabricatorDatabaseRefParser())
331 ->setDefaultPort($default_port)
332 ->setDefaultUser($default_user)
333 ->setDefaultPass($default_pass)
337 public static function queryAll() {
338 $refs = self
::getActiveDatabaseRefs();
339 return self
::queryRefs($refs);
342 private static function queryRefs(array $refs) {
343 foreach ($refs as $ref) {
344 $conn = $ref->newManagementConnection();
346 $t_start = microtime(true);
347 $replica_status = false;
349 $replica_status = queryfx_one($conn, 'SHOW SLAVE STATUS');
350 $ref->setConnectionStatus(self
::STATUS_OKAY
);
351 } catch (AphrontAccessDeniedQueryException
$ex) {
352 $ref->setConnectionStatus(self
::STATUS_REPLICATION_CLIENT
);
353 $ref->setConnectionMessage(
355 'No permission to run "SHOW SLAVE STATUS". Grant this user '.
356 '"REPLICATION CLIENT" permission to allow Phabricator 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
);
364 $class = get_class($ex);
365 $message = $ex->getMessage();
366 $ref->setConnectionMessage(
372 $t_end = microtime(true);
373 $ref->setConnectionLatency($t_end - $t_start);
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(
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(
388 'This host has a "replica" role, but is not replicating data '.
389 'from a master (no output from "SHOW SLAVE STATUS").'));
391 $ref->setReplicaStatus(self
::REPLICATION_OKAY
);
395 $latency = idx($replica_status, 'Seconds_Behind_Master');
396 if (!strlen($latency)) {
397 $ref->setReplicaStatus(self
::REPLICATION_NOT_REPLICATING
);
399 $latency = (int)$latency;
400 $ref->setReplicaDelay($latency);
402 $ref->setReplicaStatus(self
::REPLICATION_SLOW
);
403 $ref->setReplicaMessage(
405 'This replica is lagging far behind the master. Data is at '.
416 public function newManagementConnection() {
417 return $this->newConnection(
424 public function newApplicationConnection($database) {
425 return $this->newConnection(
427 'database' => $database,
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()) {
440 if ($this->didFailToConnect
) {
444 $record = $this->getHealthRecord();
445 $is_healthy = $record->getIsHealthy();
453 public function isReachable(AphrontDatabaseConnection
$connection) {
454 $record = $this->getHealthRecord();
455 $should_check = $record->getShouldCheck();
457 if ($this->isSevered() && !$should_check) {
461 $this->connectionException
= null;
463 $connection->openConnection();
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
471 } catch (Exception
$ex) {
472 $this->connectionException
= $ex;
477 $record->didHealthCheck($reachable);
481 $this->didFailToConnect
= true;
487 public function checkHealth() {
488 $health = $this->getHealthRecord();
490 $should_check = $health->getShouldCheck();
492 // This does an implicit health update.
493 $connection = $this->newManagementConnection();
494 $this->isReachable($connection);
500 private function getHealthRecordCacheKey() {
501 $host = $this->getHost();
502 $port = $this->getPort();
503 $key = self
::KEY_HEALTH
;
505 return "{$key}({$host}, {$port})";
508 public function getHealthRecord() {
509 if (!$this->healthRecord
) {
510 $this->healthRecord
= new PhabricatorClusterServiceHealthRecord(
511 $this->getHealthRecordCacheKey());
513 return $this->healthRecord
;
516 public function getConnectionException() {
517 return $this->connectionException
;
520 public static function getActiveDatabaseRefs() {
523 foreach (self
::getMasterDatabaseRefs() as $ref) {
527 foreach (self
::getReplicaDatabaseRefs() as $ref) {
534 public static function getAllMasterDatabaseRefs() {
535 $refs = self
::getClusterRefs();
538 return array(self
::getLiveIndividualRef());
542 foreach ($refs as $ref) {
543 if ($ref->getIsMaster()) {
551 public static function getMasterDatabaseRefs() {
552 $refs = self
::getAllMasterDatabaseRefs();
553 return self
::getEnabledRefs($refs);
556 public function isApplicationHost($database) {
557 return isset($this->applicationMap
[$database]);
560 public function loadRawMySQLConfigValue($key) {
561 $conn = $this->newManagementConnection();
564 $value = queryfx_one($conn, 'SELECT @@%C', $key);
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);
572 } catch (AphrontQueryException
$ex) {
579 public static function getMasterDatabaseRefForApplication($application) {
580 $masters = self
::getMasterDatabaseRefs();
582 $application_master = null;
583 $default_master = null;
584 foreach ($masters as $master) {
585 if ($master->isApplicationHost($application)) {
586 $application_master = $master;
589 if ($master->getIsDefaultPartition()) {
590 $default_master = $master;
594 if ($application_master) {
595 $masters = array($application_master);
596 } else if ($default_master) {
597 $masters = array($default_master);
602 $masters = self
::getEnabledRefs($masters);
603 $master = head($masters);
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');
615 return id(new self())
616 ->setUser($default_user)
617 ->setPass($default_pass)
618 ->setHost($default_host)
619 ->setPort($default_port)
620 ->setIsIndividual(true)
622 ->setIsDefaultPartition(true)
623 ->setUsePersistentConnections(false);
626 public static function getAllReplicaDatabaseRefs() {
627 $refs = self
::getClusterRefs();
634 foreach ($refs as $ref) {
635 if ($ref->getIsMaster()) {
645 public static function getReplicaDatabaseRefs() {
646 $refs = self
::getAllReplicaDatabaseRefs();
647 return self
::getEnabledRefs($refs);
650 private static function getEnabledRefs(array $refs) {
651 foreach ($refs as $key => $ref) {
652 if ($ref->getDisabled()) {
659 public static function getReplicaDatabaseRefForApplication($application) {
660 $replicas = self
::getReplicaDatabaseRefs();
662 $application_replicas = array();
663 $default_replicas = array();
664 foreach ($replicas as $replica) {
665 $master = $replica->getMasterRef();
667 if ($master->isApplicationHost($application)) {
668 $application_replicas[] = $replica;
671 if ($master->getIsDefaultPartition()) {
672 $default_replicas[] = $replica;
676 if ($application_replicas) {
677 $replicas = $application_replicas;
679 $replicas = $default_replicas;
682 $replicas = self
::getEnabledRefs($replicas);
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.
689 return head($replicas);
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;
701 $default_retries = 0;
702 $default_timeout = 2;
705 $spec = $options +
array(
706 'user' => $this->getUser(),
707 'pass' => $this->getPass(),
708 'host' => $this->getHost(),
709 'port' => $this->getPort(),
711 'retries' => $default_retries,
712 'timeout' => $default_timeout,
713 'persistent' => $this->getUsePersistentConnections(),
716 $is_cli = (php_sapi_name() == 'cli');
718 $use_persistent = false;
719 if (!empty($spec['persistent']) && !$is_cli) {
720 $use_persistent = true;
722 unset($spec['persistent']);
724 $connection = self
::newRawConnection($spec);
726 // If configured, use persistent connections. See T11672 for details.
727 if ($use_persistent) {
728 $connection->setPersistent($use_persistent);
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.
734 $connection->setQueryTimeout(30);
740 public static function newRawConnection(array $options) {
741 if (extension_loaded('mysqli')) {
742 return new AphrontMySQLiDatabaseConnection($options);
744 return new AphrontMySQLDatabaseConnection($options);