Remove product literal strings in "pht()", part 18
[phabricator.git] / src / infrastructure / cluster / PhabricatorDatabaseRef.php
blobda9f5e0d5e193d0dc059d6ff25d48af095c8fd21
1 <?php
3 final class PhabricatorDatabaseRef
4 extends Phobject {
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';
21 private $host;
22 private $port;
23 private $user;
24 private $pass;
25 private $disabled;
26 private $isMaster;
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();
43 private $masterRef;
44 private $replicaRefs = array();
45 private $usePersistentConnections;
47 public function setHost($host) {
48 $this->host = $host;
49 return $this;
52 public function getHost() {
53 return $this->host;
56 public function setPort($port) {
57 $this->port = $port;
58 return $this;
61 public function getPort() {
62 return $this->port;
65 public function setUser($user) {
66 $this->user = $user;
67 return $this;
70 public function getUser() {
71 return $this->user;
74 public function setPass(PhutilOpaqueEnvelope $pass) {
75 $this->pass = $pass;
76 return $this;
79 public function getPass() {
80 return $this->pass;
83 public function setIsMaster($is_master) {
84 $this->isMaster = $is_master;
85 return $this;
88 public function getIsMaster() {
89 return $this->isMaster;
92 public function setDisabled($disabled) {
93 $this->disabled = $disabled;
94 return $this;
97 public function getDisabled() {
98 return $this->disabled;
101 public function setConnectionLatency($connection_latency) {
102 $this->connectionLatency = $connection_latency;
103 return $this;
106 public function getConnectionLatency() {
107 return $this->connectionLatency;
110 public function setConnectionStatus($connection_status) {
111 $this->connectionStatus = $connection_status;
112 return $this;
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;
125 return $this;
128 public function getConnectionMessage() {
129 return $this->connectionMessage;
132 public function setReplicaStatus($replica_status) {
133 $this->replicaStatus = $replica_status;
134 return $this;
137 public function getReplicaStatus() {
138 return $this->replicaStatus;
141 public function setReplicaMessage($replica_message) {
142 $this->replicaMessage = $replica_message;
143 return $this;
146 public function getReplicaMessage() {
147 return $this->replicaMessage;
150 public function setReplicaDelay($replica_delay) {
151 $this->replicaDelay = $replica_delay;
152 return $this;
155 public function getReplicaDelay() {
156 return $this->replicaDelay;
159 public function setIsIndividual($is_individual) {
160 $this->isIndividual = $is_individual;
161 return $this;
164 public function getIsIndividual() {
165 return $this->isIndividual;
168 public function setIsDefaultPartition($is_default_partition) {
169 $this->isDefaultPartition = $is_default_partition;
170 return $this;
173 public function getIsDefaultPartition() {
174 return $this->isDefaultPartition;
177 public function setUsePersistentConnections($use_persistent_connections) {
178 $this->usePersistentConnections = $use_persistent_connections;
179 return $this;
182 public function getUsePersistentConnections() {
183 return $this->usePersistentConnections;
186 public function setApplicationMap(array $application_map) {
187 $this->applicationMap = $application_map;
188 return $this;
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;
208 return $this;
211 public function getMasterRef() {
212 return $this->masterRef;
215 public function addReplicaRef(PhabricatorDatabaseRef $replica_ref) {
216 $this->replicaRefs[] = $replica_ref;
217 return $this;
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();
232 if (strlen($port)) {
233 return "{$host}:{$port}";
236 return $host;
239 public static function getConnectionStatusMap() {
240 return array(
241 self::STATUS_OKAY => array(
242 'icon' => 'fa-exchange',
243 'color' => 'green',
244 'label' => pht('Okay'),
246 self::STATUS_FAIL => array(
247 'icon' => 'fa-times',
248 'color' => 'red',
249 'label' => pht('Failed'),
251 self::STATUS_AUTH => array(
252 'icon' => 'fa-key',
253 'color' => 'red',
254 'label' => pht('Invalid Credentials'),
256 self::STATUS_REPLICATION_CLIENT => array(
257 'icon' => 'fa-eye-slash',
258 'color' => 'yellow',
259 'label' => pht('Missing Permission'),
264 public static function getReplicaStatusMap() {
265 return array(
266 self::REPLICATION_OKAY => array(
267 'icon' => 'fa-download',
268 'color' => 'green',
269 'label' => pht('Okay'),
271 self::REPLICATION_MASTER_REPLICA => array(
272 'icon' => 'fa-database',
273 'color' => 'red',
274 'label' => pht('Replicating Master'),
276 self::REPLICATION_REPLICA_NONE => array(
277 'icon' => 'fa-download',
278 'color' => 'red',
279 'label' => pht('Not A Replica'),
281 self::REPLICATION_SLOW => array(
282 'icon' => 'fa-hourglass',
283 'color' => 'red',
284 'label' => pht('Slow Replication'),
286 self::REPLICATION_NOT_REPLICATING => array(
287 'icon' => 'fa-exclamation-triangle',
288 'color' => 'red',
289 'label' => pht('Not Replicating'),
294 public static function getClusterRefs() {
295 $cache = PhabricatorCaches::getRequestCache();
297 $refs = $cache->getKey(self::KEY_REFS);
298 if (!$refs) {
299 $refs = self::newRefs();
300 $cache->setKey(self::KEY_REFS, $refs);
303 return $refs;
306 public static function getLiveIndividualRef() {
307 $cache = PhabricatorCaches::getRequestCache();
309 $ref = $cache->getKey(self::KEY_INDIVIDUAL);
310 if (!$ref) {
311 $ref = self::newIndividualRef();
312 $cache->setKey(self::KEY_INDIVIDUAL, $ref);
315 return $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)
334 ->newRefs($config);
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;
348 try {
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(
354 pht(
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(
367 pht(
368 '%s: %s',
369 get_class($ex),
370 $ex->getMessage()));
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(
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 SLAVE STATUS").'));
390 } else {
391 $ref->setReplicaStatus(self::REPLICATION_OKAY);
394 if ($is_replica) {
395 $latency = idx($replica_status, 'Seconds_Behind_Master');
396 if (!strlen($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!'));
413 return $refs;
416 public function newManagementConnection() {
417 return $this->newConnection(
418 array(
419 'retries' => 0,
420 'timeout' => 2,
424 public function newApplicationConnection($database) {
425 return $this->newConnection(
426 array(
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()) {
437 return false;
440 if ($this->didFailToConnect) {
441 return true;
444 $record = $this->getHealthRecord();
445 $is_healthy = $record->getIsHealthy();
446 if (!$is_healthy) {
447 return true;
450 return false;
453 public function isReachable(AphrontDatabaseConnection $connection) {
454 $record = $this->getHealthRecord();
455 $should_check = $record->getShouldCheck();
457 if ($this->isSevered() && !$should_check) {
458 return false;
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;
476 if ($should_check) {
477 $record->didHealthCheck($reachable);
480 if (!$reachable) {
481 $this->didFailToConnect = true;
484 return $reachable;
487 public function checkHealth() {
488 $health = $this->getHealthRecord();
490 $should_check = $health->getShouldCheck();
491 if ($should_check) {
492 // This does an implicit health update.
493 $connection = $this->newManagementConnection();
494 $this->isReachable($connection);
497 return $this;
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() {
521 $refs = array();
523 foreach (self::getMasterDatabaseRefs() as $ref) {
524 $refs[] = $ref;
527 foreach (self::getReplicaDatabaseRefs() as $ref) {
528 $refs[] = $ref;
531 return $refs;
534 public static function getAllMasterDatabaseRefs() {
535 $refs = self::getClusterRefs();
537 if (!$refs) {
538 return array(self::getLiveIndividualRef());
541 $masters = array();
542 foreach ($refs as $ref) {
543 if ($ref->getIsMaster()) {
544 $masters[] = $ref;
548 return $masters;
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();
563 try {
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) {
573 $value = null;
576 return $value;
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;
587 break;
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);
598 } else {
599 $masters = array();
602 $masters = self::getEnabledRefs($masters);
603 $master = head($masters);
605 return $master;
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)
621 ->setIsMaster(true)
622 ->setIsDefaultPartition(true)
623 ->setUsePersistentConnections(false);
626 public static function getAllReplicaDatabaseRefs() {
627 $refs = self::getClusterRefs();
629 if (!$refs) {
630 return array();
633 $replicas = array();
634 foreach ($refs as $ref) {
635 if ($ref->getIsMaster()) {
636 continue;
639 $replicas[] = $ref;
642 return $replicas;
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()) {
653 unset($refs[$key]);
656 return $refs;
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;
678 } else {
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;
700 } else {
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(),
710 'database' => null,
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.
733 if (!$is_cli) {
734 $connection->setQueryTimeout(30);
737 return $connection;
740 public static function newRawConnection(array $options) {
741 if (extension_loaded('mysqli')) {
742 return new AphrontMySQLiDatabaseConnection($options);
743 } else {
744 return new AphrontMySQLDatabaseConnection($options);