1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-12 08:36:13 +01:00
phorge-phorge/src/applications/config/check/PhabricatorSetupCheck.php

302 lines
8.8 KiB
PHP
Raw Normal View History

<?php
abstract class PhabricatorSetupCheck extends Phobject {
private $issues;
abstract protected function executeChecks();
const GROUP_OTHER = 'other';
const GROUP_MYSQL = 'mysql';
const GROUP_PHP = 'php';
const GROUP_IMPORTANT = 'important';
public function getExecutionOrder() {
if ($this->isPreflightCheck()) {
return 0;
} else {
return 1000;
}
}
/**
* Should this check execute before we load configuration?
*
* The majority of checks (particularly, those checks which examine
* configuration) should run in the normal setup phase, after configuration
* loads. However, a small set of critical checks (mostly, tests for PHP
* setup and extensions) need to run before we can load configuration.
*
* @return bool True to execute before configuration is loaded.
*/
public function isPreflightCheck() {
return false;
}
final protected function newIssue($key) {
$issue = id(new PhabricatorSetupIssue())
->setIssueKey($key);
$this->issues[$key] = $issue;
if ($this->getDefaultGroup()) {
$issue->setGroup($this->getDefaultGroup());
}
return $issue;
}
final public function getIssues() {
return $this->issues;
}
protected function addIssue(PhabricatorSetupIssue $issue) {
$this->issues[$issue->getIssueKey()] = $issue;
return $this;
}
public function getDefaultGroup() {
return null;
}
final public function runSetupChecks() {
$this->issues = array();
$this->executeChecks();
}
final public static function getOpenSetupIssueKeys() {
$cache = PhabricatorCaches::getSetupCache();
return $cache->getKey('phabricator.setup.issue-keys');
}
When we "discover" new fatal setup issues, stop serving traffic Summary: Ref T10759. We may "discover" the presence of a fatal setup error later, after starting Phabricator. This can happen in a few ways, but most are unlikely. The one I'm immediately concerned about is: - Phabricator starts up during a disaster with some databases unreachable. - We start with warnings (unreachable databases are generally not fatal, since it's OK for some subset of hosts to be down in replicated/partitioned setups). - The unreachable databases later recover and become accessible again. - When we run checks against them, we discover that they are misconfigured. Currently, "fatal" setup issues are not truly fatal if we're "in flight" -- we've survived setup checks at least once in the past. This is bad in the scenario above. Especially with partitioning, it could lead to mangled data in a disaster scenario where operations staff makes a small configuration mistake while trying to get things running again. Instead, if we "discover" a fatal error while already "in flight", reset the whole setup process as though the webserver had just restarted. Don't serve requests again until we can make it through setup without hitting fatals. Test Plan: - Started Phabricator with multiple masters, one of which was down and broken. - Got a warning about the bad master. - Revived the master. - Before: Phabricator detects the fatal, but keeps serving requests. - After: Phabricator detects the fatal, resets the webserver, and stops serving requests until the fatal is resolved. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10759 Differential Revision: https://secure.phabricator.com/D16903
2016-11-21 15:09:10 +01:00
final public static function resetSetupState() {
$cache = PhabricatorCaches::getSetupCache();
$cache->deleteKey('phabricator.setup.issue-keys');
$server_cache = PhabricatorCaches::getServerStateCache();
$server_cache->deleteKey('phabricator.in-flight');
$use_scope = AphrontWriteGuard::isGuardActive();
if ($use_scope) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
} else {
AphrontWriteGuard::allowDangerousUnguardedWrites(true);
}
try {
$db_cache = new PhabricatorKeyValueDatabaseCache();
$db_cache->deleteKey('phabricator.setup.issue-keys');
} catch (Exception $ex) {
// If we hit an exception here, just ignore it. In particular, this can
// happen on initial startup before the databases are initialized.
When we "discover" new fatal setup issues, stop serving traffic Summary: Ref T10759. We may "discover" the presence of a fatal setup error later, after starting Phabricator. This can happen in a few ways, but most are unlikely. The one I'm immediately concerned about is: - Phabricator starts up during a disaster with some databases unreachable. - We start with warnings (unreachable databases are generally not fatal, since it's OK for some subset of hosts to be down in replicated/partitioned setups). - The unreachable databases later recover and become accessible again. - When we run checks against them, we discover that they are misconfigured. Currently, "fatal" setup issues are not truly fatal if we're "in flight" -- we've survived setup checks at least once in the past. This is bad in the scenario above. Especially with partitioning, it could lead to mangled data in a disaster scenario where operations staff makes a small configuration mistake while trying to get things running again. Instead, if we "discover" a fatal error while already "in flight", reset the whole setup process as though the webserver had just restarted. Don't serve requests again until we can make it through setup without hitting fatals. Test Plan: - Started Phabricator with multiple masters, one of which was down and broken. - Got a warning about the bad master. - Revived the master. - Before: Phabricator detects the fatal, but keeps serving requests. - After: Phabricator detects the fatal, resets the webserver, and stops serving requests until the fatal is resolved. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10759 Differential Revision: https://secure.phabricator.com/D16903
2016-11-21 15:09:10 +01:00
}
if ($use_scope) {
unset($unguarded);
} else {
AphrontWriteGuard::allowDangerousUnguardedWrites(false);
}
}
2016-04-26 16:33:07 +02:00
final public static function setOpenSetupIssueKeys(
array $keys,
$update_database) {
$cache = PhabricatorCaches::getSetupCache();
$cache->setKey('phabricator.setup.issue-keys', $keys);
2016-04-26 16:33:07 +02:00
$server_cache = PhabricatorCaches::getServerStateCache();
$server_cache->setKey('phabricator.in-flight', 1);
2016-04-26 16:33:07 +02:00
if ($update_database) {
$db_cache = new PhabricatorKeyValueDatabaseCache();
try {
$json = phutil_json_encode($keys);
$db_cache->setKey('phabricator.setup.issue-keys', $json);
} catch (Exception $ex) {
// Ignore any write failures, since they likely just indicate that we
// have a database-related setup issue that needs to be resolved.
}
}
}
final public static function getOpenSetupIssueKeysFromDatabase() {
$db_cache = new PhabricatorKeyValueDatabaseCache();
try {
$value = $db_cache->getKey('phabricator.setup.issue-keys');
if (!strlen($value)) {
return null;
}
return phutil_json_decode($value);
} catch (Exception $ex) {
return null;
}
}
final public static function getUnignoredIssueKeys(array $all_issues) {
assert_instances_of($all_issues, 'PhabricatorSetupIssue');
$keys = array();
foreach ($all_issues as $issue) {
if (!$issue->getIsIgnored()) {
$keys[] = $issue->getIssueKey();
}
}
return $keys;
}
final public static function getConfigNeedsRepair() {
$cache = PhabricatorCaches::getSetupCache();
return $cache->getKey('phabricator.setup.needs-repair');
}
final public static function setConfigNeedsRepair($needs_repair) {
$cache = PhabricatorCaches::getSetupCache();
$cache->setKey('phabricator.setup.needs-repair', $needs_repair);
}
final public static function deleteSetupCheckCache() {
$cache = PhabricatorCaches::getSetupCache();
$cache->deleteKeys(
array(
'phabricator.setup.needs-repair',
'phabricator.setup.issue-keys',
));
}
final public static function willPreflightRequest() {
$checks = self::loadAllChecks();
foreach ($checks as $check) {
if (!$check->isPreflightCheck()) {
continue;
}
$check->runSetupChecks();
foreach ($check->getIssues() as $key => $issue) {
return self::newIssueResponse($issue);
}
}
return null;
}
public static function newIssueResponse(PhabricatorSetupIssue $issue) {
$view = id(new PhabricatorSetupIssueView())
->setIssue($issue);
return id(new PhabricatorConfigResponse())
->setView($view);
}
final public static function willProcessRequest() {
$issue_keys = self::getOpenSetupIssueKeys();
if ($issue_keys === null) {
When we "discover" new fatal setup issues, stop serving traffic Summary: Ref T10759. We may "discover" the presence of a fatal setup error later, after starting Phabricator. This can happen in a few ways, but most are unlikely. The one I'm immediately concerned about is: - Phabricator starts up during a disaster with some databases unreachable. - We start with warnings (unreachable databases are generally not fatal, since it's OK for some subset of hosts to be down in replicated/partitioned setups). - The unreachable databases later recover and become accessible again. - When we run checks against them, we discover that they are misconfigured. Currently, "fatal" setup issues are not truly fatal if we're "in flight" -- we've survived setup checks at least once in the past. This is bad in the scenario above. Especially with partitioning, it could lead to mangled data in a disaster scenario where operations staff makes a small configuration mistake while trying to get things running again. Instead, if we "discover" a fatal error while already "in flight", reset the whole setup process as though the webserver had just restarted. Don't serve requests again until we can make it through setup without hitting fatals. Test Plan: - Started Phabricator with multiple masters, one of which was down and broken. - Got a warning about the bad master. - Revived the master. - Before: Phabricator detects the fatal, but keeps serving requests. - After: Phabricator detects the fatal, resets the webserver, and stops serving requests until the fatal is resolved. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10759 Differential Revision: https://secure.phabricator.com/D16903
2016-11-21 15:09:10 +01:00
$engine = new PhabricatorSetupEngine();
$response = $engine->execute();
if ($response) {
return $response;
}
2016-04-26 16:33:07 +02:00
} else if ($issue_keys) {
// If Phabricator is configured in a cluster with multiple web devices,
// we can end up with setup issues cached on every device. This can cause
// a warning banner to show on every device so that each one needs to
// be dismissed individually, which is pretty annoying. See T10876.
// To avoid this, check if the issues we found have already been cleared
// in the database. If they have, we'll just wipe out our own cache and
// move on.
$issue_keys = self::getOpenSetupIssueKeysFromDatabase();
if ($issue_keys !== null) {
self::setOpenSetupIssueKeys($issue_keys, $update_database = false);
}
}
// Try to repair configuration unless we have a clean bill of health on it.
// We need to keep doing this on every page load until all the problems
// are fixed, which is why it's separate from setup checks (which run
// once per restart).
$needs_repair = self::getConfigNeedsRepair();
if ($needs_repair !== false) {
$needs_repair = self::repairConfig();
self::setConfigNeedsRepair($needs_repair);
}
}
/**
* Test if we've survived through setup on at least one normal request
* without fataling.
*
* If we've made it through setup without hitting any fatals, we switch
* to render a more friendly error page when encountering issues like
* database connection failures. This gives users a smoother experience in
* the face of intermittent failures.
*
* @return bool True if we've made it through setup since the last restart.
*/
final public static function isInFlight() {
$cache = PhabricatorCaches::getServerStateCache();
return (bool)$cache->getKey('phabricator.in-flight');
}
final public static function loadAllChecks() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setSortMethod('getExecutionOrder')
->execute();
}
final public static function runNormalChecks() {
$checks = self::loadAllChecks();
foreach ($checks as $key => $check) {
if ($check->isPreflightCheck()) {
unset($checks[$key]);
}
}
$issues = array();
foreach ($checks as $check) {
$check->runSetupChecks();
foreach ($check->getIssues() as $key => $issue) {
if (isset($issues[$key])) {
throw new Exception(
pht(
"Two setup checks raised an issue with key '%s'!",
$key));
}
$issues[$key] = $issue;
if ($issue->getIsFatal()) {
break 2;
}
}
}
$ignore_issues = PhabricatorEnv::getEnvConfig('config.ignore-issues');
foreach ($ignore_issues as $ignorable => $derp) {
if (isset($issues[$ignorable])) {
$issues[$ignorable]->setIsIgnored(true);
}
}
return $issues;
}
final public static function repairConfig() {
$needs_repair = false;
$options = PhabricatorApplicationConfigOptions::loadAllOptions();
foreach ($options as $option) {
try {
$option->getGroup()->validateOption(
$option,
PhabricatorEnv::getEnvConfig($option->getKey()));
} catch (PhabricatorConfigValidationException $ex) {
PhabricatorEnv::repairConfig($option->getKey(), $option->getDefault());
$needs_repair = true;
}
}
return $needs_repair;
}
}