diff --git a/src/applications/config/application/PhabricatorConfigApplication.php b/src/applications/config/application/PhabricatorConfigApplication.php index dda50a2b73..6b2704b0b4 100644 --- a/src/applications/config/application/PhabricatorConfigApplication.php +++ b/src/applications/config/application/PhabricatorConfigApplication.php @@ -1,76 +1,77 @@ getIsAdmin(); } public function getTitleGlyph() { return "\xE2\x98\xBA"; } public function getApplicationGroup() { return self::GROUP_ADMIN; } public function canUninstall() { return false; } public function getName() { return 'Config'; } public function getShortDescription() { return pht('Configure Phabricator'); } public function getRoutes() { return array( '/config/' => array( '' => 'PhabricatorConfigListController', 'application/' => 'PhabricatorConfigApplicationController', 'all/' => 'PhabricatorConfigAllController', 'history/' => 'PhabricatorConfigHistoryController', 'edit/(?P[\w\.\-]+)/' => 'PhabricatorConfigEditController', 'group/(?P[^/]+)/' => 'PhabricatorConfigGroupController', 'version/' => 'PhabricatorConfigVersionController', 'database/'. + '(?:(?P[^/]+)/'. '(?:(?P[^/]+)/'. '(?:(?P[^/]+)/'. - '(?:(?:col/(?P[^/]+)|key/(?P[^/]+))/)?)?)?' + '(?:(?:col/(?P[^/]+)|key/(?P[^/]+))/)?)?)?)?' => 'PhabricatorConfigDatabaseStatusController', 'dbissue/' => 'PhabricatorConfigDatabaseIssueController', '(?Pignore|unignore)/(?P[^/]+)/' => 'PhabricatorConfigIgnoreController', 'issue/' => array( '' => 'PhabricatorConfigIssueListController', 'panel/' => 'PhabricatorConfigIssuePanelController', '(?P[^/]+)/' => 'PhabricatorConfigIssueViewController', ), 'cache/' => array( '' => 'PhabricatorConfigCacheController', 'purge/' => 'PhabricatorConfigPurgeCacheController', ), 'module/' => array( '(?P[^/]+)/' => 'PhabricatorConfigModuleController', ), 'cluster/' => array( 'databases/' => 'PhabricatorConfigClusterDatabasesController', 'notifications/' => 'PhabricatorConfigClusterNotificationsController', 'repositories/' => 'PhabricatorConfigClusterRepositoriesController', ), ), ); } } diff --git a/src/applications/config/controller/PhabricatorConfigDatabaseController.php b/src/applications/config/controller/PhabricatorConfigDatabaseController.php index c9f2aa6a3c..53af9a6b92 100644 --- a/src/applications/config/controller/PhabricatorConfigDatabaseController.php +++ b/src/applications/config/controller/PhabricatorConfigDatabaseController.php @@ -1,63 +1,47 @@ setUser($ref->getUser()) - ->setHost($ref->getHost()) - ->setPort($ref->getPort()) - ->setNamespace(PhabricatorLiskDAO::getDefaultStorageNamespace()) - ->setPassword($ref->getPass()); - - $query = id(new PhabricatorConfigSchemaQuery()) - ->setAPI($api); - - return $query; - } - protected function renderIcon($status) { switch ($status) { case PhabricatorConfigStorageSchema::STATUS_OKAY: $icon = 'fa-check-circle green'; break; case PhabricatorConfigStorageSchema::STATUS_WARN: $icon = 'fa-exclamation-circle yellow'; break; case PhabricatorConfigStorageSchema::STATUS_FAIL: default: $icon = 'fa-times-circle red'; break; } return id(new PHUIIconView()) ->setIcon($icon); } protected function renderAttr($attr, $issue) { if ($issue) { return phutil_tag( 'span', array( 'style' => 'color: #aa0000;', ), $attr); } else { return $attr; } } protected function renderBoolean($value) { if ($value === null) { return ''; } else if ($value === true) { return pht('Yes'); } else { return pht('No'); } } } diff --git a/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php b/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php index f1a91d4d5b..7674d28f51 100644 --- a/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php +++ b/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php @@ -1,171 +1,181 @@ getViewer(); - $query = $this->buildSchemaQuery(); + $query = new PhabricatorConfigSchemaQuery(); - $actual = $query->loadActualSchema(); - $expect = $query->loadExpectedSchema(); - $comp = $query->buildComparisonSchema($expect, $actual); + $actual = $query->loadActualSchemata(); + $expect = $query->loadExpectedSchemata(); + $comp_servers = $query->buildComparisonSchemata($expect, $actual); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Database Issues')); $crumbs->setBorder(true); // Collect all open issues. $issues = array(); - foreach ($comp->getDatabases() as $database_name => $database) { - foreach ($database->getLocalIssues() as $issue) { - $issues[] = array( - $database_name, - null, - null, - null, - $issue, - ); - } - foreach ($database->getTables() as $table_name => $table) { - foreach ($table->getLocalIssues() as $issue) { + foreach ($comp_servers as $ref_name => $comp) { + foreach ($comp->getDatabases() as $database_name => $database) { + foreach ($database->getLocalIssues() as $issue) { $issues[] = array( + $ref_name, $database_name, - $table_name, + null, null, null, $issue, ); } - foreach ($table->getColumns() as $column_name => $column) { - foreach ($column->getLocalIssues() as $issue) { + foreach ($database->getTables() as $table_name => $table) { + foreach ($table->getLocalIssues() as $issue) { $issues[] = array( + $ref_name, $database_name, $table_name, - 'column', - $column_name, + null, + null, $issue, ); } - } - foreach ($table->getKeys() as $key_name => $key) { - foreach ($key->getLocalIssues() as $issue) { - $issues[] = array( - $database_name, - $table_name, - 'key', - $key_name, - $issue, - ); + foreach ($table->getColumns() as $column_name => $column) { + foreach ($column->getLocalIssues() as $issue) { + $issues[] = array( + $ref_name, + $database_name, + $table_name, + 'column', + $column_name, + $issue, + ); + } + } + foreach ($table->getKeys() as $key_name => $key) { + foreach ($key->getLocalIssues() as $issue) { + $issues[] = array( + $ref_name, + $database_name, + $table_name, + 'key', + $key_name, + $issue, + ); + } } } } } - // Sort all open issues so that the most severe issues appear first. $order = array(); $counts = array(); foreach ($issues as $key => $issue) { - $const = $issue[4]; + $const = $issue[5]; $status = PhabricatorConfigStorageSchema::getIssueStatus($const); $severity = PhabricatorConfigStorageSchema::getStatusSeverity($status); $order[$key] = sprintf( '~%d~%s%s%s', 9 - $severity, - $issue[0], $issue[1], - $issue[3]); + $issue[2], + $issue[4]); if (empty($counts[$status])) { $counts[$status] = 0; } $counts[$status]++; } asort($order); $issues = array_select_keys($issues, array_keys($order)); // Render the issues. $rows = array(); foreach ($issues as $issue) { - $const = $issue[4]; + $const = $issue[5]; + + $uri = $this->getApplicationURI('/database/'.$issue[0].'/'.$issue[1].'/'); $database_link = phutil_tag( 'a', array( - 'href' => $this->getApplicationURI('/database/'.$issue[0].'/'), + 'href' => $uri, ), - $issue[0]); + $issue[1]); $rows[] = array( $this->renderIcon( PhabricatorConfigStorageSchema::getIssueStatus($const)), + $issue[0], $database_link, - $issue[1], $issue[2], $issue[3], + $issue[4], PhabricatorConfigStorageSchema::getIssueDescription($const), ); } $table = id(new AphrontTableView($rows)) ->setNoDataString( pht('No databases have any issues.')) ->setHeaders( array( null, + pht('Server'), pht('Database'), pht('Table'), pht('Type'), pht('Column/Key'), pht('Issue'), )) ->setColumnClasses( array( null, null, null, null, null, + null, 'wide', )); $errors = array(); if (isset($counts[PhabricatorConfigStorageSchema::STATUS_FAIL])) { $errors[] = pht( 'Detected %s serious issue(s) with the schemata.', new PhutilNumber($counts[PhabricatorConfigStorageSchema::STATUS_FAIL])); } if (isset($counts[PhabricatorConfigStorageSchema::STATUS_WARN])) { $errors[] = pht( 'Detected %s warning(s) with the schemata.', new PhutilNumber($counts[PhabricatorConfigStorageSchema::STATUS_WARN])); } $title = pht('Database Issues'); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setProfileHeader(true); $nav = $this->buildSideNavView(); $nav->selectFilter('dbissue/'); $content = id(new PhabricatorConfigPageView()) ->setHeader($header) ->setContent($table); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->setNavigation($nav) ->appendChild($content) ->addClass('white-background'); } } diff --git a/src/applications/config/controller/PhabricatorConfigDatabaseStatusController.php b/src/applications/config/controller/PhabricatorConfigDatabaseStatusController.php index bdeb254437..d49a546387 100644 --- a/src/applications/config/controller/PhabricatorConfigDatabaseStatusController.php +++ b/src/applications/config/controller/PhabricatorConfigDatabaseStatusController.php @@ -1,748 +1,851 @@ getViewer(); $this->database = $request->getURIData('database'); $this->table = $request->getURIData('table'); $this->column = $request->getURIData('column'); $this->key = $request->getURIData('key'); + $this->ref = $request->getURIData('ref'); - $query = $this->buildSchemaQuery(); - - $actual = $query->loadActualSchema(); - $expect = $query->loadExpectedSchema(); - $comp = $query->buildComparisonSchema($expect, $actual); - - if ($this->column) { - return $this->renderColumn( - $comp, - $expect, - $actual, - $this->database, - $this->table, - $this->column); - } else if ($this->key) { - return $this->renderKey( - $comp, - $expect, - $actual, - $this->database, - $this->table, - $this->key); - } else if ($this->table) { - return $this->renderTable( - $comp, - $expect, - $actual, - $this->database, - $this->table); - } else if ($this->database) { - return $this->renderDatabase( - $comp, - $expect, - $actual, - $this->database); - } else { - return $this->renderServer( - $comp, - $expect, - $actual); + $query = new PhabricatorConfigSchemaQuery(); + + $actual = $query->loadActualSchemata(); + $expect = $query->loadExpectedSchemata(); + $comp = $query->buildComparisonSchemata($expect, $actual); + + if ($this->ref !== null) { + $server_actual = idx($actual, $this->ref); + if (!$server_actual) { + return new Aphront404Response(); + } + + $server_comparison = $comp[$this->ref]; + $server_expect = $expect[$this->ref]; + + if ($this->column) { + return $this->renderColumn( + $server_comparison, + $server_expect, + $server_actual, + $this->database, + $this->table, + $this->column); + } else if ($this->key) { + return $this->renderKey( + $server_comparison, + $server_expect, + $server_actual, + $this->database, + $this->table, + $this->key); + } else if ($this->table) { + return $this->renderTable( + $server_comparison, + $server_expect, + $server_actual, + $this->database, + $this->table); + } else if ($this->database) { + return $this->renderDatabase( + $server_comparison, + $server_expect, + $server_actual, + $this->database); + } } + + return $this->renderServers( + $comp, + $expect, + $actual); } private function buildResponse($title, $body) { $nav = $this->buildSideNavView(); $nav->selectFilter('database/'); if (!$title) { $title = pht('Database Status'); } + $ref = $this->ref; + $database = $this->database; + $table = $this->table; + $column = $this->column; + $key = $this->key; + + $links = array(); + $links[] = array( + pht('Database Status'), + 'database/', + ); + + if ($database) { + $links[] = array( + $database, + "database/{$ref}/{$database}/", + ); + } + + if ($table) { + $links[] = array( + $table, + "database/{$ref}/{$database}/{$table}/", + ); + } + + if ($column) { + $links[] = array( + $column, + "database/{$ref}/{$database}/{$table}/col/{$column}/", + ); + } + + if ($key) { + $links[] = array( + $key, + "database/{$ref}/{$database}/{$table}/key/{$key}/", + ); + } + $crumbs = $this->buildApplicationCrumbs(); $crumbs->setBorder(true); - if ($this->database) { - $crumbs->addTextCrumb( - pht('Database Status'), - $this->getApplicationURI('database/')); - if ($this->table) { - $crumbs->addTextCrumb( - $this->database, - $this->getApplicationURI('database/'.$this->database.'/')); - if ($this->column || $this->key) { - $crumbs->addTextCrumb( - $this->table, - $this->getApplicationURI( - 'database/'.$this->database.'/'.$this->table.'/')); - if ($this->column) { - $crumbs->addTextCrumb($this->column); - } else { - $crumbs->addTextCrumb($this->key); - } - } else { - $crumbs->addTextCrumb($this->table); - } + + $last_key = last_key($links); + foreach ($links as $link_key => $link) { + list($name, $href) = $link; + if ($link_key == $last_key) { + $crumbs->addTextCrumb($name); } else { - $crumbs->addTextCrumb($this->database); + $crumbs->addTextCrumb($name, $this->getApplicationURI($href)); } - } else { - $crumbs->addTextCrumb(pht('Database Status')); } $doc_link = PhabricatorEnv::getDoclink('Managing Storage Adjustments'); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setProfileHeader(true) ->addActionLink( id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-book') ->setHref($doc_link) ->setText(pht('Learn More'))); $content = id(new PhabricatorConfigPageView()) ->setHeader($header) ->setContent($body); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->setNavigation($nav) ->appendChild($content) ->addClass('white-background'); } - private function renderServer( - PhabricatorConfigServerSchema $comp, - PhabricatorConfigServerSchema $expect, - PhabricatorConfigServerSchema $actual) { + private function renderServers( + array $comp_servers, + array $expect_servers, + array $actual_servers) { $charset_issue = PhabricatorConfigStorageSchema::ISSUE_CHARSET; $collation_issue = PhabricatorConfigStorageSchema::ISSUE_COLLATION; $rows = array(); - foreach ($comp->getDatabases() as $database_name => $database) { - $actual_database = $actual->getDatabase($database_name); - if ($actual_database) { - $charset = $actual_database->getCharacterSet(); - $collation = $actual_database->getCollation(); - } else { - $charset = null; - $collation = null; - } + foreach ($comp_servers as $ref_key => $comp) { + $actual = $actual_servers[$ref_key]; + $expect = $expect_servers[$ref_key]; + foreach ($comp->getDatabases() as $database_name => $database) { + $actual_database = $actual->getDatabase($database_name); + if ($actual_database) { + $charset = $actual_database->getCharacterSet(); + $collation = $actual_database->getCollation(); + } else { + $charset = null; + $collation = null; + } - $status = $database->getStatus(); - $issues = $database->getIssues(); + $status = $database->getStatus(); + $issues = $database->getIssues(); - $rows[] = array( - $this->renderIcon($status), - phutil_tag( - 'a', + $uri = $this->getURI( array( - 'href' => $this->getApplicationURI( - '/database/'.$database_name.'/'), - ), - $database_name), - $this->renderAttr($charset, $database->hasIssue($charset_issue)), - $this->renderAttr($collation, $database->hasIssue($collation_issue)), - ); + 'ref' => $ref_key, + 'database' => $database_name, + )); + + $rows[] = array( + $this->renderIcon($status), + $ref_key, + phutil_tag( + 'a', + array( + 'href' => $uri, + ), + $database_name), + $this->renderAttr($charset, $database->hasIssue($charset_issue)), + $this->renderAttr($collation, $database->hasIssue($collation_issue)), + ); + } } $table = id(new AphrontTableView($rows)) ->setHeaders( array( null, + pht('Server'), pht('Database'), pht('Charset'), pht('Collation'), )) ->setColumnClasses( array( + null, null, 'wide pri', null, null, )); $title = pht('Database Status'); $properties = $this->buildProperties( array( ), $comp->getIssues()); return $this->buildResponse($title, array($properties, $table)); } private function renderDatabase( PhabricatorConfigServerSchema $comp, PhabricatorConfigServerSchema $expect, PhabricatorConfigServerSchema $actual, $database_name) { $collation_issue = PhabricatorConfigStorageSchema::ISSUE_COLLATION; $database = $comp->getDatabase($database_name); if (!$database) { return new Aphront404Response(); } $rows = array(); foreach ($database->getTables() as $table_name => $table) { $status = $table->getStatus(); + $uri = $this->getURI( + array( + 'table' => $table_name, + )); + $rows[] = array( $this->renderIcon($status), phutil_tag( 'a', array( - 'href' => $this->getApplicationURI( - '/database/'.$database_name.'/'.$table_name.'/'), + 'href' => $uri, ), $table_name), $this->renderAttr( $table->getCollation(), $table->hasIssue($collation_issue)), ); } $table = id(new AphrontTableView($rows)) ->setHeaders( array( null, pht('Table'), pht('Collation'), )) ->setColumnClasses( array( null, 'wide pri', null, )); $title = pht('Database: %s', $database_name); $actual_database = $actual->getDatabase($database_name); if ($actual_database) { $actual_charset = $actual_database->getCharacterSet(); $actual_collation = $actual_database->getCollation(); } else { $actual_charset = null; $actual_collation = null; } $expect_database = $expect->getDatabase($database_name); if ($expect_database) { $expect_charset = $expect_database->getCharacterSet(); $expect_collation = $expect_database->getCollation(); } else { $expect_charset = null; $expect_collation = null; } $properties = $this->buildProperties( array( + array( + pht('Server'), + $this->ref, + ), array( pht('Character Set'), $actual_charset, ), array( pht('Expected Character Set'), $expect_charset, ), array( pht('Collation'), $actual_collation, ), array( pht('Expected Collation'), $expect_collation, ), ), $database->getIssues()); return $this->buildResponse($title, array($properties, $table)); } private function renderTable( PhabricatorConfigServerSchema $comp, PhabricatorConfigServerSchema $expect, PhabricatorConfigServerSchema $actual, $database_name, $table_name) { $type_issue = PhabricatorConfigStorageSchema::ISSUE_COLUMNTYPE; $charset_issue = PhabricatorConfigStorageSchema::ISSUE_CHARSET; $collation_issue = PhabricatorConfigStorageSchema::ISSUE_COLLATION; $nullable_issue = PhabricatorConfigStorageSchema::ISSUE_NULLABLE; $unique_issue = PhabricatorConfigStorageSchema::ISSUE_UNIQUE; $columns_issue = PhabricatorConfigStorageSchema::ISSUE_KEYCOLUMNS; $longkey_issue = PhabricatorConfigStorageSchema::ISSUE_LONGKEY; $auto_issue = PhabricatorConfigStorageSchema::ISSUE_AUTOINCREMENT; $database = $comp->getDatabase($database_name); if (!$database) { return new Aphront404Response(); } $table = $database->getTable($table_name); if (!$table) { return new Aphront404Response(); } $actual_database = $actual->getDatabase($database_name); $actual_table = null; if ($actual_database) { $actual_table = $actual_database->getTable($table_name); } $expect_database = $expect->getDatabase($database_name); $expect_table = null; if ($expect_database) { $expect_table = $expect_database->getTable($table_name); } $rows = array(); foreach ($table->getColumns() as $column_name => $column) { $expect_column = null; if ($expect_table) { $expect_column = $expect_table->getColumn($column_name); } $status = $column->getStatus(); $data_type = null; if ($expect_column) { $data_type = $expect_column->getDataType(); } + $uri = $this->getURI( + array( + 'column' => $column_name, + )); + $rows[] = array( $this->renderIcon($status), phutil_tag( 'a', array( - 'href' => $this->getApplicationURI( - 'database/'. - $database_name.'/'. - $table_name.'/'. - 'col/'. - $column_name.'/'), + 'href' => $uri, ), $column_name), $data_type, $this->renderAttr( $column->getColumnType(), $column->hasIssue($type_issue)), $this->renderAttr( $this->renderBoolean($column->getNullable()), $column->hasIssue($nullable_issue)), $this->renderAttr( $this->renderBoolean($column->getAutoIncrement()), $column->hasIssue($auto_issue)), $this->renderAttr( $column->getCharacterSet(), $column->hasIssue($charset_issue)), $this->renderAttr( $column->getCollation(), $column->hasIssue($collation_issue)), ); } $table_view = id(new AphrontTableView($rows)) ->setHeaders( array( null, pht('Column'), pht('Data Type'), pht('Column Type'), pht('Nullable'), pht('Autoincrement'), pht('Character Set'), pht('Collation'), )) ->setColumnClasses( array( null, 'wide pri', null, null, null, null, null, )); $key_rows = array(); foreach ($table->getKeys() as $key_name => $key) { $expect_key = null; if ($expect_table) { $expect_key = $expect_table->getKey($key_name); } $status = $key->getStatus(); $size = 0; foreach ($key->getColumnNames() as $column_spec) { list($column_name, $prefix) = $key->getKeyColumnAndPrefix($column_spec); $column = $table->getColumn($column_name); if (!$column) { $size = 0; break; } $size += $column->getKeyByteLength($prefix); } $size_formatted = null; if ($size) { $size_formatted = $this->renderAttr( $size, $key->hasIssue($longkey_issue)); } + $uri = $this->getURI( + array( + 'key' => $key_name, + )); + $key_rows[] = array( $this->renderIcon($status), phutil_tag( 'a', array( - 'href' => $this->getApplicationURI( - 'database/'. - $database_name.'/'. - $table_name.'/'. - 'key/'. - $key_name.'/'), + 'href' => $uri, ), $key_name), $this->renderAttr( implode(', ', $key->getColumnNames()), $key->hasIssue($columns_issue)), $this->renderAttr( $this->renderBoolean($key->getUnique()), $key->hasIssue($unique_issue)), $size_formatted, ); } $keys_view = id(new AphrontTableView($key_rows)) ->setHeaders( array( null, pht('Key'), pht('Columns'), pht('Unique'), pht('Size'), )) ->setColumnClasses( array( null, 'wide pri', null, null, null, )); $title = pht('Database: %s.%s', $database_name, $table_name); if ($actual_table) { $actual_collation = $actual_table->getCollation(); } else { $actual_collation = null; } if ($expect_table) { $expect_collation = $expect_table->getCollation(); } else { $expect_collation = null; } $properties = $this->buildProperties( array( + array( + pht('Server'), + $this->ref, + ), array( pht('Collation'), $actual_collation, ), array( pht('Expected Collation'), $expect_collation, ), ), $table->getIssues()); return $this->buildResponse( $title, array($properties, $table_view, $keys_view)); } private function renderColumn( PhabricatorConfigServerSchema $comp, PhabricatorConfigServerSchema $expect, PhabricatorConfigServerSchema $actual, $database_name, $table_name, $column_name) { $database = $comp->getDatabase($database_name); if (!$database) { return new Aphront404Response(); } $table = $database->getTable($table_name); if (!$table) { return new Aphront404Response(); } $column = $table->getColumn($column_name); if (!$column) { return new Aphront404Response(); } $actual_database = $actual->getDatabase($database_name); $actual_table = null; $actual_column = null; if ($actual_database) { $actual_table = $actual_database->getTable($table_name); if ($actual_table) { $actual_column = $actual_table->getColumn($column_name); } } $expect_database = $expect->getDatabase($database_name); $expect_table = null; $expect_column = null; if ($expect_database) { $expect_table = $expect_database->getTable($table_name); if ($expect_table) { $expect_column = $expect_table->getColumn($column_name); } } if ($actual_column) { $actual_coltype = $actual_column->getColumnType(); $actual_charset = $actual_column->getCharacterSet(); $actual_collation = $actual_column->getCollation(); $actual_nullable = $actual_column->getNullable(); $actual_auto = $actual_column->getAutoIncrement(); } else { $actual_coltype = null; $actual_charset = null; $actual_collation = null; $actual_nullable = null; $actual_auto = null; } if ($expect_column) { $data_type = $expect_column->getDataType(); $expect_coltype = $expect_column->getColumnType(); $expect_charset = $expect_column->getCharacterSet(); $expect_collation = $expect_column->getCollation(); $expect_nullable = $expect_column->getNullable(); $expect_auto = $expect_column->getAutoIncrement(); } else { $data_type = null; $expect_coltype = null; $expect_charset = null; $expect_collation = null; $expect_nullable = null; $expect_auto = null; } $title = pht( 'Database Status: %s.%s.%s', $database_name, $table_name, $column_name); $properties = $this->buildProperties( array( + array( + pht('Server'), + $this->ref, + ), array( pht('Data Type'), $data_type, ), array( pht('Column Type'), $actual_coltype, ), array( pht('Expected Column Type'), $expect_coltype, ), array( pht('Character Set'), $actual_charset, ), array( pht('Expected Character Set'), $expect_charset, ), array( pht('Collation'), $actual_collation, ), array( pht('Expected Collation'), $expect_collation, ), array( pht('Nullable'), $this->renderBoolean($actual_nullable), ), array( pht('Expected Nullable'), $this->renderBoolean($expect_nullable), ), array( pht('Autoincrement'), $this->renderBoolean($actual_auto), ), array( pht('Expected Autoincrement'), $this->renderBoolean($expect_auto), ), ), $column->getIssues()); return $this->buildResponse($title, $properties); } private function renderKey( PhabricatorConfigServerSchema $comp, PhabricatorConfigServerSchema $expect, PhabricatorConfigServerSchema $actual, $database_name, $table_name, $key_name) { $database = $comp->getDatabase($database_name); if (!$database) { return new Aphront404Response(); } $table = $database->getTable($table_name); if (!$table) { return new Aphront404Response(); } $key = $table->getKey($key_name); if (!$key) { return new Aphront404Response(); } $actual_database = $actual->getDatabase($database_name); $actual_table = null; $actual_key = null; if ($actual_database) { $actual_table = $actual_database->getTable($table_name); if ($actual_table) { $actual_key = $actual_table->getKey($key_name); } } $expect_database = $expect->getDatabase($database_name); $expect_table = null; $expect_key = null; if ($expect_database) { $expect_table = $expect_database->getTable($table_name); if ($expect_table) { $expect_key = $expect_table->getKey($key_name); } } if ($actual_key) { $actual_columns = $actual_key->getColumnNames(); $actual_unique = $actual_key->getUnique(); } else { $actual_columns = array(); $actual_unique = null; } if ($expect_key) { $expect_columns = $expect_key->getColumnNames(); $expect_unique = $expect_key->getUnique(); } else { $expect_columns = array(); $expect_unique = null; } $title = pht( 'Database Status: %s.%s (%s)', $database_name, $table_name, $key_name); $properties = $this->buildProperties( array( + array( + pht('Server'), + $this->ref, + ), array( pht('Unique'), $this->renderBoolean($actual_unique), ), array( pht('Expected Unique'), $this->renderBoolean($expect_unique), ), array( pht('Columns'), implode(', ', $actual_columns), ), array( pht('Expected Columns'), implode(', ', $expect_columns), ), ), $key->getIssues()); return $this->buildResponse($title, $properties); } private function buildProperties(array $properties, array $issues) { $view = id(new PHUIPropertyListView()) ->setUser($this->getRequest()->getUser()); foreach ($properties as $property) { list($key, $value) = $property; $view->addProperty($key, $value); } $status_view = new PHUIStatusListView(); if (!$issues) { $status_view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') ->setTarget(pht('No Schema Issues'))); } else { foreach ($issues as $issue) { $note = PhabricatorConfigStorageSchema::getIssueDescription($issue); $status = PhabricatorConfigStorageSchema::getIssueStatus($issue); switch ($status) { case PhabricatorConfigStorageSchema::STATUS_WARN: $icon = PHUIStatusItemView::ICON_WARNING; $color = 'yellow'; break; case PhabricatorConfigStorageSchema::STATUS_FAIL: default: $icon = PHUIStatusItemView::ICON_REJECT; $color = 'red'; break; } $item = id(new PHUIStatusItemView()) ->setTarget(PhabricatorConfigStorageSchema::getIssueName($issue)) ->setIcon($icon, $color) ->setNote($note); $status_view->addItem($item); } } $view->addProperty(pht('Schema Status'), $status_view); return phutil_tag_div('config-page-property', $view); } + private function getURI(array $properties) { + $defaults = array( + 'ref' => $this->ref, + 'database' => $this->database, + 'table' => $this->table, + 'column' => $this->column, + 'key' => $this->key, + ); + + $properties = $properties + $defaults; + $properties = array_select_keys($properties, array_keys($defaults)); + + $parts = array(); + foreach ($properties as $key => $property) { + if (!strlen($property)) { + continue; + } + + if ($key == 'column') { + $parts[] = 'col'; + } else if ($key == 'key') { + $parts[] = 'key'; + } + + $parts[] = $property; + } + + if ($parts) { + $parts = implode('/', $parts).'/'; + } else { + $parts = null; + } + + return $this->getApplicationURI('/database/'.$parts); + } + } diff --git a/src/applications/config/schema/PhabricatorConfigSchemaQuery.php b/src/applications/config/schema/PhabricatorConfigSchemaQuery.php index ca9b44cc07..0ef5e484b1 100644 --- a/src/applications/config/schema/PhabricatorConfigSchemaQuery.php +++ b/src/applications/config/schema/PhabricatorConfigSchemaQuery.php @@ -1,314 +1,347 @@ getAPI($ref); + $patches = PhabricatorSQLPatchList::buildAllPatches(); + return $api->getDatabaseList( + $patches, + $only_living = true); + } - public function setAPI(PhabricatorStorageManagementAPI $api) { - $this->api = $api; - return $this; + private function getAPI(PhabricatorDatabaseRef $ref) { + return id(new PhabricatorStorageManagementAPI()) + ->setUser($ref->getUser()) + ->setHost($ref->getHost()) + ->setPort($ref->getPort()) + ->setNamespace(PhabricatorLiskDAO::getDefaultStorageNamespace()) + ->setPassword($ref->getPass()); } - protected function getAPI() { - if (!$this->api) { - throw new PhutilInvalidStateException('setAPI'); + public function loadActualSchemata() { + $refs = PhabricatorDatabaseRef::getMasterDatabaseRefs(); + + $schemata = array(); + foreach ($refs as $ref) { + $schema = $this->loadActualSchemaForServer($ref); + $schemata[$schema->getRef()->getRefKey()] = $schema; } - return $this->api; - } - protected function getConn() { - return $this->getAPI()->getConn(null); + return $schemata; } - private function getDatabaseNames() { - $api = $this->getAPI(); - $patches = PhabricatorSQLPatchList::buildAllPatches(); - return $api->getDatabaseList( - $patches, - $only_living = true); - } + private function loadActualSchemaForServer(PhabricatorDatabaseRef $ref) { + $databases = $this->getDatabaseNames($ref); - public function loadActualSchema() { - $databases = $this->getDatabaseNames(); + $conn = $ref->newManagementConnection(); - $conn = $this->getConn(); $tables = queryfx_all( $conn, 'SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_COLLATION FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA IN (%Ls)', $databases); $database_info = queryfx_all( $conn, 'SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME IN (%Ls)', $databases); $database_info = ipull($database_info, null, 'SCHEMA_NAME'); // Find databases which exist, but which the user does not have permission // to see. $invisible_databases = array(); foreach ($databases as $database_name) { if (isset($database_info[$database_name])) { continue; } try { queryfx($conn, 'SHOW TABLES IN %T', $database_name); } catch (AphrontAccessDeniedQueryException $ex) { // This database exists, the user just doesn't have permission to // see it. $invisible_databases[] = $database_name; } catch (AphrontSchemaQueryException $ex) { // This database is legitimately missing. } } $sql = array(); foreach ($tables as $table) { $sql[] = qsprintf( $conn, '(TABLE_SCHEMA = %s AND TABLE_NAME = %s)', $table['TABLE_SCHEMA'], $table['TABLE_NAME']); } if ($sql) { $column_info = queryfx_all( $conn, 'SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, CHARACTER_SET_NAME, COLLATION_NAME, COLUMN_TYPE, IS_NULLABLE, EXTRA FROM INFORMATION_SCHEMA.COLUMNS WHERE (%Q)', '('.implode(') OR (', $sql).')'); $column_info = igroup($column_info, 'TABLE_SCHEMA'); } else { $column_info = array(); } // NOTE: Tables like KEY_COLUMN_USAGE and TABLE_CONSTRAINTS only contain // primary, unique, and foreign keys, so we can't use them here. We pull // indexes later on using SHOW INDEXES. - $server_schema = new PhabricatorConfigServerSchema(); + $server_schema = id(new PhabricatorConfigServerSchema()) + ->setRef($ref); $tables = igroup($tables, 'TABLE_SCHEMA'); foreach ($tables as $database_name => $database_tables) { $info = $database_info[$database_name]; $database_schema = id(new PhabricatorConfigDatabaseSchema()) ->setName($database_name) ->setCharacterSet($info['DEFAULT_CHARACTER_SET_NAME']) ->setCollation($info['DEFAULT_COLLATION_NAME']); $database_column_info = idx($column_info, $database_name, array()); $database_column_info = igroup($database_column_info, 'TABLE_NAME'); foreach ($database_tables as $table) { $table_name = $table['TABLE_NAME']; $table_schema = id(new PhabricatorConfigTableSchema()) ->setName($table_name) ->setCollation($table['TABLE_COLLATION']); $columns = idx($database_column_info, $table_name, array()); foreach ($columns as $column) { if (strpos($column['EXTRA'], 'auto_increment') === false) { $auto_increment = false; } else { $auto_increment = true; } $column_schema = id(new PhabricatorConfigColumnSchema()) ->setName($column['COLUMN_NAME']) ->setCharacterSet($column['CHARACTER_SET_NAME']) ->setCollation($column['COLLATION_NAME']) ->setColumnType($column['COLUMN_TYPE']) ->setNullable($column['IS_NULLABLE'] == 'YES') ->setAutoIncrement($auto_increment); $table_schema->addColumn($column_schema); } $key_parts = queryfx_all( $conn, 'SHOW INDEXES FROM %T.%T', $database_name, $table_name); $keys = igroup($key_parts, 'Key_name'); foreach ($keys as $key_name => $key_pieces) { $key_pieces = isort($key_pieces, 'Seq_in_index'); $head = head($key_pieces); // This handles string indexes which index only a prefix of a field. $column_names = array(); foreach ($key_pieces as $piece) { $name = $piece['Column_name']; if ($piece['Sub_part']) { $name = $name.'('.$piece['Sub_part'].')'; } $column_names[] = $name; } $key_schema = id(new PhabricatorConfigKeySchema()) ->setName($key_name) ->setColumnNames($column_names) ->setUnique(!$head['Non_unique']) ->setIndexType($head['Index_type']); $table_schema->addKey($key_schema); } $database_schema->addTable($table_schema); } $server_schema->addDatabase($database_schema); } foreach ($invisible_databases as $database_name) { $server_schema->addDatabase( id(new PhabricatorConfigDatabaseSchema()) ->setName($database_name) ->setAccessDenied(true)); } return $server_schema; } - public function loadExpectedSchema() { - $databases = $this->getDatabaseNames(); - $info = $this->getAPI()->getCharsetInfo(); + public function loadExpectedSchemata() { + $refs = PhabricatorDatabaseRef::getMasterDatabaseRefs(); + + $schemata = array(); + foreach ($refs as $ref) { + $schema = $this->loadExpectedSchemaForServer($ref); + $schemata[$schema->getRef()->getRefKey()] = $schema; + } + + return $schemata; + } + + public function loadExpectedSchemaForServer(PhabricatorDatabaseRef $ref) { + $databases = $this->getDatabaseNames($ref); + $info = $this->getAPI($ref)->getCharsetInfo(); $specs = id(new PhutilClassMapQuery()) ->setAncestorClass('PhabricatorConfigSchemaSpec') ->execute(); - $server_schema = new PhabricatorConfigServerSchema(); + $server_schema = id(new PhabricatorConfigServerSchema()) + ->setRef($ref); + foreach ($specs as $spec) { $spec ->setUTF8Charset( $info[PhabricatorStorageManagementAPI::CHARSET_DEFAULT]) ->setUTF8BinaryCollation( $info[PhabricatorStorageManagementAPI::COLLATE_TEXT]) ->setUTF8SortingCollation( $info[PhabricatorStorageManagementAPI::COLLATE_SORT]) ->setServer($server_schema) ->buildSchemata($server_schema); } return $server_schema; } - public function buildComparisonSchema( + public function buildComparisonSchemata( + array $expect_servers, + array $actual_servers) { + + $schemata = array(); + foreach ($actual_servers as $key => $actual_server) { + $schemata[$key] = $this->buildComparisonSchemaForServer( + $expect_servers[$key], + $actual_server); + } + + return $schemata; + } + + private function buildComparisonSchemaForServer( PhabricatorConfigServerSchema $expect, PhabricatorConfigServerSchema $actual) { $comp_server = $actual->newEmptyClone(); $all_databases = $actual->getDatabases() + $expect->getDatabases(); foreach ($all_databases as $database_name => $database_template) { $actual_database = $actual->getDatabase($database_name); $expect_database = $expect->getDatabase($database_name); $issues = $this->compareSchemata($expect_database, $actual_database); $comp_database = $database_template->newEmptyClone() ->setIssues($issues); if (!$actual_database) { $actual_database = $expect_database->newEmptyClone(); } if (!$expect_database) { $expect_database = $actual_database->newEmptyClone(); } $all_tables = $actual_database->getTables() + $expect_database->getTables(); foreach ($all_tables as $table_name => $table_template) { $actual_table = $actual_database->getTable($table_name); $expect_table = $expect_database->getTable($table_name); $issues = $this->compareSchemata($expect_table, $actual_table); $comp_table = $table_template->newEmptyClone() ->setIssues($issues); if (!$actual_table) { $actual_table = $expect_table->newEmptyClone(); } if (!$expect_table) { $expect_table = $actual_table->newEmptyClone(); } $all_columns = $actual_table->getColumns() + $expect_table->getColumns(); foreach ($all_columns as $column_name => $column_template) { $actual_column = $actual_table->getColumn($column_name); $expect_column = $expect_table->getColumn($column_name); $issues = $this->compareSchemata($expect_column, $actual_column); $comp_column = $column_template->newEmptyClone() ->setIssues($issues); $comp_table->addColumn($comp_column); } $all_keys = $actual_table->getKeys() + $expect_table->getKeys(); foreach ($all_keys as $key_name => $key_template) { $actual_key = $actual_table->getKey($key_name); $expect_key = $expect_table->getKey($key_name); $issues = $this->compareSchemata($expect_key, $actual_key); $comp_key = $key_template->newEmptyClone() ->setIssues($issues); $comp_table->addKey($comp_key); } $comp_database->addTable($comp_table); } $comp_server->addDatabase($comp_database); } return $comp_server; } private function compareSchemata( PhabricatorConfigStorageSchema $expect = null, PhabricatorConfigStorageSchema $actual = null) { $expect_is_key = ($expect instanceof PhabricatorConfigKeySchema); $actual_is_key = ($actual instanceof PhabricatorConfigKeySchema); if ($expect_is_key || $actual_is_key) { $missing_issue = PhabricatorConfigStorageSchema::ISSUE_MISSINGKEY; $surplus_issue = PhabricatorConfigStorageSchema::ISSUE_SURPLUSKEY; } else { $missing_issue = PhabricatorConfigStorageSchema::ISSUE_MISSING; $surplus_issue = PhabricatorConfigStorageSchema::ISSUE_SURPLUS; } if (!$expect && !$actual) { throw new Exception(pht('Can not compare two missing schemata!')); } else if ($expect && !$actual) { $issues = array($missing_issue); } else if ($actual && !$expect) { $issues = array($surplus_issue); } else { $issues = $actual->compareTo($expect); } return $issues; } } diff --git a/src/applications/config/schema/PhabricatorConfigServerSchema.php b/src/applications/config/schema/PhabricatorConfigServerSchema.php index b8b21fe919..f067534b23 100644 --- a/src/applications/config/schema/PhabricatorConfigServerSchema.php +++ b/src/applications/config/schema/PhabricatorConfigServerSchema.php @@ -1,41 +1,51 @@ ref = $ref; + return $this; + } + + public function getRef() { + return $this->ref; + } + public function addDatabase(PhabricatorConfigDatabaseSchema $database) { $key = $database->getName(); if (isset($this->databases[$key])) { throw new Exception( pht('Trying to add duplicate database "%s"!', $key)); } $this->databases[$key] = $database; return $this; } public function getDatabases() { return $this->databases; } public function getDatabase($key) { return idx($this->getDatabases(), $key); } protected function getSubschemata() { return $this->getDatabases(); } protected function compareToSimilarSchema( PhabricatorConfigStorageSchema $expect) { return array(); } public function newEmptyClone() { $clone = clone $this; $clone->databases = array(); return $clone; } } diff --git a/src/infrastructure/cluster/PhabricatorDatabaseRef.php b/src/infrastructure/cluster/PhabricatorDatabaseRef.php index 2cab28104f..9033ba99ef 100644 --- a/src/infrastructure/cluster/PhabricatorDatabaseRef.php +++ b/src/infrastructure/cluster/PhabricatorDatabaseRef.php @@ -1,563 +1,574 @@ host = $host; return $this; } public function getHost() { return $this->host; } public function setPort($port) { $this->port = $port; return $this; } public function getPort() { return $this->port; } public function setUser($user) { $this->user = $user; return $this; } public function getUser() { return $this->user; } public function setPass(PhutilOpaqueEnvelope $pass) { $this->pass = $pass; return $this; } public function getPass() { return $this->pass; } public function setIsMaster($is_master) { $this->isMaster = $is_master; return $this; } public function getIsMaster() { return $this->isMaster; } public function setDisabled($disabled) { $this->disabled = $disabled; return $this; } public function getDisabled() { return $this->disabled; } public function setConnectionLatency($connection_latency) { $this->connectionLatency = $connection_latency; return $this; } public function getConnectionLatency() { return $this->connectionLatency; } public function setConnectionStatus($connection_status) { $this->connectionStatus = $connection_status; return $this; } public function getConnectionStatus() { if ($this->connectionStatus === null) { throw new PhutilInvalidStateException('queryAll'); } return $this->connectionStatus; } public function setConnectionMessage($connection_message) { $this->connectionMessage = $connection_message; return $this; } public function getConnectionMessage() { return $this->connectionMessage; } public function setReplicaStatus($replica_status) { $this->replicaStatus = $replica_status; return $this; } public function getReplicaStatus() { return $this->replicaStatus; } public function setReplicaMessage($replica_message) { $this->replicaMessage = $replica_message; return $this; } public function getReplicaMessage() { return $this->replicaMessage; } public function setReplicaDelay($replica_delay) { $this->replicaDelay = $replica_delay; return $this; } public function getReplicaDelay() { return $this->replicaDelay; } public function setIsIndividual($is_individual) { $this->isIndividual = $is_individual; return $this; } public function getIsIndividual() { return $this->isIndividual; } + public function getRefKey() { + $host = $this->getHost(); + + $port = $this->getPort(); + if (strlen($port)) { + return "{$host}:{$port}"; + } + + return $host; + } + public static function getConnectionStatusMap() { return array( self::STATUS_OKAY => array( 'icon' => 'fa-exchange', 'color' => 'green', 'label' => pht('Okay'), ), self::STATUS_FAIL => array( 'icon' => 'fa-times', 'color' => 'red', 'label' => pht('Failed'), ), self::STATUS_AUTH => array( 'icon' => 'fa-key', 'color' => 'red', 'label' => pht('Invalid Credentials'), ), self::STATUS_REPLICATION_CLIENT => array( 'icon' => 'fa-eye-slash', 'color' => 'yellow', 'label' => pht('Missing Permission'), ), ); } public static function getReplicaStatusMap() { return array( self::REPLICATION_OKAY => array( 'icon' => 'fa-download', 'color' => 'green', 'label' => pht('Okay'), ), self::REPLICATION_MASTER_REPLICA => array( 'icon' => 'fa-database', 'color' => 'red', 'label' => pht('Replicating Master'), ), self::REPLICATION_REPLICA_NONE => array( 'icon' => 'fa-download', 'color' => 'red', 'label' => pht('Not A Replica'), ), self::REPLICATION_SLOW => array( 'icon' => 'fa-hourglass', 'color' => 'red', 'label' => pht('Slow Replication'), ), self::REPLICATION_NOT_REPLICATING => array( 'icon' => 'fa-exclamation-triangle', 'color' => 'red', 'label' => pht('Not Replicating'), ), ); } public static function getLiveRefs() { $cache = PhabricatorCaches::getRequestCache(); $refs = $cache->getKey(self::KEY_REFS); if (!$refs) { $refs = self::newRefs(); $cache->setKey(self::KEY_REFS, $refs); } return $refs; } public static function getLiveIndividualRef() { $cache = PhabricatorCaches::getRequestCache(); $ref = $cache->getKey(self::KEY_INDIVIDUAL); if (!$ref) { $ref = self::newIndividualRef(); $cache->setKey(self::KEY_INDIVIDUAL, $ref); } return $ref; } public static function newRefs() { $refs = array(); $default_port = PhabricatorEnv::getEnvConfig('mysql.port'); $default_port = nonempty($default_port, 3306); $default_user = PhabricatorEnv::getEnvConfig('mysql.user'); $default_pass = PhabricatorEnv::getEnvConfig('mysql.pass'); $default_pass = new PhutilOpaqueEnvelope($default_pass); $config = PhabricatorEnv::getEnvConfig('cluster.databases'); foreach ($config as $server) { $host = $server['host']; $port = idx($server, 'port', $default_port); $user = idx($server, 'user', $default_user); $disabled = idx($server, 'disabled', false); $pass = idx($server, 'pass'); if ($pass) { $pass = new PhutilOpaqueEnvelope($pass); } else { $pass = clone $default_pass; } $role = $server['role']; $ref = id(new self()) ->setHost($host) ->setPort($port) ->setUser($user) ->setPass($pass) ->setDisabled($disabled) ->setIsMaster(($role == 'master')); $refs[] = $ref; } return $refs; } public static function queryAll() { $refs = self::newRefs(); foreach ($refs as $ref) { if ($ref->getDisabled()) { continue; } $conn = $ref->newManagementConnection(); $t_start = microtime(true); $replica_status = false; try { $replica_status = queryfx_one($conn, 'SHOW SLAVE STATUS'); $ref->setConnectionStatus(self::STATUS_OKAY); } catch (AphrontAccessDeniedQueryException $ex) { $ref->setConnectionStatus(self::STATUS_REPLICATION_CLIENT); $ref->setConnectionMessage( pht( 'No permission to run "SHOW SLAVE STATUS". Grant this user '. '"REPLICATION CLIENT" permission to allow Phabricator to '. 'monitor replica health.')); } catch (AphrontInvalidCredentialsQueryException $ex) { $ref->setConnectionStatus(self::STATUS_AUTH); $ref->setConnectionMessage($ex->getMessage()); } catch (AphrontQueryException $ex) { $ref->setConnectionStatus(self::STATUS_FAIL); $class = get_class($ex); $message = $ex->getMessage(); $ref->setConnectionMessage( pht( '%s: %s', get_class($ex), $ex->getMessage())); } $t_end = microtime(true); $ref->setConnectionLatency($t_end - $t_start); if ($replica_status !== false) { $is_replica = (bool)$replica_status; if ($ref->getIsMaster() && $is_replica) { $ref->setReplicaStatus(self::REPLICATION_MASTER_REPLICA); $ref->setReplicaMessage( pht( 'This host has a "master" role, but is replicating data from '. 'another host ("%s")!', idx($replica_status, 'Master_Host'))); } else if (!$ref->getIsMaster() && !$is_replica) { $ref->setReplicaStatus(self::REPLICATION_REPLICA_NONE); $ref->setReplicaMessage( pht( 'This host has a "replica" role, but is not replicating data '. 'from a master (no output from "SHOW SLAVE STATUS").')); } else { $ref->setReplicaStatus(self::REPLICATION_OKAY); } if ($is_replica) { $latency = idx($replica_status, 'Seconds_Behind_Master'); if (!strlen($latency)) { $ref->setReplicaStatus(self::REPLICATION_NOT_REPLICATING); } else { $latency = (int)$latency; $ref->setReplicaDelay($latency); if ($latency > 30) { $ref->setReplicaStatus(self::REPLICATION_SLOW); $ref->setReplicaMessage( pht( 'This replica is lagging far behind the master. Data is at '. 'risk!')); } } } } } return $refs; } public function newManagementConnection() { return $this->newConnection( array( 'retries' => 0, 'timeout' => 2, )); } public function newApplicationConnection($database) { return $this->newConnection( array( 'database' => $database, )); } public function isSevered() { // If we only have an individual database, never sever our connection to // it, at least for now. It's possible that using the same severing rules // might eventually make sense to help alleviate load-related failures, // but we should wait for all the cluster stuff to stabilize first. if ($this->getIsIndividual()) { return false; } if ($this->didFailToConnect) { return true; } $record = $this->getHealthRecord(); $is_healthy = $record->getIsHealthy(); if (!$is_healthy) { return true; } return false; } public function isReachable(AphrontDatabaseConnection $connection) { $record = $this->getHealthRecord(); $should_check = $record->getShouldCheck(); if ($this->isSevered() && !$should_check) { return false; } try { $connection->openConnection(); $reachable = true; } catch (AphrontSchemaQueryException $ex) { // We get one of these if the database we're trying to select does not // exist. In this case, just re-throw the exception. This is expected // during first-time setup, when databases like "config" will not exist // yet. throw $ex; } catch (Exception $ex) { $reachable = false; } if ($should_check) { $record->didHealthCheck($reachable); } if (!$reachable) { $this->didFailToConnect = true; } return $reachable; } public function checkHealth() { $health = $this->getHealthRecord(); $should_check = $health->getShouldCheck(); if ($should_check) { // This does an implicit health update. $connection = $this->newManagementConnection(); $this->isReachable($connection); } return $this; } public function getHealthRecord() { if (!$this->healthRecord) { $this->healthRecord = new PhabricatorDatabaseHealthRecord($this); } return $this->healthRecord; } public static function getMasterDatabaseRefs() { $refs = self::getLiveRefs(); if (!$refs) { return array(self::getLiveIndividualRef()); } $masters = array(); foreach ($refs as $ref) { if ($ref->getDisabled()) { continue; } if ($ref->getIsMaster()) { $masters[] = $ref; } } return $masters; } public static function getMasterDatabaseRef() { // TODO: Remove this method; it no longer makes sense with application // partitioning. return head(self::getMasterDatabaseRefs()); } public static function getMasterDatabaseRefForDatabase($database) { $masters = self::getMasterDatabaseRefs(); // TODO: Actually implement this. return head($masters); } public static function newIndividualRef() { $conf = PhabricatorEnv::newObjectFromConfig( 'mysql.configuration-provider', array(null, 'w', null)); return id(new self()) ->setHost($conf->getHost()) ->setPort($conf->getPort()) ->setUser($conf->getUser()) ->setPass($conf->getPassword()) ->setIsIndividual(true) ->setIsMaster(true); } public static function getReplicaDatabaseRefs() { $refs = self::getLiveRefs(); if (!$refs) { return array(); } $replicas = array(); foreach ($refs as $ref) { if ($ref->getDisabled()) { continue; } if ($ref->getIsMaster()) { continue; } $replicas[] = $ref; } return $replicas; } public static function getReplicaDatabaseRefForDatabase($database) { $replicas = self::getReplicaDatabaseRefs(); // TODO: Actually implement this. // TODO: We may have multiple replicas to choose from, and could make // more of an effort to pick the "best" one here instead of always // picking the first one. Once we've picked one, we should try to use // the same replica for the rest of the request, though. return head($replicas); } private function newConnection(array $options) { // If we believe the database is unhealthy, don't spend as much time // trying to connect to it, since it's likely to continue to fail and // hammering it can only make the problem worse. $record = $this->getHealthRecord(); if ($record->getIsHealthy()) { $default_retries = 3; $default_timeout = 10; } else { $default_retries = 0; $default_timeout = 2; } $spec = $options + array( 'user' => $this->getUser(), 'pass' => $this->getPass(), 'host' => $this->getHost(), 'port' => $this->getPort(), 'database' => null, 'retries' => $default_retries, 'timeout' => $default_timeout, ); return PhabricatorEnv::newObjectFromConfig( 'mysql.implementation', array( $spec, )); } }